mirror of
https://github.com/fergalmoran/podnoms.git
synced 2025-12-22 09:18:08 +00:00
Merge branch 'release/v0.16'
This commit is contained in:
10
client/package-lock.json
generated
10
client/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pod-noms.web",
|
||||
"version": "0.13.0",
|
||||
"version": "0.16.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -522,14 +522,6 @@
|
||||
"moment": "2.21.0"
|
||||
}
|
||||
},
|
||||
"angulartics2": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/angulartics2/-/angulartics2-5.1.2.tgz",
|
||||
"integrity": "sha512-ZTmSXH6t7Set8TT4oY6HcCLr7bIbK56oJnUVlDJNXSnCTS2DeZW0lP/4pcGpdELCTAF5xfosjar+DM+7kqWiPQ==",
|
||||
"requires": {
|
||||
"tslib": "1.9.0"
|
||||
}
|
||||
},
|
||||
"ansi-html": {
|
||||
"version": "0.0.7",
|
||||
"resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pod-noms.web",
|
||||
"version": "0.15.0",
|
||||
"version": "0.16.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
|
||||
@@ -2,7 +2,6 @@ import { GlobalsService } from './services/globals.service';
|
||||
import { Component, HostBinding } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AuthService } from 'app/services/auth.service';
|
||||
import { Angulartics2GoogleAnalytics } from 'angulartics2/ga';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
|
||||
@@ -48,6 +48,7 @@ import { ResetComponent } from './components/reset/reset.component';
|
||||
import { ProfileComponent } from './components/profile/profile.component';
|
||||
import { AboutComponent } from './components/about/about.component';
|
||||
import { FooterComponent } from './components/footer/footer.component';
|
||||
import { JobsService } from 'app/services/jobs.service';
|
||||
|
||||
export function authHttpServiceFactory(http: Http, options: RequestOptions) {
|
||||
return new AuthHttp(
|
||||
@@ -122,6 +123,7 @@ export function authHttpServiceFactory(http: Http, options: RequestOptions) {
|
||||
PodcastService,
|
||||
ImageService,
|
||||
DebugService,
|
||||
JobsService,
|
||||
GlobalsService
|
||||
],
|
||||
bootstrap: [AppComponent]
|
||||
|
||||
@@ -20,6 +20,17 @@
|
||||
</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 class="col-md-6">
|
||||
<div class="block">
|
||||
@@ -30,11 +41,11 @@
|
||||
</pre>
|
||||
</div>
|
||||
<div class="row">
|
||||
API Host: {{apiHost}} <br />
|
||||
SignalR Host: {{signalrHost}} <br />
|
||||
API Host: {{apiHost}} <br />
|
||||
SignalR Host: {{signalrHost}} <br />
|
||||
Ping: {{pingPong}} </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { SignalRService } from 'app/services/signalr.service';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { DebugService } from 'app/services/debug.service';
|
||||
import { environment } from 'environments/environment';
|
||||
import { JobsService } from 'app/services/jobs.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-debug',
|
||||
@@ -18,7 +19,8 @@ export class DebugComponent implements OnInit {
|
||||
signalrHost = environment.SIGNALR_HOST;
|
||||
pingPong = '';
|
||||
|
||||
constructor(private _debugService: DebugService, private _signalrService: SignalRService) {}
|
||||
constructor(private _debugService: DebugService, private _jobsService: JobsService,
|
||||
private _signalrService: SignalRService) {}
|
||||
ngOnInit() {
|
||||
this._signalrService
|
||||
.init(`${environment.SIGNALR_HOST}hubs/debug`)
|
||||
@@ -41,4 +43,9 @@ export class DebugComponent implements OnInit {
|
||||
doSomething() {
|
||||
alert('doSomething was did');
|
||||
}
|
||||
|
||||
processOrphans(){
|
||||
this._jobsService.processOrphans()
|
||||
.subscribe(e => console.log('debug.component.ts', 'processOrphans', e));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
<a [routerLink]="['add']">
|
||||
<i class="fa fa-plus"></i> Add Podcast
|
||||
</a>
|
||||
<!-- <a [routerLink]="['debug']">
|
||||
<i class="fa fa-plus"></i> Debug
|
||||
</a> -->
|
||||
</div>
|
||||
<div class="content-header-section" *ngIf="user$ | async; let user; else loading">
|
||||
<img [src]="user.profileImage"
|
||||
@@ -25,4 +28,4 @@
|
||||
</div>
|
||||
<ng-template #loading><i class="fa fa-sun-o fa-spin text-white"></i></ng-template>
|
||||
</div>
|
||||
</header>
|
||||
</header>
|
||||
|
||||
@@ -17,6 +17,7 @@ import { PodcastService } from 'app/services/podcast.service';
|
||||
export class PodcastAddUrlFormComponent implements AfterViewInit {
|
||||
@Input() podcast: PodcastModel;
|
||||
@Output() onUrlAddComplete: EventEmitter<any> = new EventEmitter();
|
||||
@Output() onUploadDeferred: EventEmitter<any> = new EventEmitter();
|
||||
newEntrySourceUrl: string;
|
||||
errorText: string;
|
||||
isPosting: boolean = false;
|
||||
@@ -36,20 +37,23 @@ export class PodcastAddUrlFormComponent implements AfterViewInit {
|
||||
this.errorText = '';
|
||||
if (this.isValidURL(urlToCheck)) {
|
||||
this.isPosting = true;
|
||||
const entry = new PodcastEntryModel(
|
||||
this.podcast.id,
|
||||
urlToCheck
|
||||
);
|
||||
this._service.addEntry(entry)
|
||||
.subscribe(e => {
|
||||
const entry = new PodcastEntryModel(this.podcast.id, urlToCheck);
|
||||
this._service.addEntry(entry).subscribe(
|
||||
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.errorText = 'This does not look like a valid URL';
|
||||
this.newEntrySourceUrl = urlToCheck;
|
||||
});
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.isPosting = false;
|
||||
this.errorText = 'This does not look like a valid URL';
|
||||
|
||||
@@ -45,7 +45,8 @@
|
||||
</app-podcast-upload-form>
|
||||
<app-podcast-add-url-form *ngIf="urlMode"
|
||||
[podcast]="podcast"
|
||||
(onUrlAddComplete)="onUrlAddComplete($event)">
|
||||
(onUrlAddComplete)="onUrlAddComplete($event)"
|
||||
(onUploadDeferred)="onEntryUploadDeferred($event)">
|
||||
</app-podcast-add-url-form>
|
||||
<table class="js-table-checkable table table-hover js-table-checkable-enabled">
|
||||
<tbody>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { AppComponent } from 'app/app.component';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { ApplicationState } from 'app/store';
|
||||
import { HostListener } from '@angular/core';
|
||||
import {Location} from '@angular/common';
|
||||
import { Location } from '@angular/common';
|
||||
|
||||
import { UpdateAction, AddAction } from 'app/actions/entries.actions';
|
||||
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.entries$ = _store.select(fromPodcast.getEntries);
|
||||
@@ -48,8 +52,12 @@ export class PodcastComponent {
|
||||
this.selectedPodcast$.subscribe(r => {
|
||||
if (r) {
|
||||
slug = r.slug;
|
||||
_store.dispatch(new fromEntriesActions.LoadAction(slug));
|
||||
_store.dispatch(new fromPodcastActions.SelectAction(slug));
|
||||
_store.dispatch(
|
||||
new fromEntriesActions.LoadAction(slug)
|
||||
);
|
||||
_store.dispatch(
|
||||
new fromPodcastActions.SelectAction(slug)
|
||||
);
|
||||
|
||||
this._location.go('/podcasts/' + slug);
|
||||
}
|
||||
@@ -79,6 +87,9 @@ export class PodcastComponent {
|
||||
// so do a funky success/update dance
|
||||
this._store.dispatch(new fromEntriesActions.AddSuccessAction(entry));
|
||||
this._store.dispatch(new fromEntriesActions.UpdateAction(entry));
|
||||
}
|
||||
onEntryUploadDeferred($event) {
|
||||
|
||||
}
|
||||
onUrlAddComplete(entry: PodcastEntryModel) {
|
||||
this.urlMode = false;
|
||||
|
||||
0
client/src/app/models/podcasts.models.ts
Executable file → Normal file
0
client/src/app/models/podcasts.models.ts
Executable file → Normal file
0
client/src/app/models/profile.model.ts
Executable file → Normal file
0
client/src/app/models/profile.model.ts
Executable file → Normal file
0
client/src/app/services/auth.guard.ts
Executable file → Normal file
0
client/src/app/services/auth.guard.ts
Executable file → Normal file
0
client/src/app/services/auth.service.ts
Executable file → Normal file
0
client/src/app/services/auth.service.ts
Executable file → Normal file
14
client/src/app/services/jobs.service.ts
Normal file
14
client/src/app/services/jobs.service.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Response } from '@angular/http';
|
||||
import { AuthHttp } from 'angular2-jwt';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { environment } from 'environments/environment';
|
||||
|
||||
@Injectable()
|
||||
export class JobsService {
|
||||
constructor(private _http: AuthHttp) {}
|
||||
|
||||
processOrphans(): Observable<Response> {
|
||||
return this._http.get(environment.API_HOST + '/job/processorphans');
|
||||
}
|
||||
}
|
||||
0
client/src/app/services/profile.service.ts
Executable file → Normal file
0
client/src/app/services/profile.service.ts
Executable file → Normal file
0
client/src/app/services/pusher.service.ts
Executable file → Normal file
0
client/src/app/services/pusher.service.ts
Executable file → Normal file
0
client/src/assets/img/logo-icon.png
Executable file → Normal file
0
client/src/assets/img/logo-icon.png
Executable file → Normal file
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
0
client/src/assets/img/robothand.jpg
Executable file → Normal file
0
client/src/assets/img/robothand.jpg
Executable file → Normal file
|
Before Width: | Height: | Size: 193 KiB After Width: | Height: | Size: 193 KiB |
0
client/src/environments/environment.prod.ts
Executable file → Normal file
0
client/src/environments/environment.prod.ts
Executable file → Normal file
0
client/src/environments/environment.ts
Executable file → Normal file
0
client/src/environments/environment.ts
Executable file → Normal file
0
client/upgrade_cli
Executable file → Normal file
0
client/upgrade_cli
Executable file → Normal file
2
docker/docker-compose.yml
Executable file → Normal file
2
docker/docker-compose.yml
Executable file → Normal file
@@ -1,7 +1,7 @@
|
||||
version: '2'
|
||||
services:
|
||||
nginx:
|
||||
restart: "always"
|
||||
# restart: "always"
|
||||
build: ./nginx/
|
||||
ports:
|
||||
- "80:80"
|
||||
|
||||
13
docker/letsencrypt.sh
Executable file → Normal file
13
docker/letsencrypt.sh
Executable file → Normal file
@@ -13,6 +13,19 @@ if [[ $web == "Y" || $web == "y" ]]; then
|
||||
fi
|
||||
echo \n
|
||||
|
||||
read -n1 -p "Renew api? [y,n]" api
|
||||
if [[ $api == "Y" || $api == "y" ]]; then
|
||||
docker run -it --rm -p 443:443 -p 80:80 --name certbot \
|
||||
-v "/etc/letsencrypt:/etc/letsencrypt" \
|
||||
-v "/var/lib/letsencrypt:/var/lib/letsencrypt" \
|
||||
certbot/certbot certonly \
|
||||
--agree-tos \
|
||||
--renew-by-default \
|
||||
-d api.podnoms.com \
|
||||
-m fergal.moran@gmail.com
|
||||
fi
|
||||
echo \n
|
||||
|
||||
read -n1 -p "Renew realtime? [y,n]" rt
|
||||
if [[ $rt == "Y" || $rt == "y" ]]; then
|
||||
docker run -it --rm -p 443:443 -p 80:80 --name certbot \
|
||||
|
||||
@@ -42,6 +42,31 @@ server {
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen 443 ssl;
|
||||
|
||||
server_name api.podnoms.com;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/api.podnoms.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/api.podnoms.com/privkey.pem;
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';
|
||||
|
||||
if ($ssl_protocol = "") {
|
||||
rewrite ^((?!/rss/).) https://$server_name$request_uri? permanent;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://api:5000/;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen 443 ssl;
|
||||
@@ -79,7 +104,7 @@ server {
|
||||
ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';
|
||||
|
||||
location / {
|
||||
proxy_pass http://api:5000/hangfire;
|
||||
proxy_pass http://api:5000/;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
0
server/.gitignore
vendored
Executable file → Normal file
0
server/.gitignore
vendored
Executable file → Normal file
@@ -11,6 +11,7 @@ using Microsoft.Extensions.Options;
|
||||
using PodNoms.Api.Models;
|
||||
using PodNoms.Api.Models.ViewModels;
|
||||
using PodNoms.Api.Persistence;
|
||||
using PodNoms.Api.Services;
|
||||
using PodNoms.Api.Services.Processor;
|
||||
using PodNoms.Api.Services.Storage;
|
||||
|
||||
@@ -67,18 +68,24 @@ namespace PodNoms.Api.Controllers {
|
||||
// first check url is valid
|
||||
var entry = _mapper.Map<PodcastEntryViewModel, PodcastEntry>(item);
|
||||
var podcast = await _podcastRepository.GetAsync(item.PodcastId);
|
||||
if (podcast != null && await _processor.GetInformation(entry)) {
|
||||
if (entry.ProcessingStatus == ProcessingStatus.Processing) {
|
||||
if (string.IsNullOrEmpty(entry.ImageUrl)) {
|
||||
entry.ImageUrl = $"{_storageSettings.CdnUrl}static/images/default-entry.png";
|
||||
if (podcast != null) {
|
||||
var status = await _processor.GetInformation(entry);
|
||||
if (status == AudioType.Valid) {
|
||||
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;
|
||||
entry.Processed = false;
|
||||
await _repository.AddOrUpdateAsync(entry);
|
||||
await _unitOfWork.CompleteAsync();
|
||||
_processEntry(entry);
|
||||
var result = _mapper.Map<PodcastEntry, PodcastEntryViewModel>(entry);
|
||||
return result;
|
||||
} else if (status == AudioType.Playlist) {
|
||||
entry.ProcessingStatus = ProcessingStatus.Deferred;
|
||||
return Accepted(entry);
|
||||
}
|
||||
}
|
||||
return BadRequest();
|
||||
|
||||
0
server/Controllers/PodcastController.cs
Executable file → Normal file
0
server/Controllers/PodcastController.cs
Executable file → Normal file
0
server/Controllers/ProfileController.cs
Executable file → Normal file
0
server/Controllers/ProfileController.cs
Executable file → Normal file
0
server/Controllers/RssController.cs
Executable file → Normal file
0
server/Controllers/RssController.cs
Executable file → Normal file
0
server/Dockerfile
Executable file → Normal file
0
server/Dockerfile
Executable file → Normal file
0
server/Models/AppSettings.cs
Executable file → Normal file
0
server/Models/AppSettings.cs
Executable file → Normal file
0
server/Models/BaseModel.cs
Executable file → Normal file
0
server/Models/BaseModel.cs
Executable file → Normal file
0
server/Models/Podcast.cs
Executable file → Normal file
0
server/Models/Podcast.cs
Executable file → Normal file
3
server/Models/PodcastEntry.cs
Executable file → Normal file
3
server/Models/PodcastEntry.cs
Executable file → Normal file
@@ -7,7 +7,8 @@ namespace PodNoms.Api.Models {
|
||||
Processing, //2
|
||||
Uploading, //3
|
||||
Processed, //4
|
||||
Failed //5
|
||||
Failed, //5
|
||||
Deferred //6
|
||||
}
|
||||
public class PodcastEntry : BaseModel {
|
||||
|
||||
|
||||
0
server/Models/User.cs
Executable file → Normal file
0
server/Models/User.cs
Executable file → Normal file
0
server/Models/ViewModels/ProfileViewModel.cs
Executable file → Normal file
0
server/Models/ViewModels/ProfileViewModel.cs
Executable file → Normal file
0
server/Models/ViewModels/RssViewModels/PodcastRssViewModel.cs
Executable file → Normal file
0
server/Models/ViewModels/RssViewModels/PodcastRssViewModel.cs
Executable file → Normal file
0
server/Persistence/IPodcastRepository.cs
Executable file → Normal file
0
server/Persistence/IPodcastRepository.cs
Executable file → Normal file
0
server/Persistence/IUserRepository.cs
Executable file → Normal file
0
server/Persistence/IUserRepository.cs
Executable file → Normal file
0
server/Persistence/PodcastRepository.cs
Executable file → Normal file
0
server/Persistence/PodcastRepository.cs
Executable file → Normal file
0
server/Persistence/PodnomsContext.cs
Executable file → Normal file
0
server/Persistence/PodnomsContext.cs
Executable file → Normal file
0
server/Persistence/UserRepository.cs
Executable file → Normal file
0
server/Persistence/UserRepository.cs
Executable file → Normal file
0
server/Resources/podcast.xml
Executable file → Normal file
0
server/Resources/podcast.xml
Executable file → Normal file
0
server/Services/Auth/AuthenticationMiddleware.cs
Executable file → Normal file
0
server/Services/Auth/AuthenticationMiddleware.cs
Executable file → Normal file
@@ -13,6 +13,7 @@ using static NYoutubeDL.Helpers.Enums;
|
||||
|
||||
namespace PodNoms.Api.Services.Downloader {
|
||||
public class AudioDownloader {
|
||||
|
||||
private readonly string _url;
|
||||
private readonly string _downloader;
|
||||
public VideoDownloadInfo Properties { get; private set; }
|
||||
@@ -49,17 +50,22 @@ namespace PodNoms.Api.Services.Downloader {
|
||||
return $"{{\"Error\": \"{ex.Message}\"}}";
|
||||
}
|
||||
}
|
||||
public async Task<bool> GetInfo() {
|
||||
var ret = false;
|
||||
public async Task<AudioType> GetInfo() {
|
||||
var ret = AudioType.Invalid;
|
||||
await Task.Run(() => {
|
||||
var youtubeDl = new YoutubeDL();
|
||||
youtubeDl.VideoUrl = this._url;
|
||||
var info = youtubeDl.GetDownloadInfo() as VideoDownloadInfo;
|
||||
ret = (
|
||||
info != null &&
|
||||
info is VideoDownloadInfo && //make sure it's not a playlist
|
||||
(info.Errors.Count == 0 || info.VideoSize != null));
|
||||
if (ret) this.Properties = info;
|
||||
DownloadInfo info = youtubeDl.GetDownloadInfo();
|
||||
|
||||
if (info != null &&
|
||||
(info.Errors.Count == 0 || info.VideoSize != null)) {
|
||||
if (info is PlaylistDownloadInfo) {
|
||||
ret = AudioType.Playlist;
|
||||
} else if (info is VideoDownloadInfo) {
|
||||
ret = AudioType.Valid;
|
||||
this.Properties = (VideoDownloadInfo)info;
|
||||
}
|
||||
}
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -7,18 +7,15 @@ using Microsoft.WindowsAzure.Storage.Blob;
|
||||
using PodNoms.Api.Models;
|
||||
using PodNoms.Api.Persistence;
|
||||
|
||||
namespace PodNoms.Api.Services.Jobs
|
||||
{
|
||||
public class ClearOrphanAudioJob : IJob
|
||||
{
|
||||
namespace PodNoms.Api.Services.Jobs {
|
||||
public class ClearOrphanAudioJob : IJob {
|
||||
public readonly IEntryRepository _entryRepository;
|
||||
public readonly StorageSettings _storageSettings;
|
||||
public readonly AudioFileStorageSettings _audioStorageSettings;
|
||||
private readonly ILogger<ClearOrphanAudioJob> _logger;
|
||||
|
||||
public ClearOrphanAudioJob(IEntryRepository entryRepository, IOptions<StorageSettings> storageSettings,
|
||||
IOptions<AudioFileStorageSettings> audioStorageSettings, ILoggerFactory logger)
|
||||
{
|
||||
IOptions<AudioFileStorageSettings> audioStorageSettings, ILoggerFactory logger) {
|
||||
this._storageSettings = storageSettings.Value;
|
||||
this._audioStorageSettings = audioStorageSettings.Value;
|
||||
this._entryRepository = entryRepository;
|
||||
@@ -26,38 +23,28 @@ namespace PodNoms.Api.Services.Jobs
|
||||
this._logger = logger.CreateLogger<ClearOrphanAudioJob>();
|
||||
}
|
||||
|
||||
public async Task Execute()
|
||||
{
|
||||
try
|
||||
{
|
||||
public async Task Execute() {
|
||||
try {
|
||||
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(_storageSettings.ConnectionString);
|
||||
CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();
|
||||
CloudBlobContainer container = blobClient.GetContainerReference(_audioStorageSettings.ContainerName);
|
||||
|
||||
var blobs = await container.ListBlobsSegmentedAsync(null);
|
||||
foreach (CloudBlockBlob blob in blobs.Results)
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (CloudBlockBlob blob in blobs.Results) {
|
||||
try {
|
||||
Console.WriteLine(blob.StorageUri);
|
||||
var guid = blob.Name.Split('.')[0];
|
||||
if (!string.IsNullOrEmpty(guid))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(guid)) {
|
||||
var entry = await _entryRepository.GetByUidAsync(guid);
|
||||
if (entry == null)
|
||||
{
|
||||
if (entry == null) {
|
||||
await blob.DeleteIfExistsAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
} catch (Exception e) {
|
||||
_logger.LogWarning($"Error processing blob {blob.Uri}\n{e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
} catch (Exception ex) {
|
||||
_logger.LogError($"Error clearing orphans\n{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
13
server/Services/Jobs/JobBootstrapper.cs
Normal file
13
server/Services/Jobs/JobBootstrapper.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Hangfire;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using PodNoms.Api.Models;
|
||||
using PodNoms.Api.Persistence;
|
||||
|
||||
namespace PodNoms.Api.Services.Jobs {
|
||||
public static class JobBootstrapper {
|
||||
public static void BootstrapJobs() {
|
||||
RecurringJob.AddOrUpdate<ClearOrphanAudioJob>(x => x.Execute(), Cron.Daily(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,9 @@ using PodNoms.Api.Models;
|
||||
|
||||
namespace PodNoms.Api.Services.Processor {
|
||||
public interface IUrlProcessService {
|
||||
Task<bool> GetInformation (int entryId);
|
||||
Task<bool> GetInformation (PodcastEntry entry);
|
||||
Task<bool> DownloadAudio (int entryId);
|
||||
|
||||
Task<AudioType> GetInformation(int entryId);
|
||||
Task<AudioType> GetInformation(PodcastEntry entry);
|
||||
Task<bool> DownloadAudio(int entryId);
|
||||
}
|
||||
}
|
||||
@@ -37,23 +37,23 @@ namespace PodNoms.Api.Services.Processor {
|
||||
uid,
|
||||
e);
|
||||
}
|
||||
public async Task<bool> GetInformation(int entryId) {
|
||||
public async Task<AudioType> GetInformation(int entryId) {
|
||||
var entry = await _repository.GetAsync(entryId);
|
||||
if (entry == null || string.IsNullOrEmpty(entry.SourceUrl)) {
|
||||
_logger.LogError("Unable to process item");
|
||||
return false;
|
||||
return AudioType.Invalid;
|
||||
}
|
||||
if (entry.SourceUrl.EndsWith(".mp3") || entry.SourceUrl.EndsWith(".wav") || entry.SourceUrl.EndsWith(".aif")) {
|
||||
return true;
|
||||
return AudioType.Valid;
|
||||
}
|
||||
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);
|
||||
await downloader.GetInfo();
|
||||
if (downloader.Properties != null) {
|
||||
var ret = await downloader.GetInfo();
|
||||
if (ret == AudioType.Valid) {
|
||||
entry.Title = downloader.Properties?.Title;
|
||||
entry.Description = downloader.Properties?.Description;
|
||||
entry.ImageUrl = downloader.Properties?.Thumbnail;
|
||||
@@ -68,11 +68,8 @@ namespace PodNoms.Api.Services.Processor {
|
||||
|
||||
_logger.LogDebug("***DOWNLOAD INFO RETRIEVED****\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) {
|
||||
var entry = await _repository.GetAsync(entryId);
|
||||
|
||||
7
server/Services/RemoteAudioTypes.cs
Normal file
7
server/Services/RemoteAudioTypes.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace PodNoms.Api.Services {
|
||||
public enum AudioType {
|
||||
Invalid,
|
||||
Valid,
|
||||
Playlist
|
||||
}
|
||||
}
|
||||
@@ -39,61 +39,61 @@ namespace PodNoms.Api {
|
||||
public class Startup {
|
||||
public IConfiguration Configuration { get; }
|
||||
|
||||
public Startup (IConfiguration configuration) {
|
||||
public Startup(IConfiguration configuration) {
|
||||
Configuration = configuration;
|
||||
}
|
||||
|
||||
public void ConfigureProductionServices (IServiceCollection services) {
|
||||
ConfigureServices (services);
|
||||
services.AddHangfire (config => {
|
||||
config.UseSqlServerStorage (Configuration["ConnectionStrings:DefaultConnection"]);
|
||||
public void ConfigureProductionServices(IServiceCollection services) {
|
||||
ConfigureServices(services);
|
||||
services.AddHangfire(config => {
|
||||
config.UseSqlServerStorage(Configuration["ConnectionStrings:DefaultConnection"]);
|
||||
});
|
||||
}
|
||||
public void ConfigureDevelopmentServices (IServiceCollection services) {
|
||||
ConfigureServices (services);
|
||||
services.AddHangfire (config => {
|
||||
config.UseMemoryStorage ();
|
||||
public void ConfigureDevelopmentServices(IServiceCollection services) {
|
||||
ConfigureServices(services);
|
||||
services.AddHangfire(config => {
|
||||
config.UseMemoryStorage();
|
||||
});
|
||||
}
|
||||
public void ConfigureServices (IServiceCollection services) {
|
||||
Console.WriteLine ($"Configuring services: {Configuration.ToString()}");
|
||||
public void ConfigureServices(IServiceCollection services) {
|
||||
Console.WriteLine($"Configuring services: {Configuration.ToString()}");
|
||||
|
||||
services.AddDbContext<PodnomsDbContext> (options =>
|
||||
options.UseSqlServer (Configuration.GetConnectionString ("DefaultConnection")));
|
||||
services.AddDbContext<PodnomsDbContext>(options =>
|
||||
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
|
||||
|
||||
services.AddOptions ();
|
||||
services.Configure<AppSettings> (Configuration.GetSection ("App"));
|
||||
services.Configure<StorageSettings> (Configuration.GetSection ("Storage"));
|
||||
services.Configure<ApplicationsSettings> (Configuration.GetSection ("ApplicationsSettings"));
|
||||
services.Configure<ImageFileStorageSettings> (Configuration.GetSection ("ImageFileStorageSettings"));
|
||||
services.Configure<AudioFileStorageSettings> (Configuration.GetSection ("AudioFileStorageSettings"));
|
||||
services.Configure<FormOptions> (options => {
|
||||
services.AddOptions();
|
||||
services.Configure<AppSettings>(Configuration.GetSection("App"));
|
||||
services.Configure<StorageSettings>(Configuration.GetSection("Storage"));
|
||||
services.Configure<ApplicationsSettings>(Configuration.GetSection("ApplicationsSettings"));
|
||||
services.Configure<ImageFileStorageSettings>(Configuration.GetSection("ImageFileStorageSettings"));
|
||||
services.Configure<AudioFileStorageSettings>(Configuration.GetSection("AudioFileStorageSettings"));
|
||||
services.Configure<FormOptions>(options => {
|
||||
options.ValueCountLimit = 10;
|
||||
options.ValueLengthLimit = int.MaxValue;
|
||||
options.MemoryBufferThreshold = Int32.MaxValue;
|
||||
options.MultipartBodyLengthLimit = long.MaxValue;
|
||||
});
|
||||
|
||||
services.AddAutoMapper (e => {
|
||||
e.AddProfile (new MappingProvider (Configuration));
|
||||
services.AddAutoMapper(e => {
|
||||
e.AddProfile(new MappingProvider(Configuration));
|
||||
});
|
||||
|
||||
services.AddAuthentication (options => {
|
||||
services.AddAuthentication(options => {
|
||||
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
options.DefaultSignInScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
}).AddJwtBearer (options => {
|
||||
}).AddJwtBearer(options => {
|
||||
options.Audience = Configuration["auth0:clientId"];
|
||||
options.Authority = $"https://{Configuration["auth0:domain"]}/";
|
||||
options.TokenValidationParameters = new TokenValidationParameters {
|
||||
NameClaimType = "name"
|
||||
};
|
||||
options.Events = new JwtBearerEvents () {
|
||||
options.Events = new JwtBearerEvents() {
|
||||
OnTokenValidated = AuthenticationMiddleware.OnTokenValidated
|
||||
};
|
||||
options.Events.OnMessageReceived = context => {
|
||||
StringValues token;
|
||||
if (context.Request.Path.Value.StartsWith ("/hubs/") && context.Request.Query.TryGetValue ("token", out token)) {
|
||||
if (context.Request.Path.Value.StartsWith("/hubs/") && context.Request.Query.TryGetValue("token", out token)) {
|
||||
context.Token = token;
|
||||
}
|
||||
|
||||
@@ -102,107 +102,107 @@ namespace PodNoms.Api {
|
||||
});
|
||||
|
||||
var defaultPolicy =
|
||||
new AuthorizationPolicyBuilder ()
|
||||
.AddAuthenticationSchemes ("Bearer")
|
||||
.RequireAuthenticatedUser ()
|
||||
.Build ();
|
||||
new AuthorizationPolicyBuilder()
|
||||
.AddAuthenticationSchemes("Bearer")
|
||||
.RequireAuthenticatedUser()
|
||||
.Build();
|
||||
|
||||
services.AddAuthorization (j => {
|
||||
services.AddAuthorization(j => {
|
||||
j.DefaultPolicy = defaultPolicy;
|
||||
});
|
||||
|
||||
services.AddMvc (options => {
|
||||
options.OutputFormatters.Add (new XmlSerializerOutputFormatter ());
|
||||
options.OutputFormatters
|
||||
.OfType<StringOutputFormatter> ()
|
||||
.Single ().SupportedMediaTypes.Add ("text/html");
|
||||
})
|
||||
.SetCompatibilityVersion (CompatibilityVersion.Version_2_1)
|
||||
.AddJsonOptions (options => {
|
||||
options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver ();
|
||||
services.AddMvc(options => {
|
||||
options.OutputFormatters.Add(new XmlSerializerOutputFormatter());
|
||||
options.OutputFormatters
|
||||
.OfType<StringOutputFormatter>()
|
||||
.Single().SupportedMediaTypes.Add("text/html");
|
||||
})
|
||||
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
|
||||
.AddJsonOptions(options => {
|
||||
options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
|
||||
options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Serialize;
|
||||
})
|
||||
.AddXmlSerializerFormatters ();
|
||||
.AddXmlSerializerFormatters();
|
||||
|
||||
services.Configure<FormOptions> (x => {
|
||||
services.Configure<FormOptions>(x => {
|
||||
x.ValueLengthLimit = int.MaxValue;
|
||||
x.MultipartBodyLengthLimit = int.MaxValue; // In case of multipart
|
||||
});
|
||||
services.AddSignalR (config => { });
|
||||
services.AddSignalR(config => { });
|
||||
|
||||
services.AddCors (options => {
|
||||
options.AddPolicy ("AllowAllOrigins",
|
||||
services.AddCors(options => {
|
||||
options.AddPolicy("AllowAllOrigins",
|
||||
builder => builder
|
||||
.AllowAnyOrigin ()
|
||||
.AllowAnyMethod ()
|
||||
.AllowAnyHeader ()
|
||||
.AllowCredentials ());
|
||||
.AllowAnyOrigin()
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader()
|
||||
.AllowCredentials());
|
||||
});
|
||||
|
||||
services.AddTransient<IFileUploader, AzureFileUploader> ();
|
||||
services.AddTransient<IRealTimeUpdater, SignalRUpdater> ();
|
||||
services.AddScoped<IUnitOfWork, UnitOfWork> ();
|
||||
services.AddScoped<IPodcastRepository, PodcastRepository> ();
|
||||
services.AddScoped<IEntryRepository, EntryRepository> ();
|
||||
services.AddScoped<IUserRepository, UserRepository> ();
|
||||
services.AddScoped<IUrlProcessService, UrlProcessService> ();
|
||||
services.AddScoped<IAudioUploadProcessService, AudioUploadProcessService> ();
|
||||
services.AddTransient<IFileUploader, AzureFileUploader>();
|
||||
services.AddTransient<IRealTimeUpdater, SignalRUpdater>();
|
||||
services.AddScoped<IUnitOfWork, UnitOfWork>();
|
||||
services.AddScoped<IPodcastRepository, PodcastRepository>();
|
||||
services.AddScoped<IEntryRepository, EntryRepository>();
|
||||
services.AddScoped<IUserRepository, UserRepository>();
|
||||
services.AddScoped<IUrlProcessService, UrlProcessService>();
|
||||
services.AddScoped<IAudioUploadProcessService, AudioUploadProcessService>();
|
||||
|
||||
services.AddSingleton (typeof (HubLifetimeManager<DebugHub>),
|
||||
typeof (DebugHubLifetimeManager<DebugHub>));
|
||||
services.AddSingleton(typeof(HubLifetimeManager<DebugHub>),
|
||||
typeof(DebugHubLifetimeManager<DebugHub>));
|
||||
|
||||
//register the codepages (required for slugify)
|
||||
var instance = CodePagesEncodingProvider.Instance;
|
||||
Encoding.RegisterProvider (instance);
|
||||
Encoding.RegisterProvider(instance);
|
||||
|
||||
}
|
||||
|
||||
public void Configure (IApplicationBuilder app, IHostingEnvironment env,
|
||||
public void Configure(IApplicationBuilder app, IHostingEnvironment env,
|
||||
ILoggerFactory loggerFactory, IServiceProvider serviceProvider) {
|
||||
|
||||
if (env.IsDevelopment ()) {
|
||||
app.UseDeveloperExceptionPage ();
|
||||
if (env.IsDevelopment()) {
|
||||
app.UseDeveloperExceptionPage();
|
||||
} else {
|
||||
app.UseExceptionHandler ("/Home/Error");
|
||||
app.UseExceptionHandler("/Home/Error");
|
||||
}
|
||||
|
||||
Console.WriteLine ("Performing migrations");
|
||||
Console.WriteLine("Performing migrations");
|
||||
//TODO: Fix this when EF sucks less
|
||||
// using (var context = new PodnomsDbContext(
|
||||
// app.ApplicationServices.GetRequiredService<DbContextOptions<PodnomsDbContext>>()))
|
||||
// {
|
||||
// context.Database.Migrate();
|
||||
// }
|
||||
Console.WriteLine ("Successfully migrated");
|
||||
Console.WriteLine("Successfully migrated");
|
||||
|
||||
// app.UseHsts();
|
||||
// app.UseHttpsRedirection();
|
||||
app.UseStaticFiles ();
|
||||
app.UseStaticFiles();
|
||||
|
||||
GlobalConfiguration.Configuration.UseActivator (new ServiceProviderActivator (serviceProvider));
|
||||
GlobalConfiguration.Configuration.UseActivator(new ServiceProviderActivator(serviceProvider));
|
||||
|
||||
if ((env.IsProduction () || true)) {
|
||||
app.UseHangfireServer ();
|
||||
app.UseHangfireDashboard ("/hangfire", new DashboardOptions {
|
||||
Authorization = new [] { new HangFireAuthorizationFilter () }
|
||||
if ((env.IsProduction() || true)) {
|
||||
app.UseHangfireServer();
|
||||
app.UseHangfireDashboard("/hangfire", new DashboardOptions {
|
||||
Authorization = new[] { new HangFireAuthorizationFilter() }
|
||||
});
|
||||
}
|
||||
|
||||
app.UseCors ("AllowAllOrigins");
|
||||
app.UseCors("AllowAllOrigins");
|
||||
|
||||
app.UseSignalR (routes => {
|
||||
routes.MapHub<AudioProcessingHub> ("/hubs/audioprocessing");
|
||||
routes.MapHub<DebugHub> ("/hubs/debug");
|
||||
app.UseSignalR(routes => {
|
||||
routes.MapHub<AudioProcessingHub>("/hubs/audioprocessing");
|
||||
routes.MapHub<DebugHub>("/hubs/debug");
|
||||
});
|
||||
|
||||
app.UseMvc (routes => {
|
||||
routes.MapRoute (
|
||||
app.UseMvc(routes => {
|
||||
routes.MapRoute(
|
||||
name: "default",
|
||||
template: "{controller=Home}/{action=Index}/{id?}");
|
||||
});
|
||||
|
||||
//start hangfire jobs
|
||||
//RecurringJob.AddOrUpdate<ClearOrphanAudioJob>(x => x.Execute(), Cron.Hourly);
|
||||
JobBootstrapper.BootstrapJobs();
|
||||
}
|
||||
}
|
||||
}
|
||||
0
server/Utils/Crypt/HMACGenerator.cs
Executable file → Normal file
0
server/Utils/Crypt/HMACGenerator.cs
Executable file → Normal file
0
server/Utils/Crypt/MD5Generator.cs
Executable file → Normal file
0
server/Utils/Crypt/MD5Generator.cs
Executable file → Normal file
0
server/Utils/DateUtils.cs
Executable file → Normal file
0
server/Utils/DateUtils.cs
Executable file → Normal file
0
server/Utils/Extensions/DateTime.cs
Executable file → Normal file
0
server/Utils/Extensions/DateTime.cs
Executable file → Normal file
0
server/Utils/Extensions/String.cs
Executable file → Normal file
0
server/Utils/Extensions/String.cs
Executable file → Normal file
0
server/Utils/HttpUtils.cs
Executable file → Normal file
0
server/Utils/HttpUtils.cs
Executable file → Normal file
0
server/Utils/HumanFriendlyInteger.cs
Executable file → Normal file
0
server/Utils/HumanFriendlyInteger.cs
Executable file → Normal file
0
server/Utils/ImageUtils.cs
Executable file → Normal file
0
server/Utils/ImageUtils.cs
Executable file → Normal file
0
server/Utils/Randomisers.cs
Executable file → Normal file
0
server/Utils/Randomisers.cs
Executable file → Normal file
0
server/Utils/ResourceReader.cs
Executable file → Normal file
0
server/Utils/ResourceReader.cs
Executable file → Normal file
@@ -8,7 +8,7 @@
|
||||
}
|
||||
},
|
||||
"App": {
|
||||
"Version": "0.15.0"
|
||||
"Version": "0.16.0"
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "server=localhost;database=PodNoms;user id=sa;password=cTXu1nJLCpC/c"
|
||||
|
||||
@@ -3,3 +3,4 @@ unset DOCKER_HOST
|
||||
unset DOCKER_TLS_VERIFY
|
||||
|
||||
docker build --rm -f Dockerfile -t fergalmoran/podnoms.api . && docker push fergalmoran/podnoms.api
|
||||
docker push fergalmoran/podnoms.api
|
||||
|
||||
0
server/rebuild.sh
Executable file → Normal file
0
server/rebuild.sh
Executable file → Normal file
0
server/resetdb.sh
Executable file → Normal file
0
server/resetdb.sh
Executable file → Normal file
Reference in New Issue
Block a user