diff --git a/client/.angular-cli.json b/client/.angular-cli.json index 634daad..c43db47 100644 --- a/client/.angular-cli.json +++ b/client/.angular-cli.json @@ -16,10 +16,6 @@ "testTsconfig": "tsconfig.spec.json", "prefix": "app", "styles": [ - "../node_modules/ng2-toasty/bundles/style-bootstrap.css", - "../node_modules/font-awesome/css/font-awesome.css", - "../node_modules/simple-line-icons/css/simple-line-icons.css", - "../node_modules/@qontu/ngx-inline-editor/themes/bootstrap.css", "styles.css" ], "scripts": [ diff --git a/client/src/app/app.component.html b/client/src/app/app.component.html index bba6df3..7183970 100644 --- a/client/src/app/app.component.html +++ b/client/src/app/app.component.html @@ -11,4 +11,5 @@ - \ No newline at end of file + + diff --git a/client/src/app/components/podcast/entry-list-item/entry-list-item.component.html b/client/src/app/components/podcast/entry-list-item/entry-list-item.component.html index 2765605..4487802 100644 --- a/client/src/app/components/podcast/entry-list-item/entry-list-item.component.html +++ b/client/src/app/components/podcast/entry-list-item/entry-list-item.component.html @@ -4,22 +4,23 @@
+ *ngIf="entry.processingStatus !== 'Accepted'"> + class="img-avatar img-avatar48" + alt="">

+ [(ngModel)]="entry.title" + (onSave)="updateTitle($event)" + name="title" + size="32">

-

+

{{entry.processingStatus}}| {{entry.createDate | amTimeAgo}}

@@ -29,7 +30,11 @@ style="width: 100%"> - + + +
@@ -57,40 +62,40 @@ {{entry.title}} - + + (click)="entryDeleteDialog.show()"> \ No newline at end of file +
diff --git a/client/src/app/components/podcast/entry-list-item/entry-list-item.component.ts b/client/src/app/components/podcast/entry-list-item/entry-list-item.component.ts index 969662b..600f932 100644 --- a/client/src/app/components/podcast/entry-list-item/entry-list-item.component.ts +++ b/client/src/app/components/podcast/entry-list-item/entry-list-item.component.ts @@ -4,9 +4,10 @@ import { Store } from '@ngrx/store'; import { SignalRService } from 'app/services/signalr.service'; import { PodcastModel, PodcastEntryModel } from 'app/models/podcasts.models'; import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; -import { ToastyService } from 'ng2-toasty'; +import { ToastyService, ToastData, ToastOptions } from 'ng2-toasty'; import * as fromEntriesActions from 'app/actions/entries.actions'; +import { PodcastService } from '../../../services/podcast.service'; @Component({ selector: '[app-entry-list-item]', @@ -20,44 +21,98 @@ export class EntryListItemComponent implements OnInit { percentageProcessed = 0; currentSpeed: string = ''; - constructor(private _signalrService: SignalRService, private _store: Store) {} + constructor( + private _signalrService: SignalRService, + private _entryService: PodcastService, + private _store: Store, + private _toasty: ToastyService + ) {} ngOnInit() { - if (this.entry && !this.entry.processed && this.entry.processingStatus !== 'Failed') { - const signalRHost = `${environment.SIGNALR_HOST}hubs/audioprocessing`; + if ( + this.entry && + !this.entry.processed && + this.entry.processingStatus !== 'Failed' + ) { + const signalRHost = `${ + environment.SIGNALR_HOST + }hubs/audioprocessing`; - console.log('entry-list-item.component.ts', 'ngOnInit', `Initting signalr: ${signalRHost}`); + console.log( + 'entry-list-item.component.ts', + 'ngOnInit', + `Initting signalr: ${signalRHost}` + ); this._signalrService .init(signalRHost) .then(() => { - console.log('entry-list-item.component.ts', 'ngOnInit', 'Initted signalr'); - const updateChannel: string = `${this.entry.uid}__progress_update`; - const processedChannel: string = `${this.entry.uid}__info_processed`; - console.log('EntryListItemComponent', 'updateChannel', updateChannel); - console.log('EntryListItemComponent', 'processedChannel', processedChannel); + console.log( + 'entry-list-item.component.ts', + 'ngOnInit', + 'Initted signalr' + ); + const updateChannel: string = `${ + this.entry.uid + }__progress_update`; + const processedChannel: string = `${ + this.entry.uid + }__info_processed`; + console.log( + 'EntryListItemComponent', + 'updateChannel', + updateChannel + ); + console.log( + 'EntryListItemComponent', + 'processedChannel', + processedChannel + ); - this._signalrService.connection.on(updateChannel, result => { - this.percentageProcessed = result.percentage; - this.currentSpeed = result.currentSpeed; - }); - this._signalrService.connection.on(processedChannel, result => { - this.entry = result; - if (this.entry.processingStatus === 'Processed') { - // only update the store when we're finished. - this._store.dispatch(new fromEntriesActions.UpdateSuccessAction(result)); + this._signalrService.connection.on( + updateChannel, + result => { + this.percentageProcessed = result.percentage; + this.currentSpeed = result.currentSpeed; } - }); + ); + this._signalrService.connection.on( + processedChannel, + result => { + this.entry = result; + if (this.entry.processingStatus === 'Processed') { + // only update the store when we're finished. + this._store.dispatch( + new fromEntriesActions.UpdateSuccessAction( + result + ) + ); + } + } + ); }) - .catch(err => console.error('entry-list-item.component.ts', '_signalrService.init', err)); + .catch(err => + console.error( + 'entry-list-item.component.ts', + '_signalrService.init', + err + ) + ); } } deleteEntry() { - this._store.dispatch(new fromEntriesActions.DeleteAction(this.entry.id)); + this._store.dispatch( + new fromEntriesActions.DeleteAction(this.entry.id) + ); } updateTitle($event: Event) { this._store.dispatch(new fromEntriesActions.UpdateAction(this.entry)); } - goto(entry: PodcastEntryModel) { window.open(entry.sourceUrl); } + retry(entry: PodcastEntryModel) { + this._entryService.reSubmitEntry(entry).subscribe(r => { + this.entry = r; + this._toasty.info('Submitted podcast for re-processing'); + }); + } } diff --git a/client/src/app/components/podcast/podcast-upload-form/podcast-upload-form.component.ts b/client/src/app/components/podcast/podcast-upload-form/podcast-upload-form.component.ts index c780065..da5040c 100644 --- a/client/src/app/components/podcast/podcast-upload-form/podcast-upload-form.component.ts +++ b/client/src/app/components/podcast/podcast-upload-form/podcast-upload-form.component.ts @@ -5,6 +5,7 @@ import { Component, Input, OnInit, Output, EventEmitter, ElementRef, ViewChild } import { AuthService } from 'app/services/auth.service'; import { PodcastModel } from 'app/models/podcasts.models'; +import { environment } from 'environments/environment'; import * as Dropzone from 'dropzone'; @@ -21,7 +22,7 @@ export class PodcastUploadFormComponent implements OnInit { constructor(private _toastyService: ToastyService, private _auth: AuthService) {} ngOnInit() { const config = { - url: `/api/podcast/${this.podcast.slug}/audioupload`, + url: `${environment.API_HOST}/podcast/${this.podcast.slug}/audioupload`, acceptedFiles: 'audio/*', maxFilesize: 4000, // 4Gb timeout: 1000 * (60 * 120), /// 2 hours diff --git a/client/src/app/services/image.service.ts b/client/src/app/services/image.service.ts index 8d99dc9..263c2a7 100644 --- a/client/src/app/services/image.service.ts +++ b/client/src/app/services/image.service.ts @@ -1,6 +1,7 @@ import {Http, Headers} from '@angular/http'; import {Injectable} from '@angular/core'; import {AuthService} from './auth.service'; +import { environment } from 'environments/environment'; @Injectable() export class ImageService { @@ -14,6 +15,6 @@ export class ImageService { formData.append('file', image); const headers = new Headers(); headers.append('Authorization', 'Bearer ' + this._auth.getToken()); - return this._http.post(`/api/podcast/${podcastSlug}/imageupload`, formData, {headers: headers}); + return this._http.post(`${environment.API_HOST}/podcast/${podcastSlug}/imageupload`, formData, {headers: headers}); } } diff --git a/client/src/app/services/podcast.service.ts b/client/src/app/services/podcast.service.ts index 0cf6bce..0d2783b 100644 --- a/client/src/app/services/podcast.service.ts +++ b/client/src/app/services/podcast.service.ts @@ -66,6 +66,10 @@ export class PodcastService { return Observable.throw(new Error(error.status)); }); } - + reSubmitEntry(entry: PodcastEntryModel): Observable { + return this._http + .post(environment.API_HOST + '/entry/resubmit', entry) + .map(res => res.json()); + } //#endregion } diff --git a/client/src/index.html b/client/src/index.html index 9b9365b..28584cf 100644 --- a/client/src/index.html +++ b/client/src/index.html @@ -11,5 +11,4 @@ - - \ No newline at end of file + diff --git a/client/src/styles.css b/client/src/styles.css index 66ebb36..5a24217 100644 --- a/client/src/styles.css +++ b/client/src/styles.css @@ -1,4 +1,9 @@ @import 'assets/css/style.css'; +@import '~ng2-toasty/bundles/style.css'; +@import '~ng2-toasty/bundles/style-bootstrap.css'; +@import '~font-awesome/css/font-awesome.css'; +@import '~simple-line-icons/css/simple-line-icons.css'; +@import '~@qontu/ngx-inline-editor/themes/bootstrap.css'; a:hover { cursor:pointer; @@ -39,7 +44,7 @@ a:hover { color: #fff; } -.btn-dribbble:focus, +.btn-dribbble:focus, .btn-dribbble:hover, .btn-dropbox:focus, .btn-dropbox:hover, @@ -106,4 +111,4 @@ a:hover { background-color: #00ACED; } -/**====== Social-Buttons css end ======**/ \ No newline at end of file +/**====== Social-Buttons css end ======**/ diff --git a/server/Controllers/EntryController.cs b/server/Controllers/EntryController.cs index 001aeb2..b54575a 100644 --- a/server/Controllers/EntryController.cs +++ b/server/Controllers/EntryController.cs @@ -1,4 +1,8 @@ using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; using AutoMapper; using Hangfire; using Microsoft.AspNetCore.Mvc; @@ -9,17 +13,13 @@ using PodNoms.Api.Models.ViewModels; using PodNoms.Api.Persistence; using PodNoms.Api.Services.Processor; using PodNoms.Api.Services.Storage; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; namespace PodNoms.Api.Controllers { [Route ("[controller]")] public class EntryController : Controller { private readonly IPodcastRepository _podcastRepository; private readonly IEntryRepository _repository; - private readonly IUnitOfWork _uow; + private readonly IUnitOfWork _unitOfWork; private readonly IMapper _mapper; private readonly IUrlProcessService _processor; private readonly ILogger _logger; @@ -35,7 +35,7 @@ namespace PodNoms.Api.Controllers { this._podcastRepository = podcastRepository; this._repository = repository; this._storageSettings = storageSettings.Value; - this._uow = unitOfWork; + this._unitOfWork = unitOfWork; this._audioFileStorageSettings = audioFileStorageSettings.Value; this._mapper = mapper; this._processor = processor; @@ -75,10 +75,14 @@ namespace PodNoms.Api.Controllers { entry.Podcast = podcast; entry.Processed = false; - entry.Title = "Waiting for information"; + if (string.IsNullOrEmpty (item.Title)) { + entry.Title = "Waiting for information"; + } else { + entry.Title = item.Title; + } } await _repository.AddOrUpdateAsync (entry); - await _uow.CompleteAsync (); + await _unitOfWork.CompleteAsync (); if (entry.ProcessingStatus.Equals (ProcessingStatus.Accepted) && entry.Id != 0) { _processEntry (entry); @@ -91,7 +95,7 @@ namespace PodNoms.Api.Controllers { [HttpDelete ("{id}")] public async Task Delete (int id) { await this._repository.DeleteAsync (id); - await _uow.CompleteAsync (); + await _unitOfWork.CompleteAsync (); return Ok (); } @@ -103,5 +107,17 @@ namespace PodNoms.Api.Controllers { } return BadRequest (); } + + [HttpPost ("resubmit")] + public async Task ReSubmit ([FromBody] PodcastEntryViewModel item) { + var entry = await _repository.GetAsync (item.Id); + entry.ProcessingStatus = ProcessingStatus.Processing; + await _unitOfWork.CompleteAsync (); + if (entry.ProcessingStatus != ProcessingStatus.Processed) { + _processEntry (entry); + } + + return Ok (entry); + } } } \ No newline at end of file diff --git a/server/Services/Processor/AudioUploadProcessService.cs b/server/Services/Processor/AudioUploadProcessService.cs index c9e6c74..ef131ed 100644 --- a/server/Services/Processor/AudioUploadProcessService.cs +++ b/server/Services/Processor/AudioUploadProcessService.cs @@ -11,76 +11,70 @@ using PodNoms.Api.Persistence; using PodNoms.Api.Services.Realtime; using PodNoms.Api.Services.Storage; -namespace PodNoms.Api.Services.Processor -{ - internal class AudioUploadProcessService : ProcessService, IAudioUploadProcessService - { +namespace PodNoms.Api.Services.Processor { + internal class AudioUploadProcessService : ProcessService, IAudioUploadProcessService { private readonly IUnitOfWork _unitOfWork; private readonly IEntryRepository _repository; private readonly IFileUploader _fileUploader; private readonly AudioFileStorageSettings _audioStorageSettings; - public AudioUploadProcessService(IEntryRepository repository, IUnitOfWork unitOfWork, - IFileUploader fileUploader, IOptions audioStorageSettings, - ILoggerFactory logger, IMapper mapper, IRealTimeUpdater realtimeUpdater) - : base(logger, mapper, realtimeUpdater) - { + public AudioUploadProcessService (IEntryRepository repository, IUnitOfWork unitOfWork, + IFileUploader fileUploader, IOptions audioStorageSettings, + ILoggerFactory logger, IMapper mapper, IRealTimeUpdater realtimeUpdater) : base (logger, mapper, realtimeUpdater) { this._repository = repository; this._unitOfWork = unitOfWork; this._fileUploader = fileUploader; this._audioStorageSettings = audioStorageSettings.Value; } - public async Task UploadAudio(int entryId, string localFile) - { - var entry = await _repository.GetAsync(entryId); - if (entry == null) - { - _logger.LogError($"Unable to find entry with id: {entryId}"); + public async Task UploadAudio (int entryId, string localFile) { + var entry = await _repository.GetAsync (entryId); + if (entry == null) { + _logger.LogError ($"Unable to find entry with id: {entryId}"); return false; } - entry.ProcessingStatus = ProcessingStatus.Uploading;entry.ProcessingStatus = ProcessingStatus.Uploading; - await _unitOfWork.CompleteAsync(); - try - { + entry.ProcessingStatus = ProcessingStatus.Uploading; + entry.ProcessingStatus = ProcessingStatus.Uploading; + await _unitOfWork.CompleteAsync (); + try { // bit messy but can't figure how to pass youtube-dl job result to this job // so using AudioUrl as a proxy - if (string.IsNullOrEmpty(localFile)) + if (string.IsNullOrEmpty (localFile)) localFile = entry.AudioUrl; - if (File.Exists(localFile)) - { - var fileName = new FileInfo(localFile).Name; - await _fileUploader.UploadFile(localFile, _audioStorageSettings.ContainerName, fileName, - async (p, t) => - { - if (p % 1 == 0) - { - await _sendProgressUpdate( - entry.Podcast.User.GetUserId(), - entry.Uid, - new ProcessProgressEvent - { - Percentage = p, - CurrentSpeed = string.Empty, - TotalSize = t.ToString() - }); - } - }); + if (File.Exists (localFile)) { + var fileName = new FileInfo (localFile).Name; + await _fileUploader.UploadFile (localFile, _audioStorageSettings.ContainerName, fileName, + async (p, t) => { + if (p % 1 == 0) { + await _sendProgressUpdate ( + entry.Podcast.User.GetUserId (), + entry.Uid, + new ProcessProgressEvent { + Percentage = p, + CurrentSpeed = string.Empty, + TotalSize = t.ToString () + }); + } + }); entry.Processed = true; entry.ProcessingStatus = ProcessingStatus.Processed; entry.AudioUrl = $"{_audioStorageSettings.ContainerName}/{fileName}"; - await _unitOfWork.CompleteAsync(); - await _sendProcessCompleteMessage(entry); + await _unitOfWork.CompleteAsync (); + await _sendProcessCompleteMessage (entry); return true; + } else { + _logger.LogError ($"Error uploading audio file: {entry.AudioUrl} does not exist"); + entry.ProcessingStatus = ProcessingStatus.Failed; + entry.ProcessingPayload = $"Unable to find {entry.AudioUrl}"; + await _unitOfWork.CompleteAsync (); + await _sendProcessCompleteMessage (entry); } - else - { - _logger.LogError($"Error uploading audio file: {entry.AudioUrl} does not exist"); - } - } - catch (Exception ex) - { - _logger.LogError($"Error uploading audio file: {ex.Message}"); + } catch (Exception ex) { + _logger.LogError ($"Error uploading audio file: {ex.Message}"); + entry.ProcessingStatus = ProcessingStatus.Failed; + entry.ProcessingPayload = ex.Message; + await _unitOfWork.CompleteAsync (); + await _sendProcessCompleteMessage (entry); } return false; } diff --git a/server/Services/Processor/UrlProcessService.cs b/server/Services/Processor/UrlProcessService.cs index e5a6d2c..1713e6e 100644 --- a/server/Services/Processor/UrlProcessService.cs +++ b/server/Services/Processor/UrlProcessService.cs @@ -1,4 +1,8 @@ using System; +using System.ComponentModel; +using System.Dynamic; +using System.IO; +using System.Threading.Tasks; using AutoMapper; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -12,10 +16,6 @@ using PodNoms.Api.Services.Downloader; using PodNoms.Api.Services.Realtime; using PodNoms.Api.Services.Storage; using PusherServer; -using System.ComponentModel; -using System.Dynamic; -using System.IO; -using System.Threading.Tasks; namespace PodNoms.Api.Services.Processor { internal class UrlProcessService : ProcessService, IUrlProcessService { @@ -80,23 +80,30 @@ namespace PodNoms.Api.Services.Processor { var entry = await _repository.GetAsync (entryId); if (entry == null) return false; + try { + var downloader = new AudioDownloader (entry.SourceUrl, _applicationsSettings.Downloader); + var outputFile = + Path.Combine (System.IO.Path.GetTempPath (), $"{System.Guid.NewGuid().ToString()}.mp3"); - var downloader = new AudioDownloader (entry.SourceUrl, _applicationsSettings.Downloader); - var outputFile = - Path.Combine (System.IO.Path.GetTempPath (), $"{System.Guid.NewGuid().ToString()}.mp3"); + downloader.DownloadProgress += async (s, e) => await __downloader_progress (entry.Podcast.User.GetUserId (), entry.Uid, e); - downloader.DownloadProgress += async (s, e) => await __downloader_progress (entry.Podcast.User.GetUserId (), entry.Uid, e); + downloader.PostProcessing += (s, e) => { + Console.WriteLine (e); + }; + var sourceFile = downloader.DownloadAudio (entry.Uid); + if (!string.IsNullOrEmpty (sourceFile)) { + entry.ProcessingStatus = ProcessingStatus.Uploading; + entry.AudioUrl = sourceFile; - downloader.PostProcessing += (s, e) => { - Console.WriteLine (e); - }; - var sourceFile = downloader.DownloadAudio (entry.Uid); - if (!string.IsNullOrEmpty (sourceFile)) { - entry.ProcessingStatus = ProcessingStatus.Uploading; - entry.AudioUrl = sourceFile; - - await _sendProcessCompleteMessage (entry); + await _sendProcessCompleteMessage (entry); + await _unitOfWork.CompleteAsync (); + } + } catch (Exception ex) { + _logger.LogError ($"Entry: {entryId}\n{ex.Message}"); + entry.ProcessingStatus = ProcessingStatus.Failed; + entry.ProcessingPayload = ex.Message; await _unitOfWork.CompleteAsync (); + await _sendProcessCompleteMessage (entry); } return false; }