Audio cleanup job created

This commit is contained in:
Fergal Moran
2018-03-11 15:59:26 +00:00
parent ce64ad1edb
commit f56f5466b9
15 changed files with 121 additions and 75 deletions

View File

@@ -48,6 +48,7 @@ import { ResetComponent } from './components/reset/reset.component';
import { ProfileComponent } from './components/profile/profile.component'; import { ProfileComponent } from './components/profile/profile.component';
import { AboutComponent } from './components/about/about.component'; import { AboutComponent } from './components/about/about.component';
import { FooterComponent } from './components/footer/footer.component'; import { FooterComponent } from './components/footer/footer.component';
import { JobsService } from 'app/services/jobs.service';
export function authHttpServiceFactory(http: Http, options: RequestOptions) { export function authHttpServiceFactory(http: Http, options: RequestOptions) {
return new AuthHttp( return new AuthHttp(
@@ -122,6 +123,7 @@ export function authHttpServiceFactory(http: Http, options: RequestOptions) {
PodcastService, PodcastService,
ImageService, ImageService,
DebugService, DebugService,
JobsService,
GlobalsService GlobalsService
], ],
bootstrap: [AppComponent] bootstrap: [AppComponent]

View File

@@ -20,6 +20,17 @@
</div> </div>
</div> </div>
</div> </div>
<div class="row">
<div class="block">
<div class="block-header">
Jobs
</div>
<div class="block-content">
<button class="btn btn-primary"
(click)="processOrphans()">Process Orphans</button>
</div>
</div>
</div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div class="block"> <div class="block">
@@ -30,11 +41,11 @@
</pre> </pre>
</div> </div>
<div class="row"> <div class="row">
API Host: {{apiHost}} <br /> API Host: {{apiHost}} <br />
SignalR Host: {{signalrHost}} <br /> SignalR Host: {{signalrHost}} <br />
Ping: {{pingPong}} </div> Ping: {{pingPong}} </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -3,6 +3,7 @@ import { SignalRService } from 'app/services/signalr.service';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { DebugService } from 'app/services/debug.service'; import { DebugService } from 'app/services/debug.service';
import { environment } from 'environments/environment'; import { environment } from 'environments/environment';
import { JobsService } from 'app/services/jobs.service';
@Component({ @Component({
selector: 'app-debug', selector: 'app-debug',
@@ -18,7 +19,8 @@ export class DebugComponent implements OnInit {
signalrHost = environment.SIGNALR_HOST; signalrHost = environment.SIGNALR_HOST;
pingPong = ''; pingPong = '';
constructor(private _debugService: DebugService, private _signalrService: SignalRService) {} constructor(private _debugService: DebugService, private _jobsService: JobsService,
private _signalrService: SignalRService) {}
ngOnInit() { ngOnInit() {
this._signalrService this._signalrService
.init(`${environment.SIGNALR_HOST}hubs/debug`) .init(`${environment.SIGNALR_HOST}hubs/debug`)
@@ -41,4 +43,9 @@ export class DebugComponent implements OnInit {
doSomething() { doSomething() {
alert('doSomething was did'); alert('doSomething was did');
} }
processOrphans(){
this._jobsService.processOrphans()
.subscribe(e => console.log('debug.component.ts', 'processOrphans', e));
}
} }

View File

@@ -4,6 +4,9 @@
<a [routerLink]="['add']"> <a [routerLink]="['add']">
<i class="fa fa-plus"></i> Add Podcast <i class="fa fa-plus"></i> Add Podcast
</a> </a>
<!-- <a [routerLink]="['debug']">
<i class="fa fa-plus"></i> Debug
</a> -->
</div> </div>
<div class="content-header-section" *ngIf="user$ | async; let user; else loading"> <div class="content-header-section" *ngIf="user$ | async; let user; else loading">
<img [src]="user.profileImage" <img [src]="user.profileImage"
@@ -25,4 +28,4 @@
</div> </div>
<ng-template #loading><i class="fa fa-sun-o fa-spin text-white"></i></ng-template> <ng-template #loading><i class="fa fa-sun-o fa-spin text-white"></i></ng-template>
</div> </div>
</header> </header>

View File

@@ -17,6 +17,7 @@ import { PodcastService } from 'app/services/podcast.service';
export class PodcastAddUrlFormComponent implements AfterViewInit { export class PodcastAddUrlFormComponent implements AfterViewInit {
@Input() podcast: PodcastModel; @Input() podcast: PodcastModel;
@Output() onUrlAddComplete: EventEmitter<any> = new EventEmitter(); @Output() onUrlAddComplete: EventEmitter<any> = new EventEmitter();
@Output() onUploadDeferred: EventEmitter<any> = new EventEmitter();
newEntrySourceUrl: string; newEntrySourceUrl: string;
errorText: string; errorText: string;
isPosting: boolean = false; isPosting: boolean = false;
@@ -36,20 +37,24 @@ export class PodcastAddUrlFormComponent implements AfterViewInit {
this.errorText = ''; this.errorText = '';
if (this.isValidURL(urlToCheck)) { if (this.isValidURL(urlToCheck)) {
this.isPosting = true; this.isPosting = true;
const entry = new PodcastEntryModel( const entry = new PodcastEntryModel(this.podcast.id, urlToCheck);
this.podcast.id, this._service.addEntry(entry).subscribe(
urlToCheck e => {
); debugger;
this._service.addEntry(entry)
.subscribe(e => {
if (e) { if (e) {
this.onUrlAddComplete.emit(e); if (e.ProcessingStatus == 6) {
this.onUploadDeferred.emit(e);
} else {
this.onUrlAddComplete.emit(e);
}
} }
}, (err) => { },
err => {
this.isPosting = false; this.isPosting = false;
this.errorText = 'This does not look like a valid URL'; this.errorText = 'This does not look like a valid URL';
this.newEntrySourceUrl = urlToCheck; this.newEntrySourceUrl = urlToCheck;
}); }
);
} else { } else {
this.isPosting = false; this.isPosting = false;
this.errorText = 'This does not look like a valid URL'; this.errorText = 'This does not look like a valid URL';

View File

@@ -45,7 +45,8 @@
</app-podcast-upload-form> </app-podcast-upload-form>
<app-podcast-add-url-form *ngIf="urlMode" <app-podcast-add-url-form *ngIf="urlMode"
[podcast]="podcast" [podcast]="podcast"
(onUrlAddComplete)="onUrlAddComplete($event)"> (onUrlAddComplete)="onUrlAddComplete($event)"
(onUploadDeferred)="onEntryUploadDeferred($event)">
</app-podcast-add-url-form> </app-podcast-add-url-form>
<table class="js-table-checkable table table-hover js-table-checkable-enabled"> <table class="js-table-checkable table table-hover js-table-checkable-enabled">
<tbody> <tbody>

View File

@@ -7,7 +7,7 @@ import { AppComponent } from 'app/app.component';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { ApplicationState } from 'app/store'; import { ApplicationState } from 'app/store';
import { HostListener } from '@angular/core'; import { HostListener } from '@angular/core';
import {Location} from '@angular/common'; import { Location } from '@angular/common';
import { UpdateAction, AddAction } from 'app/actions/entries.actions'; import { UpdateAction, AddAction } from 'app/actions/entries.actions';
import * as fromPodcast from 'app/reducers'; import * as fromPodcast from 'app/reducers';
@@ -34,7 +34,11 @@ export class PodcastComponent {
} }
} }
constructor(private _store: Store<ApplicationState>, route: ActivatedRoute, private _location: Location) { constructor(
private _store: Store<ApplicationState>,
route: ActivatedRoute,
private _location: Location
) {
this.selectedPodcast$ = _store.select(fromPodcast.getSelectedPodcast); this.selectedPodcast$ = _store.select(fromPodcast.getSelectedPodcast);
this.entries$ = _store.select(fromPodcast.getEntries); this.entries$ = _store.select(fromPodcast.getEntries);
@@ -48,8 +52,12 @@ export class PodcastComponent {
this.selectedPodcast$.subscribe(r => { this.selectedPodcast$.subscribe(r => {
if (r) { if (r) {
slug = r.slug; slug = r.slug;
_store.dispatch(new fromEntriesActions.LoadAction(slug)); _store.dispatch(
_store.dispatch(new fromPodcastActions.SelectAction(slug)); new fromEntriesActions.LoadAction(slug)
);
_store.dispatch(
new fromPodcastActions.SelectAction(slug)
);
this._location.go('/podcasts/' + slug); this._location.go('/podcasts/' + slug);
} }
@@ -79,6 +87,9 @@ export class PodcastComponent {
// so do a funky success/update dance // so do a funky success/update dance
this._store.dispatch(new fromEntriesActions.AddSuccessAction(entry)); this._store.dispatch(new fromEntriesActions.AddSuccessAction(entry));
this._store.dispatch(new fromEntriesActions.UpdateAction(entry)); this._store.dispatch(new fromEntriesActions.UpdateAction(entry));
}
onEntryUploadDeferred($event) {
} }
onUrlAddComplete(entry: PodcastEntryModel) { onUrlAddComplete(entry: PodcastEntryModel) {
this.urlMode = false; this.urlMode = false;

View File

@@ -11,6 +11,7 @@ using Microsoft.Extensions.Options;
using PodNoms.Api.Models; using PodNoms.Api.Models;
using PodNoms.Api.Models.ViewModels; using PodNoms.Api.Models.ViewModels;
using PodNoms.Api.Persistence; using PodNoms.Api.Persistence;
using PodNoms.Api.Services;
using PodNoms.Api.Services.Processor; using PodNoms.Api.Services.Processor;
using PodNoms.Api.Services.Storage; using PodNoms.Api.Services.Storage;
@@ -67,18 +68,23 @@ namespace PodNoms.Api.Controllers {
// first check url is valid // first check url is valid
var entry = _mapper.Map<PodcastEntryViewModel, PodcastEntry>(item); var entry = _mapper.Map<PodcastEntryViewModel, PodcastEntry>(item);
var podcast = await _podcastRepository.GetAsync(item.PodcastId); var podcast = await _podcastRepository.GetAsync(item.PodcastId);
if (podcast != null && await _processor.GetInformation(entry)) { if (podcast != null) {
if (entry.ProcessingStatus == ProcessingStatus.Processing) { var status = await _processor.GetInformation(entry);
if (string.IsNullOrEmpty(entry.ImageUrl)) { if (status == AudioType.Valid) {
entry.ImageUrl = $"{_storageSettings.CdnUrl}static/images/default-entry.png"; if (entry.ProcessingStatus == ProcessingStatus.Processing) {
if (string.IsNullOrEmpty(entry.ImageUrl)) {
entry.ImageUrl = $"{_storageSettings.CdnUrl}static/images/default-entry.png";
}
entry.Podcast = podcast;
entry.Processed = false;
await _repository.AddOrUpdateAsync(entry);
await _unitOfWork.CompleteAsync();
_processEntry(entry);
var result = _mapper.Map<PodcastEntry, PodcastEntryViewModel>(entry);
return result;
} }
entry.Podcast = podcast; } else if (status == AudioType.Playlist) {
entry.Processed = false; return Accepted(entry);
await _repository.AddOrUpdateAsync(entry);
await _unitOfWork.CompleteAsync();
_processEntry(entry);
var result = _mapper.Map<PodcastEntry, PodcastEntryViewModel>(entry);
return result;
} }
} }
return BadRequest(); return BadRequest();

View File

@@ -7,7 +7,8 @@ namespace PodNoms.Api.Models {
Processing, //2 Processing, //2
Uploading, //3 Uploading, //3
Processed, //4 Processed, //4
Failed //5 Failed, //5
Deferred //6
} }
public class PodcastEntry : BaseModel { public class PodcastEntry : BaseModel {

View File

@@ -13,6 +13,7 @@ using static NYoutubeDL.Helpers.Enums;
namespace PodNoms.Api.Services.Downloader { namespace PodNoms.Api.Services.Downloader {
public class AudioDownloader { public class AudioDownloader {
private readonly string _url; private readonly string _url;
private readonly string _downloader; private readonly string _downloader;
public VideoDownloadInfo Properties { get; private set; } public VideoDownloadInfo Properties { get; private set; }
@@ -49,17 +50,22 @@ namespace PodNoms.Api.Services.Downloader {
return $"{{\"Error\": \"{ex.Message}\"}}"; return $"{{\"Error\": \"{ex.Message}\"}}";
} }
} }
public async Task<bool> GetInfo() { public async Task<AudioType> GetInfo() {
var ret = false; var ret = AudioType.Invalid;
await Task.Run(() => { await Task.Run(() => {
var youtubeDl = new YoutubeDL(); var youtubeDl = new YoutubeDL();
youtubeDl.VideoUrl = this._url; youtubeDl.VideoUrl = this._url;
DownloadInfo info = youtubeDl.GetDownloadInfo(); DownloadInfo info = youtubeDl.GetDownloadInfo();
ret = (
info != null && if (info != null &&
info is VideoDownloadInfo && //make sure it's not a playlist (info.Errors.Count == 0 || info.VideoSize != null)) {
(info.Errors.Count == 0 || info.VideoSize != null)); if (info is PlaylistDownloadInfo) {
if (ret) this.Properties = (VideoDownloadInfo)info; ret = AudioType.Playlist;
} else if (info is VideoDownloadInfo) {
ret = AudioType.Valid;
this.Properties = (VideoDownloadInfo)info;
}
}
}); });
return ret; return ret;
} }

View File

@@ -7,18 +7,15 @@ using Microsoft.WindowsAzure.Storage.Blob;
using PodNoms.Api.Models; using PodNoms.Api.Models;
using PodNoms.Api.Persistence; using PodNoms.Api.Persistence;
namespace PodNoms.Api.Services.Jobs namespace PodNoms.Api.Services.Jobs {
{ public class ClearOrphanAudioJob : IJob {
public class ClearOrphanAudioJob : IJob
{
public readonly IEntryRepository _entryRepository; public readonly IEntryRepository _entryRepository;
public readonly StorageSettings _storageSettings; public readonly StorageSettings _storageSettings;
public readonly AudioFileStorageSettings _audioStorageSettings; public readonly AudioFileStorageSettings _audioStorageSettings;
private readonly ILogger<ClearOrphanAudioJob> _logger; private readonly ILogger<ClearOrphanAudioJob> _logger;
public ClearOrphanAudioJob(IEntryRepository entryRepository, IOptions<StorageSettings> storageSettings, public ClearOrphanAudioJob(IEntryRepository entryRepository, IOptions<StorageSettings> storageSettings,
IOptions<AudioFileStorageSettings> audioStorageSettings, ILoggerFactory logger) IOptions<AudioFileStorageSettings> audioStorageSettings, ILoggerFactory logger) {
{
this._storageSettings = storageSettings.Value; this._storageSettings = storageSettings.Value;
this._audioStorageSettings = audioStorageSettings.Value; this._audioStorageSettings = audioStorageSettings.Value;
this._entryRepository = entryRepository; this._entryRepository = entryRepository;
@@ -26,38 +23,28 @@ namespace PodNoms.Api.Services.Jobs
this._logger = logger.CreateLogger<ClearOrphanAudioJob>(); this._logger = logger.CreateLogger<ClearOrphanAudioJob>();
} }
public async Task Execute() public async Task Execute() {
{ try {
try
{
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(_storageSettings.ConnectionString); CloudStorageAccount storageAccount = CloudStorageAccount.Parse(_storageSettings.ConnectionString);
CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient(); CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();
CloudBlobContainer container = blobClient.GetContainerReference(_audioStorageSettings.ContainerName); CloudBlobContainer container = blobClient.GetContainerReference(_audioStorageSettings.ContainerName);
var blobs = await container.ListBlobsSegmentedAsync(null); var blobs = await container.ListBlobsSegmentedAsync(null);
foreach (CloudBlockBlob blob in blobs.Results) foreach (CloudBlockBlob blob in blobs.Results) {
{ try {
try
{
Console.WriteLine(blob.StorageUri); Console.WriteLine(blob.StorageUri);
var guid = blob.Name.Split('.')[0]; var guid = blob.Name.Split('.')[0];
if (!string.IsNullOrEmpty(guid)) if (!string.IsNullOrEmpty(guid)) {
{
var entry = await _entryRepository.GetByUidAsync(guid); var entry = await _entryRepository.GetByUidAsync(guid);
if (entry == null) if (entry == null) {
{
await blob.DeleteIfExistsAsync(); await blob.DeleteIfExistsAsync();
} }
} }
} } catch (Exception e) {
catch (Exception e)
{
_logger.LogWarning($"Error processing blob {blob.Uri}\n{e.Message}"); _logger.LogWarning($"Error processing blob {blob.Uri}\n{e.Message}");
} }
} }
} } catch (Exception ex) {
catch (Exception ex)
{
_logger.LogError($"Error clearing orphans\n{ex.Message}"); _logger.LogError($"Error clearing orphans\n{ex.Message}");
} }
} }

View File

@@ -3,8 +3,9 @@ using PodNoms.Api.Models;
namespace PodNoms.Api.Services.Processor { namespace PodNoms.Api.Services.Processor {
public interface IUrlProcessService { public interface IUrlProcessService {
Task<bool> GetInformation (int entryId);
Task<bool> GetInformation (PodcastEntry entry); Task<AudioType> GetInformation(int entryId);
Task<bool> DownloadAudio (int entryId); Task<AudioType> GetInformation(PodcastEntry entry);
Task<bool> DownloadAudio(int entryId);
} }
} }

View File

@@ -37,23 +37,23 @@ namespace PodNoms.Api.Services.Processor {
uid, uid,
e); e);
} }
public async Task<bool> GetInformation(int entryId) { public async Task<AudioType> GetInformation(int entryId) {
var entry = await _repository.GetAsync(entryId); var entry = await _repository.GetAsync(entryId);
if (entry == null || string.IsNullOrEmpty(entry.SourceUrl)) { if (entry == null || string.IsNullOrEmpty(entry.SourceUrl)) {
_logger.LogError("Unable to process item"); _logger.LogError("Unable to process item");
return false; return AudioType.Invalid;
} }
if (entry.SourceUrl.EndsWith(".mp3") || entry.SourceUrl.EndsWith(".wav") || entry.SourceUrl.EndsWith(".aif")) { if (entry.SourceUrl.EndsWith(".mp3") || entry.SourceUrl.EndsWith(".wav") || entry.SourceUrl.EndsWith(".aif")) {
return true; return AudioType.Valid;
} }
return await GetInformation(entry); return await GetInformation(entry);
} }
public async Task<bool> GetInformation(PodcastEntry entry) { public async Task<AudioType> GetInformation(PodcastEntry entry) {
var downloader = new AudioDownloader(entry.SourceUrl, _applicationsSettings.Downloader); var downloader = new AudioDownloader(entry.SourceUrl, _applicationsSettings.Downloader);
await downloader.GetInfo(); var ret = await downloader.GetInfo();
if (downloader.Properties != null) { if (ret == AudioType.Valid) {
entry.Title = downloader.Properties?.Title; entry.Title = downloader.Properties?.Title;
entry.Description = downloader.Properties?.Description; entry.Description = downloader.Properties?.Description;
entry.ImageUrl = downloader.Properties?.Thumbnail; entry.ImageUrl = downloader.Properties?.Thumbnail;
@@ -68,11 +68,8 @@ namespace PodNoms.Api.Services.Processor {
_logger.LogDebug("***DOWNLOAD INFO RETRIEVED****\n"); _logger.LogDebug("***DOWNLOAD INFO RETRIEVED****\n");
_logger.LogDebug($"Title: {entry.Title}\nDescription: {entry.Description}\nAuthor: {entry.Author}\n"); _logger.LogDebug($"Title: {entry.Title}\nDescription: {entry.Description}\nAuthor: {entry.Author}\n");
// var pusherResult = await _sendProcessCompleteMessage(entry);
return true;
} }
return false; return ret;
} }
public async Task<bool> DownloadAudio(int entryId) { public async Task<bool> DownloadAudio(int entryId) {
var entry = await _repository.GetAsync(entryId); var entry = await _repository.GetAsync(entryId);

View File

@@ -0,0 +1,7 @@
namespace PodNoms.Api.Services {
public enum AudioType {
Invalid,
Valid,
Playlist
}
}

View File

@@ -3,3 +3,4 @@ unset DOCKER_HOST
unset DOCKER_TLS_VERIFY unset DOCKER_TLS_VERIFY
docker build --rm -f Dockerfile -t fergalmoran/podnoms.api . && docker push fergalmoran/podnoms.api docker build --rm -f Dockerfile -t fergalmoran/podnoms.api . && docker push fergalmoran/podnoms.api
docker push fergalmoran/podnoms.api