diff --git a/client/angular.json b/client/angular.json index 1d5834b..1b7f0b3 100644 --- a/client/angular.json +++ b/client/angular.json @@ -31,6 +31,8 @@ "node_modules/popper.js/dist/umd/popper.min.js", "node_modules/bootstrap/dist/js/bootstrap.js", "node_modules/quill/dist/quill.js", + "node_modules/easy-pie-chart/dist/easypiechart.js", + "node_modules/easy-pie-chart/dist/jquery.easypiechart.js", "node_modules/howler/dist/howler.js" ] }, @@ -73,32 +75,6 @@ "browserTarget": "podnoms-web:build" } }, - "test": { - "builder": "@angular-devkit/build-angular:karma", - "options": { - "main": "src/test.ts", - "karmaConfig": "./karma.conf.js", - "polyfills": "src/polyfills.ts", - "tsConfig": "src/tsconfig.spec.json", - "scripts": [ - "node_modules/jquery/dist/jquery.js", - "node_modules/cookieconsent/build/cookieconsent.min.js", - "node_modules/tether/dist/js/tether.js", - "node_modules/popper.js/dist/umd/popper.min.js", - "node_modules/bootstrap/dist/js/bootstrap.js", - "node_modules/quill/dist/quill.js", - "node_modules/howler/dist/howler.js" - ], - "styles": ["src/styles.css"], - "assets": [ - "src/assets", - "src/favicon.ico", - "src/firebase-messaging-sw.js", - "src/facebook-auth.html", - "src/manifest.json" - ] - } - }, "lint": { "builder": "@angular-devkit/build-angular:tslint", "options": { @@ -110,27 +86,6 @@ } } } - }, - "podnoms-web-e2e": { - "root": "", - "sourceRoot": "", - "projectType": "application", - "architect": { - "e2e": { - "builder": "@angular-devkit/build-angular:protractor", - "options": { - "protractorConfig": "./protractor.conf.js", - "devServerTarget": "podnoms-web:serve" - } - }, - "lint": { - "builder": "@angular-devkit/build-angular:tslint", - "options": { - "tsConfig": ["e2e/tsconfig.e2e.json"], - "exclude": [] - } - } - } } }, "defaultProject": "podnoms-web", diff --git a/client/package.json b/client/package.json index b519169..c239cf1 100644 --- a/client/package.json +++ b/client/package.json @@ -6,7 +6,6 @@ "ng": "ng", "start": "ng serve --aot", "build": "ng build", - "test": "ng test", "lint": "ng lint" }, "ngrxGen": { @@ -29,6 +28,7 @@ "@ngrx/store": "^5.1.0", "@ngrx/store-devtools": "^5.1.0", "@qontu/ngx-inline-editor": "^0.2.0-alpha.12", + "@types/jquery": "^3.3.1", "angular2-jwt": "^0.2.3", "angular2-moment": "^1.8.0", "angularfire2": "^5.0.0-rc.7", @@ -38,12 +38,14 @@ "cookieconsent": "^3.0.6", "core-js": "^2.5.3", "dropzone": "^5.3.0", + "easy-pie-chart": "^2.1.7", "firebase": "^4.12.1", "font-awesome": "^4.7.0", "howler": "^2.0.9", "jquery": "^3.3.1", "lodash": "^4.17.5", "ng2-toasty": "^4.0.3", + "ng2modules-easypiechart": "0.0.4", "ngx-bootstrap": "^2.0.4", "ngx-clipboard": "^10.0.0", "ngx-cookieconsent": "^1.0.1", @@ -70,12 +72,6 @@ "codelyzer": "~4.2.1", "jasmine-core": "~2.99.1", "jasmine-spec-reporter": "~4.2.1", - "karma": "~1.7.1", - "karma-chrome-launcher": "~2.2.0", - "karma-coverage-istanbul-reporter": "~1.4.2", - "karma-jasmine": "~1.1.1", - "karma-jasmine-html-reporter": "^0.2.2", - "protractor": "~5.3.0", "ts-node": "~5.0.1", "tslint": "~5.9.1", "@types/applicationinsights-js": "^1.0.5" diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts index 54852c6..1b5f225 100644 --- a/client/src/app/app.module.ts +++ b/client/src/app/app.module.ts @@ -18,6 +18,7 @@ import { AngularFireAuthModule } from 'angularfire2/auth'; import { AngularFireModule } from 'angularfire2'; import { AngularFirestoreModule } from 'angularfire2/firestore'; import { QuillModule } from 'ngx-quill'; +import { EasyPieChartModule } from 'ng2modules-easypiechart'; import { SocialLoginModule, AuthServiceConfig } from 'angularx-social-login'; import { @@ -81,6 +82,7 @@ import { BoilerplateComponent } from './components/boilerplate/boilerplate.compo import { BasePageComponent } from './components/base-page/base-page.component'; import { ChatWidgetComponent } from './components/chat-widget/chat-widget.component'; import { ChatService } from 'app/services/chat.service'; +import { BytesToHumanPipe } from './pipes/bytes-to-human.pipe'; const cookieConfig: NgcCookieConsentConfig = { cookie: { @@ -130,6 +132,7 @@ export function provideConfig() { NavbarComponent, FilterEntryPipe, + BytesToHumanPipe, OrderByPipe, PrettyPrintPipe, EntryListItemComponent, @@ -148,7 +151,8 @@ export function provideConfig() { SideOverlayComponent, BoilerplateComponent, BasePageComponent, - ChatWidgetComponent + ChatWidgetComponent, + BytesToHumanPipe ], imports: [ BrowserModule, @@ -172,6 +176,7 @@ export function provideConfig() { InlineEditorModule, MomentModule, QuillModule, + EasyPieChartModule, ModalModule.forRoot(), ProgressbarModule.forRoot(), ToastyModule.forRoot(), diff --git a/client/src/app/components/profile/profile.component.html b/client/src/app/components/profile/profile.component.html index fa2976b..b13640a 100644 --- a/client/src/app/components/profile/profile.component.html +++ b/client/src/app/components/profile/profile.component.html @@ -2,10 +2,17 @@

Personal Details

-

- Tell us as much or as little as you like, nobody sees this anyway -

-

Avatar Image

+ +
+
+ {{limit.storageUsed | bytesToHuman}} +
+ of {{limit.storageQuota| bytesToHuman}} +
+
+

Storage space used

+
+
@@ -44,7 +51,7 @@
- +
diff --git a/client/src/app/components/profile/profile.component.ts b/client/src/app/components/profile/profile.component.ts index eb05db6..3e62bcc 100644 --- a/client/src/app/components/profile/profile.component.ts +++ b/client/src/app/components/profile/profile.component.ts @@ -2,20 +2,35 @@ import { ProfileService } from 'app/services/profile.service'; import { Store } from '@ngrx/store'; import { ApplicationState } from './../../store/index'; import { Observable } from 'rxjs/Observable'; -import { Component, OnInit, ViewChild, ElementRef } from '@angular/core'; +import { + Component, + OnInit, + ViewChild, + ElementRef, + AfterViewInit, + NgZone, + ViewChildren, + ViewContainerRef +} from '@angular/core'; import { ProfileModel } from 'app/models/profile.model'; import * as fromProfile from 'app/reducers'; import * as fromProfileActions from 'app/actions/profile.actions'; import { Router } from '@angular/router'; import { ImageService } from 'app/services/image.service'; import { BasePageComponent } from '../base-page/base-page.component'; +import { ProfileLimitsModel } from 'app/models/profile.limits'; +import { fromEvent } from 'rxjs'; +import { map, filter, debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators'; + +declare let jQuery: any; @Component({ selector: 'app-profile', templateUrl: './profile.component.html', styleUrls: ['./profile.component.css'] }) -export class ProfileComponent extends BasePageComponent implements OnInit { +export class ProfileComponent extends BasePageComponent + implements AfterViewInit { profile$: Observable; originalSlug: string; @@ -27,12 +42,19 @@ export class ProfileComponent extends BasePageComponent implements OnInit { _imageFileBuffer: File; @ViewChild('fileInput') fileInput: ElementRef; + limits$: Observable; + private usageChart: ElementRef; + + @ViewChildren('usageChart', { read: ViewContainerRef }) + viewContainerRefs; + limit: any; constructor( private _store: Store, private _service: ProfileService, private _imageService: ImageService, - private _router: Router + private _router: Router, + private _zone: NgZone ) { super(); this.profile$ = _store.select(fromProfile.getProfile); @@ -41,7 +63,46 @@ export class ProfileComponent extends BasePageComponent implements OnInit { this.image.src = p.profileImage; }); } - ngOnInit() {} + ngAfterViewInit() { + this._service.getLimits().subscribe((l) => { + this.limit = l; + this.viewContainerRefs.changes.subscribe((r) => { + if (this.viewContainerRefs.length !== 0) { + const el = r.first.element.nativeElement; + this._zone.runOutsideAngular(() => { + jQuery(el).easyPieChart({ + easing: 'easeOutBounce', + onStep: function(from, to, percent) { + jQuery(el) + .find('.percent') + .text(Math.round(percent)); + }, + barColor: jQuery(this).attr('data-rel'), + trackColor: 'rgba(0,0,0,0)', + size: 84, + scaleLength: 0, + animation: 2000, + lineWidth: 9, + lineCap: 'round' + }); + }); + } + }); + }); + + const searchBox = document.getElementById('slug-box'); + + const typeahead = fromEvent(searchBox, 'input').pipe( + map((e: KeyboardEvent) => e.target.value), + filter((text) => text.length > 2), + debounceTime(10), + distinctUntilChanged(), + switchMap((v) => this.onSlugChanged(v)) + ); + typeahead.subscribe((data) => { + // Handle the data from the API + }); + } private _parseImageData(file: File) { const myReader: FileReader = new FileReader(); @@ -52,10 +113,10 @@ export class ProfileComponent extends BasePageComponent implements OnInit { }; myReader.readAsDataURL(file); } - onSlugChanged(slug: string) { + onSlugChanged(slug: string) : boolean { this._service.checkSlug(slug).subscribe((v) => { console.log('profile.component.ts', 'onSlugChanged', v); - if (v.status == 404) this.slugError = ''; + if (v) this.slugError = ''; else this.slugError = 'Slug already exists'; }); } diff --git a/client/src/app/models/profile.limits.ts b/client/src/app/models/profile.limits.ts new file mode 100644 index 0000000..f1acf77 --- /dev/null +++ b/client/src/app/models/profile.limits.ts @@ -0,0 +1,7 @@ +import { ProfileModel } from 'app/models/profile.model'; + +export class ProfileLimitsModel { + user: ProfileModel; + storageQuota: number; + storageUsed: number; +} diff --git a/client/src/app/pipes/bytes-to-human.pipe.ts b/client/src/app/pipes/bytes-to-human.pipe.ts new file mode 100644 index 0000000..d2f5818 --- /dev/null +++ b/client/src/app/pipes/bytes-to-human.pipe.ts @@ -0,0 +1,19 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'bytesToHuman' +}) +export class BytesToHumanPipe implements PipeTransform { + sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + + transform(bytes: number, args?: any): any { + if (bytes == 0) return '0 Bytes'; + const k = 1024, + dm = 0, + sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'], + i = Math.floor(Math.log(bytes) / Math.log(k)); + return ( + parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i] + ); + } +} diff --git a/client/src/app/services/profile.service.ts b/client/src/app/services/profile.service.ts index 48c9e48..76d20c7 100644 --- a/client/src/app/services/profile.service.ts +++ b/client/src/app/services/profile.service.ts @@ -3,9 +3,9 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs/Observable'; import { ProfileModel } from 'app/models/profile.model'; import 'rxjs/add/operator/map'; -import { Profile } from 'selenium-webdriver/firefox'; import { HttpClient } from '@angular/common/http'; import { PodnomsAuthService } from './podnoms-auth.service'; +import { ProfileLimitsModel } from 'app/models/profile.limits'; @Injectable() export class ProfileService { @@ -38,9 +38,9 @@ export class ProfileService { ); } - checkSlug(slug): Observable { + checkSlug(slug): Observable { console.log('profile.service.ts', 'checkSlug', slug); - return this._http.get( + return this._http.get( environment.API_HOST + '/profile/checkslug/' + slug ); } @@ -50,4 +50,9 @@ export class ProfileService { null ); } + getLimits(): Observable { + return this._http.get( + environment.API_HOST + '/profile/limits' + ); + } } diff --git a/client/tsconfig.json b/client/tsconfig.json index 45468ef..93f2883 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -9,7 +9,9 @@ "emitDecoratorMetadata": true, "experimentalDecorators": true, "target": "es5", + "typeRoots": ["node_modules/@types"], // "target": "es2015", - "lib": ["es2017", "dom"] + "lib": ["es2017", "dom"], + "plugins": [{ "name": "tslint-language-service" }] } } diff --git a/server/Controllers/ProfileController.cs b/server/Controllers/ProfileController.cs index 3fc1635..bdd4dae 100644 --- a/server/Controllers/ProfileController.cs +++ b/server/Controllers/ProfileController.cs @@ -18,13 +18,14 @@ namespace PodNoms.Api.Controllers { public class ProfileController : BaseAuthController { public IUnitOfWork _unitOfWork { get; } - - public IMapper _mapper { get; } + private readonly IEntryRepository _entryRepository; public ProfileController(IMapper mapper, IUnitOfWork unitOfWork, + IEntryRepository entryRepository, UserManager userManager, IHttpContextAccessor contextAccessor) : base(contextAccessor, userManager) { + this._entryRepository = entryRepository; this._mapper = mapper; this._unitOfWork = unitOfWork; } @@ -45,13 +46,23 @@ namespace PodNoms.Api.Controllers { } [HttpGet("checkslug/{slug}")] - public async Task CheckSlug(string slug) { + public async Task> CheckSlug(string slug) { var slugValid = await _userManager.CheckSlug(slug); + return Ok(slugValid); + } - if (slugValid) - return NotFound(); - - return Ok(); + [HttpGet("limits")] + public async Task> GetProfileLimits() { + var entries = await _entryRepository.GetAllForUserAsync(_applicationUser.Id); + var user = _mapper.Map(_applicationUser); + var sum = entries.Select(x => x.AudioFileSize) + .Sum(); + var vm = new ProfileLimitsViewModel { + StorageQuota = 5368709120, //5Gb + StorageUsed = sum, + User = user + }; + return Ok(vm); } } } \ No newline at end of file diff --git a/server/Models/ViewModels/ChatViewModel.cs b/server/Models/ViewModels/Resources/ChatViewModel.cs similarity index 100% rename from server/Models/ViewModels/ChatViewModel.cs rename to server/Models/ViewModels/Resources/ChatViewModel.cs diff --git a/server/Models/ViewModels/Resources/PodcastViewModel.cs b/server/Models/ViewModels/Resources/PodcastViewModel.cs index 3d800c1..be72894 100644 --- a/server/Models/ViewModels/Resources/PodcastViewModel.cs +++ b/server/Models/ViewModels/Resources/PodcastViewModel.cs @@ -10,6 +10,6 @@ namespace PodNoms.Api.Models.ViewModels { public string ImageUrl { get; set; } public string ThumbnailUrl { get; set; } public string RssUrl { get; set; } - // public ICollection PodcastEntries { get; set; } + public ICollection PodcastEntries { get; set; } } } \ No newline at end of file diff --git a/server/Models/ViewModels/Resources/ProfileLimitsViewModel.cs b/server/Models/ViewModels/Resources/ProfileLimitsViewModel.cs new file mode 100644 index 0000000..4bc4528 --- /dev/null +++ b/server/Models/ViewModels/Resources/ProfileLimitsViewModel.cs @@ -0,0 +1,10 @@ +using System; +using System.Numerics; + +namespace PodNoms.Api.Models.ViewModels { + public class ProfileLimitsViewModel { + public decimal StorageQuota { get; set; } + public decimal StorageUsed { get; set; } + public ProfileViewModel User { get; set; } + } +} \ No newline at end of file diff --git a/server/Models/ViewModels/ProfileViewModel.cs b/server/Models/ViewModels/Resources/ProfileViewModel.cs similarity index 100% rename from server/Models/ViewModels/ProfileViewModel.cs rename to server/Models/ViewModels/Resources/ProfileViewModel.cs diff --git a/server/Services/Jobs/ClearOrphanAudioJob.cs b/server/Services/Jobs/ClearOrphanAudioJob.cs index 7b309d3..b38b5b4 100644 --- a/server/Services/Jobs/ClearOrphanAudioJob.cs +++ b/server/Services/Jobs/ClearOrphanAudioJob.cs @@ -26,7 +26,7 @@ namespace PodNoms.Api.Services.Jobs { this._logger = logger.CreateLogger(); } - public async Task Execute() { + public async Task Execute() { try { CloudStorageAccount storageAccount = CloudStorageAccount.Parse(_storageSettings.ConnectionString); CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient(); @@ -49,9 +49,11 @@ namespace PodNoms.Api.Services.Jobs { } } await this._mailSender.SendEmailAsync("fergal.moran@gmail.com", $"ClearOrphanAudioJob: Complete {blobCount}", string.Empty); + return true; } catch (Exception ex) { _logger.LogError($"Error clearing orphans\n{ex.Message}"); } + return false; } } } \ No newline at end of file diff --git a/server/Services/Jobs/IJob.cs b/server/Services/Jobs/IJob.cs index 0c4b66e..50609fb 100644 --- a/server/Services/Jobs/IJob.cs +++ b/server/Services/Jobs/IJob.cs @@ -2,6 +2,6 @@ using System.Threading.Tasks; namespace PodNoms.Api.Services.Jobs { public interface IJob { - Task Execute(); + Task Execute(); } } \ No newline at end of file diff --git a/server/Services/Jobs/JobBootstrapper.cs b/server/Services/Jobs/JobBootstrapper.cs index 33f27ff..b40d27a 100644 --- a/server/Services/Jobs/JobBootstrapper.cs +++ b/server/Services/Jobs/JobBootstrapper.cs @@ -12,6 +12,10 @@ namespace PodNoms.Api.Services.Jobs { RecurringJob.AddOrUpdate(x => x.Execute(), Cron.Daily(1, 30)); BackgroundJob.Schedule(x => x.Execute(3), TimeSpan.FromSeconds(1)); RecurringJob.AddOrUpdate(x => x.Execute(), Cron.Daily(2)); + + BackgroundJob.Schedule( + x => x.Execute(), + TimeSpan.FromSeconds(Int16.MaxValue)); } } } diff --git a/server/Services/Jobs/ProcessPlaylistItemJob.cs b/server/Services/Jobs/ProcessPlaylistItemJob.cs index 6670845..a19d369 100644 --- a/server/Services/Jobs/ProcessPlaylistItemJob.cs +++ b/server/Services/Jobs/ProcessPlaylistItemJob.cs @@ -34,14 +34,15 @@ namespace PodNoms.Api.Services.Jobs { this._logger = logger; } [Mutex("ProcessPlaylistItemJob")] - public async Task Execute() { + public async Task Execute() { var items = await _playlistRepository.GetUnprocessedItems(); foreach (var item in items) { await ExecuteForItem(item.VideoId, item.Playlist.Id); } + return true; } [Mutex("ProcessPlaylistItemJob")] - public async Task ExecuteForItem(string itemId, int playlistId) { + public async Task ExecuteForItem(string itemId, int playlistId) { var item = await _playlistRepository.GetParsedItem(itemId, playlistId); if (item != null && !string.IsNullOrEmpty(item.VideoType) && (item.VideoType.Equals("youtube") || item.VideoType.Equals("mixcloud"))) { @@ -80,9 +81,11 @@ namespace PodNoms.Api.Services.Jobs { } } else { _logger.LogError($"Processing playlist item {itemId} failed"); + return false; } } } + return true; } } } \ No newline at end of file diff --git a/server/Services/Jobs/ProcessPlaylistsJob.cs b/server/Services/Jobs/ProcessPlaylistsJob.cs index bd2c89b..76ac799 100644 --- a/server/Services/Jobs/ProcessPlaylistsJob.cs +++ b/server/Services/Jobs/ProcessPlaylistsJob.cs @@ -37,17 +37,20 @@ namespace PodNoms.Api.Services.Jobs { this._logger = logger.CreateLogger(); } - public async Task Execute() { + public async Task Execute() { var playlists = _playlistRepository.GetAll() .ToList(); foreach (var playlist in playlists) { await Execute(playlist.Id); } + return true; } - public async Task Execute(int playlistId) { + public async Task Execute(int playlistId) { try { var playlist = await _playlistRepository.GetAsync(playlistId); + if (playlist == null) + return false; var resultList = new List(); var downloader = new AudioDownloader(playlist.SourceUrl, _helpersSettings.Downloader); @@ -62,7 +65,7 @@ namespace PodNoms.Api.Services.Jobs { } else if (MixcloudParser.ValidateUrl(playlist.SourceUrl)) { resultList = await _mixcloudParser.GetEntries(playlist.SourceUrl); } - if (resultList != null) { + if (resultList != null) { //order in reverse so the newest item is added first foreach (var item in resultList?.OrderBy(r => r.UploadDate)) { if (!playlist.ParsedPlaylistItems.Any(p => p.VideoId == item.Id)) { @@ -76,9 +79,11 @@ namespace PodNoms.Api.Services.Jobs { } } } + return true; } catch (Exception ex) { _logger.LogError(ex.Message); } + return false; } } } \ No newline at end of file diff --git a/server/Services/Jobs/ProcessRemoteAudioFileAttributesJob.cs b/server/Services/Jobs/ProcessRemoteAudioFileAttributesJob.cs new file mode 100644 index 0000000..a746c05 --- /dev/null +++ b/server/Services/Jobs/ProcessRemoteAudioFileAttributesJob.cs @@ -0,0 +1,43 @@ +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using PodNoms.Api.Models.Settings; +using PodNoms.Api.Persistence; +using PodNoms.Api.Services.Storage; + +namespace PodNoms.Api.Services.Jobs { + public class ProcessRemoteAudioFileAttributesJob : IJob { + private readonly IEntryRepository _entryRepository; + private readonly IFileUtilities _fileUtilities; + private readonly ILogger _logger; + private readonly IUnitOfWork _unitOfWork; + + public ProcessRemoteAudioFileAttributesJob(IEntryRepository entryRepository, + IFileUtilities fileUtilities, ILogger logger, + IUnitOfWork unitOfWork) { + this._logger = logger; + this._entryRepository = entryRepository; + this._fileUtilities = fileUtilities; + this._unitOfWork = unitOfWork; + } + public async Task Execute() { + var entries = await _entryRepository.GetAll() + .ToListAsync(); + foreach (var entry in entries) { + string[] parts = entry.AudioUrl.Split("/"); + + if (parts.Length == 2) { + _logger.LogInformation($"Processing remote: {entry.AudioUrl}"); + var size = await _fileUtilities.GetRemoteFileSize( + parts[0], parts[1]); + if (size != -1) { + entry.AudioFileSize = size; + } + } + } + await _unitOfWork.CompleteAsync(); + return false; + } + } +} \ No newline at end of file diff --git a/server/Services/Jobs/UpdateYouTubeDlJob.cs b/server/Services/Jobs/UpdateYouTubeDlJob.cs index 789dfd8..3b2b57a 100644 --- a/server/Services/Jobs/UpdateYouTubeDlJob.cs +++ b/server/Services/Jobs/UpdateYouTubeDlJob.cs @@ -10,12 +10,12 @@ namespace PodNoms.Api.Services.Jobs { private readonly IMailSender _sender; private readonly ILogger _logger; - public UpdateYouTubeDlJob(IMailSender sender, ILogger logger){ + public UpdateYouTubeDlJob(IMailSender sender, ILogger logger) { this._sender = sender; this._logger = logger; } - public async Task Execute() { + public async Task Execute() { _logger.LogInformation("Updating YoutubeDL"); var yt = new YoutubeDL(); @@ -24,6 +24,7 @@ namespace PodNoms.Api.Services.Jobs { var results = await _sender.SendEmailAsync("fergal.moran@gmail.com", "PodNoms: UpdateYouTubeDlJob completed", "As you were"); _logger.LogInformation($"{results}"); + return true; } } } diff --git a/server/Services/Processor/AudioUploadProcessService.cs b/server/Services/Processor/AudioUploadProcessService.cs index 7f465f4..40cb3df 100644 --- a/server/Services/Processor/AudioUploadProcessService.cs +++ b/server/Services/Processor/AudioUploadProcessService.cs @@ -42,7 +42,8 @@ namespace PodNoms.Api.Services.Processor { localFile = entry.AudioUrl; if (File.Exists(localFile)) { - var fileName = new FileInfo(localFile).Name; + FileInfo fileInfo = new FileInfo(localFile); + var fileName = fileInfo.Name; await _fileUploader.UploadFile(localFile, _audioStorageSettings.ContainerName, fileName, "application/mpeg", async (p, t) => { @@ -60,6 +61,7 @@ namespace PodNoms.Api.Services.Processor { entry.Processed = true; entry.ProcessingStatus = ProcessingStatus.Processed; entry.AudioUrl = $"{_audioStorageSettings.ContainerName}/{fileName}"; + entry.AudioFileSize = fileInfo.Length; await _unitOfWork.CompleteAsync(); await _sendProcessCompleteMessage(entry); return true; diff --git a/server/Services/Storage/AzureFileUploader.cs b/server/Services/Storage/AzureFileUploader.cs index 3e7ddeb..0bfd42c 100644 --- a/server/Services/Storage/AzureFileUploader.cs +++ b/server/Services/Storage/AzureFileUploader.cs @@ -15,6 +15,7 @@ using PodNoms.Api.Services.Realtime; using PodNoms.Api.Utils.Extensions; namespace PodNoms.Api.Services.Storage { + internal class AzureFileUploader : IFileUploader { private readonly StorageSettings _settings; public AzureFileUploader(IOptions settings, ILoggerFactory logger) { diff --git a/server/Services/Storage/AzureFileUtilities.cs b/server/Services/Storage/AzureFileUtilities.cs new file mode 100644 index 0000000..a1c62cb --- /dev/null +++ b/server/Services/Storage/AzureFileUtilities.cs @@ -0,0 +1,30 @@ +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.WindowsAzure.Storage; +using Microsoft.WindowsAzure.Storage.Blob; +using PodNoms.Api.Models.Settings; + +namespace PodNoms.Api.Services.Storage { + internal class AzureFileUtilities : IFileUtilities { + private readonly StorageSettings _settings; + public AzureFileUtilities(IOptions settings, ILoggerFactory logger) { + this._settings = settings.Value; + } + public async Task GetRemoteFileSize(string containerName, string fileName) { + CloudStorageAccount storageAccount = CloudStorageAccount.Parse(_settings.ConnectionString); + CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient(); + CloudBlobContainer container = blobClient.GetContainerReference(containerName); + var exists = await container.ExistsAsync(); + if (exists) { + CloudBlockBlob blob = container.GetBlockBlobReference(fileName); + exists = await blob.ExistsAsync(); + if (exists) { + await blob.FetchAttributesAsync(); + return blob.Properties.Length; + } + } + return -1; + } + } +} \ No newline at end of file diff --git a/server/Services/Storage/IFileUtilities.cs b/server/Services/Storage/IFileUtilities.cs new file mode 100644 index 0000000..3434fb2 --- /dev/null +++ b/server/Services/Storage/IFileUtilities.cs @@ -0,0 +1,7 @@ +using System.Threading.Tasks; + +namespace PodNoms.Api.Services.Storage { + public interface IFileUtilities { + Task GetRemoteFileSize(string containerName, string fileName); + } +} \ No newline at end of file diff --git a/server/Startup.cs b/server/Startup.cs index af2e7a8..d20d968 100644 --- a/server/Startup.cs +++ b/server/Startup.cs @@ -255,6 +255,7 @@ namespace PodNoms.Api { services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped();