From 68c55ede0841df4bf8e8304bbcc9045e1a9f42a2 Mon Sep 17 00:00:00 2001 From: Dion Date: Sat, 4 Jan 2025 15:07:06 +0100 Subject: [PATCH] #1833 refactor thumbnail Service into a factory --- .../Services/ExportService.cs | 125 +-- .../starsky.feature.import/Services/Import.cs | 2 +- .../Services/MetaUpdateService.cs | 36 +- .../DatabaseThumbnailGenerationService.cs | 3 +- .../ManualThumbnailGenerationService.cs | 4 +- .../Services/WebHtmlPublishService.cs | 711 +++++++++--------- .../ThumbnailResultDataTransferModel.cs | 65 +- .../Query/QueryCollections.cs | 89 +-- .../Query/QueryGetNextPrevInFolder.cs | 2 +- .../Helpers/ExtensionRolesHelper.cs | 9 +- .../ThumbnailSizeType.cs | 16 +- .../Thumbnails/ThumbnailSizes.cs | 18 + .../Storage/ThumbnailFileMoveAllSizes.cs | 47 +- .../Storage/ThumbnailNameHelper.cs | 228 +++--- .../CompositeThumbnailGenerator.cs | 39 + .../ErrorGenerationResultModel.cs | 26 + .../GenerationFactory/FolderToFileList.cs | 40 + .../FfmpegVideoThumbnailGenerator.cs | 30 + .../ImageSharpThumbnailGenerator.cs | 64 ++ .../Interfaces/IThumbnailGenerator.cs | 13 + .../NotSupportedFallbackThumbnailGenerator.cs | 18 + .../ImageSharp/ImageSharpImageResizeHelper.cs | 35 + .../ResizeThumbnailFromSourceImageHelper.cs | 79 ++ ...ResizeThumbnailFromThumbnailImageHelper.cs | 86 +++ .../SaveThumbnailImageFormatHelper.cs | 61 ++ .../Interfaces/IThumbnailService.cs | 25 + .../RotateThumbnailHelper.cs | 68 ++ .../Testers/ErrorLogItemFullPath.cs | 16 + .../GenerationFactory/Testers/Preflight.cs | 57 ++ .../ThumbnailGeneratorFactory.cs | 27 + .../GenerationFactory/ThumbnailService.cs | 129 ++++ .../Helpers/Thumbnail.cs | 6 +- .../Helpers/ThumbnailCli.cs | 111 +-- .../Helpers/ThumbnailVideo.cs | 99 ++- ...mbnailService.cs => IThumbnailService.bak} | 0 .../Models/GenerationResultModel.cs | 2 +- .../Services/ThumbnailCleaner.cs | 2 +- ...umbnailService.cs => ThumbnailService.bak} | 0 .../UpdateStatusGeneratedThumbnailService.cs | 2 +- ...rsky.foundation.thumbnailgeneration.csproj | 4 + .../Services/MetaExifThumbnailService.cs | 225 +++--- .../Services/WriteMetaThumbnailService.cs | 12 +- .../Process/FfmpegStreamToStreamRunner.cs | 64 ++ .../Process/Interfaces/IVideoProcess.cs | 2 +- .../Interfaces/IVideoProcessThumbnailPost.cs | 8 + .../Process/{ => Types}/VideoProcessTypes.cs | 0 .../Process/VideoProcess.cs | 145 +--- .../Process/VideoProcessThumbnailPost.cs | 61 ++ .../Process/VideoResult.cs | 22 + .../Controllers/AllowedTypesController.cs | 4 +- .../Controllers/DownloadPhotoController.cs | 10 +- .../Controllers/ImportThumbnailController.cs | 42 +- .../Controllers/ThumbnailController.cs | 16 +- .../Controllers/ThumbnailControllerTest.cs | 8 +- .../FakeMocks/FakeIThumbnailService.cs | 34 +- .../FakeMocks/FakeIVideoProcess.cs | 5 +- .../Services/MetaUpdateServiceTest.cs | 2 +- .../Thumbnails/ThumbnailQueryTest.cs | 22 +- .../ThumbnailResultDataTransferModelTest.cs | 2 +- .../Helpers/ExtensionRolesHelperTest.cs | 18 +- .../Storage/ThumbnailNameHelperTest.cs | 165 ++-- .../Helpers/ThumbnailTest.cs | 610 +++++++-------- .../Services/ThumbnailCleanerTest.cs | 2 +- .../Services/ThumbnailServiceTest.cs | 5 +- ...dateStatusGeneratedThumbnailServiceTest.cs | 135 ++-- .../Services/ReadMetaThumbnailTest.cs | 2 +- .../Services/WriteMetaThumbnailServiceTest.cs | 170 +++-- .../VideoProcessTests.cs | 12 +- starsky/starskythumbnailcli/Program.cs | 52 +- 69 files changed, 2608 insertions(+), 1641 deletions(-) rename starsky/starsky.foundation.platform/{Enums => Thumbnails}/ThumbnailSizeType.cs (54%) create mode 100644 starsky/starsky.foundation.platform/Thumbnails/ThumbnailSizes.cs create mode 100644 starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/CompositeThumbnailGenerator.cs create mode 100644 starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/ErrorGenerationResultModel.cs create mode 100644 starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/FolderToFileList.cs create mode 100644 starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/Generators/FfmpegVideoThumbnailGenerator.cs create mode 100644 starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/Generators/ImageSharpThumbnailGenerator.cs create mode 100644 starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/Generators/Interfaces/IThumbnailGenerator.cs create mode 100644 starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/Generators/NotSupportedFallbackThumbnailGenerator.cs create mode 100644 starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/ImageSharp/ImageSharpImageResizeHelper.cs create mode 100644 starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/ImageSharp/ResizeThumbnailFromSourceImageHelper.cs create mode 100644 starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/ImageSharp/ResizeThumbnailFromThumbnailImageHelper.cs create mode 100644 starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/ImageSharp/SaveThumbnailImageFormatHelper.cs create mode 100644 starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/Interfaces/IThumbnailService.cs create mode 100644 starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/RotateThumbnailHelper.cs create mode 100644 starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/Testers/ErrorLogItemFullPath.cs create mode 100644 starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/Testers/Preflight.cs create mode 100644 starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/ThumbnailGeneratorFactory.cs create mode 100644 starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/ThumbnailService.cs rename starsky/starsky.foundation.thumbnailgeneration/Interfaces/{IThumbnailService.cs => IThumbnailService.bak} (100%) rename starsky/starsky.foundation.thumbnailgeneration/Services/{ThumbnailService.cs => ThumbnailService.bak} (100%) create mode 100644 starsky/starsky.foundation.video/Process/FfmpegStreamToStreamRunner.cs create mode 100644 starsky/starsky.foundation.video/Process/Interfaces/IVideoProcessThumbnailPost.cs rename starsky/starsky.foundation.video/Process/{ => Types}/VideoProcessTypes.cs (100%) create mode 100644 starsky/starsky.foundation.video/Process/VideoProcessThumbnailPost.cs create mode 100644 starsky/starsky.foundation.video/Process/VideoResult.cs diff --git a/starsky/starsky.feature.export/Services/ExportService.cs b/starsky/starsky.feature.export/Services/ExportService.cs index 773be92118..da903a9d94 100644 --- a/starsky/starsky.feature.export/Services/ExportService.cs +++ b/starsky/starsky.feature.export/Services/ExportService.cs @@ -11,32 +11,32 @@ using starsky.foundation.database.Interfaces; using starsky.foundation.database.Models; using starsky.foundation.injection; -using starsky.foundation.platform.Enums; using starsky.foundation.platform.Helpers; using starsky.foundation.platform.Interfaces; using starsky.foundation.platform.Models; +using starsky.foundation.platform.Thumbnails; using starsky.foundation.storage.ArchiveFormats; using starsky.foundation.storage.Helpers; using starsky.foundation.storage.Interfaces; using starsky.foundation.storage.Models; using starsky.foundation.storage.Storage; -using starsky.foundation.thumbnailgeneration.Interfaces; +using starsky.foundation.thumbnailgeneration.GenerationFactory.Interfaces; [assembly: InternalsVisibleTo("starskytest")] namespace starsky.feature.export.Services; /// -/// Also known as Download +/// Also known as Download /// [Service(typeof(IExport), InjectionLifetime = InjectionLifetime.Scoped)] public class ExportService : IExport { - private readonly IQuery _query; private readonly AppSettings _appSettings; - private readonly IStorage _iStorage; private readonly IStorage _hostFileSystemStorage; + private readonly IStorage _iStorage; private readonly IWebLogger _logger; + private readonly IQuery _query; private readonly IThumbnailService _thumbnailService; public ExportService(IQuery query, AppSettings appSettings, @@ -52,7 +52,7 @@ public ExportService(IQuery query, AppSettings appSettings, } /// - /// Export preflight + /// Export preflight /// /// list of subPaths /// is stack collections enabled @@ -78,7 +78,7 @@ public async Task>> PreflightAsync( } if ( _iStorage.IsFolderOrFile(detailView.FileIndexItem.FilePath) == - FolderOrFileModel.FolderOrFileTypeList.Deleted ) + FolderOrFileModel.FolderOrFileTypeList.Deleted ) { StatusCodesHelper.ReturnExifStatusError(detailView.FileIndexItem, FileIndexItem.ExifStatus.NotFoundSourceMissing, @@ -102,6 +102,52 @@ public async Task>> PreflightAsync( return new Tuple>(zipHash, fileIndexResultsList); } + /// + /// Based on the preflight create a Zip Export + /// + /// Result of Preflight + /// isThumbnail? + /// filename of zip file (no extension) + /// nothing + public async Task CreateZip(List fileIndexResultsList, bool thumbnail, + string zipOutputFileName) + { + var filePaths = await CreateListToExport(fileIndexResultsList, thumbnail); + var fileNames = await FilePathToFileNameAsync(filePaths, thumbnail); + + new Zipper(_logger).CreateZip(_appSettings.TempFolder, filePaths, fileNames, + zipOutputFileName); + + // Write a single file to be sure that writing is ready + var doneFileFullPath = Path.Combine(_appSettings.TempFolder, zipOutputFileName) + ".done"; + await _hostFileSystemStorage.WriteStreamAsync(StringToStreamHelper.StringToStream("OK"), + doneFileFullPath); + if ( _appSettings.IsVerbose() ) + { + _logger.LogInformation("[CreateZip] Zip done: " + doneFileFullPath); + } + } + + /// + /// Is Zip Ready? + /// + /// fileName without extension + /// null if status file is not found, true if done file exist + public Tuple StatusIsReady(string zipOutputFileName) + { + var sourceFullPath = Path.Combine(_appSettings.TempFolder, zipOutputFileName) + ".zip"; + var doneFileFullPath = Path.Combine(_appSettings.TempFolder, zipOutputFileName) + ".done"; + + if ( !_hostFileSystemStorage.ExistFile(sourceFullPath) ) + { + return new Tuple(null, null); + } + + // Read a single file to be sure that writing is ready + return new Tuple(_hostFileSystemStorage.ExistFile(doneFileFullPath), + sourceFullPath); + } + private async Task AddFileIndexResultsListForDirectory(DetailView detailView, List fileIndexResultsList) { @@ -109,8 +155,8 @@ private async Task AddFileIndexResultsListForDirectory(DetailView detailView, await _query.GetAllRecursiveAsync(detailView .FileIndexItem?.FilePath!); foreach ( var item in - allFilesInFolder.Where(item => - item.FilePath != null && _iStorage.ExistFile(item.FilePath)) ) + allFilesInFolder.Where(item => + item.FilePath != null && _iStorage.ExistFile(item.FilePath)) ) { item.Status = FileIndexItem.ExifStatus.Ok; fileIndexResultsList.Add(item); @@ -137,32 +183,7 @@ private void AddCollectionBasedImages(DetailView detailView, } /// - /// Based on the preflight create a Zip Export - /// - /// Result of Preflight - /// isThumbnail? - /// filename of zip file (no extension) - /// nothing - public async Task CreateZip(List fileIndexResultsList, bool thumbnail, - string zipOutputFileName) - { - var filePaths = await CreateListToExport(fileIndexResultsList, thumbnail); - var fileNames = await FilePathToFileNameAsync(filePaths, thumbnail); - - new Zipper(_logger).CreateZip(_appSettings.TempFolder, filePaths, fileNames, zipOutputFileName); - - // Write a single file to be sure that writing is ready - var doneFileFullPath = Path.Combine(_appSettings.TempFolder, zipOutputFileName) + ".done"; - await _hostFileSystemStorage.WriteStreamAsync(StringToStreamHelper.StringToStream("OK"), - doneFileFullPath); - if ( _appSettings.IsVerbose() ) - { - _logger.LogInformation("[CreateZip] Zip done: " + doneFileFullPath); - } - } - - /// - /// This list will be included in the zip - Export is called Download in the UI + /// This list will be included in the zip - Export is called Download in the UI /// /// the items /// add the thumbnail or the source image @@ -173,7 +194,7 @@ public async Task> CreateListToExport(List fileIndex var filePaths = new List(); foreach ( var item in fileIndexResultsList.Where(p => - p.Status == FileIndexItem.ExifStatus.Ok && p.FileHash != null).ToList() ) + p.Status == FileIndexItem.ExifStatus.Ok && p.FileHash != null).ToList() ) { if ( thumbnail ) { @@ -181,7 +202,7 @@ public async Task> CreateListToExport(List fileIndex ThumbnailNameHelper.Combine(item.FileHash!, ThumbnailSize.Large, true)); await _thumbnailService - .CreateThumbAsync(item.FilePath!, item.FileHash!, true); + .GenerateThumbnail(item.FilePath!, item.FileHash!, true); filePaths.Add(sourceThumb); continue; @@ -199,9 +220,9 @@ await _thumbnailService // when there is .xmp sidecar file (but only when file is a RAW file, ignored when for example jpeg) if ( !ExtensionRolesHelper.IsExtensionForceXmp(item.FilePath) || - !_iStorage.ExistFile( - ExtensionRolesHelper.ReplaceExtensionWithXmp( - item.FilePath)) ) + !_iStorage.ExistFile( + ExtensionRolesHelper.ReplaceExtensionWithXmp( + item.FilePath)) ) { continue; } @@ -222,7 +243,7 @@ await _thumbnailService } /// - /// Get the filename (in case of thumbnail the source image name) + /// Get the filename (in case of thumbnail the source image name) /// /// the full file paths /// copy the thumbnail (true) or the source image (false) @@ -252,7 +273,7 @@ internal async Task> FilePathToFileNameAsync(IEnumerable fi } /// - /// to create a unique name of the zip using c# get hashcode + /// to create a unique name of the zip using c# get hashcode /// /// list of objects with fileHashes /// unique 'get hashcode' string @@ -270,24 +291,4 @@ private static string GetName(IEnumerable fileIndexResultsList) return shortName; } - - /// - /// Is Zip Ready? - /// - /// fileName without extension - /// null if status file is not found, true if done file exist - public Tuple StatusIsReady(string zipOutputFileName) - { - var sourceFullPath = Path.Combine(_appSettings.TempFolder, zipOutputFileName) + ".zip"; - var doneFileFullPath = Path.Combine(_appSettings.TempFolder, zipOutputFileName) + ".done"; - - if ( !_hostFileSystemStorage.ExistFile(sourceFullPath) ) - { - return new Tuple(null, null); - } - - // Read a single file to be sure that writing is ready - return new Tuple(_hostFileSystemStorage.ExistFile(doneFileFullPath), - sourceFullPath); - } } diff --git a/starsky/starsky.feature.import/Services/Import.cs b/starsky/starsky.feature.import/Services/Import.cs index 63f641172b..ff1154345f 100644 --- a/starsky/starsky.feature.import/Services/Import.cs +++ b/starsky/starsky.feature.import/Services/Import.cs @@ -18,11 +18,11 @@ using starsky.foundation.database.Query; using starsky.foundation.database.Thumbnails; using starsky.foundation.injection; -using starsky.foundation.platform.Enums; using starsky.foundation.platform.Extensions; using starsky.foundation.platform.Helpers; using starsky.foundation.platform.Interfaces; using starsky.foundation.platform.Models; +using starsky.foundation.platform.Thumbnails; using starsky.foundation.readmeta.Services; using starsky.foundation.storage.Interfaces; using starsky.foundation.storage.Models; diff --git a/starsky/starsky.feature.metaupdate/Services/MetaUpdateService.cs b/starsky/starsky.feature.metaupdate/Services/MetaUpdateService.cs index f1cc5a9bc3..f58ee3c3d1 100644 --- a/starsky/starsky.feature.metaupdate/Services/MetaUpdateService.cs +++ b/starsky/starsky.feature.metaupdate/Services/MetaUpdateService.cs @@ -15,7 +15,7 @@ using starsky.foundation.storage.Interfaces; using starsky.foundation.storage.Services; using starsky.foundation.storage.Storage; -using starsky.foundation.thumbnailgeneration.Interfaces; +using starsky.foundation.thumbnailgeneration.GenerationFactory.Interfaces; using starsky.foundation.writemeta.Interfaces; using starsky.foundation.writemeta.JsonService; using ExifToolCmdHelper = starsky.foundation.writemeta.Helpers.ExifToolCmdHelper; @@ -27,15 +27,15 @@ namespace starsky.feature.metaupdate.Services; [Service(typeof(IMetaUpdateService), InjectionLifetime = InjectionLifetime.Scoped)] public class MetaUpdateService : IMetaUpdateService { - private readonly IQuery _query; private readonly IExifTool _exifTool; - private readonly IReadMetaSubPathStorage _readMeta; private readonly IStorage _iStorage; - private readonly IStorage _thumbnailStorage; - private readonly IMetaPreflight _metaPreflight; private readonly IWebLogger _logger; - private readonly IThumbnailService _thumbnailService; + private readonly IMetaPreflight _metaPreflight; + private readonly IQuery _query; + private readonly IReadMetaSubPathStorage _readMeta; private readonly IThumbnailQuery _thumbnailQuery; + private readonly IThumbnailService _thumbnailService; + private readonly IStorage _thumbnailStorage; [SuppressMessage("Usage", "S107: Constructor has 8 parameters, which is greater than the 7 authorized")] @@ -61,13 +61,17 @@ public MetaUpdateService( } /// - /// Run Update + /// Run Update /// - /// Per file stored string{fileHash}, - /// List*string*{FileIndexItem.name (e.g. Tags) that are changed} + /// + /// Per file stored string{fileHash}, + /// List*string*{FileIndexItem.name (e.g. Tags) that are changed} + /// /// items stored in the database - /// (only used when cache is disabled) - /// This model is overwritten in the database and ExifTool + /// + /// (only used when cache is disabled) + /// This model is overwritten in the database and ExifTool + /// /// enable or disable this feature /// only for disabled cache or changedFileIndexItemName=null /// rotation value 1 left, -1 right, 0 nothing @@ -103,7 +107,7 @@ public async Task> UpdateAsync( _logger.LogError($"Missing in key: {fileIndexItem.FilePath}", new InvalidDataException($"changedFileIndexItemName: " + - $"{string.Join(",", changedFileIndexItemName)}")); + $"{string.Join(",", changedFileIndexItemName)}")); throw new ArgumentException($"Missing in key: {fileIndexItem.FilePath}", nameof(changedFileIndexItemName)); } @@ -117,7 +121,7 @@ public void UpdateReadMetaCache(IEnumerable returnNewResultList) } /// - /// Update ExifTool, Thumbnail, Database and if needed rotateClock + /// Update ExifTool, Thumbnail, Database and if needed rotateClock /// /// output database object /// name of fields updated by exifTool @@ -129,7 +133,7 @@ private async Task UpdateWriteDiskDatabase(FileIndexItem fileIndexItem, await RotationThumbnailExecute(rotateClock, fileIndexItem); if ( fileIndexItem.IsDirectory != true - && ExtensionRolesHelper.IsExtensionExifToolSupported(fileIndexItem.FileName) ) + && ExtensionRolesHelper.IsExtensionExifToolSupported(fileIndexItem.FileName) ) { // feature to exif update var exifUpdateFilePaths = new List { fileIndexItem.FilePath! }; @@ -152,7 +156,7 @@ private async Task UpdateWriteDiskDatabase(FileIndexItem fileIndexItem, : $"[UpdateWriteDiskDatabase] ExifTool result: {exifResult} path:{fileIndexItem.FilePath}"); } else if ( fileIndexItem.ImageFormat != ExtensionRolesHelper.ImageFormat.xmp && - fileIndexItem.ImageFormat != ExtensionRolesHelper.ImageFormat.meta_json ) + fileIndexItem.ImageFormat != ExtensionRolesHelper.ImageFormat.meta_json ) { await new FileIndexItemJsonParser(_iStorage).WriteAsync(fileIndexItem); } @@ -192,7 +196,7 @@ internal async Task ApplyOrGenerateUpdatedFileHash(List newFileHashes, } /// - /// Run the Orientation changes on the thumbnail (only relative) + /// Run the Orientation changes on the thumbnail (only relative) /// /// -1 or 1 /// object contains fileHash diff --git a/starsky/starsky.feature.thumbnail/Services/DatabaseThumbnailGenerationService.cs b/starsky/starsky.feature.thumbnail/Services/DatabaseThumbnailGenerationService.cs index 84b1f38f6c..92f82bd735 100644 --- a/starsky/starsky.feature.thumbnail/Services/DatabaseThumbnailGenerationService.cs +++ b/starsky/starsky.feature.thumbnail/Services/DatabaseThumbnailGenerationService.cs @@ -11,6 +11,7 @@ using starsky.foundation.platform.Interfaces; using starsky.foundation.platform.Models; using starsky.foundation.realtime.Interfaces; +using starsky.foundation.thumbnailgeneration.GenerationFactory.Interfaces; using starsky.foundation.thumbnailgeneration.Interfaces; using starsky.foundation.worker.ThumbnailServices.Interfaces; @@ -122,7 +123,7 @@ internal async Task> WorkThumbnailGeneration( } var generationResultModels = ( - await _thumbnailService.CreateThumbAsync(fileIndexItem + await _thumbnailService.GenerateThumbnail(fileIndexItem .FilePath!, fileIndexItem.FileHash!) ).ToList(); _bgTaskQueue.ThrowExceptionIfCpuUsageIsToHigh("WorkThumbnailGeneration"); diff --git a/starsky/starsky.feature.thumbnail/Services/ManualThumbnailGenerationService.cs b/starsky/starsky.feature.thumbnail/Services/ManualThumbnailGenerationService.cs index c46f33e6e4..818edd105c 100644 --- a/starsky/starsky.feature.thumbnail/Services/ManualThumbnailGenerationService.cs +++ b/starsky/starsky.feature.thumbnail/Services/ManualThumbnailGenerationService.cs @@ -12,7 +12,7 @@ using starsky.foundation.platform.Interfaces; using starsky.foundation.platform.Models; using starsky.foundation.realtime.Interfaces; -using starsky.foundation.thumbnailgeneration.Interfaces; +using starsky.foundation.thumbnailgeneration.GenerationFactory.Interfaces; using starsky.foundation.thumbnailgeneration.Models; using starsky.foundation.worker.ThumbnailServices.Interfaces; @@ -54,7 +54,7 @@ internal async Task WorkThumbnailGeneration(string subPath) try { _logger.LogInformation($"[ThumbnailGenerationController] start {subPath}"); - var thumbs = await _thumbnailService.CreateThumbnailAsync(subPath); + var thumbs = await _thumbnailService.GenerateThumbnail(subPath); var getAllFilesAsync = await _query.GetAllFilesAsync(subPath); var result = diff --git a/starsky/starsky.feature.webhtmlpublish/Services/WebHtmlPublishService.cs b/starsky/starsky.feature.webhtmlpublish/Services/WebHtmlPublishService.cs index 1af4e323d6..c2b0591436 100644 --- a/starsky/starsky.feature.webhtmlpublish/Services/WebHtmlPublishService.cs +++ b/starsky/starsky.feature.webhtmlpublish/Services/WebHtmlPublishService.cs @@ -12,448 +12,447 @@ using starsky.foundation.database.Helpers; using starsky.foundation.database.Models; using starsky.foundation.injection; -using starsky.foundation.platform.Enums; using starsky.foundation.platform.Helpers; using starsky.foundation.platform.Interfaces; using starsky.foundation.platform.Models; +using starsky.foundation.platform.Thumbnails; using starsky.foundation.storage.ArchiveFormats; using starsky.foundation.storage.Exceptions; using starsky.foundation.storage.Helpers; using starsky.foundation.storage.Interfaces; using starsky.foundation.storage.Services; using starsky.foundation.storage.Storage; -using starsky.foundation.thumbnailgeneration.Interfaces; +using starsky.foundation.thumbnailgeneration.GenerationFactory.Interfaces; using starsky.foundation.writemeta.Helpers; using starsky.foundation.writemeta.Interfaces; [assembly: InternalsVisibleTo("starskytest")] -namespace starsky.feature.webhtmlpublish.Services +namespace starsky.feature.webhtmlpublish.Services; + +[Service(typeof(IWebHtmlPublishService), InjectionLifetime = InjectionLifetime.Scoped)] +public class WebHtmlPublishService : IWebHtmlPublishService { - [Service(typeof(IWebHtmlPublishService), InjectionLifetime = InjectionLifetime.Scoped)] - public class WebHtmlPublishService : IWebHtmlPublishService + private readonly AppSettings _appSettings; + private readonly IConsole _console; + private readonly CopyPublishedContent _copyPublishedContent; + private readonly IExifTool _exifTool; + private readonly IStorage _hostFileSystemStorage; + private readonly IWebLogger _logger; + private readonly IOverlayImage _overlayImage; + private readonly PublishManifest _publishManifest; + private readonly IPublishPreflight _publishPreflight; + private readonly IStorage _subPathStorage; + private readonly IThumbnailService _thumbnailService; + private readonly IStorage _thumbnailStorage; + private readonly ToCreateSubfolder _toCreateSubfolder; + + [SuppressMessage("Usage", + "S107: Constructor has 8 parameters, which is greater than the 7 authorized")] + public WebHtmlPublishService(IPublishPreflight publishPreflight, ISelectorStorage + selectorStorage, AppSettings appSettings, IExifToolHostStorage exifTool, + IOverlayImage overlayImage, IConsole console, IWebLogger logger, + IThumbnailService thumbnailService) { - private readonly AppSettings _appSettings; - private readonly IExifTool _exifTool; - private readonly IStorage _subPathStorage; - private readonly IStorage _thumbnailStorage; - private readonly IStorage _hostFileSystemStorage; - private readonly IConsole _console; - private readonly IOverlayImage _overlayImage; - private readonly PublishManifest _publishManifest; - private readonly IPublishPreflight _publishPreflight; - private readonly CopyPublishedContent _copyPublishedContent; - private readonly ToCreateSubfolder _toCreateSubfolder; - private readonly IThumbnailService _thumbnailService; - private readonly IWebLogger _logger; - - [SuppressMessage("Usage", - "S107: Constructor has 8 parameters, which is greater than the 7 authorized")] - public WebHtmlPublishService(IPublishPreflight publishPreflight, ISelectorStorage - selectorStorage, AppSettings appSettings, IExifToolHostStorage exifTool, - IOverlayImage overlayImage, IConsole console, IWebLogger logger, - IThumbnailService thumbnailService) - { - _publishPreflight = publishPreflight; - _subPathStorage = selectorStorage.Get(SelectorStorage.StorageServices.SubPath); - _thumbnailStorage = selectorStorage.Get(SelectorStorage.StorageServices.Thumbnail); - _hostFileSystemStorage = - selectorStorage.Get(SelectorStorage.StorageServices.HostFilesystem); - _appSettings = appSettings; - _exifTool = exifTool; - _console = console; - _overlayImage = overlayImage; - _publishManifest = new PublishManifest(_hostFileSystemStorage); - _toCreateSubfolder = new ToCreateSubfolder(_hostFileSystemStorage); - _copyPublishedContent = new CopyPublishedContent(_toCreateSubfolder, - selectorStorage); - _logger = logger; - _thumbnailService = thumbnailService; - } + _publishPreflight = publishPreflight; + _subPathStorage = selectorStorage.Get(SelectorStorage.StorageServices.SubPath); + _thumbnailStorage = selectorStorage.Get(SelectorStorage.StorageServices.Thumbnail); + _hostFileSystemStorage = + selectorStorage.Get(SelectorStorage.StorageServices.HostFilesystem); + _appSettings = appSettings; + _exifTool = exifTool; + _console = console; + _overlayImage = overlayImage; + _publishManifest = new PublishManifest(_hostFileSystemStorage); + _toCreateSubfolder = new ToCreateSubfolder(_hostFileSystemStorage); + _copyPublishedContent = new CopyPublishedContent(_toCreateSubfolder, + selectorStorage); + _logger = logger; + _thumbnailService = thumbnailService; + } - public async Task?> RenderCopy( - List fileIndexItemsList, - string publishProfileName, string itemName, string outputParentFullFilePathFolder, - bool moveSourceFiles = false) - { - fileIndexItemsList = AddFileHashIfNotExist(fileIndexItemsList); + public async Task?> RenderCopy( + List fileIndexItemsList, + string publishProfileName, string itemName, string outputParentFullFilePathFolder, + bool moveSourceFiles = false) + { + fileIndexItemsList = AddFileHashIfNotExist(fileIndexItemsList); - await PreGenerateThumbnail(fileIndexItemsList, publishProfileName); - var base64ImageArray = await Base64DataUriList(fileIndexItemsList); + await PreGenerateThumbnail(fileIndexItemsList, publishProfileName); + var base64ImageArray = await Base64DataUriList(fileIndexItemsList); - var copyContent = await Render(fileIndexItemsList, base64ImageArray, - publishProfileName, itemName, outputParentFullFilePathFolder, moveSourceFiles); + var copyContent = await Render(fileIndexItemsList, base64ImageArray, + publishProfileName, itemName, outputParentFullFilePathFolder, moveSourceFiles); - _publishManifest.ExportManifest(outputParentFullFilePathFolder, itemName, copyContent); + _publishManifest.ExportManifest(outputParentFullFilePathFolder, itemName, copyContent); - return copyContent; - } + return copyContent; + } - internal List AddFileHashIfNotExist(List fileIndexItemsList) - { - foreach ( var item in fileIndexItemsList.Where(item => - string.IsNullOrEmpty(item.FileHash)) ) - { - item.FileHash = new FileHash(_subPathStorage).GetHashCode(item.FilePath!).Key; - } + /// + /// Generate Zip on Host + /// + /// One folder deeper than where the folder + /// blog item name + /// [[string,bool],[]] + /// + public async Task GenerateZip(string fullFileParentFolderPath, string itemName, + Dictionary? renderCopyResult, + bool deleteFolderAfterwards = false) + { + ArgumentNullException.ThrowIfNull(renderCopyResult); - return fileIndexItemsList; - } + // to keep non publish files out use Where(p => p.Value) + var fileNames = renderCopyResult.Select(p => p.Key).ToList(); + + var slugItemName = GenerateSlugHelper.GenerateSlug(itemName, true); + var filePaths = fileNames + .Select(p => Path.Combine(fullFileParentFolderPath, slugItemName, p)).ToList(); + + new Zipper(_logger).CreateZip(fullFileParentFolderPath, filePaths, fileNames, + slugItemName); + + // Write a single file to be sure that writing is ready + var doneFileFullPath = Path.Combine(_appSettings.TempFolder, slugItemName) + ".done"; + await _hostFileSystemStorage.WriteStreamAsync(StringToStreamHelper.StringToStream("OK"), + doneFileFullPath); - internal bool ShouldSkipExtraLarge(string publishProfileName) + if ( deleteFolderAfterwards ) { - var skipExtraLarge = _publishPreflight.GetPublishProfileName(publishProfileName) - .TrueForAll(p => p.SourceMaxWidth <= 1999); - return skipExtraLarge; + _hostFileSystemStorage.FolderDelete(Path.Combine(_appSettings.TempFolder, + slugItemName)); } + } - internal async Task PreGenerateThumbnail(IEnumerable fileIndexItemsList, - string publishProfileName) + internal List AddFileHashIfNotExist(List fileIndexItemsList) + { + foreach ( var item in fileIndexItemsList.Where(item => + string.IsNullOrEmpty(item.FileHash)) ) { - var skipExtraLarge = ShouldSkipExtraLarge(publishProfileName); - foreach ( var item in fileIndexItemsList ) - { - await _thumbnailService.CreateThumbAsync(item.FilePath, item.FileHash!, - skipExtraLarge); - } + item.FileHash = new FileHash(_subPathStorage).GetHashCode(item.FilePath!).Key; } - /// - /// Get base64 uri lists - /// - /// - private Task Base64DataUriList(IEnumerable fileIndexItemsList) + return fileIndexItemsList; + } + + internal bool ShouldSkipExtraLarge(string publishProfileName) + { + var skipExtraLarge = _publishPreflight.GetPublishProfileName(publishProfileName) + .TrueForAll(p => p.SourceMaxWidth <= 1999); + return skipExtraLarge; + } + + internal async Task PreGenerateThumbnail(IEnumerable fileIndexItemsList, + string publishProfileName) + { + var skipExtraLarge = ShouldSkipExtraLarge(publishProfileName); + foreach ( var item in fileIndexItemsList ) { - return new ToBase64DataUriList(_subPathStorage, - _thumbnailStorage, _logger, _appSettings).Create(fileIndexItemsList.ToList()); + await _thumbnailService.GenerateThumbnail(item.FilePath!, item.FileHash!, + skipExtraLarge); } + } - /// - /// Render output of Publish action - /// - /// which items need to be published - /// list of base64 hashes for html pages - /// name of profile - /// output publish item name - /// path on host disk where to publish to - /// include source files (false by default) - /// - private async Task?> Render(List fileIndexItemsList, - string[]? base64ImageArray, string publishProfileName, string itemName, - string outputParentFullFilePathFolder, bool moveSourceFiles = false) + /// + /// Get base64 uri lists + /// + /// + private Task Base64DataUriList(IEnumerable fileIndexItemsList) + { + return new ToBase64DataUriList(_subPathStorage, + _thumbnailStorage, _logger, _appSettings).Create(fileIndexItemsList.ToList()); + } + + /// + /// Render output of Publish action + /// + /// which items need to be published + /// list of base64 hashes for html pages + /// name of profile + /// output publish item name + /// path on host disk where to publish to + /// include source files (false by default) + /// + private async Task?> Render(List fileIndexItemsList, + string[]? base64ImageArray, string publishProfileName, string itemName, + string outputParentFullFilePathFolder, bool moveSourceFiles = false) + { + if ( _appSettings.PublishProfiles?.Count == 0 ) { - if ( _appSettings.PublishProfiles?.Count == 0 ) - { - _console.WriteLine("There are no config items"); - return null; - } + _console.WriteLine("There are no config items"); + return null; + } - if ( _appSettings.PublishProfiles?.ContainsKey(publishProfileName) == false ) - { - _console.WriteLine("Key not found"); - return null; - } + if ( _appSettings.PublishProfiles?.ContainsKey(publishProfileName) == false ) + { + _console.WriteLine("Key not found"); + return null; + } - if ( !_hostFileSystemStorage.ExistFolder(outputParentFullFilePathFolder) ) - { - _hostFileSystemStorage.CreateDirectory(outputParentFullFilePathFolder); - } + if ( !_hostFileSystemStorage.ExistFolder(outputParentFullFilePathFolder) ) + { + _hostFileSystemStorage.CreateDirectory(outputParentFullFilePathFolder); + } - base64ImageArray ??= new string[fileIndexItemsList.Count]; + base64ImageArray ??= new string[fileIndexItemsList.Count]; - // Order alphabetically - // Ignore Items with Errors - fileIndexItemsList = fileIndexItemsList - .Where(p => p.Status is FileIndexItem.ExifStatus.Ok - or FileIndexItem.ExifStatus.ReadOnly) - .OrderBy(p => p.FileName).ToList(); + // Order alphabetically + // Ignore Items with Errors + fileIndexItemsList = fileIndexItemsList + .Where(p => p.Status is FileIndexItem.ExifStatus.Ok + or FileIndexItem.ExifStatus.ReadOnly) + .OrderBy(p => p.FileName).ToList(); - var copyResult = new Dictionary(); + var copyResult = new Dictionary(); - var profiles = _publishPreflight.GetPublishProfileName(publishProfileName); - foreach ( var currentProfile in profiles ) + var profiles = _publishPreflight.GetPublishProfileName(publishProfileName); + foreach ( var currentProfile in profiles ) + { + switch ( currentProfile.ContentType ) { - switch ( currentProfile.ContentType ) - { - case TemplateContentType.Html: - copyResult.AddRangeOverride(await GenerateWebHtml(profiles, currentProfile, - itemName, - base64ImageArray, fileIndexItemsList, outputParentFullFilePathFolder)); - break; - case TemplateContentType.Jpeg: - copyResult.AddRangeOverride(await GenerateJpeg(currentProfile, - fileIndexItemsList, - outputParentFullFilePathFolder)); - break; - case TemplateContentType.MoveSourceFiles: - copyResult.AddRangeOverride(await GenerateMoveSourceFiles(currentProfile, - fileIndexItemsList, - outputParentFullFilePathFolder, moveSourceFiles)); - break; - case TemplateContentType.PublishContent: - // Copy all items in the subFolder content for example JavaScripts - copyResult.AddRangeOverride( - _copyPublishedContent.CopyContent(currentProfile, - outputParentFullFilePathFolder)); - break; - case TemplateContentType.PublishManifest: - copyResult.Add( - _overlayImage.FilePathOverlayImage("_settings.json", currentProfile) - , true); - break; - case TemplateContentType.OnlyFirstJpeg: - var item = fileIndexItemsList.FirstOrDefault(); - if ( item == null ) - { - break; - } - - var firstInList = new List { item }; - copyResult.AddRangeOverride(await GenerateJpeg(currentProfile, firstInList, + case TemplateContentType.Html: + copyResult.AddRangeOverride(await GenerateWebHtml(profiles, currentProfile, + itemName, + base64ImageArray, fileIndexItemsList, outputParentFullFilePathFolder)); + break; + case TemplateContentType.Jpeg: + copyResult.AddRangeOverride(await GenerateJpeg(currentProfile, + fileIndexItemsList, + outputParentFullFilePathFolder)); + break; + case TemplateContentType.MoveSourceFiles: + copyResult.AddRangeOverride(await GenerateMoveSourceFiles(currentProfile, + fileIndexItemsList, + outputParentFullFilePathFolder, moveSourceFiles)); + break; + case TemplateContentType.PublishContent: + // Copy all items in the subFolder content for example JavaScripts + copyResult.AddRangeOverride( + _copyPublishedContent.CopyContent(currentProfile, outputParentFullFilePathFolder)); + break; + case TemplateContentType.PublishManifest: + copyResult.Add( + _overlayImage.FilePathOverlayImage("_settings.json", currentProfile) + , true); + break; + case TemplateContentType.OnlyFirstJpeg: + var item = fileIndexItemsList.FirstOrDefault(); + if ( item == null ) + { break; - } - } + } - return copyResult; + var firstInList = new List { item }; + copyResult.AddRangeOverride(await GenerateJpeg(currentProfile, firstInList, + outputParentFullFilePathFolder)); + break; + } } - internal async Task> GenerateWebHtml( - List profiles, - AppSettingsPublishProfiles currentProfile, string itemName, string[] base64ImageArray, - IEnumerable fileIndexItemsList, string outputParentFullFilePathFolder) - { - if ( string.IsNullOrEmpty(currentProfile.Template) ) - { - _console.WriteLine("CurrentProfile Template not configured"); - return new Dictionary(); - } + return copyResult; + } - // Generates html by razorLight - var viewModel = new WebHtmlViewModel - { - ItemName = itemName, - Profiles = profiles, - AppSettings = _appSettings, - CurrentProfile = currentProfile, - Base64ImageArray = base64ImageArray, - // apply slug to items, but use it only in the copy - FileIndexItems = fileIndexItemsList.Select(c => c.Clone()).ToList(), - }; - - // add to IClonable - foreach ( var item in viewModel.FileIndexItems ) - { - item.FileName = GenerateSlugHelper.GenerateSlug(item.FileCollectionName!, true) + - Path.GetExtension(item.FileName); - } + internal async Task> GenerateWebHtml( + List profiles, + AppSettingsPublishProfiles currentProfile, string itemName, string[] base64ImageArray, + IEnumerable fileIndexItemsList, string outputParentFullFilePathFolder) + { + if ( string.IsNullOrEmpty(currentProfile.Template) ) + { + _console.WriteLine("CurrentProfile Template not configured"); + return new Dictionary(); + } - // has a direct dependency on the filesystem - var embeddedResult = await new ParseRazor(_hostFileSystemStorage, _logger) - .EmbeddedViews(currentProfile.Template, viewModel); + // Generates html by razorLight + var viewModel = new WebHtmlViewModel + { + ItemName = itemName, + Profiles = profiles, + AppSettings = _appSettings, + CurrentProfile = currentProfile, + Base64ImageArray = base64ImageArray, + // apply slug to items, but use it only in the copy + FileIndexItems = fileIndexItemsList.Select(c => c.Clone()).ToList() + }; + + // add to IClonable + foreach ( var item in viewModel.FileIndexItems ) + { + item.FileName = GenerateSlugHelper.GenerateSlug(item.FileCollectionName!, true) + + Path.GetExtension(item.FileName); + } - var stream = StringToStreamHelper.StringToStream(embeddedResult); - await _hostFileSystemStorage.WriteStreamAsync(stream, - Path.Combine(outputParentFullFilePathFolder, currentProfile.Path)); + // has a direct dependency on the filesystem + var embeddedResult = await new ParseRazor(_hostFileSystemStorage, _logger) + .EmbeddedViews(currentProfile.Template, viewModel); - _console.Write(_appSettings.IsVerbose() ? embeddedResult + "\n" : "•"); + var stream = StringToStreamHelper.StringToStream(embeddedResult); + await _hostFileSystemStorage.WriteStreamAsync(stream, + Path.Combine(outputParentFullFilePathFolder, currentProfile.Path)); - return new Dictionary - { - { - currentProfile.Path.Replace(outputParentFullFilePathFolder, string.Empty), - currentProfile.Copy - } - }; - } + _console.Write(_appSettings.IsVerbose() ? embeddedResult + "\n" : "•"); - /// - /// Generate loop of Jpeg images with overlay image - /// With Retry included - /// - /// contains sizes - /// list of items to generate jpeg for - /// outputParentFullFilePathFolder - /// when failed output, has default value - /// - internal async Task> GenerateJpeg( - AppSettingsPublishProfiles profile, - IReadOnlyCollection fileIndexItemsList, - string outputParentFullFilePathFolder, int delay = 6) + return new Dictionary { - _toCreateSubfolder.Create(profile, outputParentFullFilePathFolder); - - foreach ( var item in fileIndexItemsList ) { - var outputPath = _overlayImage.FilePathOverlayImage(outputParentFullFilePathFolder, - item.FilePath!, profile); - - async Task ResizerLocal() - { - return await Resizer(outputPath, profile, item); - } - - try - { - await RetryHelper.DoAsync(ResizerLocal, TimeSpan.FromSeconds(delay)); - } - catch ( AggregateException e ) - { - _logger.LogError( - $"[ResizerLocal] Skip due errors: (catch-ed exception) {item.FilePath} {item.FileHash}"); - foreach ( var exception in e.InnerExceptions ) - { - _logger.LogError("[ResizerLocal] " + exception.Message, exception); - } - } + currentProfile.Path.Replace(outputParentFullFilePathFolder, string.Empty), + currentProfile.Copy } + }; + } - return fileIndexItemsList.ToDictionary(item => - _overlayImage.FilePathOverlayImage(item.FilePath!, profile), - _ => profile.Copy); - } + /// + /// Generate loop of Jpeg images with overlay image + /// With Retry included + /// + /// contains sizes + /// list of items to generate jpeg for + /// outputParentFullFilePathFolder + /// when failed output, has default value + /// + internal async Task> GenerateJpeg( + AppSettingsPublishProfiles profile, + IReadOnlyCollection fileIndexItemsList, + string outputParentFullFilePathFolder, int delay = 6) + { + _toCreateSubfolder.Create(profile, outputParentFullFilePathFolder); - /// - /// Resize image with overlay - /// - /// absolute path of output on host disk - /// size of output, overlay size, must contain metaData - /// database item with filePath - /// true when success - /// when output is not valid - private async Task Resizer(string outputPath, AppSettingsPublishProfiles profile, - FileIndexItem item) + foreach ( var item in fileIndexItemsList ) { - // for less than 1000px - if ( profile.SourceMaxWidth <= 1000 && - _thumbnailStorage.ExistFile( - ThumbnailNameHelper.Combine(item.FileHash!, ThumbnailSize.Large)) ) - { - await _overlayImage.ResizeOverlayImageThumbnails(item.FileHash!, outputPath, - profile); - } - else if ( profile.SourceMaxWidth <= 2000 && - _thumbnailStorage.ExistFile( - ThumbnailNameHelper.Combine(item.FileHash!, ThumbnailSize.ExtraLarge)) ) - { - await _overlayImage.ResizeOverlayImageThumbnails( - ThumbnailNameHelper.Combine(item.FileHash!, ThumbnailSize.ExtraLarge), - outputPath, profile); - } - else if ( _subPathStorage.ExistFile(item.FilePath!) ) + var outputPath = _overlayImage.FilePathOverlayImage(outputParentFullFilePathFolder, + item.FilePath!, profile); + + async Task ResizerLocal() { - // Thumbs are 2000 px (and larger) - await _overlayImage.ResizeOverlayImageLarge(item.FilePath!, outputPath, profile); + return await Resizer(outputPath, profile, item); } - if ( profile.MetaData ) + try { - await MetaData(item, outputPath); + await RetryHelper.DoAsync(ResizerLocal, TimeSpan.FromSeconds(delay)); } - - var imageFormat = - ExtensionRolesHelper.GetImageFormat( - _hostFileSystemStorage.ReadStream(outputPath, 160)); - if ( imageFormat == ExtensionRolesHelper.ImageFormat.jpg ) + catch ( AggregateException e ) { - return true; + _logger.LogError( + $"[ResizerLocal] Skip due errors: (catch-ed exception) {item.FilePath} {item.FileHash}"); + foreach ( var exception in e.InnerExceptions ) + { + _logger.LogError("[ResizerLocal] " + exception.Message, exception); + } } + } - _hostFileSystemStorage.FileDelete(outputPath); + return fileIndexItemsList.ToDictionary(item => + _overlayImage.FilePathOverlayImage(item.FilePath!, profile), + _ => profile.Copy); + } - throw new DecodingException("[WebHtmlPublishService] image output is not valid"); + /// + /// Resize image with overlay + /// + /// absolute path of output on host disk + /// size of output, overlay size, must contain metaData + /// database item with filePath + /// true when success + /// when output is not valid + private async Task Resizer(string outputPath, AppSettingsPublishProfiles profile, + FileIndexItem item) + { + // for less than 1000px + if ( profile.SourceMaxWidth <= 1000 && + _thumbnailStorage.ExistFile( + ThumbnailNameHelper.Combine(item.FileHash!, ThumbnailSize.Large)) ) + { + await _overlayImage.ResizeOverlayImageThumbnails(item.FileHash!, outputPath, + profile); + } + else if ( profile.SourceMaxWidth <= 2000 && + _thumbnailStorage.ExistFile( + ThumbnailNameHelper.Combine(item.FileHash!, ThumbnailSize.ExtraLarge)) ) + { + await _overlayImage.ResizeOverlayImageThumbnails( + ThumbnailNameHelper.Combine(item.FileHash!, ThumbnailSize.ExtraLarge), + outputPath, profile); + } + else if ( _subPathStorage.ExistFile(item.FilePath!) ) + { + // Thumbs are 2000 px (and larger) + await _overlayImage.ResizeOverlayImageLarge(item.FilePath!, outputPath, profile); } - /// - /// Copy the metaData over the output path - /// - /// all the meta data - /// absolute path on host disk - private async Task MetaData(FileIndexItem item, string outputPath) + if ( profile.MetaData ) { - if ( !_subPathStorage.ExistFile(item.FilePath!) ) - { - return; - } + await MetaData(item, outputPath); + } - // Write the metadata to the new created file - var comparedNames = FileIndexCompareHelper.Compare( - new FileIndexItem(), item); + var imageFormat = + ExtensionRolesHelper.GetImageFormat( + _hostFileSystemStorage.ReadStream(outputPath, 160)); + if ( imageFormat == ExtensionRolesHelper.ImageFormat.jpg ) + { + return true; + } - // Output has already rotated the image - var rotation = nameof(FileIndexItem.Orientation).ToLowerInvariant(); + _hostFileSystemStorage.FileDelete(outputPath); - // should already check if it exists - comparedNames.Remove(rotation); + throw new DecodingException("[WebHtmlPublishService] image output is not valid"); + } - // Write it back - await new ExifToolCmdHelper(_exifTool, _hostFileSystemStorage, - _thumbnailStorage, null!, null!, _logger).UpdateAsync(item, - new List { outputPath }, comparedNames, - false, false); + /// + /// Copy the metaData over the output path + /// + /// all the meta data + /// absolute path on host disk + private async Task MetaData(FileIndexItem item, string outputPath) + { + if ( !_subPathStorage.ExistFile(item.FilePath!) ) + { + return; } - internal async Task> GenerateMoveSourceFiles( - AppSettingsPublishProfiles profile, - IReadOnlyCollection fileIndexItemsList, - string outputParentFullFilePathFolder, bool moveSourceFiles) - { - _toCreateSubfolder.Create(profile, outputParentFullFilePathFolder); + // Write the metadata to the new created file + var comparedNames = FileIndexCompareHelper.Compare( + new FileIndexItem(), item); - foreach ( var subPath in fileIndexItemsList.Select(p => p.FilePath!) ) - { - // input: item.FilePath - var outputPath = _overlayImage.FilePathOverlayImage(outputParentFullFilePathFolder, - subPath, profile); + // Output has already rotated the image + var rotation = nameof(FileIndexItem.Orientation).ToLowerInvariant(); - await _hostFileSystemStorage.WriteStreamAsync(_subPathStorage.ReadStream(subPath), - outputPath); + // should already check if it exists + comparedNames.Remove(rotation); - // only delete when using in cli mode - if ( moveSourceFiles ) - { - _subPathStorage.FileDelete(subPath); - } - } + // Write it back + await new ExifToolCmdHelper(_exifTool, _hostFileSystemStorage, + _thumbnailStorage, null!, null!, _logger).UpdateAsync(item, + new List { outputPath }, comparedNames, + false, false); + } - return fileIndexItemsList.ToDictionary(item => - _overlayImage.FilePathOverlayImage(item.FilePath!, profile), - _ => profile.Copy); - } + internal async Task> GenerateMoveSourceFiles( + AppSettingsPublishProfiles profile, + IReadOnlyCollection fileIndexItemsList, + string outputParentFullFilePathFolder, bool moveSourceFiles) + { + _toCreateSubfolder.Create(profile, outputParentFullFilePathFolder); - /// - /// Generate Zip on Host - /// - /// One folder deeper than where the folder - /// blog item name - /// [[string,bool],[]] - /// - public async Task GenerateZip(string fullFileParentFolderPath, string itemName, - Dictionary? renderCopyResult, - bool deleteFolderAfterwards = false) + foreach ( var subPath in fileIndexItemsList.Select(p => p.FilePath!) ) { - ArgumentNullException.ThrowIfNull(renderCopyResult); - - // to keep non publish files out use Where(p => p.Value) - var fileNames = renderCopyResult.Select(p => p.Key).ToList(); + // input: item.FilePath + var outputPath = _overlayImage.FilePathOverlayImage(outputParentFullFilePathFolder, + subPath, profile); - var slugItemName = GenerateSlugHelper.GenerateSlug(itemName, true); - var filePaths = fileNames - .Select(p => Path.Combine(fullFileParentFolderPath, slugItemName, p)).ToList(); + await _hostFileSystemStorage.WriteStreamAsync(_subPathStorage.ReadStream(subPath), + outputPath); - new Zipper(_logger).CreateZip(fullFileParentFolderPath, filePaths, fileNames, - slugItemName); - - // Write a single file to be sure that writing is ready - var doneFileFullPath = Path.Combine(_appSettings.TempFolder, slugItemName) + ".done"; - await _hostFileSystemStorage.WriteStreamAsync(StringToStreamHelper.StringToStream("OK"), - doneFileFullPath); - - if ( deleteFolderAfterwards ) + // only delete when using in cli mode + if ( moveSourceFiles ) { - _hostFileSystemStorage.FolderDelete(Path.Combine(_appSettings.TempFolder, - slugItemName)); + _subPathStorage.FileDelete(subPath); } } + + return fileIndexItemsList.ToDictionary(item => + _overlayImage.FilePathOverlayImage(item.FilePath!, profile), + _ => profile.Copy); } } diff --git a/starsky/starsky.foundation.database/Models/ThumbnailResultDataTransferModel.cs b/starsky/starsky.foundation.database/Models/ThumbnailResultDataTransferModel.cs index ea9a146a03..f7ffc54ccc 100644 --- a/starsky/starsky.foundation.database/Models/ThumbnailResultDataTransferModel.cs +++ b/starsky/starsky.foundation.database/Models/ThumbnailResultDataTransferModel.cs @@ -1,11 +1,12 @@ using System; -using starsky.foundation.platform.Enums; +using starsky.foundation.platform.Thumbnails; namespace starsky.foundation.database.Models; public class ThumbnailResultDataTransferModel { - public ThumbnailResultDataTransferModel(string fileHash, bool? tinyMeta = null, bool? small = null, bool? large = null, bool? extraLarge = null) + public ThumbnailResultDataTransferModel(string fileHash, bool? tinyMeta = null, + bool? small = null, bool? large = null, bool? extraLarge = null) { FileHash = fileHash; if ( tinyMeta != null ) @@ -29,10 +30,37 @@ public ThumbnailResultDataTransferModel(string fileHash, bool? tinyMeta = null, } } + public string? FileHash { get; set; } + + /// + /// 150px, null is to-do, false is error, true, is done + /// + public bool? TinyMeta { get; set; } + + /// + /// 300px, null is to-do, false is error, true, is done + /// + public bool? Small { get; set; } + + /// + /// 1000px, null is to-do, false is error, true, is done + /// + public bool? Large { get; set; } + + /// + /// 2000px, null is to-do, false is error, true, is done + /// + public bool? ExtraLarge { get; set; } + + /// + /// When something went wrong add message here + /// + public string? Reasons { get; set; } + /// - /// Null is to-do - /// True is done - /// False is Failed + /// Null is to-do + /// True is done + /// False is Failed /// /// The size /// Null is to-do | True is done | False is Failed @@ -59,31 +87,4 @@ public void Change(ThumbnailSize? thumbnailSize = null, bool? setStatus = null) throw new ArgumentOutOfRangeException(nameof(thumbnailSize), thumbnailSize, null); } } - - public string? FileHash { get; set; } - - /// - /// 150px, null is to-do, false is error, true, is done - /// - public bool? TinyMeta { get; set; } - - /// - /// 300px, null is to-do, false is error, true, is done - /// - public bool? Small { get; set; } - - /// - /// 1000px, null is to-do, false is error, true, is done - /// - public bool? Large { get; set; } - - /// - /// 2000px, null is to-do, false is error, true, is done - /// - public bool? ExtraLarge { get; set; } - - /// - /// When something went wrong add message here - /// - public string? Reasons { get; set; } } diff --git a/starsky/starsky.foundation.database/Query/QueryCollections.cs b/starsky/starsky.foundation.database/Query/QueryCollections.cs index 34a63acd58..59034ea717 100644 --- a/starsky/starsky.foundation.database/Query/QueryCollections.cs +++ b/starsky/starsky.foundation.database/Query/QueryCollections.cs @@ -4,59 +4,62 @@ using starsky.foundation.database.Models; using starsky.foundation.platform.Helpers; -namespace starsky.foundation.database.Query +namespace starsky.foundation.database.Query; + +public partial class Query // QueryCollections { - public partial class Query // QueryCollections + internal static List StackCollections(List databaseSubFolderList) { - internal static List StackCollections(List databaseSubFolderList) - { - // Get a list of duplicate items - var stackItemsByFileCollectionName = databaseSubFolderList - .GroupBy(item => item.FileCollectionName) - .SelectMany(grp => grp.Skip(1).Take(1)).ToList(); - // databaseSubFolderList.ToList() > Collection was modified; enumeration operation may not execute. - - // duplicateItemsByFilePath > - // If you have 3 item with the same name it will include 1 name - // So we do a linq query to search simalar items - // We keep the first item - // And Delete duplicate items - - var querySubFolderList = new List(); - // Do not remove it from: databaseSubFolderList otherwise it will be deleted from cache - - foreach ( var stackItemByName in stackItemsByFileCollectionName ) - { - var duplicateItems = databaseSubFolderList.Where(p => - p.FileCollectionName == stackItemByName.FileCollectionName).ToList(); + // Get a list of duplicate items + var stackItemsByFileCollectionName = databaseSubFolderList + .GroupBy(item => item.FileCollectionName) + .SelectMany(grp => grp.Skip(1).Take(1)).ToList(); + // databaseSubFolderList.ToList() > Collection was modified; enumeration operation may not execute. - // The idea to pick thumbnail based images first, followed by non-thumb supported - // when not pick alphabetically + // duplicateItemsByFilePath > + // If you have 3 item with the same name it will include 1 name + // So we do a linq query to search simalar items + // We keep the first item + // And Delete duplicate items - querySubFolderList.AddRange(duplicateItems.Where(item => - ExtensionRolesHelper.IsExtensionThumbnailSupported(item.FileName))); - } + var querySubFolderList = new List(); + // Do not remove it from: databaseSubFolderList otherwise it will be deleted from cache + + foreach ( var stackItemByName in stackItemsByFileCollectionName ) + { + var duplicateItems = databaseSubFolderList.Where(p => + p.FileCollectionName == stackItemByName.FileCollectionName).ToList(); - return AddNonDuplicateBackToList(databaseSubFolderList, stackItemsByFileCollectionName, querySubFolderList); + // The idea to pick thumbnail based images first, followed by non-thumb supported + // when not pick alphabetically + + querySubFolderList.AddRange(duplicateItems.Where(item => + ExtensionRolesHelper.IsExtensionImageSharpThumbnailSupported(item.FileName))); } - [SuppressMessage("Usage", "S3267:Loops should be simplified with LINQ expressions")] - [SuppressMessage("Performance", "CA1859:Use concrete types when possible for improved performance")] - private static List AddNonDuplicateBackToList(IEnumerable databaseSubFolderList, - IReadOnlyCollection stackItemsByFileCollectionName, ICollection querySubFolderList) + return AddNonDuplicateBackToList(databaseSubFolderList, stackItemsByFileCollectionName, + querySubFolderList); + } + + [SuppressMessage("Usage", "S3267:Loops should be simplified with LINQ expressions")] + [SuppressMessage("Performance", + "CA1859:Use concrete types when possible for improved performance")] + private static List AddNonDuplicateBackToList( + IEnumerable databaseSubFolderList, + IReadOnlyCollection stackItemsByFileCollectionName, + ICollection querySubFolderList) + { + // Then add the items that are non duplicate back to the list + foreach ( var dbItem in databaseSubFolderList.ToList() ) { - // Then add the items that are non duplicate back to the list - foreach ( var dbItem in databaseSubFolderList.ToList() ) + // check if any item is duplicate + if ( stackItemsByFileCollectionName.All(p => + p.FileCollectionName != dbItem.FileCollectionName) ) { - // check if any item is duplicate - if ( stackItemsByFileCollectionName.All(p => - p.FileCollectionName != dbItem.FileCollectionName) ) - { - querySubFolderList.Add(dbItem); - } + querySubFolderList.Add(dbItem); } - - return querySubFolderList.OrderBy(p => p.FileName).ToList(); } + + return querySubFolderList.OrderBy(p => p.FileName).ToList(); } } diff --git a/starsky/starsky.foundation.database/Query/QueryGetNextPrevInFolder.cs b/starsky/starsky.foundation.database/Query/QueryGetNextPrevInFolder.cs index 5a8775c758..c7abbbc6da 100644 --- a/starsky/starsky.foundation.database/Query/QueryGetNextPrevInFolder.cs +++ b/starsky/starsky.foundation.database/Query/QueryGetNextPrevInFolder.cs @@ -24,7 +24,7 @@ List LocalQuery(ApplicationDbContext context) .GroupBy(item => item.FileCollectionName) .Select(group => group.OrderByDescending(p => - ExtensionRolesHelper.IsExtensionThumbnailSupported(p.FilePath) + ExtensionRolesHelper.IsExtensionImageSharpThumbnailSupported(p.FilePath) ).First() ) .ToList(); diff --git a/starsky/starsky.foundation.platform/Helpers/ExtensionRolesHelper.cs b/starsky/starsky.foundation.platform/Helpers/ExtensionRolesHelper.cs index ad1da767e6..29829947f2 100644 --- a/starsky/starsky.foundation.platform/Helpers/ExtensionRolesHelper.cs +++ b/starsky/starsky.foundation.platform/Helpers/ExtensionRolesHelper.cs @@ -189,7 +189,7 @@ private static List ExtensionExifToolSupportedList /// /// The extension thumb supported list. /// - public static List ExtensionThumbSupportedList + public static List ExtensionImageSharpThumbnailSupportedList { get { @@ -313,13 +313,14 @@ public static bool IsExtensionSyncSupported(string filename) /// /// the name of the file with extenstion /// true, if imageSharp can write to this - public static bool IsExtensionThumbnailSupported(string? filename) + public static bool IsExtensionImageSharpThumbnailSupported(string? filename) { - return IsExtensionForce(filename?.ToLowerInvariant(), ExtensionThumbSupportedList); + return IsExtensionForce(filename?.ToLowerInvariant(), + ExtensionImageSharpThumbnailSupportedList); } - public static bool IsExtensionVideoSupported(string fileName) + public static bool IsExtensionVideoSupported(string? fileName) { return IsExtensionForce(fileName?.ToLowerInvariant(), ExtensionVideoSupportedList); } diff --git a/starsky/starsky.foundation.platform/Enums/ThumbnailSizeType.cs b/starsky/starsky.foundation.platform/Thumbnails/ThumbnailSizeType.cs similarity index 54% rename from starsky/starsky.foundation.platform/Enums/ThumbnailSizeType.cs rename to starsky/starsky.foundation.platform/Thumbnails/ThumbnailSizeType.cs index a629053526..911757dae5 100644 --- a/starsky/starsky.foundation.platform/Enums/ThumbnailSizeType.cs +++ b/starsky/starsky.foundation.platform/Thumbnails/ThumbnailSizeType.cs @@ -1,32 +1,32 @@ -namespace starsky.foundation.platform.Enums; +namespace starsky.foundation.platform.Thumbnails; /// -/// These values are stored in a database, so don't change them +/// These values are stored in a database, so don't change them /// public enum ThumbnailSize { /// - /// Should not use this one + /// Should not use this one /// Unknown = 0, /// - /// 150px + /// 150px /// TinyMeta = 10, /// - /// 300px + /// 300px /// Small = 20, /// - /// 1000px + /// 1000px /// Large = 30, /// - /// 2000px + /// 2000px /// - ExtraLarge = 40, + ExtraLarge = 40 } diff --git a/starsky/starsky.foundation.platform/Thumbnails/ThumbnailSizes.cs b/starsky/starsky.foundation.platform/Thumbnails/ThumbnailSizes.cs new file mode 100644 index 0000000000..e2b37fff8f --- /dev/null +++ b/starsky/starsky.foundation.platform/Thumbnails/ThumbnailSizes.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace starsky.foundation.platform.Thumbnails; + +public static class ThumbnailSizes +{ + public static List GetSizes(bool skipExtraLarge) + { + var sizesList = new List { ThumbnailSize.Small, ThumbnailSize.Large }; + + if ( !skipExtraLarge ) + { + sizesList.Add(ThumbnailSize.ExtraLarge); + } + + return sizesList; + } +} diff --git a/starsky/starsky.foundation.storage/Storage/ThumbnailFileMoveAllSizes.cs b/starsky/starsky.foundation.storage/Storage/ThumbnailFileMoveAllSizes.cs index 01db129444..e38df4aabd 100644 --- a/starsky/starsky.foundation.storage/Storage/ThumbnailFileMoveAllSizes.cs +++ b/starsky/starsky.foundation.storage/Storage/ThumbnailFileMoveAllSizes.cs @@ -1,31 +1,30 @@ -using starsky.foundation.platform.Enums; +using starsky.foundation.platform.Thumbnails; using starsky.foundation.storage.Interfaces; -namespace starsky.foundation.storage.Storage +namespace starsky.foundation.storage.Storage; + +public sealed class ThumbnailFileMoveAllSizes { - public sealed class ThumbnailFileMoveAllSizes - { - private readonly IStorage _thumbnailStorage; + private readonly IStorage _thumbnailStorage; - public ThumbnailFileMoveAllSizes(IStorage thumbnailStorage) - { - _thumbnailStorage = thumbnailStorage; - } + public ThumbnailFileMoveAllSizes(IStorage thumbnailStorage) + { + _thumbnailStorage = thumbnailStorage; + } - public void FileMove(string oldFileHash, string newHashCode) - { - _thumbnailStorage.FileMove( - ThumbnailNameHelper.Combine(oldFileHash, ThumbnailSize.Large), - ThumbnailNameHelper.Combine(newHashCode, ThumbnailSize.Large)); - _thumbnailStorage.FileMove( - ThumbnailNameHelper.Combine(oldFileHash, ThumbnailSize.Small), - ThumbnailNameHelper.Combine(newHashCode, ThumbnailSize.Small)); - _thumbnailStorage.FileMove( - ThumbnailNameHelper.Combine(oldFileHash, ThumbnailSize.ExtraLarge), - ThumbnailNameHelper.Combine(newHashCode, ThumbnailSize.ExtraLarge)); - _thumbnailStorage.FileMove( - ThumbnailNameHelper.Combine(oldFileHash, ThumbnailSize.TinyMeta), - ThumbnailNameHelper.Combine(newHashCode, ThumbnailSize.TinyMeta)); - } + public void FileMove(string oldFileHash, string newHashCode) + { + _thumbnailStorage.FileMove( + ThumbnailNameHelper.Combine(oldFileHash, ThumbnailSize.Large), + ThumbnailNameHelper.Combine(newHashCode, ThumbnailSize.Large)); + _thumbnailStorage.FileMove( + ThumbnailNameHelper.Combine(oldFileHash, ThumbnailSize.Small), + ThumbnailNameHelper.Combine(newHashCode, ThumbnailSize.Small)); + _thumbnailStorage.FileMove( + ThumbnailNameHelper.Combine(oldFileHash, ThumbnailSize.ExtraLarge), + ThumbnailNameHelper.Combine(newHashCode, ThumbnailSize.ExtraLarge)); + _thumbnailStorage.FileMove( + ThumbnailNameHelper.Combine(oldFileHash, ThumbnailSize.TinyMeta), + ThumbnailNameHelper.Combine(newHashCode, ThumbnailSize.TinyMeta)); } } diff --git a/starsky/starsky.foundation.storage/Storage/ThumbnailNameHelper.cs b/starsky/starsky.foundation.storage/Storage/ThumbnailNameHelper.cs index e8ea952bb2..53f9401653 100644 --- a/starsky/starsky.foundation.storage/Storage/ThumbnailNameHelper.cs +++ b/starsky/starsky.foundation.storage/Storage/ThumbnailNameHelper.cs @@ -1,149 +1,147 @@ using System; using System.Globalization; using System.Text.RegularExpressions; -using starsky.foundation.platform.Enums; +using starsky.foundation.platform.Thumbnails; -namespace starsky.foundation.storage.Storage +namespace starsky.foundation.storage.Storage; + +/// +/// ThumbnailNamesHelper +/// +public static partial class ThumbnailNameHelper { /// - /// ThumbnailNamesHelper + /// Without TinyMeta /// - public static partial class ThumbnailNameHelper + public static readonly ThumbnailSize[] GeneratedThumbnailSizes = new[] { - /// - /// Without TinyMeta - /// - public static readonly ThumbnailSize[] GeneratedThumbnailSizes = new ThumbnailSize[] - { - ThumbnailSize.ExtraLarge, ThumbnailSize.Small, ThumbnailSize.Large - }; + ThumbnailSize.ExtraLarge, ThumbnailSize.Small, ThumbnailSize.Large + }; - public static readonly ThumbnailSize[] SecondGeneratedThumbnailSizes = new ThumbnailSize[] - { - ThumbnailSize.Small, - ThumbnailSize - .Large // <- will be false when skipExtraLarge = true, its already created - }; + public static readonly ThumbnailSize[] SecondGeneratedThumbnailSizes = new[] + { + ThumbnailSize.Small, ThumbnailSize + .Large // <- will be false when skipExtraLarge = true, its already created + }; - public static readonly ThumbnailSize[] AllThumbnailSizes = new ThumbnailSize[] - { - ThumbnailSize.TinyMeta, ThumbnailSize.ExtraLarge, ThumbnailSize.Small, - ThumbnailSize.Large - }; + public static readonly ThumbnailSize[] AllThumbnailSizes = new[] + { + ThumbnailSize.TinyMeta, ThumbnailSize.ExtraLarge, ThumbnailSize.Small, + ThumbnailSize.Large + }; - public static int GetSize(ThumbnailSize size) + public static int GetSize(ThumbnailSize size) + { + switch ( size ) { - switch ( size ) - { - case ThumbnailSize.TinyMeta: - return 150; - case ThumbnailSize.Small: - return 300; - case ThumbnailSize.Large: - return 1000; - case ThumbnailSize.ExtraLarge: - return 2000; - default: - throw new ArgumentOutOfRangeException(nameof(size), size, null); - } + case ThumbnailSize.TinyMeta: + return 150; + case ThumbnailSize.Small: + return 300; + case ThumbnailSize.Large: + return 1000; + case ThumbnailSize.ExtraLarge: + return 2000; + default: + throw new ArgumentOutOfRangeException(nameof(size), size, null); } + } - public static ThumbnailSize GetSize(int size) + public static ThumbnailSize GetSize(int size) + { + switch ( size ) { - switch ( size ) - { - case 150: - return ThumbnailSize.TinyMeta; - case 300: - return ThumbnailSize.Small; - case 1000: - return ThumbnailSize.Large; - case 2000: - return ThumbnailSize.ExtraLarge; - default: - return ThumbnailSize.Unknown; - } + case 150: + return ThumbnailSize.TinyMeta; + case 300: + return ThumbnailSize.Small; + case 1000: + return ThumbnailSize.Large; + case 2000: + return ThumbnailSize.ExtraLarge; + default: + return ThumbnailSize.Unknown; } + } - public static ThumbnailSize GetSize(string fileName) - { - var fileNameWithoutExtension = - fileName.Replace(".jpg", string.Empty); - - var afterAtString = Regex.Match(fileNameWithoutExtension, "@\\d+", - RegexOptions.None, TimeSpan.FromMilliseconds(200)) - .Value.Replace("@", string.Empty); - - if ( fileNameWithoutExtension.Replace($"@{afterAtString}", string.Empty).Length != 26 ) - { - return ThumbnailSize.Unknown; - } + public static ThumbnailSize GetSize(string fileName) + { + var fileNameWithoutExtension = + fileName.Replace(".jpg", string.Empty); - if ( string.IsNullOrEmpty(afterAtString) ) - { - return ThumbnailSize.Large; - } + var afterAtString = Regex.Match(fileNameWithoutExtension, "@\\d+", + RegexOptions.None, TimeSpan.FromMilliseconds(200)) + .Value.Replace("@", string.Empty); - int.TryParse(afterAtString, NumberStyles.Number, - CultureInfo.InvariantCulture, out var afterAt); - return GetSize(afterAt); + if ( fileNameWithoutExtension.Replace($"@{afterAtString}", string.Empty).Length != 26 ) + { + return ThumbnailSize.Unknown; } - public static string Combine(string fileHash, int size) + if ( string.IsNullOrEmpty(afterAtString) ) { - return Combine(fileHash, GetSize(size)); + return ThumbnailSize.Large; } - public static string Combine(string fileHash, ThumbnailSize size, - bool appendExtension = false) - { - if ( appendExtension ) - { - return fileHash + GetAppend(size) + ".jpg"; - } + int.TryParse(afterAtString, NumberStyles.Number, + CultureInfo.InvariantCulture, out var afterAt); + return GetSize(afterAt); + } - return fileHash + GetAppend(size); - } + public static string Combine(string fileHash, int size) + { + return Combine(fileHash, GetSize(size)); + } - private static string GetAppend(ThumbnailSize size) + public static string Combine(string fileHash, ThumbnailSize size, + bool appendExtension = false) + { + if ( appendExtension ) { - switch ( size ) - { - case ThumbnailSize.TinyMeta: - return "@meta"; - case ThumbnailSize.Small: - return "@300"; - case ThumbnailSize.Large: - return string.Empty; - case ThumbnailSize.ExtraLarge: - return "@2000"; - default: - throw new ArgumentOutOfRangeException(nameof(size), size, null); - } + return fileHash + GetAppend(size) + ".jpg"; } - public static string RemoveSuffix(string? thumbnailOutputHash) - { - return thumbnailOutputHash == null - ? string.Empty - : Regex.Replace(thumbnailOutputHash, "@\\d+", - string.Empty, RegexOptions.None, TimeSpan.FromMilliseconds(100)); - } + return fileHash + GetAppend(size); + } - /// - /// ThumbnailName - /// Regex.IsMatch (pre compiled regex) - /// - /// Regex object - [GeneratedRegex( - "^[a-zA-Z0-9_-]+$", - RegexOptions.None, - matchTimeoutMilliseconds: 100)] - private static partial Regex ThumbnailNameRegex(); - - public static bool ValidateThumbnailName(string thumbnailName) + private static string GetAppend(ThumbnailSize size) + { + switch ( size ) { - return ThumbnailNameRegex().IsMatch(thumbnailName); + case ThumbnailSize.TinyMeta: + return "@meta"; + case ThumbnailSize.Small: + return "@300"; + case ThumbnailSize.Large: + return string.Empty; + case ThumbnailSize.ExtraLarge: + return "@2000"; + default: + throw new ArgumentOutOfRangeException(nameof(size), size, null); } } + + public static string RemoveSuffix(string? thumbnailOutputHash) + { + return thumbnailOutputHash == null + ? string.Empty + : Regex.Replace(thumbnailOutputHash, "@\\d+", + string.Empty, RegexOptions.None, TimeSpan.FromMilliseconds(100)); + } + + /// + /// ThumbnailName + /// Regex.IsMatch (pre compiled regex) + /// + /// Regex object + [GeneratedRegex( + "^[a-zA-Z0-9_-]+$", + RegexOptions.None, + 100)] + private static partial Regex ThumbnailNameRegex(); + + public static bool ValidateThumbnailName(string thumbnailName) + { + return ThumbnailNameRegex().IsMatch(thumbnailName); + } } diff --git a/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/CompositeThumbnailGenerator.cs b/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/CompositeThumbnailGenerator.cs new file mode 100644 index 0000000000..a1d45ec519 --- /dev/null +++ b/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/CompositeThumbnailGenerator.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using starsky.foundation.platform.Interfaces; +using starsky.foundation.platform.Thumbnails; +using starsky.foundation.thumbnailgeneration.GenerationFactory.Generators.Interfaces; +using starsky.foundation.thumbnailgeneration.Models; + +namespace starsky.foundation.thumbnailgeneration.GenerationFactory; + +public class CompositeThumbnailGenerator(List generators, IWebLogger logger) + : IThumbnailGenerator +{ + public async Task> GenerateThumbnail(string singleSubPath, + string fileHash, List thumbnailSizes) + { + foreach ( var generator in generators ) + { + try + { + var results = + ( await generator.GenerateThumbnail(singleSubPath, fileHash, thumbnailSizes) ) + .ToList(); + if ( results.All(p => p.Success) ) + { + return results; + } + } + catch ( Exception ex ) + { + logger.LogError($"Generator {generator.GetType().Name} failed: {ex.Message}"); + } + } + + return ErrorGenerationResultModel.FailedResult(thumbnailSizes, singleSubPath, + string.Empty, true, "CompositeThumbnailGenerator failed"); + } +} diff --git a/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/ErrorGenerationResultModel.cs b/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/ErrorGenerationResultModel.cs new file mode 100644 index 0000000000..7c1b9bc936 --- /dev/null +++ b/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/ErrorGenerationResultModel.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Linq; +using starsky.foundation.platform.Thumbnails; +using starsky.foundation.thumbnailgeneration.Models; + +namespace starsky.foundation.thumbnailgeneration.GenerationFactory; + +public static class ErrorGenerationResultModel +{ + internal static List FailedResult(List sizes, + string subPath, string fileHash, + bool existsFile, + string errorMessage) + { + return sizes.Select(size => + new GenerationResultModel + { + SubPath = subPath, + FileHash = fileHash, + Success = false, + IsNotFound = !existsFile, + ErrorMessage = errorMessage, + Size = size + }).ToList(); + } +} diff --git a/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/FolderToFileList.cs b/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/FolderToFileList.cs new file mode 100644 index 0000000000..c8cf5b4bdd --- /dev/null +++ b/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/FolderToFileList.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using starsky.foundation.storage.Interfaces; +using starsky.foundation.storage.Models; +using starsky.foundation.storage.Storage; + +namespace starsky.foundation.thumbnailgeneration.GenerationFactory; + +public class FolderToFileList(ISelectorStorage selectorStorage) +{ + private readonly IStorage _iStorage = + selectorStorage.Get(SelectorStorage.StorageServices.SubPath); + + public (bool success, List toAddFilePaths) AddFiles(string subPath, + Func delegateToCheckIfExtensionIsSupported) + { + var toAddFilePaths = new List(); + switch ( _iStorage.IsFolderOrFile(subPath) ) + { + case FolderOrFileModel.FolderOrFileTypeList.Deleted: + return ( false, [] ); + case FolderOrFileModel.FolderOrFileTypeList.Folder: + { + var contentOfDir = _iStorage.GetAllFilesInDirectoryRecursive(subPath) + .Where(delegateToCheckIfExtensionIsSupported).ToList(); + toAddFilePaths.AddRange(contentOfDir); + break; + } + case FolderOrFileModel.FolderOrFileTypeList.File: + default: + { + toAddFilePaths.Add(subPath); + break; + } + } + + return ( true, toAddFilePaths ); + } +} diff --git a/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/Generators/FfmpegVideoThumbnailGenerator.cs b/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/Generators/FfmpegVideoThumbnailGenerator.cs new file mode 100644 index 0000000000..405fcdc14c --- /dev/null +++ b/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/Generators/FfmpegVideoThumbnailGenerator.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using starsky.foundation.platform.Thumbnails; +using starsky.foundation.storage.Interfaces; +using starsky.foundation.storage.Storage; +using starsky.foundation.thumbnailgeneration.GenerationFactory.Generators.Interfaces; +using starsky.foundation.thumbnailgeneration.GenerationFactory.Testers; +using starsky.foundation.thumbnailgeneration.Models; + +namespace starsky.foundation.thumbnailgeneration.GenerationFactory.Generators; + +public class FfmpegVideoThumbnailGenerator(ISelectorStorage selectorStorage) : IThumbnailGenerator +{ + private readonly IStorage + _storage = selectorStorage.Get(SelectorStorage.StorageServices.SubPath); + + public async Task> GenerateThumbnail(string singleSubPath, + string fileHash, + List thumbnailSizes) + { + var preflightResult = new Preflight(_storage).Test(thumbnailSizes, singleSubPath, fileHash); + if ( preflightResult != null ) + { + return preflightResult; + } + + throw new NotImplementedException(); + } +} diff --git a/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/Generators/ImageSharpThumbnailGenerator.cs b/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/Generators/ImageSharpThumbnailGenerator.cs new file mode 100644 index 0000000000..1c82ac88e2 --- /dev/null +++ b/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/Generators/ImageSharpThumbnailGenerator.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using starsky.foundation.platform.Extensions; +using starsky.foundation.platform.Interfaces; +using starsky.foundation.platform.Thumbnails; +using starsky.foundation.storage.Interfaces; +using starsky.foundation.storage.Storage; +using starsky.foundation.thumbnailgeneration.GenerationFactory.Generators.Interfaces; +using starsky.foundation.thumbnailgeneration.GenerationFactory.ImageSharp; +using starsky.foundation.thumbnailgeneration.GenerationFactory.Testers; +using starsky.foundation.thumbnailgeneration.Models; + +namespace starsky.foundation.thumbnailgeneration.GenerationFactory.Generators; + +public class ImageSharpThumbnailGenerator(ISelectorStorage selectorStorage, IWebLogger logger) + : IThumbnailGenerator +{ + private readonly IStorage + _storage = selectorStorage.Get(SelectorStorage.StorageServices.SubPath); + + public async Task> GenerateThumbnail(string singleSubPath, + string fileHash, + List thumbnailSizes) + { + var preflightResult = new Preflight(_storage).Test(thumbnailSizes, singleSubPath, fileHash); + if ( preflightResult != null ) + { + return preflightResult; + } + + var (_, largeImageResult) = + await ResizeThumbnailFromSourceImage(thumbnailSizes[0], singleSubPath, fileHash); + + var results = await thumbnailSizes.Skip(1).ForEachAsync( + async size + => await ResizeThumbnailFromThumbnailImage( + fileHash, // source location + ThumbnailNameHelper.GetSize(size), + singleSubPath, // used for reference only + ThumbnailNameHelper.Combine(fileHash, size)), + thumbnailSizes.Count); + + return results!.Select(p => p.Item2).Append(largeImageResult); + } + + private async Task<(MemoryStream?, GenerationResultModel)> ResizeThumbnailFromThumbnailImage( + string fileHash, // source location + int width, string? subPathReference = null, string? thumbnailOutputHash = null) + { + var service = new ResizeThumbnailFromThumbnailImageHelper(selectorStorage, logger); + return await service.ResizeThumbnailFromThumbnailImage(fileHash, width, subPathReference, + thumbnailOutputHash); + } + + private async Task<(MemoryStream?, GenerationResultModel)> ResizeThumbnailFromSourceImage( + ThumbnailSize biggestThumbnailSize, string singleSubPath, string fileHash) + { + var service = new ResizeThumbnailFromSourceImageHelper(selectorStorage, logger); + return await service.ResizeThumbnailFromSourceImage(singleSubPath, + ThumbnailNameHelper.GetSize(biggestThumbnailSize), fileHash); + } +} diff --git a/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/Generators/Interfaces/IThumbnailGenerator.cs b/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/Generators/Interfaces/IThumbnailGenerator.cs new file mode 100644 index 0000000000..c1e1d14701 --- /dev/null +++ b/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/Generators/Interfaces/IThumbnailGenerator.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using starsky.foundation.platform.Thumbnails; +using starsky.foundation.thumbnailgeneration.Models; + +namespace starsky.foundation.thumbnailgeneration.GenerationFactory.Generators.Interfaces; + +public interface IThumbnailGenerator +{ + Task> GenerateThumbnail(string singleSubPath, + string fileHash, + List thumbnailSizes); +} diff --git a/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/Generators/NotSupportedFallbackThumbnailGenerator.cs b/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/Generators/NotSupportedFallbackThumbnailGenerator.cs new file mode 100644 index 0000000000..cd1f877e16 --- /dev/null +++ b/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/Generators/NotSupportedFallbackThumbnailGenerator.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using starsky.foundation.platform.Thumbnails; +using starsky.foundation.thumbnailgeneration.GenerationFactory.Generators.Interfaces; +using starsky.foundation.thumbnailgeneration.Models; + +namespace starsky.foundation.thumbnailgeneration.GenerationFactory.Generators; + +public class NotSupportedFallbackThumbnailGenerator : IThumbnailGenerator +{ + public Task> GenerateThumbnail(string singleSubPath, + string fileHash, + List thumbnailSizes) + { + return Task.FromResult(new List().AsEnumerable()); + } +} diff --git a/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/ImageSharp/ImageSharpImageResizeHelper.cs b/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/ImageSharp/ImageSharpImageResizeHelper.cs new file mode 100644 index 0000000000..b48e78a836 --- /dev/null +++ b/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/ImageSharp/ImageSharpImageResizeHelper.cs @@ -0,0 +1,35 @@ +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Processing; + +namespace starsky.foundation.thumbnailgeneration.GenerationFactory.ImageSharp; + +public static class ImageSharpImageResizeHelper +{ + public static void ImageSharpImageResize(Image image, int width, bool removeExif) + { + // Add original rotation to the image as json + if ( image.Metadata.ExifProfile != null && !removeExif ) + { + image.Metadata.ExifProfile.SetValue(ExifTag.Software, "Starsky"); + } + + if ( image.Metadata.ExifProfile != null && removeExif ) + { + image.Metadata.ExifProfile = null; + image.Metadata.IccProfile = null; + } + + var height = 0; + if ( image.Height >= image.Width ) + { + height = width; + width = 0; + } + + image.Mutate(x => x.AutoOrient()); + image.Mutate(x => x + .Resize(width, height, KnownResamplers.Lanczos3) + ); + } +} diff --git a/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/ImageSharp/ResizeThumbnailFromSourceImageHelper.cs b/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/ImageSharp/ResizeThumbnailFromSourceImageHelper.cs new file mode 100644 index 0000000000..d027e300ff --- /dev/null +++ b/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/ImageSharp/ResizeThumbnailFromSourceImageHelper.cs @@ -0,0 +1,79 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using SixLabors.ImageSharp; +using starsky.foundation.platform.Helpers; +using starsky.foundation.platform.Interfaces; +using starsky.foundation.storage.Interfaces; +using starsky.foundation.storage.Storage; +using starsky.foundation.thumbnailgeneration.Models; + +namespace starsky.foundation.thumbnailgeneration.GenerationFactory.ImageSharp; + +public class ResizeThumbnailFromSourceImageHelper( + ISelectorStorage selectorStorage, + IWebLogger logger) +{ + private readonly IStorage _storage = + selectorStorage.Get(SelectorStorage.StorageServices.SubPath); + + private readonly IStorage _thumbnailStorage = + selectorStorage.Get(SelectorStorage.StorageServices.Thumbnail); + + public async Task<(MemoryStream?, GenerationResultModel)> ResizeThumbnailFromSourceImage( + string subPath, + int width, string? thumbnailOutputHash = null, + bool removeExif = false, + ExtensionRolesHelper.ImageFormat imageFormat = ExtensionRolesHelper.ImageFormat.jpg) + { + var outputStream = new MemoryStream(); + var result = new GenerationResultModel + { + FileHash = ThumbnailNameHelper.RemoveSuffix(thumbnailOutputHash), + IsNotFound = false, + SizeInPixels = width, + Success = true, + SubPath = subPath + }; + + try + { + // resize the image and save it to the output stream + using ( var inputStream = _storage.ReadStream(subPath) ) + using ( var image = await Image.LoadAsync(inputStream) ) + { + ImageSharpImageResizeHelper.ImageSharpImageResize(image, width, removeExif); + await SaveThumbnailImageFormatHelper.SaveThumbnailImageFormat(image, imageFormat, + outputStream); + + // When thumbnailOutputHash is nothing return stream instead of writing down + if ( string.IsNullOrEmpty(thumbnailOutputHash) ) + { + result.ErrorMessage = "Ok give stream back instead of disk write"; + return ( outputStream, result ); + } + + // only when a hash exists + await _thumbnailStorage.WriteStreamAsync(outputStream, thumbnailOutputHash); + // Disposed in WriteStreamAsync + } + } + catch ( Exception ex ) + { + const string imageCannotBeLoadedErrorMessage = "Image cannot be loaded"; + var message = ex.Message; + if ( message.StartsWith(imageCannotBeLoadedErrorMessage) ) + { + message = imageCannotBeLoadedErrorMessage; + } + + logger.LogError($"[ResizeThumbnailFromSourceImage] Exception {subPath} {message}", ex); + result.Success = false; + result.ErrorMessage = message; + return ( null, result ); + } + + result.ErrorMessage = "Ok but written to disk"; + return ( null, result ); + } +} diff --git a/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/ImageSharp/ResizeThumbnailFromThumbnailImageHelper.cs b/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/ImageSharp/ResizeThumbnailFromThumbnailImageHelper.cs new file mode 100644 index 0000000000..47a1b0b3f5 --- /dev/null +++ b/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/ImageSharp/ResizeThumbnailFromThumbnailImageHelper.cs @@ -0,0 +1,86 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using SixLabors.ImageSharp; +using starsky.foundation.platform.Helpers; +using starsky.foundation.platform.Interfaces; +using starsky.foundation.storage.Interfaces; +using starsky.foundation.storage.Storage; +using starsky.foundation.thumbnailgeneration.Models; + +namespace starsky.foundation.thumbnailgeneration.GenerationFactory.ImageSharp; + +public class ResizeThumbnailFromThumbnailImageHelper( + ISelectorStorage selectorStorage, + IWebLogger logger) +{ + private readonly IStorage _thumbnailStorage = + selectorStorage.Get(SelectorStorage.StorageServices.Thumbnail); + + /// + /// Resize image from other thumbnail + /// + /// source location + /// width in pixels + /// name of output file + /// remove meta data + /// jpg, or png + /// for reference only + /// (stream, fileHash, and is ok) + public async Task<(MemoryStream?, GenerationResultModel)> ResizeThumbnailFromThumbnailImage( + string fileHash, // source location + int width, string? subPathReference = null, string? thumbnailOutputHash = null, + bool removeExif = false, + ExtensionRolesHelper.ImageFormat imageFormat = ExtensionRolesHelper.ImageFormat.jpg + ) + { + var outputStream = new MemoryStream(); + var result = new GenerationResultModel + { + FileHash = ThumbnailNameHelper.RemoveSuffix(thumbnailOutputHash), + IsNotFound = false, + SizeInPixels = width, + Success = true, + SubPath = subPathReference! + }; + + try + { + // resize the image and save it to the output stream + using ( var inputStream = _thumbnailStorage.ReadStream(fileHash) ) + using ( var image = await Image.LoadAsync(inputStream) ) + { + ImageSharpImageResizeHelper.ImageSharpImageResize(image, width, removeExif); + await SaveThumbnailImageFormatHelper.SaveThumbnailImageFormat(image, imageFormat, + outputStream); + + // When thumbnailOutputHash is nothing return stream instead of writing down + if ( string.IsNullOrEmpty(thumbnailOutputHash) ) + { + return ( outputStream, result ); + } + + // only when a hash exists + await _thumbnailStorage.WriteStreamAsync(outputStream, thumbnailOutputHash); + // Disposed in WriteStreamAsync + } + } + catch ( Exception ex ) + { + const string imageCannotBeLoadedErrorMessage = "Image cannot be loaded"; + var message = ex.Message; + if ( message.StartsWith(imageCannotBeLoadedErrorMessage) ) + { + message = imageCannotBeLoadedErrorMessage; + } + + logger.LogError($"[ResizeThumbnailFromThumbnailImage] Exception {fileHash} {message}", + ex); + result.Success = false; + result.ErrorMessage = message; + return ( null, result ); + } + + return ( outputStream, result ); + } +} diff --git a/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/ImageSharp/SaveThumbnailImageFormatHelper.cs b/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/ImageSharp/SaveThumbnailImageFormatHelper.cs new file mode 100644 index 0000000000..11c7b41180 --- /dev/null +++ b/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/ImageSharp/SaveThumbnailImageFormatHelper.cs @@ -0,0 +1,61 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Formats.Webp; +using starsky.foundation.platform.Helpers; + +namespace starsky.foundation.thumbnailgeneration.GenerationFactory.ImageSharp; + +public static class SaveThumbnailImageFormatHelper +{ + /// + /// Used in ResizeThumbnailToStream to save based on the input settings + /// + /// Rgba32 image + /// Files ImageFormat + /// input stream to save + internal static Task SaveThumbnailImageFormat(Image image, + ExtensionRolesHelper.ImageFormat imageFormat, + MemoryStream outputStream) + { + ArgumentNullException.ThrowIfNull(outputStream); + + return SaveThumbnailImageFormatInternal(image, imageFormat, outputStream); + } + + /// + /// Private: use => SaveThumbnailImageFormat + /// Used in ResizeThumbnailToStream to save based on the input settings + /// + /// Rgba32 image + /// Files ImageFormat + /// input stream to save + private static async Task SaveThumbnailImageFormatInternal(Image image, + ExtensionRolesHelper.ImageFormat imageFormat, + MemoryStream outputStream) + { + switch ( imageFormat ) + { + case ExtensionRolesHelper.ImageFormat.png: + await image.SaveAsync(outputStream, + new PngEncoder + { + ColorType = PngColorType.Rgb, + CompressionLevel = PngCompressionLevel.BestSpeed, + SkipMetadata = true, + TransparentColorMode = PngTransparentColorMode.Clear + }); + return; + case ExtensionRolesHelper.ImageFormat.webp: + await image.SaveAsync(outputStream, + new WebpEncoder { Quality = 80, EntropyPasses = 1, SkipMetadata = true }); + return; + default: + await image.SaveAsync(outputStream, new JpegEncoder { Quality = 90 }); + break; + } + } +} diff --git a/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/Interfaces/IThumbnailService.cs b/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/Interfaces/IThumbnailService.cs new file mode 100644 index 0000000000..b91b6c2c90 --- /dev/null +++ b/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/Interfaces/IThumbnailService.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using starsky.foundation.thumbnailgeneration.Models; + +namespace starsky.foundation.thumbnailgeneration.GenerationFactory.Interfaces; + +public interface IThumbnailService +{ + Task> GenerateThumbnail(string fileOrFolderPath, + bool skipExtraLarge = false); + + Task> GenerateThumbnail(string subPath, string fileHash, + bool skipExtraLarge = false); + + /// + /// Rotate a thumbnail + /// + /// fileHash to rename + /// which direction + /// height of output + /// 0 = keep in shape + /// + Task RotateThumbnail(string fileHash, int orientation, + int width = 1000, int height = 0); +} diff --git a/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/RotateThumbnailHelper.cs b/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/RotateThumbnailHelper.cs new file mode 100644 index 0000000000..769ca118c7 --- /dev/null +++ b/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/RotateThumbnailHelper.cs @@ -0,0 +1,68 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; +using starsky.foundation.platform.Helpers; +using starsky.foundation.storage.Interfaces; +using starsky.foundation.storage.Storage; +using starsky.foundation.thumbnailgeneration.GenerationFactory.ImageSharp; + +namespace starsky.foundation.thumbnailgeneration.GenerationFactory; + +public class RotateThumbnailHelper(ISelectorStorage selectorStorage) +{ + private readonly IStorage + _thumbnailStorage = selectorStorage.Get(SelectorStorage.StorageServices.Thumbnail); + + /// + /// Rotate an image, by rotating the pixels and resize the thumbnail.Please do not apply any + /// orientation exif-tag on this file + /// + /// + /// -1 > Rotate -90degrees, anything else 90 degrees + /// to resize, default 1000 + /// to resize, default keep ratio (0) + /// Is successful? // private feature + internal async Task RotateThumbnail(string fileHash, int orientation, int width = 1000, + int height = 0) + { + if ( !_thumbnailStorage.ExistFile(fileHash) ) + { + return false; + } + + // the orientation is -1 or 1 + var rotateMode = RotateMode.Rotate90; + if ( orientation == -1 ) + { + rotateMode = RotateMode.Rotate270; + } + + try + { + using ( var inputStream = _thumbnailStorage.ReadStream(fileHash) ) + using ( var image = await Image.LoadAsync(inputStream) ) + using ( var stream = new MemoryStream() ) + { + image.Mutate(x => x + .Resize(width, height, KnownResamplers.Lanczos3) + ); + image.Mutate(x => x + .Rotate(rotateMode)); + + // Image image, ExtensionRolesHelper.ImageFormat imageFormat, MemoryStream outputStream + await SaveThumbnailImageFormatHelper.SaveThumbnailImageFormat(image, + ExtensionRolesHelper.ImageFormat.jpg, stream); + await _thumbnailStorage.WriteStreamAsync(stream, fileHash); + } + } + catch ( Exception ex ) + { + Console.WriteLine(ex); + return false; + } + + return true; + } +} diff --git a/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/Testers/ErrorLogItemFullPath.cs b/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/Testers/ErrorLogItemFullPath.cs new file mode 100644 index 0000000000..d9f9f8fdfc --- /dev/null +++ b/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/Testers/ErrorLogItemFullPath.cs @@ -0,0 +1,16 @@ +using System.IO; +using starsky.foundation.platform.Helpers; + +namespace starsky.foundation.thumbnailgeneration.GenerationFactory.Testers; + +public static class ErrorLogItemFullPath +{ + internal static string GetErrorLogItemFullPath(string subPath) + { + return FilenamesHelper.GetParentPath(subPath) + + "/" + + "_" + + Path.GetFileNameWithoutExtension(PathHelper.GetFileName(subPath)) + + ".log"; + } +} diff --git a/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/Testers/Preflight.cs b/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/Testers/Preflight.cs new file mode 100644 index 0000000000..7d2e004e0d --- /dev/null +++ b/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/Testers/Preflight.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using System.Linq; +using starsky.foundation.platform.Helpers; +using starsky.foundation.platform.Thumbnails; +using starsky.foundation.storage.Interfaces; +using starsky.foundation.thumbnailgeneration.Models; + +namespace starsky.foundation.thumbnailgeneration.GenerationFactory.Testers; + +public class Preflight(IStorage storage) +{ + public List? Test(List thumbnailSizes, string subPath, + string fileHash) + { + if ( thumbnailSizes.Count < ThumbnailSizes.GetSizes(true).Count ) + { + return ErrorGenerationResultModel.FailedResult( + ThumbnailSizes.GetSizes(true), + subPath, fileHash, false, + $"thumbnailSizes.Count <= {ThumbnailSizes.GetSizes(true).Count}"); + } + + var extensionSupported = + ExtensionRolesHelper.IsExtensionImageSharpThumbnailSupported(subPath); + var existsFile = storage.ExistFile(subPath); + if ( !extensionSupported || !existsFile ) + { + return thumbnailSizes.Select(size => + new GenerationResultModel + { + SubPath = subPath, + FileHash = fileHash, + Success = false, + IsNotFound = !existsFile, + ErrorMessage = !extensionSupported ? "not supported" : "File is not found", + Size = size + }).ToList(); + } + + // File is already tested + if ( storage.ExistFile(ErrorLogItemFullPath.GetErrorLogItemFullPath(subPath)) ) + { + return thumbnailSizes.Select(size => + new GenerationResultModel + { + SubPath = subPath, + FileHash = fileHash, + Success = false, + IsNotFound = false, + ErrorMessage = "File already failed before", + Size = size + }).ToList(); + } + + return null; + } +} diff --git a/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/ThumbnailGeneratorFactory.cs b/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/ThumbnailGeneratorFactory.cs new file mode 100644 index 0000000000..fe4428616b --- /dev/null +++ b/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/ThumbnailGeneratorFactory.cs @@ -0,0 +1,27 @@ +using starsky.foundation.platform.Helpers; +using starsky.foundation.platform.Interfaces; +using starsky.foundation.storage.Interfaces; +using starsky.foundation.thumbnailgeneration.GenerationFactory.Generators; +using starsky.foundation.thumbnailgeneration.GenerationFactory.Generators.Interfaces; + +namespace starsky.foundation.thumbnailgeneration.GenerationFactory; + +public class ThumbnailGeneratorFactory(ISelectorStorage selectorStorage, IWebLogger logger) +{ + public IThumbnailGenerator GetGenerator(string filePath) + { + if ( ExtensionRolesHelper.IsExtensionImageSharpThumbnailSupported(filePath) ) + { + return new CompositeThumbnailGenerator( + [new ImageSharpThumbnailGenerator(selectorStorage, logger)], logger); + } + + if ( ExtensionRolesHelper.IsExtensionVideoSupported(filePath) ) + { + return new CompositeThumbnailGenerator( + [new FfmpegVideoThumbnailGenerator(selectorStorage)], logger); + } + + return new NotSupportedFallbackThumbnailGenerator(); + } +} diff --git a/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/ThumbnailService.cs b/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/ThumbnailService.cs new file mode 100644 index 0000000000..df6953e949 --- /dev/null +++ b/starsky/starsky.foundation.thumbnailgeneration/GenerationFactory/ThumbnailService.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using starsky.foundation.platform.Extensions; +using starsky.foundation.platform.Helpers; +using starsky.foundation.platform.Interfaces; +using starsky.foundation.platform.Models; +using starsky.foundation.platform.Thumbnails; +using starsky.foundation.storage.Interfaces; +using starsky.foundation.storage.Services; +using starsky.foundation.storage.Storage; +using starsky.foundation.thumbnailgeneration.GenerationFactory.Interfaces; +using starsky.foundation.thumbnailgeneration.Models; + +namespace starsky.foundation.thumbnailgeneration.GenerationFactory; + +public class ThumbnailService( + IWebLogger logger, + ISelectorStorage selectorStorage, + AppSettings appSettings) : IThumbnailService +{ + private readonly FolderToFileList _folderToFileList = new(selectorStorage); + + private readonly IStorage + _storage = selectorStorage.Get(SelectorStorage.StorageServices.SubPath); + + /// + /// Can be used for directories or single files + /// + /// subPath of file or folder + /// skip large format creation + /// results + public async Task> GenerateThumbnail(string fileOrFolderPath, + bool skipExtraLarge = false) + { + var (success, toAddFilePaths) = _folderToFileList.AddFiles(fileOrFolderPath, + e => + ExtensionRolesHelper.IsExtensionImageSharpThumbnailSupported(e) || + ExtensionRolesHelper.IsExtensionVideoSupported(e)); + + var sizes = ThumbnailSizes.GetSizes(skipExtraLarge); + if ( !success ) + { + return ErrorGenerationResultModel + .FailedResult(sizes, fileOrFolderPath, string.Empty, false, "File is deleted"); + } + + var resultChunkList = await toAddFilePaths.ForEachAsync( + async singleSubPath => await CreateThumbAsync(singleSubPath, sizes), + appSettings.MaxDegreesOfParallelismThumbnail); + + var results = new List(); + + foreach ( var resultChunk in resultChunkList! ) + { + results.AddRange(resultChunk); + } + + return results; + } + + /// + /// Only for one file + /// + /// hash + /// subPath of file; make sure it is NOT a folder + /// skip large format creation + /// results + /// + public async Task> GenerateThumbnail(string subPath, + string fileHash, + bool skipExtraLarge = false) + { + var (success, toAddFilePaths) = _folderToFileList.AddFiles(subPath, + e => + ExtensionRolesHelper.IsExtensionImageSharpThumbnailSupported(e) || + ExtensionRolesHelper.IsExtensionVideoSupported(e)); + + var sizes = ThumbnailSizes.GetSizes(skipExtraLarge); + if ( !success || toAddFilePaths.Count != 1 ) + { + return ErrorGenerationResultModel + .FailedResult(sizes, subPath, fileHash, false, "File is deleted"); + } + + return ( await CreateThumbAsync(subPath, sizes) ).ToList(); + } + + /// + /// Rotate a thumbnail + /// + /// fileHash to rename + /// which direction + /// height of output + /// 0 = keep in shape + /// + public Task RotateThumbnail(string fileHash, int orientation, + int width = 1000, int height = 0) + { + return new RotateThumbnailHelper(selectorStorage).RotateThumbnail(fileHash, orientation, + width, height); + } + + private Task> CreateThumbAsync(string singleSubPath, + List sizes) + { + if ( string.IsNullOrWhiteSpace(singleSubPath) ) + { + throw new ArgumentNullException(nameof(singleSubPath)); + } + + return CreateThumbInternal(singleSubPath, sizes); + } + + private async Task> CreateThumbInternal( + string singleSubPath, List sizes) + { + var generator = + new ThumbnailGeneratorFactory(selectorStorage, logger).GetGenerator(singleSubPath); + var (fileHash, success) = await new FileHash(_storage).GetHashCodeAsync(singleSubPath); + if ( !success ) + { + return []; + } + + return await generator.GenerateThumbnail(singleSubPath, fileHash, sizes); + } +} diff --git a/starsky/starsky.foundation.thumbnailgeneration/Helpers/Thumbnail.cs b/starsky/starsky.foundation.thumbnailgeneration/Helpers/Thumbnail.cs index cd3c251643..8547d99f11 100644 --- a/starsky/starsky.foundation.thumbnailgeneration/Helpers/Thumbnail.cs +++ b/starsky/starsky.foundation.thumbnailgeneration/Helpers/Thumbnail.cs @@ -9,11 +9,11 @@ using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Processing; -using starsky.foundation.platform.Enums; using starsky.foundation.platform.Extensions; using starsky.foundation.platform.Helpers; using starsky.foundation.platform.Interfaces; using starsky.foundation.platform.Models; +using starsky.foundation.platform.Thumbnails; using starsky.foundation.storage.Helpers; using starsky.foundation.storage.Interfaces; using starsky.foundation.storage.Services; @@ -24,6 +24,7 @@ namespace starsky.foundation.thumbnailgeneration.Helpers; +[Obsolete("Use Factory instead")] public sealed class Thumbnail { private readonly AppSettings _appSettings; @@ -119,7 +120,8 @@ private async Task> CreateThumbInternal(strin string fileHash, bool skipExtraLarge = false) { // FileType=supported + subPath=exit + fileHash=NOT exist - var extensionSupported = ExtensionRolesHelper.IsExtensionThumbnailSupported(subPath); + var extensionSupported = + ExtensionRolesHelper.IsExtensionImageSharpThumbnailSupported(subPath); var existsFile = _iStorage.ExistFile(subPath); if ( !extensionSupported || !existsFile ) { diff --git a/starsky/starsky.foundation.thumbnailgeneration/Helpers/ThumbnailCli.cs b/starsky/starsky.foundation.thumbnailgeneration/Helpers/ThumbnailCli.cs index 50fe01cef8..7c3d2af0c4 100644 --- a/starsky/starsky.foundation.thumbnailgeneration/Helpers/ThumbnailCli.cs +++ b/starsky/starsky.foundation.thumbnailgeneration/Helpers/ThumbnailCli.cs @@ -6,80 +6,81 @@ using starsky.foundation.storage.Interfaces; using starsky.foundation.storage.Services; using starsky.foundation.storage.Storage; +using starsky.foundation.thumbnailgeneration.GenerationFactory.Interfaces; using starsky.foundation.thumbnailgeneration.Interfaces; -namespace starsky.foundation.thumbnailgeneration.Helpers +namespace starsky.foundation.thumbnailgeneration.Helpers; + +[Obsolete("refactor to new Cli")] +public sealed class ThumbnailCli { - public sealed class ThumbnailCli + private readonly AppSettings _appSettings; + private readonly IConsole _console; + private readonly ISelectorStorage _selectorStorage; + private readonly IThumbnailCleaner _thumbnailCleaner; + private readonly IThumbnailService _thumbnailService; + + public ThumbnailCli(AppSettings appSettings, + IConsole console, IThumbnailService thumbnailService, + IThumbnailCleaner thumbnailCleaner, + ISelectorStorage selectorStorage) + { + _appSettings = appSettings; + _thumbnailService = thumbnailService; + _console = console; + _thumbnailCleaner = thumbnailCleaner; + _selectorStorage = selectorStorage; + } + + public async Task Thumbnail(string[] args) { - private readonly AppSettings _appSettings; - private readonly IConsole _console; - private readonly IThumbnailCleaner _thumbnailCleaner; - private readonly ISelectorStorage _selectorStorage; - private readonly IThumbnailService _thumbnailService; + _appSettings.Verbose = ArgsHelper.NeedVerbose(args); + _appSettings.ApplicationType = AppSettings.StarskyAppType.Thumbnail; - public ThumbnailCli(AppSettings appSettings, - IConsole console, IThumbnailService thumbnailService, - IThumbnailCleaner thumbnailCleaner, - ISelectorStorage selectorStorage) + if ( ArgsHelper.NeedHelp(args) ) { - _appSettings = appSettings; - _thumbnailService = thumbnailService; - _console = console; - _thumbnailCleaner = thumbnailCleaner; - _selectorStorage = selectorStorage; + new ArgsHelper(_appSettings, _console).NeedHelpShowDialog(); + return; } - public async Task Thumbnail(string[] args) + new ArgsHelper().SetEnvironmentByArgs(args); + + var subPath = new ArgsHelper(_appSettings).SubPathOrPathValue(args); + var getSubPathRelative = new ArgsHelper(_appSettings).GetRelativeValue(args); + if ( getSubPathRelative != null ) { - _appSettings.Verbose = ArgsHelper.NeedVerbose(args); - _appSettings.ApplicationType = AppSettings.StarskyAppType.Thumbnail; + subPath = new StructureService(_selectorStorage.Get( + SelectorStorage.StorageServices.SubPath), _appSettings.Structure) + .ParseSubfolders(getSubPathRelative)!; + } - if ( ArgsHelper.NeedHelp(args) ) + if ( ArgsHelper.GetThumbnail(args) ) + { + if ( _appSettings.IsVerbose() ) { - new ArgsHelper(_appSettings, _console).NeedHelpShowDialog(); - return; + _console.WriteLine($">> GetThumbnail True ({DateTime.UtcNow:HH:mm:ss})"); } - new ArgsHelper().SetEnvironmentByArgs(args); + var storage = _selectorStorage.Get(SelectorStorage.StorageServices.SubPath); - var subPath = new ArgsHelper(_appSettings).SubPathOrPathValue(args); - var getSubPathRelative = new ArgsHelper(_appSettings).GetRelativeValue(args); - if ( getSubPathRelative != null ) - { - subPath = new StructureService(_selectorStorage.Get( - SelectorStorage.StorageServices.SubPath), _appSettings.Structure) - .ParseSubfolders(getSubPathRelative)!; - } + var isFolderOrFile = storage.IsFolderOrFile(subPath); - if ( ArgsHelper.GetThumbnail(args) ) + if ( _appSettings.IsVerbose() ) { - if ( _appSettings.IsVerbose() ) - { - _console.WriteLine($">> GetThumbnail True ({DateTime.UtcNow:HH:mm:ss})"); - } - - var storage = _selectorStorage.Get(SelectorStorage.StorageServices.SubPath); - - var isFolderOrFile = storage.IsFolderOrFile(subPath); - - if ( _appSettings.IsVerbose() ) - { - _console.WriteLine(isFolderOrFile.ToString()); - } - - await _thumbnailService.CreateThumbnailAsync(subPath); - - _console.WriteLine($"Thumbnail Done! ({DateTime.UtcNow:HH:mm:ss})"); + _console.WriteLine(isFolderOrFile.ToString()); } - if ( ArgsHelper.NeedCleanup(args) ) - { - _console.WriteLine($"Next: Start Thumbnail Cache cleanup (-x true) ({DateTime.UtcNow:HH:mm:ss})"); - await _thumbnailCleaner.CleanAllUnusedFilesAsync(); - _console.WriteLine($"Cleanup Done! ({DateTime.UtcNow:HH:mm:ss})"); - } + await _thumbnailService.GenerateThumbnail(subPath); + _console.WriteLine($"Thumbnail Done! ({DateTime.UtcNow:HH:mm:ss})"); + } + + if ( ArgsHelper.NeedCleanup(args) ) + { + _console.WriteLine( + $"Next: Start Thumbnail Cache cleanup (-x true) ({DateTime.UtcNow:HH:mm:ss})"); + await _thumbnailCleaner.CleanAllUnusedFilesAsync(); + _console.WriteLine($"Cleanup Done! ({DateTime.UtcNow:HH:mm:ss})"); } } } diff --git a/starsky/starsky.foundation.thumbnailgeneration/Helpers/ThumbnailVideo.cs b/starsky/starsky.foundation.thumbnailgeneration/Helpers/ThumbnailVideo.cs index 68f3f32cd7..93fdde40ec 100644 --- a/starsky/starsky.foundation.thumbnailgeneration/Helpers/ThumbnailVideo.cs +++ b/starsky/starsky.foundation.thumbnailgeneration/Helpers/ThumbnailVideo.cs @@ -2,10 +2,13 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Memory; using starsky.foundation.platform.Extensions; using starsky.foundation.platform.Helpers; using starsky.foundation.platform.Interfaces; using starsky.foundation.platform.Models; +using starsky.foundation.platform.Thumbnails; +using starsky.foundation.readmeta.Services; using starsky.foundation.storage.Interfaces; using starsky.foundation.storage.Services; using starsky.foundation.storage.Storage; @@ -19,15 +22,16 @@ public class ThumbnailVideo { private readonly AppSettings _appSettings; private readonly IStorage _iStorage; - private readonly IWebLogger _logger; + private readonly ReadMeta _readMeta; private readonly IVideoProcess _videoProcess; - - public ThumbnailVideo(IStorage iStorage, IWebLogger logger, IVideoProcess videoProcess) + public ThumbnailVideo(IStorage iStorage, IWebLogger logger, IVideoProcess videoProcess, + AppSettings appSettings, IMemoryCache memoryCache) { _iStorage = iStorage; - _logger = logger; _videoProcess = videoProcess; + _readMeta = new ReadMeta(iStorage, + appSettings, memoryCache, logger); } internal async Task> CreateThumbnailAsync(string subPath) @@ -86,6 +90,22 @@ internal Task> CreateThumbAsync(string? subPa return CreateThumbInternal(subPath, fileHash, skipExtraLarge); } + private IEnumerable FailedResult(string subPath, string fileHash, + bool existsFile, + string errorMessage) + { + return ThumbnailNameHelper.GeneratedThumbnailSizes.Select(size => + new GenerationResultModel + { + SubPath = subPath, + FileHash = fileHash, + Success = false, + IsNotFound = !existsFile, + ErrorMessage = errorMessage, + Size = size + }).ToList(); + } + private async Task> CreateThumbInternal(string subPath, string fileHash, bool skipExtraLarge = false) { @@ -94,48 +114,63 @@ private async Task> CreateThumbInternal(strin if ( !extensionSupported || !existsFile ) { - return ThumbnailNameHelper.GeneratedThumbnailSizes.Select(size => - new GenerationResultModel - { - SubPath = subPath, - FileHash = fileHash, - Success = false, - IsNotFound = !existsFile, - ErrorMessage = !extensionSupported ? "not supported" : "File is not found", - Size = size - }).ToList(); + return FailedResult(subPath, fileHash, existsFile, + !extensionSupported ? "not supported" : "File is not found"); } // File is already tested if ( _iStorage.ExistFile(ErrorLogItemFullPath.GetErrorLogItemFullPath(subPath)) ) { - return ThumbnailNameHelper.GeneratedThumbnailSizes.Select(size => - new GenerationResultModel - { - SubPath = subPath, - FileHash = fileHash, - Success = false, - IsNotFound = false, - ErrorMessage = "File already failed before", - Size = size - }).ToList(); + return FailedResult(subPath, fileHash, true, + "File already failed before"); } - var result = await _videoProcess.Run(subPath, null, VideoProcessTypes.Thumbnail); + var videoResult = await _videoProcess.RunVideo(subPath, + fileHash, VideoProcessTypes.Thumbnail); - if ( !result ) + + if ( !videoResult.IsSuccess || string.IsNullOrWhiteSpace(videoResult.ResultPath) ) { - return ThumbnailNameHelper.GeneratedThumbnailSizes.Select(size => - new GenerationResultModel + return FailedResult(subPath, fileHash, true, + "Failed to create thumbnail"); + } + + var meta = await _readMeta.ReadExifAndXmpFromFileAsync(videoResult.ResultPath); + if ( meta == null ) + { + return FailedResult(subPath, fileHash, true, + "Failed to read meta"); + } + + // create based on the sizes a thumbnail image, skip if the source image is smaller + var results = ThumbnailNameHelper.GeneratedThumbnailSizes + .Where(p => !skipExtraLarge || p != ThumbnailSize.ExtraLarge) + .Select(size => + { + var sizeInPixels = ThumbnailNameHelper.GetSize(size); + if ( meta.ImageWidth < sizeInPixels || meta.ImageHeight < sizeInPixels ) + { + return new GenerationResultModel + { + SubPath = subPath, + FileHash = fileHash, + Success = false, + IsNotFound = false, + ErrorMessage = "Source image is smaller", + Size = size + }; + } + + return new GenerationResultModel { SubPath = subPath, FileHash = fileHash, - Success = false, + Success = true, IsNotFound = false, - ErrorMessage = "Failed to create thumbnail", Size = size - }).ToList(); - } + }; + }).ToList(); + return []; } diff --git a/starsky/starsky.foundation.thumbnailgeneration/Interfaces/IThumbnailService.cs b/starsky/starsky.foundation.thumbnailgeneration/Interfaces/IThumbnailService.bak similarity index 100% rename from starsky/starsky.foundation.thumbnailgeneration/Interfaces/IThumbnailService.cs rename to starsky/starsky.foundation.thumbnailgeneration/Interfaces/IThumbnailService.bak diff --git a/starsky/starsky.foundation.thumbnailgeneration/Models/GenerationResultModel.cs b/starsky/starsky.foundation.thumbnailgeneration/Models/GenerationResultModel.cs index 57d72a3408..dc46ea5c4e 100644 --- a/starsky/starsky.foundation.thumbnailgeneration/Models/GenerationResultModel.cs +++ b/starsky/starsky.foundation.thumbnailgeneration/Models/GenerationResultModel.cs @@ -1,4 +1,4 @@ -using starsky.foundation.platform.Enums; +using starsky.foundation.platform.Thumbnails; using starsky.foundation.storage.Storage; namespace starsky.foundation.thumbnailgeneration.Models; diff --git a/starsky/starsky.foundation.thumbnailgeneration/Services/ThumbnailCleaner.cs b/starsky/starsky.foundation.thumbnailgeneration/Services/ThumbnailCleaner.cs index 9e9aa30e8f..2269de23af 100644 --- a/starsky/starsky.foundation.thumbnailgeneration/Services/ThumbnailCleaner.cs +++ b/starsky/starsky.foundation.thumbnailgeneration/Services/ThumbnailCleaner.cs @@ -6,9 +6,9 @@ using starsky.foundation.database.Interfaces; using starsky.foundation.database.Models; using starsky.foundation.injection; -using starsky.foundation.platform.Enums; using starsky.foundation.platform.Extensions; using starsky.foundation.platform.Interfaces; +using starsky.foundation.platform.Thumbnails; using starsky.foundation.storage.Interfaces; using starsky.foundation.storage.Storage; using starsky.foundation.thumbnailgeneration.Interfaces; diff --git a/starsky/starsky.foundation.thumbnailgeneration/Services/ThumbnailService.cs b/starsky/starsky.foundation.thumbnailgeneration/Services/ThumbnailService.bak similarity index 100% rename from starsky/starsky.foundation.thumbnailgeneration/Services/ThumbnailService.cs rename to starsky/starsky.foundation.thumbnailgeneration/Services/ThumbnailService.bak diff --git a/starsky/starsky.foundation.thumbnailgeneration/Services/UpdateStatusGeneratedThumbnailService.cs b/starsky/starsky.foundation.thumbnailgeneration/Services/UpdateStatusGeneratedThumbnailService.cs index 02d4e757bd..0d8810c46f 100644 --- a/starsky/starsky.foundation.thumbnailgeneration/Services/UpdateStatusGeneratedThumbnailService.cs +++ b/starsky/starsky.foundation.thumbnailgeneration/Services/UpdateStatusGeneratedThumbnailService.cs @@ -4,7 +4,7 @@ using starsky.foundation.database.Interfaces; using starsky.foundation.database.Models; using starsky.foundation.injection; -using starsky.foundation.platform.Enums; +using starsky.foundation.platform.Thumbnails; using starsky.foundation.thumbnailgeneration.Interfaces; using starsky.foundation.thumbnailgeneration.Models; diff --git a/starsky/starsky.foundation.thumbnailgeneration/starsky.foundation.thumbnailgeneration.csproj b/starsky/starsky.foundation.thumbnailgeneration/starsky.foundation.thumbnailgeneration.csproj index fc5d5debfa..c37d6ba01f 100644 --- a/starsky/starsky.foundation.thumbnailgeneration/starsky.foundation.thumbnailgeneration.csproj +++ b/starsky/starsky.foundation.thumbnailgeneration/starsky.foundation.thumbnailgeneration.csproj @@ -23,6 +23,10 @@ + + + + true diff --git a/starsky/starsky.foundation.thumbnailmeta/Services/MetaExifThumbnailService.cs b/starsky/starsky.foundation.thumbnailmeta/Services/MetaExifThumbnailService.cs index 41b10a9e59..65f1c547e1 100644 --- a/starsky/starsky.foundation.thumbnailmeta/Services/MetaExifThumbnailService.cs +++ b/starsky/starsky.foundation.thumbnailmeta/Services/MetaExifThumbnailService.cs @@ -3,143 +3,154 @@ using System.Linq; using System.Threading.Tasks; using starsky.foundation.injection; -using starsky.foundation.thumbnailmeta.Interfaces; -using starsky.foundation.platform.Enums; using starsky.foundation.platform.Extensions; using starsky.foundation.platform.Helpers; using starsky.foundation.platform.Interfaces; using starsky.foundation.platform.Models; +using starsky.foundation.platform.Thumbnails; using starsky.foundation.storage.Interfaces; using starsky.foundation.storage.Models; using starsky.foundation.storage.Services; using starsky.foundation.storage.Storage; +using starsky.foundation.thumbnailmeta.Interfaces; + +namespace starsky.foundation.thumbnailmeta.Services; -namespace starsky.foundation.thumbnailmeta.Services +[Service(typeof(IMetaExifThumbnailService), InjectionLifetime = InjectionLifetime.Scoped)] +public sealed class MetaExifThumbnailService : IMetaExifThumbnailService { - [Service(typeof(IMetaExifThumbnailService), InjectionLifetime = InjectionLifetime.Scoped)] - public sealed class MetaExifThumbnailService : IMetaExifThumbnailService - { - private readonly IStorage _iStorage; - private readonly IStorage _thumbnailStorage; - private readonly IWebLogger _logger; - private readonly IOffsetDataMetaExifThumbnail _offsetDataMetaExifThumbnail; - private readonly IWriteMetaThumbnailService _writeMetaThumbnailService; - private readonly AppSettings _appSettings; + private readonly AppSettings _appSettings; + private readonly IStorage _iStorage; + private readonly IWebLogger _logger; + private readonly IOffsetDataMetaExifThumbnail _offsetDataMetaExifThumbnail; + private readonly IStorage _thumbnailStorage; + private readonly IWriteMetaThumbnailService _writeMetaThumbnailService; - public MetaExifThumbnailService(AppSettings appSettings, ISelectorStorage selectorStorage, - IOffsetDataMetaExifThumbnail offsetDataMetaExifThumbnail, - IWriteMetaThumbnailService writeMetaThumbnailService, IWebLogger logger) - { - _appSettings = appSettings; - _iStorage = selectorStorage.Get(SelectorStorage.StorageServices.SubPath); - _thumbnailStorage = selectorStorage.Get(SelectorStorage.StorageServices.Thumbnail); - _offsetDataMetaExifThumbnail = offsetDataMetaExifThumbnail; - _writeMetaThumbnailService = writeMetaThumbnailService; - _logger = logger; - } + public MetaExifThumbnailService(AppSettings appSettings, ISelectorStorage selectorStorage, + IOffsetDataMetaExifThumbnail offsetDataMetaExifThumbnail, + IWriteMetaThumbnailService writeMetaThumbnailService, IWebLogger logger) + { + _appSettings = appSettings; + _iStorage = selectorStorage.Get(SelectorStorage.StorageServices.SubPath); + _thumbnailStorage = selectorStorage.Get(SelectorStorage.StorageServices.Thumbnail); + _offsetDataMetaExifThumbnail = offsetDataMetaExifThumbnail; + _writeMetaThumbnailService = writeMetaThumbnailService; + _logger = logger; + } - /// - /// Run for list that contains subPath and FileHash at once create Meta Thumbnail - /// - /// (subPath, FileHash) - /// fail/pass, string=subPath, string?2= error reason - public async Task> AddMetaThumbnail(IEnumerable<(string, string)> subPathsAndHash) - { - return ( await subPathsAndHash - .ForEachAsync(async item => - await AddMetaThumbnail(item.Item1, item.Item2), - _appSettings.MaxDegreesOfParallelism) )!; - } + /// + /// Run for list that contains subPath and FileHash at once create Meta Thumbnail + /// + /// (subPath, FileHash) + /// fail/pass, string=subPath, string?2= error reason + public async Task> AddMetaThumbnail( + IEnumerable<(string, string)> subPathsAndHash) + { + return ( await subPathsAndHash + .ForEachAsync(async item => + await AddMetaThumbnail(item.Item1, item.Item2), + _appSettings.MaxDegreesOfParallelism) )!; + } - /// - /// This feature is used to crawl over directories and add this to the thumbnail-folder - /// Or File - /// - /// folder subPath style - /// fail/pass, right type, string=subPath, string?2= error reason - /// if folder/file not exist - public async Task> AddMetaThumbnail(string subPath) + /// + /// This feature is used to crawl over directories and add this to the thumbnail-folder + /// Or File + /// + /// folder subPath style + /// fail/pass, right type, string=subPath, string?2= error reason + /// if folder/file not exist + public async Task> AddMetaThumbnail(string subPath) + { + var isFolderOrFile = _iStorage.IsFolderOrFile(subPath); + // ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault + switch ( isFolderOrFile ) { - var isFolderOrFile = _iStorage.IsFolderOrFile(subPath); - // ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault - switch ( isFolderOrFile ) - { - case FolderOrFileModel.FolderOrFileTypeList.Deleted: - _logger.LogError($"[AddMetaThumbnail] folder or file not found {subPath}"); - return new List<(bool, bool, string, string?)> - { - (false, false, subPath, "folder or file not found") - }; - case FolderOrFileModel.FolderOrFileTypeList.Folder: + case FolderOrFileModel.FolderOrFileTypeList.Deleted: + _logger.LogError($"[AddMetaThumbnail] folder or file not found {subPath}"); + return new List<(bool, bool, string, string?)> { - var contentOfDir = _iStorage.GetAllFilesInDirectoryRecursive(subPath) - .Where(ExtensionRolesHelper.IsExtensionExifToolSupported).ToList(); + ( false, false, subPath, "folder or file not found" ) + }; + case FolderOrFileModel.FolderOrFileTypeList.Folder: + { + var contentOfDir = _iStorage.GetAllFilesInDirectoryRecursive(subPath) + .Where(ExtensionRolesHelper.IsExtensionExifToolSupported).ToList(); - var results = await contentOfDir - .ForEachAsync(async singleSubPath => + var results = await contentOfDir + .ForEachAsync(async singleSubPath => await AddMetaThumbnail(singleSubPath, null!), - _appSettings.MaxDegreesOfParallelism); + _appSettings.MaxDegreesOfParallelism); - return results!.ToList(); - } - default: - { - var result = ( await new FileHash(_iStorage).GetHashCodeAsync(subPath) ); - return !result.Value ? new List<(bool, bool, string, string?)> { (false, false, subPath, "hash not found") } : - new List<(bool, bool, string, string?)> { await AddMetaThumbnail(subPath, result.Key) }; - } + return results!.ToList(); + } + default: + { + var result = await new FileHash(_iStorage).GetHashCodeAsync(subPath); + return !result.Value + ? new List<(bool, bool, string, string?)> + { + ( false, false, subPath, "hash not found" ) + } + : new List<(bool, bool, string, string?)> + { + await AddMetaThumbnail(subPath, result.Key) + }; } } + } - /// - /// Create Meta Thumbnail - /// - /// location on disk - /// hash - /// fail/pass, right type, subPath - public async Task<(bool, bool, string, string?)> AddMetaThumbnail(string subPath, string fileHash) + /// + /// Create Meta Thumbnail + /// + /// location on disk + /// hash + /// fail/pass, right type, subPath + public async Task<(bool, bool, string, string?)> AddMetaThumbnail(string subPath, + string fileHash) + { + if ( !_iStorage.ExistFile(subPath) ) { - if ( !_iStorage.ExistFile(subPath) ) - { - return (false, false, subPath, "not found"); - } + return ( false, false, subPath, "not found" ); + } - var first50BytesStream = _iStorage.ReadStream(subPath, 50); - var imageFormat = ExtensionRolesHelper.GetImageFormat(first50BytesStream); + var first50BytesStream = _iStorage.ReadStream(subPath, 50); + var imageFormat = ExtensionRolesHelper.GetImageFormat(first50BytesStream); - if ( imageFormat != ExtensionRolesHelper.ImageFormat.jpg && imageFormat != ExtensionRolesHelper.ImageFormat.tiff ) - { - _logger.LogDebug($"[AddMetaThumbnail] {subPath} is not a jpg or tiff file"); - return (false, false, subPath, $"{subPath} is not a jpg or tiff file"); - } + if ( imageFormat != ExtensionRolesHelper.ImageFormat.jpg && + imageFormat != ExtensionRolesHelper.ImageFormat.tiff ) + { + _logger.LogDebug($"[AddMetaThumbnail] {subPath} is not a jpg or tiff file"); + return ( false, false, subPath, $"{subPath} is not a jpg or tiff file" ); + } - if ( string.IsNullOrEmpty(fileHash) ) + if ( string.IsNullOrEmpty(fileHash) ) + { + var result = await new FileHash(_iStorage).GetHashCodeAsync(subPath); + if ( !result.Value ) { - var result = ( await new FileHash(_iStorage).GetHashCodeAsync(subPath) ); - if ( !result.Value ) - { - _logger.LogError("[MetaExifThumbnail] hash failed"); - return (false, true, subPath, "hash failed"); - } - fileHash = result.Key; + _logger.LogError("[MetaExifThumbnail] hash failed"); + return ( false, true, subPath, "hash failed" ); } - if ( _thumbnailStorage.ExistFile(ThumbnailNameHelper.Combine(fileHash, ThumbnailSize.TinyMeta)) ) - { - return (true, true, subPath, "already exist"); - } + fileHash = result.Key; + } - var (exifThumbnailDir, sourceWidth, sourceHeight, rotation) = - _offsetDataMetaExifThumbnail.GetExifMetaDirectories(subPath); - var offsetData = _offsetDataMetaExifThumbnail. - ParseOffsetData(exifThumbnailDir, subPath); - if ( !offsetData.Success ) - { - return (false, true, subPath, offsetData.Reason); - } + if ( _thumbnailStorage.ExistFile( + ThumbnailNameHelper.Combine(fileHash, ThumbnailSize.TinyMeta)) ) + { + return ( true, true, subPath, "already exist" ); + } - return (await _writeMetaThumbnailService.WriteAndCropFile(fileHash, offsetData, sourceWidth, - sourceHeight, rotation, subPath), true, subPath, null); + var (exifThumbnailDir, sourceWidth, sourceHeight, rotation) = + _offsetDataMetaExifThumbnail.GetExifMetaDirectories(subPath); + var offsetData = _offsetDataMetaExifThumbnail.ParseOffsetData(exifThumbnailDir, subPath); + if ( !offsetData.Success ) + { + return ( false, true, subPath, offsetData.Reason ); } + + return ( await _writeMetaThumbnailService.WriteAndCropFile(fileHash, offsetData, + sourceWidth, + sourceHeight, rotation, subPath), true, subPath, null ); } } diff --git a/starsky/starsky.foundation.thumbnailmeta/Services/WriteMetaThumbnailService.cs b/starsky/starsky.foundation.thumbnailmeta/Services/WriteMetaThumbnailService.cs index 259fbbed92..a9bcd6373c 100644 --- a/starsky/starsky.foundation.thumbnailmeta/Services/WriteMetaThumbnailService.cs +++ b/starsky/starsky.foundation.thumbnailmeta/Services/WriteMetaThumbnailService.cs @@ -5,23 +5,23 @@ using SixLabors.ImageSharp.Processing; using starsky.foundation.database.Models; using starsky.foundation.injection; -using starsky.foundation.thumbnailmeta.Helpers; -using starsky.foundation.thumbnailmeta.Interfaces; -using starsky.foundation.thumbnailmeta.Models; -using starsky.foundation.platform.Enums; using starsky.foundation.platform.Interfaces; using starsky.foundation.platform.Models; +using starsky.foundation.platform.Thumbnails; using starsky.foundation.storage.Interfaces; using starsky.foundation.storage.Storage; +using starsky.foundation.thumbnailmeta.Helpers; +using starsky.foundation.thumbnailmeta.Interfaces; +using starsky.foundation.thumbnailmeta.Models; namespace starsky.foundation.thumbnailmeta.Services; [Service(typeof(IWriteMetaThumbnailService), InjectionLifetime = InjectionLifetime.Scoped)] public sealed class WriteMetaThumbnailService : IWriteMetaThumbnailService { + private readonly AppSettings _appSettings; private readonly IWebLogger _logger; private readonly IStorage _thumbnailStorage; - private readonly AppSettings _appSettings; public WriteMetaThumbnailService(ISelectorStorage selectorStorage, IWebLogger logger, AppSettings appSettings) @@ -44,7 +44,7 @@ public async Task WriteAndCropFile(string fileHash, try { using ( var thumbnailStream = - new MemoryStream(offsetData.Data, offsetData.Index, offsetData.Count) ) + new MemoryStream(offsetData.Data, offsetData.Index, offsetData.Count) ) using ( var smallImage = await Image.LoadAsync(thumbnailStream) ) using ( var outputStream = new MemoryStream() ) { diff --git a/starsky/starsky.foundation.video/Process/FfmpegStreamToStreamRunner.cs b/starsky/starsky.foundation.video/Process/FfmpegStreamToStreamRunner.cs new file mode 100644 index 0000000000..c7b126200c --- /dev/null +++ b/starsky/starsky.foundation.video/Process/FfmpegStreamToStreamRunner.cs @@ -0,0 +1,64 @@ +using System.ComponentModel; +using starsky.foundation.platform.Interfaces; +using static Medallion.Shell.Shell; + +namespace starsky.foundation.video.Process; + +/// +/// Handle Ffmpeg Streaming +/// +internal class FfmpegStreamToStreamRunner(string ffMpegPath, Stream sourceStream, IWebLogger logger) +{ + private readonly Stream _sourceStream = + sourceStream ?? throw new ArgumentNullException(nameof(sourceStream)); + + /// + /// Run Command async (and keep stream open) + /// + /// ffmpeg args + /// reference path (only for display) + /// output format + /// bool if success + /// if exifTool is missing + public async Task<(Stream, bool)> RunProcessAsync(string ffmpegInputArguments, string format, + string referenceInfoAndPath = "") + { + var argumentsWithPipeEnd = $"-i pipe:0 {ffmpegInputArguments} -f {format} -"; + + var memoryStream = new MemoryStream(); + + try + { + // run with pipes + var command = Default.Run(ffMpegPath, + options: opts => + { + opts.StartInfo(si => + si.Arguments = argumentsWithPipeEnd); + }) + < _sourceStream > memoryStream; + + var result = await command.Task.ConfigureAwait(false); + + if ( !result.Success ) + { + var error = await command.StandardError.ReadToEndAsync(); + logger.LogError("[RunProcessAsync] ffmpeg " + error); + } + + logger.LogInformation($"[RunProcessAsync] {result.Success} ~ ffmpeg " + + $"{referenceInfoAndPath} {ffmpegInputArguments} " + + $"run with result: {result.Success} ~ "); + + memoryStream.Seek(0, SeekOrigin.Begin); + + return ( memoryStream, result.Success ); + } + catch ( Win32Exception exception ) + { + throw new ArgumentException("Error when trying to start the ffmpeg process. " + + "Please make sure ffmpeg is installed, and its path is properly " + + "specified in the options.", exception); + } + } +} diff --git a/starsky/starsky.foundation.video/Process/Interfaces/IVideoProcess.cs b/starsky/starsky.foundation.video/Process/Interfaces/IVideoProcess.cs index 12f129777d..d8684e8190 100644 --- a/starsky/starsky.foundation.video/Process/Interfaces/IVideoProcess.cs +++ b/starsky/starsky.foundation.video/Process/Interfaces/IVideoProcess.cs @@ -2,6 +2,6 @@ namespace starsky.foundation.video.Process.Interfaces; public interface IVideoProcess { - Task Run(string subPath, + Task RunVideo(string subPath, string? beforeFileHash, VideoProcessTypes type); } diff --git a/starsky/starsky.foundation.video/Process/Interfaces/IVideoProcessThumbnailPost.cs b/starsky/starsky.foundation.video/Process/Interfaces/IVideoProcessThumbnailPost.cs new file mode 100644 index 0000000000..519b75a493 --- /dev/null +++ b/starsky/starsky.foundation.video/Process/Interfaces/IVideoProcessThumbnailPost.cs @@ -0,0 +1,8 @@ +namespace starsky.foundation.video.Process.Interfaces; + +public interface IVideoProcessThumbnailPost +{ + Task PostPrepThumbnail(VideoResult runResult, + Stream stream, + string subPath); +} diff --git a/starsky/starsky.foundation.video/Process/VideoProcessTypes.cs b/starsky/starsky.foundation.video/Process/Types/VideoProcessTypes.cs similarity index 100% rename from starsky/starsky.foundation.video/Process/VideoProcessTypes.cs rename to starsky/starsky.foundation.video/Process/Types/VideoProcessTypes.cs diff --git a/starsky/starsky.foundation.video/Process/VideoProcess.cs b/starsky/starsky.foundation.video/Process/VideoProcess.cs index 653b369ac4..6ca9875048 100644 --- a/starsky/starsky.foundation.video/Process/VideoProcess.cs +++ b/starsky/starsky.foundation.video/Process/VideoProcess.cs @@ -1,184 +1,71 @@ -using System.ComponentModel; -using starsky.foundation.database.Interfaces; -using starsky.foundation.platform.Helpers; +using starsky.foundation.injection; using starsky.foundation.platform.Interfaces; -using starsky.foundation.platform.Models; -using starsky.foundation.readmeta.Services; using starsky.foundation.storage.Interfaces; -using starsky.foundation.storage.Services; using starsky.foundation.storage.Storage; using starsky.foundation.video.GetDependencies.Interfaces; using starsky.foundation.video.GetDependencies.Models; using starsky.foundation.video.Process.Interfaces; -using starsky.foundation.writemeta.Interfaces; -using starsky.foundation.writemeta.Services; -using static Medallion.Shell.Shell; namespace starsky.foundation.video.Process; +[Service(typeof(IVideoProcessThumbnailPost), InjectionLifetime = InjectionLifetime.Scoped)] public class VideoProcess : IVideoProcess { - private readonly ExifCopy _exifCopy; - private readonly IExifTool _exifTool; private readonly IFfMpegDownload _ffMpegDownload; private readonly IWebLogger _logger; private readonly IStorage _storage; - private readonly IStorage _thumbnailStorage; + private readonly IVideoProcessThumbnailPost _thumbnailPost; public VideoProcess(ISelectorStorage selectorStorage, IFfMpegDownload ffMpegDownload, - IExifTool exifTool, IWebLogger logger, AppSettings appSettings, - IThumbnailQuery thumbnailQuery) + IVideoProcessThumbnailPost thumbnailPost, IWebLogger logger) { _ffMpegDownload = ffMpegDownload; - _exifTool = exifTool; + _thumbnailPost = thumbnailPost; _logger = logger; _storage = selectorStorage.Get(SelectorStorage.StorageServices.SubPath); - _thumbnailStorage = selectorStorage.Get(SelectorStorage.StorageServices.Thumbnail); - - _exifCopy = new ExifCopy(_storage, - _thumbnailStorage, _exifTool, new ReadMeta(_storage, - appSettings, null!, _logger), thumbnailQuery, _logger); } - public async Task Run(string subPath, + public async Task RunVideo(string subPath, string? beforeFileHash, VideoProcessTypes type) { switch ( type ) { case VideoProcessTypes.Thumbnail: - var runResult = await Run(subPath, beforeFileHash, "-frames:v 1", "image2", 300000); - return await PostPrepThumbnail(runResult, subPath, beforeFileHash); + var (runResult, stream) = await RunFfmpeg(subPath, + "-frames:v 1", "image2", 300000); + return await _thumbnailPost.PostPrepThumbnail(runResult, stream, subPath); default: - return false; + return new VideoResult(false, subPath); } } - private async Task PostPrepThumbnail(bool runResult, string subPath, - string? beforeFileHash) - { - if ( !runResult ) - { - return false; - } - - beforeFileHash ??= await FileHash.CalculateHashAsync(_storage.ReadStream(subPath), - true, CancellationToken.None); - - var readStream = _thumbnailStorage.ReadStream(beforeFileHash); - - var jpegSubPath = - $"{FilenamesHelper.GetParentPath(subPath)}/{FilenamesHelper.GetFileNameWithoutExtension(subPath)}.jpg"; - - await _storage.WriteStreamAsync(readStream, jpegSubPath); - - await _exifCopy.CopyExifPublish(subPath, jpegSubPath); - - return runResult; - } - /// /// Run Ffmpeg Command /// /// where file is located - /// what is the hash of the orginal file may be null /// passed to ffmpeg /// image2 or something else /// -1 is entire file, rest is bytes - /// to cancel /// - private async Task Run(string subPath, string? beforeFileHash, + private async Task<(VideoResult, Stream)> RunFfmpeg(string subPath, string ffmpegInputArguments, - string outputFormat, int maxRead, - CancellationToken cancellationToken = default) + string outputFormat, int maxRead) { var downloadStatus = await _ffMpegDownload.DownloadFfMpeg(); if ( downloadStatus != FfmpegDownloadStatus.Ok ) { _logger.LogDebug("[VideoProcess] FFMpeg download failed"); - return false; + return ( new VideoResult(false, null, "FFMpeg download failed"), Stream.Null ); } var sourceStream = _storage.ReadStream(subPath, maxRead); - beforeFileHash ??= await FileHash.CalculateHashAsync(sourceStream, - false, cancellationToken); var runner = - new StreamToStreamRunner(_ffMpegDownload.GetSetFfMpegPath(), sourceStream, _logger); + new FfmpegStreamToStreamRunner(_ffMpegDownload.GetSetFfMpegPath(), sourceStream, + _logger); var (stream, success) = await runner.RunProcessAsync(ffmpegInputArguments, outputFormat, subPath); - if ( !success ) - { - return false; - } - - // Only generic updates here, see Thumbnail for more specific updates - await _thumbnailStorage.WriteStreamAsync(stream, beforeFileHash); - if ( _thumbnailStorage.Info(beforeFileHash).Size > 100 ) - { - return true; - } - - _logger.LogError("[VideoProcess] Thumbnail size is 0"); - _thumbnailStorage.FileDelete(beforeFileHash); - return false; - } -} - -/// -/// Handle Ffmpeg Streaming -/// -internal class StreamToStreamRunner(string ffMpegPath, Stream sourceStream, IWebLogger logger) -{ - private readonly Stream _sourceStream = - sourceStream ?? throw new ArgumentNullException(nameof(sourceStream)); - - /// - /// Run Command async (and keep stream open) - /// - /// ffmpeg args - /// reference path (only for display) - /// output format - /// bool if success - /// if exifTool is missing - public async Task<(Stream, bool)> RunProcessAsync(string ffmpegInputArguments, string format, - string referenceInfoAndPath = "") - { - var argumentsWithPipeEnd = $"-i pipe:0 {ffmpegInputArguments} -f {format} -"; - - var memoryStream = new MemoryStream(); - try - { - // run with pipes - var command = Default.Run(ffMpegPath, - options: opts => - { - opts.StartInfo(si => - si.Arguments = argumentsWithPipeEnd); - }) - < _sourceStream > memoryStream; - - var result = await command.Task.ConfigureAwait(false); - - if ( !result.Success ) - { - var error = await command.StandardError.ReadToEndAsync(); - logger.LogError("[RunProcessAsync] ffmpeg " + error); - } - - logger.LogInformation($"[RunProcessAsync] {result.Success} ~ ffmpeg " + - $"{referenceInfoAndPath} {ffmpegInputArguments} " + - $"run with result: {result.Success} ~ "); - - memoryStream.Seek(0, SeekOrigin.Begin); - - return ( memoryStream, result.Success ); - } - catch ( Win32Exception exception ) - { - throw new ArgumentException("Error when trying to start the ffmpeg process. " + - "Please make sure ffmpeg is installed, and its path is properly " + - "specified in the options.", exception); - } + return ( new VideoResult(success, subPath), stream ); } } diff --git a/starsky/starsky.foundation.video/Process/VideoProcessThumbnailPost.cs b/starsky/starsky.foundation.video/Process/VideoProcessThumbnailPost.cs new file mode 100644 index 0000000000..e33fceda64 --- /dev/null +++ b/starsky/starsky.foundation.video/Process/VideoProcessThumbnailPost.cs @@ -0,0 +1,61 @@ +using starsky.foundation.database.Interfaces; +using starsky.foundation.injection; +using starsky.foundation.platform.Helpers; +using starsky.foundation.platform.Interfaces; +using starsky.foundation.platform.Models; +using starsky.foundation.readmeta.Services; +using starsky.foundation.storage.Interfaces; +using starsky.foundation.storage.Storage; +using starsky.foundation.video.Process.Interfaces; +using starsky.foundation.writemeta.Interfaces; +using starsky.foundation.writemeta.Services; + +namespace starsky.foundation.video.Process; + +[Service(typeof(IVideoProcessThumbnailPost), InjectionLifetime = InjectionLifetime.Scoped)] +public class VideoProcessThumbnailPost : IVideoProcessThumbnailPost +{ + private readonly ExifCopy _exifCopy; + private readonly IStorage _storage; + + public VideoProcessThumbnailPost(ISelectorStorage selectorStorage, + AppSettings appSettings, IExifTool exifTool, IWebLogger logger, + IThumbnailQuery thumbnailQuery) + { + _storage = selectorStorage.Get(SelectorStorage.StorageServices.SubPath); + var thumbnailStorage = selectorStorage.Get(SelectorStorage.StorageServices.Thumbnail); + var readMeta = new ReadMeta(_storage, + appSettings, null!, logger); + _exifCopy = new ExifCopy(_storage, + thumbnailStorage, exifTool, readMeta, thumbnailQuery, logger); + } + + public async Task PostPrepThumbnail(VideoResult runResult, + Stream stream, + string subPath) + { + if ( !runResult.IsSuccess ) + { + return runResult; + } + + var jpegInFolderSubPath = GetJpegInFolderSubPath(subPath); + await WriteStreamInFolderSubPathAsync(stream, subPath, jpegInFolderSubPath); + + return new VideoResult(true, jpegInFolderSubPath); + } + + private static string GetJpegInFolderSubPath(string subPath) + { + return $"{FilenamesHelper.GetParentPath(subPath)}/" + + $"{FilenamesHelper.GetFileNameWithoutExtension(subPath)}.jpg"; + } + + private async Task WriteStreamInFolderSubPathAsync(Stream stream, string subPath, + string jpegInFolderSubPath) + { + await _storage.WriteStreamAsync(stream, jpegInFolderSubPath); + + await _exifCopy.CopyExifPublish(subPath, jpegInFolderSubPath); + } +} diff --git a/starsky/starsky.foundation.video/Process/VideoResult.cs b/starsky/starsky.foundation.video/Process/VideoResult.cs new file mode 100644 index 0000000000..9b242b3ab9 --- /dev/null +++ b/starsky/starsky.foundation.video/Process/VideoResult.cs @@ -0,0 +1,22 @@ +namespace starsky.foundation.video.Process; + +public class VideoResult +{ + public VideoResult(bool? isSuccess = null, string? resultPath = null, + string? errorMessage = null) + { + if ( isSuccess != null ) + { + IsSuccess = ( bool ) isSuccess; + } + + ResultPath = resultPath; + ErrorMessage = errorMessage; + } + + public bool IsSuccess { get; set; } + + public string? ErrorMessage { get; set; } + + public string? ResultPath { get; set; } +} diff --git a/starsky/starsky/Controllers/AllowedTypesController.cs b/starsky/starsky/Controllers/AllowedTypesController.cs index 0882b6ccd4..34d747a73d 100644 --- a/starsky/starsky/Controllers/AllowedTypesController.cs +++ b/starsky/starsky/Controllers/AllowedTypesController.cs @@ -38,7 +38,7 @@ public IActionResult AllowedTypesMimetypeSync() [Produces("application/json")] public IActionResult AllowedTypesMimetypeSyncThumb() { - var mimeTypes = ExtensionRolesHelper.ExtensionThumbSupportedList + var mimeTypes = ExtensionRolesHelper.ExtensionImageSharpThumbnailSupportedList .Select(MimeHelper.GetMimeType).ToHashSet(); return Json(mimeTypes); } @@ -62,7 +62,7 @@ public IActionResult AllowedTypesThumb(string f) return BadRequest("ModelState is not valid"); } - var result = ExtensionRolesHelper.IsExtensionThumbnailSupported(f); + var result = ExtensionRolesHelper.IsExtensionImageSharpThumbnailSupported(f); if ( !result ) { Response.StatusCode = 415; diff --git a/starsky/starsky/Controllers/DownloadPhotoController.cs b/starsky/starsky/Controllers/DownloadPhotoController.cs index 45dd053373..aa6b9a35e3 100644 --- a/starsky/starsky/Controllers/DownloadPhotoController.cs +++ b/starsky/starsky/Controllers/DownloadPhotoController.cs @@ -3,13 +3,13 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using starsky.foundation.database.Interfaces; -using starsky.foundation.platform.Enums; using starsky.foundation.platform.Helpers; using starsky.foundation.platform.Interfaces; +using starsky.foundation.platform.Thumbnails; using starsky.foundation.storage.Interfaces; using starsky.foundation.storage.Models; using starsky.foundation.storage.Storage; -using starsky.foundation.thumbnailgeneration.Interfaces; +using starsky.foundation.thumbnailgeneration.GenerationFactory.Interfaces; using starsky.Helpers; using starsky.project.web.Helpers; @@ -140,12 +140,12 @@ public async Task DownloadPhoto(string f, bool isThumbnail = true if ( !data.Small || !data.Large || !data.ExtraLarge ) { _logger.LogDebug("Thumbnail generation started"); - await _thumbnailService.CreateThumbAsync(fileIndexItem.FilePath!, + await _thumbnailService.GenerateThumbnail(fileIndexItem.FilePath!, fileIndexItem.FileHash!); if ( !_thumbnailStorage.ExistFile( - ThumbnailNameHelper.Combine(fileIndexItem.FileHash!, - ThumbnailSize.Large)) ) + ThumbnailNameHelper.Combine(fileIndexItem.FileHash!, + ThumbnailSize.Large)) ) { Response.StatusCode = 500; return Json("Thumbnail generation failed"); diff --git a/starsky/starsky/Controllers/ImportThumbnailController.cs b/starsky/starsky/Controllers/ImportThumbnailController.cs index d70b3de581..b22cf3b8db 100644 --- a/starsky/starsky/Controllers/ImportThumbnailController.cs +++ b/starsky/starsky/Controllers/ImportThumbnailController.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -9,26 +10,26 @@ using starsky.foundation.database.Interfaces; using starsky.foundation.database.Models; using starsky.foundation.http.Streaming; -using starsky.foundation.platform.Enums; using starsky.foundation.platform.Interfaces; using starsky.foundation.platform.Models; +using starsky.foundation.platform.Thumbnails; using starsky.foundation.storage.Interfaces; using starsky.foundation.storage.Storage; namespace starsky.Controllers; -[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "S5693:Make sure the content " + - "length limit is safe here", Justification = "Is checked")] +[SuppressMessage("Usage", "S5693:Make sure the content " + + "length limit is safe here", Justification = "Is checked")] [Authorize] public class ImportThumbnailController : Controller { private readonly AppSettings _appSettings; - private readonly ISelectorStorage _selectorStorage; private readonly IStorage _hostFileSystemStorage; private readonly IWebLogger _logger; + private readonly RemoveTempAndParentStreamFolderHelper _removeTempAndParentStreamFolderHelper; + private readonly ISelectorStorage _selectorStorage; private readonly IThumbnailQuery _thumbnailQuery; private readonly IStorage _thumbnailStorage; - private readonly RemoveTempAndParentStreamFolderHelper _removeTempAndParentStreamFolderHelper; public ImportThumbnailController(AppSettings appSettings, ISelectorStorage selectorStorage, @@ -36,7 +37,8 @@ public ImportThumbnailController(AppSettings appSettings, { _appSettings = appSettings; _selectorStorage = selectorStorage; - _hostFileSystemStorage = selectorStorage.Get(SelectorStorage.StorageServices.HostFilesystem); + _hostFileSystemStorage = + selectorStorage.Get(SelectorStorage.StorageServices.HostFilesystem); _thumbnailStorage = selectorStorage.Get(SelectorStorage.StorageServices.Thumbnail); _logger = logger; _thumbnailQuery = thumbnailQuery; @@ -46,10 +48,10 @@ public ImportThumbnailController(AppSettings appSettings, } /// - /// Upload thumbnail to ThumbnailTempFolder - /// Make sure that the filename is correct, a base32 hash of length 26; - /// Overwrite if the Id is the same - /// Also known as Thumbnail Upload or Thumbnail Import + /// Upload thumbnail to ThumbnailTempFolder + /// Make sure that the filename is correct, a base32 hash of length 26; + /// Overwrite if the Id is the same + /// Also known as Thumbnail Upload or Thumbnail Import /// /// json of thumbnail urls /// done @@ -60,7 +62,7 @@ public ImportThumbnailController(AppSettings appSettings, [RequestFormLimits(MultipartBodyLengthLimit = 100_000_000)] [RequestSizeLimit(100_000_000)] // in bytes, 100MB [ProducesResponseType(typeof(List), 200)] // yes - [ProducesResponseType(typeof(List), 415)] // wrong input + [ProducesResponseType(typeof(List), 415)] // wrong input public async Task Thumbnail() { var tempImportPaths = await Request.StreamFile(_appSettings, _selectorStorage); @@ -83,7 +85,8 @@ public async Task Thumbnail() return Json(thumbnailNamesWithSuffix); } - internal static IEnumerable MapToTransferObject(List thumbnailNames) + internal static IEnumerable MapToTransferObject( + List thumbnailNames) { var items = new List(); foreach ( var thumbnailNameWithSuffix in thumbnailNames ) @@ -94,6 +97,7 @@ internal static IEnumerable MapToTransferObjec item.Change(thumb, true); items.Add(item); } + return items; } @@ -117,16 +121,19 @@ private List GetThumbnailNamesWithSuffix(List tempImportPaths) // remove existing thumbnail if exist if ( _thumbnailStorage.ExistFile(thumbToUpperCase) ) { - _logger.LogInformation($"[Import/Thumbnail] remove already exists - {thumbToUpperCase}"); + _logger.LogInformation( + $"[Import/Thumbnail] remove already exists - {thumbToUpperCase}"); _thumbnailStorage.FileDelete(thumbToUpperCase); } thumbnailNamesWithSuffix.Add(thumbToUpperCase); } + return thumbnailNamesWithSuffix; } - internal async Task WriteThumbnails(List tempImportPaths, List thumbnailNames) + internal async Task WriteThumbnails(List tempImportPaths, + List thumbnailNames) { if ( tempImportPaths.Count != thumbnailNames.Count ) { @@ -138,7 +145,8 @@ internal async Task WriteThumbnails(List tempImportPaths, List ListSizesByHash(string f) { return BadRequest(ModelError); } - + // For serving jpeg files f = FilenamesHelper.GetFileNameWithoutExtension(f); @@ -160,7 +160,7 @@ public async Task ListSizesByHash(string f) var sourcePath = await _query.GetSubPathByHashAsync(f); var isThumbnailSupported = - ExtensionRolesHelper.IsExtensionThumbnailSupported(sourcePath); + ExtensionRolesHelper.IsExtensionImageSharpThumbnailSupported(sourcePath); switch ( isThumbnailSupported ) { case true when !string.IsNullOrEmpty(sourcePath): @@ -239,7 +239,7 @@ public async Task Thumbnail( { return BadRequest(ModelError); } - + // f is Hash // isSingleItem => detailView // Retry thumbnail => is when you press reset thumbnail @@ -313,7 +313,7 @@ await _query.GetObjectByFilePathAsync(filePath) == null ) return Json("Thumbnail is not ready yet"); } - if ( ExtensionRolesHelper.IsExtensionThumbnailSupported(sourcePath) ) + if ( ExtensionRolesHelper.IsExtensionImageSharpThumbnailSupported(sourcePath) ) { var fs1 = _iStorage.ReadStream(sourcePath); @@ -355,7 +355,7 @@ public async Task ByZoomFactorAsync( { return BadRequest(ModelError); } - + // For serving jpeg files f = FilenamesHelper.GetFileNameWithoutExtension(f); @@ -378,7 +378,7 @@ public async Task ByZoomFactorAsync( sourcePath = filePath; } - if ( ExtensionRolesHelper.IsExtensionThumbnailSupported(sourcePath) ) + if ( ExtensionRolesHelper.IsExtensionImageSharpThumbnailSupported(sourcePath) ) { var fs1 = _iStorage.ReadStream(sourcePath); diff --git a/starsky/starskytest/Controllers/ThumbnailControllerTest.cs b/starsky/starskytest/Controllers/ThumbnailControllerTest.cs index d9d2f61001..538f381ed4 100644 --- a/starsky/starskytest/Controllers/ThumbnailControllerTest.cs +++ b/starsky/starskytest/Controllers/ThumbnailControllerTest.cs @@ -12,9 +12,9 @@ using starsky.foundation.database.Data; using starsky.foundation.database.Models; using starsky.foundation.database.Query; -using starsky.foundation.platform.Enums; using starsky.foundation.platform.Helpers; using starsky.foundation.platform.Models; +using starsky.foundation.platform.Thumbnails; using starsky.foundation.storage.Helpers; using starsky.foundation.storage.Models; using starsky.foundation.storage.Storage; @@ -155,7 +155,7 @@ public async Task Thumbnail_HappyFlowDisplayJson_API_Test() var thumbnailAnswer = actionResult?.Value as string; Assert.AreEqual("OK", thumbnailAnswer); } - + [TestMethod] public async Task Thumbnail_InvalidModel() { @@ -400,7 +400,7 @@ public async Task ByZoomFactor_NonExistingFile_API_Test() var thumbnailAnswer = actionResult?.StatusCode; Assert.AreEqual(404, thumbnailAnswer); } - + [TestMethod] public async Task ByZoomFactor_ModelState() { @@ -663,7 +663,7 @@ public async Task ListSizesByHash_InputBadRequest() var actionResult = await controller.ListSizesByHash("../") as BadRequestResult; Assert.AreEqual(400, actionResult?.StatusCode); } - + [TestMethod] public async Task ListSizesByHash_InvalidModel() { diff --git a/starsky/starskytest/FakeMocks/FakeIThumbnailService.cs b/starsky/starskytest/FakeMocks/FakeIThumbnailService.cs index c30a56d4af..d23848c901 100644 --- a/starsky/starskytest/FakeMocks/FakeIThumbnailService.cs +++ b/starsky/starskytest/FakeMocks/FakeIThumbnailService.cs @@ -8,7 +8,7 @@ using starsky.foundation.storage.Interfaces; using starsky.foundation.storage.Services; using starsky.foundation.storage.Storage; -using starsky.foundation.thumbnailgeneration.Interfaces; +using starsky.foundation.thumbnailgeneration.GenerationFactory.Interfaces; using starsky.foundation.thumbnailgeneration.Models; namespace starskytest.FakeMocks; @@ -30,7 +30,26 @@ public FakeIThumbnailService(FakeSelectorStorage? selectorStorage = null, public List> InputsRotate { get; set; } = new(); - public Task> CreateThumbnailAsync(string subPath) + public async Task> GenerateThumbnail(string fileOrFolderPath, + bool skipExtraLarge = false) + { + return await CreateThumbnailAsync(fileOrFolderPath); + } + + public async Task> GenerateThumbnail(string subPath, + string fileHash, bool skipExtraLarge = false) + { + return ( await CreateThumbAsync(subPath, fileHash) ).ToList(); + } + + public Task RotateThumbnail(string fileHash, int orientation, int width = 1000, + int height = 0) + { + InputsRotate.Add(new Tuple(fileHash, orientation, width, height)); + return Task.FromResult(true); + } + + private Task> CreateThumbnailAsync(string subPath) { if ( _exception != null ) { @@ -66,8 +85,8 @@ public Task> CreateThumbnailAsync(string subPath) return Task.FromResult(resultModel); } - public Task> CreateThumbAsync(string? subPath, - string fileHash, bool skipExtraLarge = false) + private Task> CreateThumbAsync(string? subPath, + string fileHash) { ArgumentNullException.ThrowIfNull(subPath); @@ -91,11 +110,4 @@ public Task> CreateThumbAsync(string? subPath } }.AsEnumerable()); } - - public Task RotateThumbnail(string fileHash, int orientation, int width = 1000, - int height = 0) - { - InputsRotate.Add(new Tuple(fileHash, orientation, width, height)); - return Task.FromResult(true); - } } diff --git a/starsky/starskytest/FakeMocks/FakeIVideoProcess.cs b/starsky/starskytest/FakeMocks/FakeIVideoProcess.cs index 081ab67b31..833b531ecf 100644 --- a/starsky/starskytest/FakeMocks/FakeIVideoProcess.cs +++ b/starsky/starskytest/FakeMocks/FakeIVideoProcess.cs @@ -6,8 +6,9 @@ namespace starskytest.FakeMocks; public class FakeIVideoProcess : IVideoProcess { - public Task Run(string subPath, string? beforeFileHash, VideoProcessTypes type) + public Task RunVideo(string subPath, string? beforeFileHash, + VideoProcessTypes type) { - return Task.FromResult(true); + return Task.FromResult(new VideoResult(true)); } } diff --git a/starsky/starskytest/starsky.feature.metaupdate/Services/MetaUpdateServiceTest.cs b/starsky/starskytest/starsky.feature.metaupdate/Services/MetaUpdateServiceTest.cs index 76e509c66b..d8ecbfcefc 100644 --- a/starsky/starskytest/starsky.feature.metaupdate/Services/MetaUpdateServiceTest.cs +++ b/starsky/starskytest/starsky.feature.metaupdate/Services/MetaUpdateServiceTest.cs @@ -11,8 +11,8 @@ using starsky.foundation.database.Data; using starsky.foundation.database.Models; using starsky.foundation.database.Query; -using starsky.foundation.platform.Enums; using starsky.foundation.platform.Models; +using starsky.foundation.platform.Thumbnails; using starsky.foundation.readmeta.Services; using starsky.foundation.storage.Storage; using starskytest.FakeCreateAn; diff --git a/starsky/starskytest/starsky.foundation.database/Thumbnails/ThumbnailQueryTest.cs b/starsky/starskytest/starsky.foundation.database/Thumbnails/ThumbnailQueryTest.cs index dac52ba290..b05d4d29b5 100644 --- a/starsky/starskytest/starsky.foundation.database/Thumbnails/ThumbnailQueryTest.cs +++ b/starsky/starskytest/starsky.foundation.database/Thumbnails/ThumbnailQueryTest.cs @@ -9,7 +9,7 @@ using starsky.foundation.database.Data; using starsky.foundation.database.Models; using starsky.foundation.database.Thumbnails; -using starsky.foundation.platform.Enums; +using starsky.foundation.platform.Thumbnails; using starskytest.FakeMocks; namespace starskytest.starsky.foundation.database.Thumbnails; @@ -673,38 +673,40 @@ public async Task GetMissingThumbnailsBatchAsync_OneResultOfTwo() var result = await query.GetMissingThumbnailsBatchAsync(0, 100); Assert.AreEqual(1, result.Count); } - + [TestMethod] public async Task GetMissingThumbnailsBatchAsync_Disposed_Recover() { var serviceScope = CreateNewScope("GetMissingThumbnailsBatchAsync_Disposed"); var context = serviceScope.CreateScope().ServiceProvider .GetRequiredService(); - + context.Thumbnails.Add(new ThumbnailItem("123", null, true, null, null)); await context.SaveChangesAsync(); - + // Dispose to check service scope await context.DisposeAsync(); - var query = new ThumbnailQuery(context, serviceScope, new FakeIWebLogger(), new FakeMemoryCache()); + var query = new ThumbnailQuery(context, serviceScope, new FakeIWebLogger(), + new FakeMemoryCache()); var result = await query.GetMissingThumbnailsBatchAsync(0, 100); Assert.AreEqual(1, result.Count); } - + [TestMethod] public async Task GetMissingThumbnailsBatchAsync_Disposed_ServiceScopeMissing() { - var serviceScope = CreateNewScope("GetMissingThumbnailsBatchAsync_Disposed_ServiceScopeMissing"); + var serviceScope = + CreateNewScope("GetMissingThumbnailsBatchAsync_Disposed_ServiceScopeMissing"); var context = serviceScope.CreateScope().ServiceProvider .GetRequiredService(); - + // Dispose to check service scope await context.DisposeAsync(); // No service scope var query = new ThumbnailQuery(context, null!, new FakeIWebLogger(), new FakeMemoryCache()); - + await Assert.ThrowsExceptionAsync(async () => { await query.GetMissingThumbnailsBatchAsync(0, 100); @@ -928,7 +930,7 @@ public void SetIsRunningJob_WithCache() var thumbnailQuery = new ThumbnailQuery(null!, null!, new FakeIWebLogger(), cache); - Assert.IsFalse(cache.TryGetValue($"{nameof(ThumbnailQuery)}_IsRunningJob", out var _)); + Assert.IsFalse(cache.TryGetValue($"{nameof(ThumbnailQuery)}_IsRunningJob", out _)); thumbnailQuery.SetRunningJob(true); diff --git a/starsky/starskytest/starsky.foundation.database/Thumbnails/ThumbnailResultDataTransferModelTest.cs b/starsky/starskytest/starsky.foundation.database/Thumbnails/ThumbnailResultDataTransferModelTest.cs index d5c60cdcb2..7784eba491 100644 --- a/starsky/starskytest/starsky.foundation.database/Thumbnails/ThumbnailResultDataTransferModelTest.cs +++ b/starsky/starskytest/starsky.foundation.database/Thumbnails/ThumbnailResultDataTransferModelTest.cs @@ -1,7 +1,7 @@ using System; using Microsoft.VisualStudio.TestTools.UnitTesting; using starsky.foundation.database.Models; -using starsky.foundation.platform.Enums; +using starsky.foundation.platform.Thumbnails; namespace starskytest.starsky.foundation.database.Thumbnails; diff --git a/starsky/starskytest/starsky.foundation.platform/Helpers/ExtensionRolesHelperTest.cs b/starsky/starskytest/starsky.foundation.platform/Helpers/ExtensionRolesHelperTest.cs index 7edcec58d1..cc2465c034 100644 --- a/starsky/starskytest/starsky.foundation.platform/Helpers/ExtensionRolesHelperTest.cs +++ b/starsky/starskytest/starsky.foundation.platform/Helpers/ExtensionRolesHelperTest.cs @@ -17,32 +17,32 @@ public sealed class ExtensionRolesHelperTest [TestMethod] public void Files_ExtensionThumbSupportedList_TiffMp4MovXMPCheck() { - Assert.IsFalse(ExtensionRolesHelper.IsExtensionThumbnailSupported("file.tiff")); - Assert.IsFalse(ExtensionRolesHelper.IsExtensionThumbnailSupported("file.mp4")); - Assert.IsFalse(ExtensionRolesHelper.IsExtensionThumbnailSupported("file.mov")); - Assert.IsFalse(ExtensionRolesHelper.IsExtensionThumbnailSupported("file.xmp")); + Assert.IsFalse(ExtensionRolesHelper.IsExtensionImageSharpThumbnailSupported("file.tiff")); + Assert.IsFalse(ExtensionRolesHelper.IsExtensionImageSharpThumbnailSupported("file.mp4")); + Assert.IsFalse(ExtensionRolesHelper.IsExtensionImageSharpThumbnailSupported("file.mov")); + Assert.IsFalse(ExtensionRolesHelper.IsExtensionImageSharpThumbnailSupported("file.xmp")); } [TestMethod] public void Files_ExtensionThumbSupportedList_JpgCheck() { - Assert.IsTrue(ExtensionRolesHelper.IsExtensionThumbnailSupported("file.jpg")); - Assert.IsTrue(ExtensionRolesHelper.IsExtensionThumbnailSupported("file.bmp")); + Assert.IsTrue(ExtensionRolesHelper.IsExtensionImageSharpThumbnailSupported("file.jpg")); + Assert.IsTrue(ExtensionRolesHelper.IsExtensionImageSharpThumbnailSupported("file.bmp")); } [TestMethod] public void Files_ExtensionThumbSupportedList_null() { - Assert.IsFalse(ExtensionRolesHelper.IsExtensionThumbnailSupported(null)); + Assert.IsFalse(ExtensionRolesHelper.IsExtensionImageSharpThumbnailSupported(null)); // equal or less then three chars - Assert.IsFalse(ExtensionRolesHelper.IsExtensionThumbnailSupported("nul")); + Assert.IsFalse(ExtensionRolesHelper.IsExtensionImageSharpThumbnailSupported("nul")); } [TestMethod] public void Files_ExtensionThumbSupportedList_FolderName() { Assert.IsFalse( - ExtensionRolesHelper.IsExtensionThumbnailSupported("Some Foldername")); + ExtensionRolesHelper.IsExtensionImageSharpThumbnailSupported("Some Foldername")); } [TestMethod] diff --git a/starsky/starskytest/starsky.foundation.storage/Storage/ThumbnailNameHelperTest.cs b/starsky/starskytest/starsky.foundation.storage/Storage/ThumbnailNameHelperTest.cs index d0e24f3f32..53350a8e4b 100644 --- a/starsky/starskytest/starsky.foundation.storage/Storage/ThumbnailNameHelperTest.cs +++ b/starsky/starskytest/starsky.foundation.storage/Storage/ThumbnailNameHelperTest.cs @@ -1,94 +1,93 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; -using starsky.foundation.platform.Enums; +using starsky.foundation.platform.Thumbnails; using starsky.foundation.storage.Storage; -namespace starskytest.starsky.foundation.storage.Storage +namespace starskytest.starsky.foundation.storage.Storage; + +[TestClass] +public sealed class ThumbnailNameHelperTest { - [TestClass] - public sealed class ThumbnailNameHelperTest + [TestMethod] + public void GetSize_TinyMeta_Enum() + { + var result = ThumbnailNameHelper.GetSize(ThumbnailSize.TinyMeta); + Assert.AreEqual(150, result); + } + + [TestMethod] + public void GetSize_TinyMeta_Int() + { + var result = ThumbnailNameHelper.GetSize(150); + Assert.AreEqual(ThumbnailSize.TinyMeta, result); + } + + [TestMethod] + public void GetSize_Small_Int() + { + var result = ThumbnailNameHelper.GetSize(300); + Assert.AreEqual(ThumbnailSize.Small, result); + } + + [TestMethod] + public void GetSize_Large_Int() + { + var result = ThumbnailNameHelper.GetSize(1000); + Assert.AreEqual(ThumbnailSize.Large, result); + } + + [TestMethod] + public void GetSize_ExtraLarge_Int() + { + var result = ThumbnailNameHelper.GetSize(2000); + Assert.AreEqual(ThumbnailSize.ExtraLarge, result); + } + + [TestMethod] + public void Combine_Compare() { - [TestMethod] - public void GetSize_TinyMeta_Enum() - { - var result = ThumbnailNameHelper.GetSize(ThumbnailSize.TinyMeta); - Assert.AreEqual(150, result); - } + var result = ThumbnailNameHelper.Combine("test_hash", 2000); + var result2 = ThumbnailNameHelper.Combine("test_hash", ThumbnailSize.ExtraLarge); + + Assert.AreEqual(result, result2); + } + + [TestMethod] + public void GetSize_Name_ExtraLarge() + { + var input = ThumbnailNameHelper.Combine("01234567890123456789123456", 2000); + var result2 = ThumbnailNameHelper.GetSize(input); + Assert.AreEqual(ThumbnailSize.ExtraLarge, result2); + } - [TestMethod] - public void GetSize_TinyMeta_Int() - { - var result =ThumbnailNameHelper.GetSize(150); - Assert.AreEqual(ThumbnailSize.TinyMeta, result); - } + [TestMethod] + public void GetSize_Name_Large() + { + var input = ThumbnailNameHelper.Combine("01234567890123456789123456", ThumbnailSize.Large); + var result2 = ThumbnailNameHelper.GetSize(input); + Assert.AreEqual(ThumbnailSize.Large, result2); + } - [TestMethod] - public void GetSize_Small_Int() - { - var result =ThumbnailNameHelper.GetSize(300); - Assert.AreEqual(ThumbnailSize.Small, result); - } - - [TestMethod] - public void GetSize_Large_Int() - { - var result =ThumbnailNameHelper.GetSize(1000); - Assert.AreEqual(ThumbnailSize.Large, result); - } - - [TestMethod] - public void GetSize_ExtraLarge_Int() - { - var result = ThumbnailNameHelper.GetSize(2000); - Assert.AreEqual(ThumbnailSize.ExtraLarge, result); - } + [TestMethod] + public void GetSize_Name_NonValidLength() + { + var input = "01234567890123456789123456@859693845"; + var result2 = ThumbnailNameHelper.GetSize(input); + Assert.AreEqual(ThumbnailSize.Unknown, result2); + } - [TestMethod] - public void Combine_Compare() - { - var result = ThumbnailNameHelper.Combine("test_hash",2000); - var result2 = ThumbnailNameHelper.Combine("test_hash",ThumbnailSize.ExtraLarge); + [TestMethod] + public void GetSize_Name_Large_NonValidLength() + { + var input = ThumbnailNameHelper.Combine("non_valid_length", ThumbnailSize.Large); + var result2 = ThumbnailNameHelper.GetSize(input); + Assert.AreEqual(ThumbnailSize.Unknown, result2); + } - Assert.AreEqual(result,result2); - } - - [TestMethod] - public void GetSize_Name_ExtraLarge() - { - var input = ThumbnailNameHelper.Combine("01234567890123456789123456",2000); - var result2 = ThumbnailNameHelper.GetSize(input); - Assert.AreEqual(ThumbnailSize.ExtraLarge, result2); - } - - [TestMethod] - public void GetSize_Name_Large() - { - var input = ThumbnailNameHelper.Combine("01234567890123456789123456",ThumbnailSize.Large); - var result2 = ThumbnailNameHelper.GetSize(input); - Assert.AreEqual(ThumbnailSize.Large, result2); - } - - [TestMethod] - public void GetSize_Name_NonValidLength() - { - var input = "01234567890123456789123456@859693845"; - var result2 = ThumbnailNameHelper.GetSize(input); - Assert.AreEqual(ThumbnailSize.Unknown, result2); - } - - [TestMethod] - public void GetSize_Name_Large_NonValidLength() - { - var input = ThumbnailNameHelper.Combine("non_valid_length",ThumbnailSize.Large); - var result2 = ThumbnailNameHelper.GetSize(input); - Assert.AreEqual(ThumbnailSize.Unknown, result2); - } - - [TestMethod] - public void GetSize_Name_UnknownSize() - { - var input = "test_hash@4789358"; - var result2 = ThumbnailNameHelper.GetSize(input); - Assert.AreEqual(ThumbnailSize.Unknown, result2); - } + [TestMethod] + public void GetSize_Name_UnknownSize() + { + var input = "test_hash@4789358"; + var result2 = ThumbnailNameHelper.GetSize(input); + Assert.AreEqual(ThumbnailSize.Unknown, result2); } } diff --git a/starsky/starskytest/starsky.foundation.thumbnailgeneration/Helpers/ThumbnailTest.cs b/starsky/starskytest/starsky.foundation.thumbnailgeneration/Helpers/ThumbnailTest.cs index 8391218212..387ff03126 100644 --- a/starsky/starskytest/starsky.foundation.thumbnailgeneration/Helpers/ThumbnailTest.cs +++ b/starsky/starskytest/starsky.foundation.thumbnailgeneration/Helpers/ThumbnailTest.cs @@ -3,9 +3,9 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; -using starsky.foundation.platform.Enums; using starsky.foundation.platform.Helpers; using starsky.foundation.platform.Models; +using starsky.foundation.platform.Thumbnails; using starsky.foundation.storage.Helpers; using starsky.foundation.storage.Services; using starsky.foundation.storage.Storage; @@ -13,341 +13,341 @@ using starskytest.FakeCreateAn; using starskytest.FakeMocks; -namespace starskytest.starsky.foundation.thumbnailgeneration.Helpers +namespace starskytest.starsky.foundation.thumbnailgeneration.Helpers; + +[TestClass] +public sealed class ThumbnailTest { - [TestClass] - public sealed class ThumbnailTest + private readonly string _fakeIStorageImageSubPath; + private readonly FakeIStorage _iStorage; + + public ThumbnailTest() { - private readonly FakeIStorage _iStorage; - private readonly string _fakeIStorageImageSubPath; + _fakeIStorageImageSubPath = "/test.jpg"; - public ThumbnailTest() - { - _fakeIStorageImageSubPath = "/test.jpg"; + _iStorage = new FakeIStorage(new List { "/" }, + new List { _fakeIStorageImageSubPath }, + new List { CreateAnImage.Bytes.ToArray() }); + } - _iStorage = new FakeIStorage(new List { "/" }, - new List { _fakeIStorageImageSubPath }, - new List { CreateAnImage.Bytes.ToArray() }); - } + [TestMethod] + public async Task CreateThumbTest_FileHash_FileHashNull() + { + // Arrange + var thumbnailService = + new Thumbnail(_iStorage, _iStorage, new FakeIWebLogger(), new AppSettings()); - [TestMethod] - public async Task CreateThumbTest_FileHash_FileHashNull() - { - // Arrange - var thumbnailService = new Thumbnail(_iStorage, _iStorage, new FakeIWebLogger(), new AppSettings()); - - // Act & Assert - await Assert.ThrowsExceptionAsync(async () => - { - await thumbnailService.CreateThumbAsync("/notfound.jpg", null!); - }); - } - - [TestMethod] - public async Task CreateThumbTest_FileHash_ImageSubPathNotFound() + // Act & Assert + await Assert.ThrowsExceptionAsync(async () => { - var isCreated = - await new Thumbnail(_iStorage, _iStorage, new FakeIWebLogger(), new AppSettings()) - .CreateThumbAsync( - "/notfound.jpg", _fakeIStorageImageSubPath); - Assert.IsFalse(isCreated.FirstOrDefault()!.Success); - } - - [TestMethod] - public async Task CreateThumbTest_FileHash_WrongImageType() - { - var isCreated = await new Thumbnail(_iStorage, - _iStorage, new FakeIWebLogger(), new AppSettings()).CreateThumbAsync( - "/notfound.dng", _fakeIStorageImageSubPath); - Assert.IsFalse(isCreated.FirstOrDefault()!.Success); - } - - [TestMethod] - public async Task CreateThumbTest_FileHash_AlreadyFailBefore() - { - var storage = new FakeIStorage(new List { "/" }, - new List { _fakeIStorageImageSubPath }, - new List { CreateAnImage.Bytes.ToArray() }); + await thumbnailService.CreateThumbAsync("/notfound.jpg", null!); + }); + } - var thumbnailService = new Thumbnail(storage, storage, - new FakeIWebLogger(), new AppSettings()); + [TestMethod] + public async Task CreateThumbTest_FileHash_ImageSubPathNotFound() + { + var isCreated = + await new Thumbnail(_iStorage, _iStorage, new FakeIWebLogger(), new AppSettings()) + .CreateThumbAsync( + "/notfound.jpg", _fakeIStorageImageSubPath); + Assert.IsFalse(isCreated.FirstOrDefault()!.Success); + } - await thumbnailService.WriteErrorMessageToBlockLog(_fakeIStorageImageSubPath, "fail"); + [TestMethod] + public async Task CreateThumbTest_FileHash_WrongImageType() + { + var isCreated = await new Thumbnail(_iStorage, + _iStorage, new FakeIWebLogger(), new AppSettings()).CreateThumbAsync( + "/notfound.dng", _fakeIStorageImageSubPath); + Assert.IsFalse(isCreated.FirstOrDefault()!.Success); + } - var isCreated = ( await thumbnailService.CreateThumbAsync(_fakeIStorageImageSubPath, - _fakeIStorageImageSubPath) ).ToList(); + [TestMethod] + public async Task CreateThumbTest_FileHash_AlreadyFailBefore() + { + var storage = new FakeIStorage(new List { "/" }, + new List { _fakeIStorageImageSubPath }, + new List { CreateAnImage.Bytes.ToArray() }); - Assert.IsFalse(isCreated.FirstOrDefault()!.Success); - Assert.AreEqual("File already failed before", isCreated.FirstOrDefault()!.ErrorMessage); - } + var thumbnailService = new Thumbnail(storage, storage, + new FakeIWebLogger(), new AppSettings()); - [TestMethod] - public async Task CreateThumbTest_FileHash_SkipExtraLarge() - { - var storage = new FakeIStorage(new List { "/" }, - new List { _fakeIStorageImageSubPath }, - new List { CreateAnImage.Bytes.ToArray() }); - - const string fileHash = "test_hash"; - - // skip extra large - var isCreated = await new Thumbnail(storage, - storage, new FakeIWebLogger(), new AppSettings()).CreateThumbAsync( - _fakeIStorageImageSubPath, fileHash, true); - Assert.IsTrue(isCreated.FirstOrDefault()!.Success); - - Assert.IsTrue(storage.ExistFile(fileHash)); - Assert.IsTrue(storage.ExistFile( - ThumbnailNameHelper.Combine(fileHash, ThumbnailSize.Small))); - Assert.IsFalse(storage.ExistFile( - ThumbnailNameHelper.Combine(fileHash, ThumbnailSize.ExtraLarge))); - } - - [TestMethod] - public async Task CreateThumbTest_FileHash_IncludeExtraLarge() - { - var storage = new FakeIStorage(new List { "/" }, - new List { _fakeIStorageImageSubPath }, - new List { CreateAnImage.Bytes.ToArray() }); - - const string fileHash = "test_hash"; - // include extra large - var isCreated = await new Thumbnail(storage, - storage, new FakeIWebLogger(), new AppSettings()).CreateThumbAsync( - _fakeIStorageImageSubPath, fileHash); - Assert.IsTrue(isCreated.FirstOrDefault()!.Success); - - Assert.IsTrue(storage.ExistFile(fileHash)); - Assert.IsTrue(storage.ExistFile( - ThumbnailNameHelper.Combine(fileHash, ThumbnailSize.Small))); - Assert.IsTrue(storage.ExistFile( - ThumbnailNameHelper.Combine(fileHash, ThumbnailSize.ExtraLarge))); - } - - [TestMethod] - public async Task CreateThumbTest_1arg_ThumbnailAlreadyExist() - { - var storage = new FakeIStorage(new List { "/" }, - new List { _fakeIStorageImageSubPath }, - new List { CreateAnImage.Bytes.ToArray() }); - - var hash = ( await new FileHash(storage).GetHashCodeAsync(_fakeIStorageImageSubPath) ) - .Key; - await storage.WriteStreamAsync( - StringToStreamHelper.StringToStream("not 0 bytes"), - ThumbnailNameHelper.Combine(hash, ThumbnailSize.ExtraLarge)); - await storage.WriteStreamAsync( - StringToStreamHelper.StringToStream("not 0 bytes"), - ThumbnailNameHelper.Combine(hash, ThumbnailSize.Large)); - await storage.WriteStreamAsync( - StringToStreamHelper.StringToStream("not 0 bytes"), - ThumbnailNameHelper.Combine(hash, ThumbnailSize.Small)); - - var isCreated = await new Thumbnail(storage, - storage, new FakeIWebLogger(), new AppSettings()).CreateThumbnailAsync( - _fakeIStorageImageSubPath); - Assert.IsTrue(isCreated[0].Success); - } - - [TestMethod] - public async Task CreateThumbTest_1arg_Folder() - { - var storage = new FakeIStorage(new List { "/" }, - new List { _fakeIStorageImageSubPath }, - new List { CreateAnImage.Bytes.ToArray() }); + await thumbnailService.WriteErrorMessageToBlockLog(_fakeIStorageImageSubPath, "fail"); - var isCreated = await new Thumbnail(storage, - storage, new FakeIWebLogger(), new AppSettings()).CreateThumbnailAsync("/"); - Assert.IsTrue(isCreated[0].Success); - } + var isCreated = ( await thumbnailService.CreateThumbAsync(_fakeIStorageImageSubPath, + _fakeIStorageImageSubPath) ).ToList(); - [TestMethod] - public async Task CreateThumbTest_NullFail() - { - var storage = new FakeIStorage(new List { "/test" }, - new List { "/test/test.jpg" }, - new List { null }); + Assert.IsFalse(isCreated.FirstOrDefault()!.Success); + Assert.AreEqual("File already failed before", isCreated.FirstOrDefault()!.ErrorMessage); + } + + [TestMethod] + public async Task CreateThumbTest_FileHash_SkipExtraLarge() + { + var storage = new FakeIStorage(new List { "/" }, + new List { _fakeIStorageImageSubPath }, + new List { CreateAnImage.Bytes.ToArray() }); + + const string fileHash = "test_hash"; + + // skip extra large + var isCreated = await new Thumbnail(storage, + storage, new FakeIWebLogger(), new AppSettings()).CreateThumbAsync( + _fakeIStorageImageSubPath, fileHash, true); + Assert.IsTrue(isCreated.FirstOrDefault()!.Success); + + Assert.IsTrue(storage.ExistFile(fileHash)); + Assert.IsTrue(storage.ExistFile( + ThumbnailNameHelper.Combine(fileHash, ThumbnailSize.Small))); + Assert.IsFalse(storage.ExistFile( + ThumbnailNameHelper.Combine(fileHash, ThumbnailSize.ExtraLarge))); + } - var isCreated = await new Thumbnail(storage, - storage, new FakeIWebLogger(), new AppSettings()) - .CreateThumbnailAsync("/test/test.jpg"); + [TestMethod] + public async Task CreateThumbTest_FileHash_IncludeExtraLarge() + { + var storage = new FakeIStorage(new List { "/" }, + new List { _fakeIStorageImageSubPath }, + new List { CreateAnImage.Bytes.ToArray() }); + + const string fileHash = "test_hash"; + // include extra large + var isCreated = await new Thumbnail(storage, + storage, new FakeIWebLogger(), new AppSettings()).CreateThumbAsync( + _fakeIStorageImageSubPath, fileHash); + Assert.IsTrue(isCreated.FirstOrDefault()!.Success); + + Assert.IsTrue(storage.ExistFile(fileHash)); + Assert.IsTrue(storage.ExistFile( + ThumbnailNameHelper.Combine(fileHash, ThumbnailSize.Small))); + Assert.IsTrue(storage.ExistFile( + ThumbnailNameHelper.Combine(fileHash, ThumbnailSize.ExtraLarge))); + } - Assert.AreEqual(0, isCreated.Count); - } + [TestMethod] + public async Task CreateThumbTest_1arg_ThumbnailAlreadyExist() + { + var storage = new FakeIStorage(new List { "/" }, + new List { _fakeIStorageImageSubPath }, + new List { CreateAnImage.Bytes.ToArray() }); + + var hash = ( await new FileHash(storage).GetHashCodeAsync(_fakeIStorageImageSubPath) ) + .Key; + await storage.WriteStreamAsync( + StringToStreamHelper.StringToStream("not 0 bytes"), + ThumbnailNameHelper.Combine(hash, ThumbnailSize.ExtraLarge)); + await storage.WriteStreamAsync( + StringToStreamHelper.StringToStream("not 0 bytes"), + ThumbnailNameHelper.Combine(hash, ThumbnailSize.Large)); + await storage.WriteStreamAsync( + StringToStreamHelper.StringToStream("not 0 bytes"), + ThumbnailNameHelper.Combine(hash, ThumbnailSize.Small)); + + var isCreated = await new Thumbnail(storage, + storage, new FakeIWebLogger(), new AppSettings()).CreateThumbnailAsync( + _fakeIStorageImageSubPath); + Assert.IsTrue(isCreated[0].Success); + } - [TestMethod] - public async Task ResizeThumbnailToStream__HostDependency__JPEG_Test() - { - var newImage = new CreateAnImage(); - var iStorage = new StorageHostFullPathFilesystem(new FakeIWebLogger()); - - // string subPath, int width, string outputHash = null,bool removeExif = false,ExtensionRolesHelper.ImageFormat - // imageFormat = ExtensionRolesHelper.ImageFormat.jpg - var thumb = await new Thumbnail(iStorage, - iStorage, new FakeIWebLogger(), new AppSettings()).ResizeThumbnailFromSourceImage( - newImage.FullFilePath, 1, null, true); - Assert.IsTrue(thumb.Item1?.CanRead); - } - - [TestMethod] - public async Task ResizeThumbnailToStream__PNG_Test() - { - var thumb = await new Thumbnail(_iStorage, - _iStorage, new FakeIWebLogger(), new AppSettings()).ResizeThumbnailFromSourceImage( - _fakeIStorageImageSubPath, 1, null, true, - ExtensionRolesHelper.ImageFormat.png); - Assert.IsTrue(thumb.Item1?.CanRead); - } - - [TestMethod] - public async Task ResizeThumbnailToStream_CorruptImage_MemoryStream() - { - var storage = new FakeIStorage( - new List { "/" }, - new List { "test" }, - new List { Array.Empty() }); + [TestMethod] + public async Task CreateThumbTest_1arg_Folder() + { + var storage = new FakeIStorage(new List { "/" }, + new List { _fakeIStorageImageSubPath }, + new List { CreateAnImage.Bytes.ToArray() }); - var result = ( await new Thumbnail(storage, + var isCreated = await new Thumbnail(storage, + storage, new FakeIWebLogger(), new AppSettings()).CreateThumbnailAsync("/"); + Assert.IsTrue(isCreated[0].Success); + } + + [TestMethod] + public async Task CreateThumbTest_NullFail() + { + var storage = new FakeIStorage(new List { "/test" }, + new List { "/test/test.jpg" }, + new List { null }); + + var isCreated = await new Thumbnail(storage, + storage, new FakeIWebLogger(), new AppSettings()) + .CreateThumbnailAsync("/test/test.jpg"); + + Assert.AreEqual(0, isCreated.Count); + } + + [TestMethod] + public async Task ResizeThumbnailToStream__HostDependency__JPEG_Test() + { + var newImage = new CreateAnImage(); + var iStorage = new StorageHostFullPathFilesystem(new FakeIWebLogger()); + + // string subPath, int width, string outputHash = null,bool removeExif = false,ExtensionRolesHelper.ImageFormat + // imageFormat = ExtensionRolesHelper.ImageFormat.jpg + var thumb = await new Thumbnail(iStorage, + iStorage, new FakeIWebLogger(), new AppSettings()).ResizeThumbnailFromSourceImage( + newImage.FullFilePath, 1, null, true); + Assert.IsTrue(thumb.Item1?.CanRead); + } + + [TestMethod] + public async Task ResizeThumbnailToStream__PNG_Test() + { + var thumb = await new Thumbnail(_iStorage, + _iStorage, new FakeIWebLogger(), new AppSettings()).ResizeThumbnailFromSourceImage( + _fakeIStorageImageSubPath, 1, null, true, + ExtensionRolesHelper.ImageFormat.png); + Assert.IsTrue(thumb.Item1?.CanRead); + } + + [TestMethod] + public async Task ResizeThumbnailToStream_CorruptImage_MemoryStream() + { + var storage = new FakeIStorage( + new List { "/" }, + new List { "test" }, + new List { Array.Empty() }); + + var result = ( await new Thumbnail(storage, storage, new FakeIWebLogger(), new AppSettings()) .ResizeThumbnailFromSourceImage("test", 1) ) - .Item1; - Assert.IsNull(result); - } + .Item1; + Assert.IsNull(result); + } - [TestMethod] - public async Task ResizeThumbnailToStream_CorruptImage_Status() - { - var storage = new FakeIStorage( - new List { "/" }, - new List { "test" }, - new List { Array.Empty() }); + [TestMethod] + public async Task ResizeThumbnailToStream_CorruptImage_Status() + { + var storage = new FakeIStorage( + new List { "/" }, + new List { "test" }, + new List { Array.Empty() }); - var result = ( await new Thumbnail(storage, + var result = ( await new Thumbnail(storage, storage, new FakeIWebLogger(), new AppSettings()) .ResizeThumbnailFromSourceImage("test", 1) ) - .Item2; - Assert.IsFalse(result); - } + .Item2; + Assert.IsFalse(result); + } - [TestMethod] - public async Task ResizeThumbnailImageFormat_NullInput() - { - await Assert.ThrowsExceptionAsync(async () => - await Thumbnail.SaveThumbnailImageFormat(null!, - ExtensionRolesHelper.ImageFormat.bmp, null!)); - } + [TestMethod] + public async Task ResizeThumbnailImageFormat_NullInput() + { + await Assert.ThrowsExceptionAsync(async () => + await Thumbnail.SaveThumbnailImageFormat(null!, + ExtensionRolesHelper.ImageFormat.bmp, null!)); + } - [TestMethod] - public void RemoveCorruptImage_RemoveCorruptImage() - { - var storage = new FakeIStorage( - new List { "/" }, - new List { ThumbnailNameHelper.Combine("test", ThumbnailSize.ExtraLarge) }, - new List { Array.Empty() }); - - var result = new Thumbnail(storage, - storage, new FakeIWebLogger(), new AppSettings()) - .RemoveCorruptImage("test", ThumbnailSize.ExtraLarge); - Assert.IsTrue(result); - } - - [TestMethod] - public void RemoveCorruptImage_ShouldIgnore() - { - var storage = new FakeIStorage( - new List { "/" }, - new List { ThumbnailNameHelper.Combine("test", ThumbnailSize.ExtraLarge) }, - new List { CreateAnImage.Bytes.ToArray() }); + [TestMethod] + public void RemoveCorruptImage_RemoveCorruptImage() + { + var storage = new FakeIStorage( + new List { "/" }, + new List { ThumbnailNameHelper.Combine("test", ThumbnailSize.ExtraLarge) }, + new List { Array.Empty() }); + + var result = new Thumbnail(storage, + storage, new FakeIWebLogger(), new AppSettings()) + .RemoveCorruptImage("test", ThumbnailSize.ExtraLarge); + Assert.IsTrue(result); + } - var result = new Thumbnail( - storage, storage, - new FakeIWebLogger(), new AppSettings()) - .RemoveCorruptImage("test", ThumbnailSize.Large); - Assert.IsFalse(result); - } + [TestMethod] + public void RemoveCorruptImage_ShouldIgnore() + { + var storage = new FakeIStorage( + new List { "/" }, + new List { ThumbnailNameHelper.Combine("test", ThumbnailSize.ExtraLarge) }, + new List { CreateAnImage.Bytes.ToArray() }); + + var result = new Thumbnail( + storage, storage, + new FakeIWebLogger(), new AppSettings()) + .RemoveCorruptImage("test", ThumbnailSize.Large); + Assert.IsFalse(result); + } - [TestMethod] - public void RemoveCorruptImage_NotExist() - { - var storage = new FakeIStorage( - new List { "/" }, - new List(), - new List { CreateAnImage.Bytes.ToArray() }); - - var result = new Thumbnail(storage, - storage, new FakeIWebLogger(), new AppSettings()) - .RemoveCorruptImage("test", ThumbnailSize.Large); - Assert.IsFalse(result); - } - - [TestMethod] - public async Task RotateThumbnail_NotFound() - { - var result = await new Thumbnail(_iStorage, - _iStorage, new FakeIWebLogger(), new AppSettings()) - .RotateThumbnail("not-found", 0, 3); - Assert.IsFalse(result); - } - - [TestMethod] - public async Task RotateThumbnail_Rotate() - { - var storage = new FakeIStorage( - new List { "/" }, - new List { "/test.jpg" }, - new List { CreateAnImage.Bytes.ToArray() }); + [TestMethod] + public void RemoveCorruptImage_NotExist() + { + var storage = new FakeIStorage( + new List { "/" }, + new List(), + new List { CreateAnImage.Bytes.ToArray() }); + + var result = new Thumbnail(storage, + storage, new FakeIWebLogger(), new AppSettings()) + .RemoveCorruptImage("test", ThumbnailSize.Large); + Assert.IsFalse(result); + } + + [TestMethod] + public async Task RotateThumbnail_NotFound() + { + var result = await new Thumbnail(_iStorage, + _iStorage, new FakeIWebLogger(), new AppSettings()) + .RotateThumbnail("not-found", 0, 3); + Assert.IsFalse(result); + } - var result = await new Thumbnail(storage, - storage, new FakeIWebLogger(), new AppSettings()) - .RotateThumbnail("/test.jpg", -1, 3); + [TestMethod] + public async Task RotateThumbnail_Rotate() + { + var storage = new FakeIStorage( + new List { "/" }, + new List { "/test.jpg" }, + new List { CreateAnImage.Bytes.ToArray() }); - Assert.IsTrue(result); - } + var result = await new Thumbnail(storage, + storage, new FakeIWebLogger(), new AppSettings()) + .RotateThumbnail("/test.jpg", -1, 3); - [TestMethod] - public async Task RotateThumbnail_Corrupt() - { - var storage = new FakeIStorage( - new List { "/" }, - new List { "test" }, - new List { Array.Empty() }); - - var result = await new Thumbnail(storage, - storage, new FakeIWebLogger(), new AppSettings()).RotateThumbnail("test", 1); - Assert.IsFalse(result); - } - - [TestMethod] - public async Task ResizeThumbnailFromThumbnailImage_CorruptInput() - { - var storage = new FakeIStorage( - new List { "/" }, - new List { "test" }, - new List { Array.Empty() }); - - var result = await new Thumbnail(storage, - storage, new FakeIWebLogger(), new AppSettings()) - .ResizeThumbnailFromThumbnailImage("test", 1); - Assert.IsNull(result.Item1); - Assert.IsFalse(result.Item2.Success); - } - - [TestMethod] - public async Task CreateLargestImageFromSource_CorruptInput() - { - var storage = new FakeIStorage( - new List { "/" }, - new List { "test" }, - new List { Array.Empty() }); - - var result = await new Thumbnail(storage, - storage, new FakeIWebLogger(), new AppSettings()) - .CreateLargestImageFromSource("test", "test", "test", ThumbnailSize.Small); - - Assert.IsFalse(result.Success); - Assert.AreEqual("Image cannot be loaded", result.ErrorMessage); - } + Assert.IsTrue(result); + } + + [TestMethod] + public async Task RotateThumbnail_Corrupt() + { + var storage = new FakeIStorage( + new List { "/" }, + new List { "test" }, + new List { Array.Empty() }); + + var result = await new Thumbnail(storage, + storage, new FakeIWebLogger(), new AppSettings()).RotateThumbnail("test", 1); + Assert.IsFalse(result); + } + + [TestMethod] + public async Task ResizeThumbnailFromThumbnailImage_CorruptInput() + { + var storage = new FakeIStorage( + new List { "/" }, + new List { "test" }, + new List { Array.Empty() }); + + var result = await new Thumbnail(storage, + storage, new FakeIWebLogger(), new AppSettings()) + .ResizeThumbnailFromThumbnailImage("test", 1); + Assert.IsNull(result.Item1); + Assert.IsFalse(result.Item2.Success); + } + + [TestMethod] + public async Task CreateLargestImageFromSource_CorruptInput() + { + var storage = new FakeIStorage( + new List { "/" }, + new List { "test" }, + new List { Array.Empty() }); + + var result = await new Thumbnail(storage, + storage, new FakeIWebLogger(), new AppSettings()) + .CreateLargestImageFromSource("test", "test", "test", ThumbnailSize.Small); + + Assert.IsFalse(result.Success); + Assert.AreEqual("Image cannot be loaded", result.ErrorMessage); } } diff --git a/starsky/starskytest/starsky.foundation.thumbnailgeneration/Services/ThumbnailCleanerTest.cs b/starsky/starskytest/starsky.foundation.thumbnailgeneration/Services/ThumbnailCleanerTest.cs index 7d7f16ce9c..91506ff79a 100644 --- a/starsky/starskytest/starsky.foundation.thumbnailgeneration/Services/ThumbnailCleanerTest.cs +++ b/starsky/starskytest/starsky.foundation.thumbnailgeneration/Services/ThumbnailCleanerTest.cs @@ -10,8 +10,8 @@ using starsky.foundation.database.Data; using starsky.foundation.database.Models; using starsky.foundation.database.Query; -using starsky.foundation.platform.Enums; using starsky.foundation.platform.Models; +using starsky.foundation.platform.Thumbnails; using starsky.foundation.storage.Storage; using starsky.foundation.thumbnailgeneration.Services; using starskytest.FakeCreateAn; diff --git a/starsky/starskytest/starsky.foundation.thumbnailgeneration/Services/ThumbnailServiceTest.cs b/starsky/starskytest/starsky.foundation.thumbnailgeneration/Services/ThumbnailServiceTest.cs index 6c46aa8ee7..8768eb5592 100644 --- a/starsky/starskytest/starsky.foundation.thumbnailgeneration/Services/ThumbnailServiceTest.cs +++ b/starsky/starskytest/starsky.foundation.thumbnailgeneration/Services/ThumbnailServiceTest.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using starsky.foundation.platform.Models; +using starsky.foundation.thumbnailgeneration.GenerationFactory; using starsky.foundation.thumbnailgeneration.Services; using starskytest.FakeMocks; @@ -16,7 +17,7 @@ public async Task NotFound() var sut = new ThumbnailService(new FakeSelectorStorage(), new FakeIWebLogger(), new AppSettings(), new UpdateStatusGeneratedThumbnailService(new FakeIThumbnailQuery())); - var resultModels = await sut.CreateThumbnailAsync("/not-found"); + var resultModels = await sut.GenerateThumbnail("/not-found"); Assert.IsFalse(resultModels.FirstOrDefault()!.Success); } @@ -27,7 +28,7 @@ public async Task NotFoundNonExistingHash() var sut = new ThumbnailService(new FakeSelectorStorage(), new FakeIWebLogger(), new AppSettings(), new UpdateStatusGeneratedThumbnailService(new FakeIThumbnailQuery())); - var result = await sut.CreateThumbAsync("/not-found", "non-existing-hash"); + var result = await sut.GenerateThumbnail("/not-found", "non-existing-hash"); Assert.IsFalse(result.FirstOrDefault()!.Success); } } diff --git a/starsky/starskytest/starsky.foundation.thumbnailgeneration/Services/UpdateStatusGeneratedThumbnailServiceTest.cs b/starsky/starskytest/starsky.foundation.thumbnailgeneration/Services/UpdateStatusGeneratedThumbnailServiceTest.cs index 28133a172a..fbc264986d 100644 --- a/starsky/starskytest/starsky.foundation.thumbnailgeneration/Services/UpdateStatusGeneratedThumbnailServiceTest.cs +++ b/starsky/starskytest/starsky.foundation.thumbnailgeneration/Services/UpdateStatusGeneratedThumbnailServiceTest.cs @@ -3,7 +3,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; -using starsky.foundation.platform.Enums; +using starsky.foundation.platform.Thumbnails; using starsky.foundation.thumbnailgeneration.Models; using starsky.foundation.thumbnailgeneration.Services; using starskytest.FakeMocks; @@ -13,19 +13,8 @@ namespace starskytest.starsky.foundation.thumbnailgeneration.Services; [TestClass] public class UpdateStatusGeneratedThumbnailServiceTest { - [TestMethod] - public async Task UpdateStatusGeneratedThumbnailService_NoItems() - { - var query = new FakeIThumbnailQuery(); - var service = new UpdateStatusGeneratedThumbnailService(query); - await service.AddOrUpdateStatusAsync(new List()); - - var getResult = await query.Get(); - Assert.AreEqual(0, getResult.Count); - } - [SuppressMessage("Usage", "S3887:Mutable, non-private fields should not be \"readonly\"")] - private static readonly List ExampleData = new List + private static readonly List ExampleData = new() { new GenerationResultModel { @@ -70,8 +59,52 @@ public async Task UpdateStatusGeneratedThumbnailService_NoItems() Success = false } }; - - + + [SuppressMessage("Usage", "S3887:Mutable, non-private fields should not be \"readonly\"")] + private static readonly List ExampleData2 = new() + { + new GenerationResultModel + { + FileHash = "image_01", + Size = ThumbnailSize.Large, + Success = true, + SubPath = "test.jpg" + }, + new GenerationResultModel + { + FileHash = "image_01", + Size = ThumbnailSize.ExtraLarge, + Success = true, + SubPath = "test.jpg" + }, + new GenerationResultModel + { + FileHash = "image_01", + Size = ThumbnailSize.Small, + Success = true, + SubPath = "test.jpg" + }, + new GenerationResultModel + { + FileHash = "image_01", + Size = ThumbnailSize.TinyMeta, + Success = false, + SubPath = "test.jpg" + } + }; + + [TestMethod] + public async Task UpdateStatusGeneratedThumbnailService_NoItems() + { + var query = new FakeIThumbnailQuery(); + var service = new UpdateStatusGeneratedThumbnailService(query); + await service.AddOrUpdateStatusAsync(new List()); + + var getResult = await query.Get(); + Assert.AreEqual(0, getResult.Count); + } + + [TestMethod] public async Task UpdateStatusGeneratedThumbnailService_Count6() { @@ -82,7 +115,7 @@ public async Task UpdateStatusGeneratedThumbnailService_Count6() var getResult = await query.Get(); Assert.AreEqual(6, getResult.Count); } - + [TestMethod] public async Task UpdateStatusGeneratedThumbnailService_Index_0() { @@ -96,22 +129,22 @@ public async Task UpdateStatusGeneratedThumbnailService_Index_0() Assert.AreEqual(null, getResult[0].ExtraLarge); Assert.AreEqual(null, getResult[0].Small); } - + [TestMethod] public async Task UpdateStatusGeneratedThumbnailService_Index_1() { var query = new FakeIThumbnailQuery(); var service = new UpdateStatusGeneratedThumbnailService(query); await service.AddOrUpdateStatusAsync(ExampleData); - // see the index + // see the index var getResult = await query.Get(ExampleData[1].FileHash); Assert.AreEqual(1, getResult.Count); Assert.IsFalse(getResult[0].Large); Assert.AreEqual(null, getResult[0].ExtraLarge); Assert.AreEqual(null, getResult[0].Small); } - - + + [TestMethod] public async Task UpdateStatusGeneratedThumbnailService_Index_2() { @@ -125,7 +158,7 @@ public async Task UpdateStatusGeneratedThumbnailService_Index_2() Assert.AreEqual(null, getResult[0].ExtraLarge); Assert.IsTrue(getResult[0].Small); } - + [TestMethod] public async Task UpdateStatusGeneratedThumbnailService_Index_3() { @@ -139,7 +172,7 @@ public async Task UpdateStatusGeneratedThumbnailService_Index_3() Assert.AreEqual(null, getResult[0].ExtraLarge); Assert.IsFalse(getResult[0].Small); } - + [TestMethod] public async Task UpdateStatusGeneratedThumbnailService_Index_4() { @@ -153,7 +186,7 @@ public async Task UpdateStatusGeneratedThumbnailService_Index_4() Assert.IsTrue(getResult[0].ExtraLarge); Assert.AreEqual(null, getResult[0].Small); } - + [TestMethod] public async Task UpdateStatusGeneratedThumbnailService_Index_5() { @@ -167,40 +200,7 @@ public async Task UpdateStatusGeneratedThumbnailService_Index_5() Assert.IsFalse(getResult[0].ExtraLarge); Assert.AreEqual(null, getResult[0].Small); } - - [SuppressMessage("Usage", "S3887:Mutable, non-private fields should not be \"readonly\"")] - private static readonly List ExampleData2 = new List - { - new GenerationResultModel - { - FileHash = "image_01", - Size = ThumbnailSize.Large, - Success = true, - SubPath = "test.jpg" - }, - new GenerationResultModel - { - FileHash = "image_01", - Size = ThumbnailSize.ExtraLarge, - Success = true, - SubPath = "test.jpg", - }, - new GenerationResultModel - { - FileHash = "image_01", - Size = ThumbnailSize.Small, - Success = true, - SubPath = "test.jpg" - }, - new GenerationResultModel - { - FileHash = "image_01", - Size = ThumbnailSize.TinyMeta, - Success = false, - SubPath = "test.jpg" - } - }; - + [TestMethod] public async Task UpdateStatusGeneratedThumbnailService_Data2_NewItem() { @@ -215,7 +215,7 @@ public async Task UpdateStatusGeneratedThumbnailService_Data2_NewItem() Assert.IsTrue(getResult[0].Small); Assert.IsFalse(getResult[0].TinyMeta); } - + [TestMethod] public async Task UpdateStatusGeneratedThumbnailService_Data2_UpdateItem() { @@ -223,14 +223,17 @@ public async Task UpdateStatusGeneratedThumbnailService_Data2_UpdateItem() var service = new UpdateStatusGeneratedThumbnailService(query); await service.AddOrUpdateStatusAsync(ExampleData2); - await service.AddOrUpdateStatusAsync(new List{new GenerationResultModel + await service.AddOrUpdateStatusAsync(new List { - FileHash = "image_01", - Size = ThumbnailSize.Large, - Success = false, - SubPath = "test.jpg" - }}); - + new() + { + FileHash = "image_01", + Size = ThumbnailSize.Large, + Success = false, + SubPath = "test.jpg" + } + }); + var getResult = await query.Get(ExampleData2[0].FileHash); Assert.AreEqual(1, getResult.Count); Assert.IsFalse(getResult[0].Large); @@ -238,6 +241,4 @@ await service.AddOrUpdateStatusAsync(new List{new Generat Assert.IsTrue(getResult[0].Small); Assert.IsFalse(getResult[0].TinyMeta); } - - } diff --git a/starsky/starskytest/starsky.foundation.thumbnailmeta/Services/ReadMetaThumbnailTest.cs b/starsky/starskytest/starsky.foundation.thumbnailmeta/Services/ReadMetaThumbnailTest.cs index b96e436c83..f4843cab3e 100644 --- a/starsky/starskytest/starsky.foundation.thumbnailmeta/Services/ReadMetaThumbnailTest.cs +++ b/starsky/starskytest/starsky.foundation.thumbnailmeta/Services/ReadMetaThumbnailTest.cs @@ -2,8 +2,8 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; -using starsky.foundation.platform.Enums; using starsky.foundation.platform.Models; +using starsky.foundation.platform.Thumbnails; using starsky.foundation.storage.Helpers; using starsky.foundation.storage.Storage; using starsky.foundation.thumbnailmeta.Services; diff --git a/starsky/starskytest/starsky.foundation.thumbnailmeta/Services/WriteMetaThumbnailServiceTest.cs b/starsky/starskytest/starsky.foundation.thumbnailmeta/Services/WriteMetaThumbnailServiceTest.cs index 8719002f39..887b41e389 100644 --- a/starsky/starskytest/starsky.foundation.thumbnailmeta/Services/WriteMetaThumbnailServiceTest.cs +++ b/starsky/starskytest/starsky.foundation.thumbnailmeta/Services/WriteMetaThumbnailServiceTest.cs @@ -4,97 +4,101 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using starsky.foundation.database.Models; -using starsky.foundation.thumbnailmeta.Models; -using starsky.foundation.thumbnailmeta.Services; -using starsky.foundation.platform.Enums; using starsky.foundation.platform.Models; +using starsky.foundation.platform.Thumbnails; using starsky.foundation.storage.Storage; +using starsky.foundation.thumbnailmeta.Models; +using starsky.foundation.thumbnailmeta.Services; using starskytest.FakeCreateAn; using starskytest.FakeMocks; -namespace starskytest.starsky.foundation.thumbnailmeta.Services +namespace starskytest.starsky.foundation.thumbnailmeta.Services; + +[TestClass] +public sealed class WriteMetaThumbnailServiceTest { - [TestClass] - public sealed class WriteMetaThumbnailServiceTest + [TestMethod] + public async Task WriteAndCropFile_Fail_BufferNull() { - [TestMethod] - public async Task WriteAndCropFile_Fail_BufferNull() - { - var storage = new FakeIStorage(new List(), - new List {"/test.jpg"}, Array.Empty()); - var service = new WriteMetaThumbnailService(new FakeSelectorStorage(storage), - new FakeIWebLogger(), new AppSettings()); - var result = await service.WriteAndCropFile("/test.jpg", new OffsetModel(), 0, 0, - FileIndexItem.Rotation.Horizontal); - Assert.IsFalse(result); - } - - [TestMethod] - public async Task WriteAndCropFile_Fail_ImageCantBeLoaded() - { - var storage = new FakeIStorage(new List(), - new List {"/test.jpg"}, Array.Empty()); // instead of new byte[0][] - var service = new WriteMetaThumbnailService(new FakeSelectorStorage(storage), - new FakeIWebLogger(), new AppSettings()); - var result = await service.WriteAndCropFile("/test.jpg", new OffsetModel - { - Data = new byte[10] - }, 0, 0, - FileIndexItem.Rotation.Horizontal); - Assert.IsFalse(result); - } - - [TestMethod] - public async Task WriteAndCropFile_FileIsWritten() - { - var storage = new FakeIStorage(); - var service = new WriteMetaThumbnailService(new FakeSelectorStorage(storage), - new FakeIWebLogger(), new AppSettings()); - var result = await service.WriteAndCropFile("test", new OffsetModel - { - Count = CreateAnImage.Bytes.Length, - Data = CreateAnImage.Bytes.ToArray(), - Index = 0 - }, 6, 6, - FileIndexItem.Rotation.Horizontal); - - Assert.IsTrue(result); - Assert.IsTrue(storage.ExistFile(ThumbnailNameHelper.Combine("test",ThumbnailSize.TinyMeta))); - } + var storage = new FakeIStorage(new List(), + new List { "/test.jpg" }, Array.Empty()); + var service = new WriteMetaThumbnailService(new FakeSelectorStorage(storage), + new FakeIWebLogger(), new AppSettings()); + var result = await service.WriteAndCropFile("/test.jpg", new OffsetModel(), 0, 0, + FileIndexItem.Rotation.Horizontal); + Assert.IsFalse(result); + } - [TestMethod] - public void RotateEnumToDegrees_Horizontal() - { - var result = WriteMetaThumbnailService.RotateEnumToDegrees(FileIndexItem.Rotation.Horizontal); - Assert.AreEqual(0,result,0.00001); - } - - [TestMethod] - public void RotateEnumToDegrees_Default() - { - var result = WriteMetaThumbnailService.RotateEnumToDegrees(FileIndexItem.Rotation.DoNotChange); - Assert.AreEqual(0,result,0.00001); - } + [TestMethod] + public async Task WriteAndCropFile_Fail_ImageCantBeLoaded() + { + var storage = new FakeIStorage(new List(), + new List { "/test.jpg" }, Array.Empty()); // instead of new byte[0][] + var service = new WriteMetaThumbnailService(new FakeSelectorStorage(storage), + new FakeIWebLogger(), new AppSettings()); + var result = await service.WriteAndCropFile("/test.jpg", + new OffsetModel { Data = new byte[10] }, 0, 0, + FileIndexItem.Rotation.Horizontal); + Assert.IsFalse(result); + } + + [TestMethod] + public async Task WriteAndCropFile_FileIsWritten() + { + var storage = new FakeIStorage(); + var service = new WriteMetaThumbnailService(new FakeSelectorStorage(storage), + new FakeIWebLogger(), new AppSettings()); + var result = await service.WriteAndCropFile("test", + new OffsetModel + { + Count = CreateAnImage.Bytes.Length, + Data = CreateAnImage.Bytes.ToArray(), + Index = 0 + }, 6, 6, + FileIndexItem.Rotation.Horizontal); - [TestMethod] - public void RotateEnumToDegrees_180() - { - var result = WriteMetaThumbnailService.RotateEnumToDegrees(FileIndexItem.Rotation.Rotate180); - Assert.AreEqual(180,result,0.00001); - } - - [TestMethod] - public void RotateEnumToDegrees_90() - { - var result = WriteMetaThumbnailService.RotateEnumToDegrees(FileIndexItem.Rotation.Rotate90Cw); - Assert.AreEqual(90,result,0.00001); - } - - [TestMethod] - public void RotateEnumToDegrees_270() - { - var result = WriteMetaThumbnailService.RotateEnumToDegrees(FileIndexItem.Rotation.Rotate270Cw); - Assert.AreEqual(270,result,0.00001); - } + Assert.IsTrue(result); + Assert.IsTrue( + storage.ExistFile(ThumbnailNameHelper.Combine("test", ThumbnailSize.TinyMeta))); + } + + [TestMethod] + public void RotateEnumToDegrees_Horizontal() + { + var result = + WriteMetaThumbnailService.RotateEnumToDegrees(FileIndexItem.Rotation.Horizontal); + Assert.AreEqual(0, result, 0.00001); + } + + [TestMethod] + public void RotateEnumToDegrees_Default() + { + var result = + WriteMetaThumbnailService.RotateEnumToDegrees(FileIndexItem.Rotation.DoNotChange); + Assert.AreEqual(0, result, 0.00001); + } + + [TestMethod] + public void RotateEnumToDegrees_180() + { + var result = + WriteMetaThumbnailService.RotateEnumToDegrees(FileIndexItem.Rotation.Rotate180); + Assert.AreEqual(180, result, 0.00001); + } + + [TestMethod] + public void RotateEnumToDegrees_90() + { + var result = + WriteMetaThumbnailService.RotateEnumToDegrees(FileIndexItem.Rotation.Rotate90Cw); + Assert.AreEqual(90, result, 0.00001); + } + + [TestMethod] + public void RotateEnumToDegrees_270() + { + var result = + WriteMetaThumbnailService.RotateEnumToDegrees(FileIndexItem.Rotation.Rotate270Cw); + Assert.AreEqual(270, result, 0.00001); } } diff --git a/starsky/starskytest/starsky.foundation.video/VideoProcessTests.cs b/starsky/starskytest/starsky.foundation.video/VideoProcessTests.cs index a558eb6bea..a1e02b3ace 100644 --- a/starsky/starskytest/starsky.foundation.video/VideoProcessTests.cs +++ b/starsky/starskytest/starsky.foundation.video/VideoProcessTests.cs @@ -25,17 +25,21 @@ public async Task VideoProcess_WithNullInput_ReturnsFalse() new FakeIFfMpegPrepareBeforeRunning(), new FfMpegPreflightRunCheck(new AppSettings(), new FakeIWebLogger())); + var exiftool = new ExifTool(hoststorage, hoststorage, new AppSettings(), + new FakeIWebLogger()); + var videoProcess = new VideoProcess(new FakeSelectorStorage(hoststorage), fakeFfmpegDownload, - new ExifTool(hoststorage, hoststorage, new AppSettings(), new FakeIWebLogger()), - new FakeIWebLogger(), new AppSettings(), new FakeIThumbnailQuery()); + new VideoProcessThumbnailPost(new FakeSelectorStorage(hoststorage), + new AppSettings(), exiftool, new FakeIWebLogger(), new FakeIThumbnailQuery()), + new FakeIWebLogger()); // Act - var result = await videoProcess.Run( + var result = await videoProcess.RunVideo( "/Users/dion/data/testcontent/deventer_op_stelten_2014-720p.mp4", "/tmp/test.jpg", VideoProcessTypes.Thumbnail); // Assert - Assert.IsTrue(result); + Assert.IsTrue(result.IsSuccess); } } diff --git a/starsky/starskythumbnailcli/Program.cs b/starsky/starskythumbnailcli/Program.cs index bbc7203d5c..59e4c2bfb1 100644 --- a/starsky/starskythumbnailcli/Program.cs +++ b/starsky/starskythumbnailcli/Program.cs @@ -6,46 +6,46 @@ using starsky.foundation.platform.Interfaces; using starsky.foundation.platform.Models; using starsky.foundation.storage.Interfaces; +using starsky.foundation.thumbnailgeneration.GenerationFactory.Interfaces; using starsky.foundation.thumbnailgeneration.Helpers; using starsky.foundation.thumbnailgeneration.Interfaces; using starsky.foundation.webtelemetry.Extensions; using starsky.foundation.webtelemetry.Helpers; -namespace starskythumbnailcli +namespace starskythumbnailcli; + +public static class Program { - public static class Program + public static async Task Main(string[] args) { - public static async Task Main(string[] args) - { - // Use args in application - new ArgsHelper().SetEnvironmentByArgs(args); + // Use args in application + new ArgsHelper().SetEnvironmentByArgs(args); - var services = new ServiceCollection(); + var services = new ServiceCollection(); - // Setup AppSettings - services = await SetupAppSettings.FirstStepToAddSingleton(services); + // Setup AppSettings + services = await SetupAppSettings.FirstStepToAddSingleton(services); - // Inject services - RegisterDependencies.Configure(services); - var serviceProvider = services.BuildServiceProvider(); - var appSettings = serviceProvider.GetRequiredService(); + // Inject services + RegisterDependencies.Configure(services); + var serviceProvider = services.BuildServiceProvider(); + var appSettings = serviceProvider.GetRequiredService(); - services.AddOpenTelemetryMonitoring(appSettings); - services.AddTelemetryLogging(appSettings); + services.AddOpenTelemetryMonitoring(appSettings); + services.AddTelemetryLogging(appSettings); - new SetupDatabaseTypes(appSettings, services).BuilderDb(); - serviceProvider = services.BuildServiceProvider(); + new SetupDatabaseTypes(appSettings, services).BuilderDb(); + serviceProvider = services.BuildServiceProvider(); - var thumbnailService = serviceProvider.GetRequiredService(); - var thumbnailCleaner = serviceProvider.GetRequiredService(); + var thumbnailService = serviceProvider.GetRequiredService(); + var thumbnailCleaner = serviceProvider.GetRequiredService(); - var console = serviceProvider.GetRequiredService(); - var selectorStorage = serviceProvider.GetRequiredService(); + var console = serviceProvider.GetRequiredService(); + var selectorStorage = serviceProvider.GetRequiredService(); - // Help and other Command Line Tools args are included in the ThumbnailCLI - var thumbnailCli = new ThumbnailCli(appSettings, console, - thumbnailService, thumbnailCleaner, selectorStorage); - await thumbnailCli.Thumbnail(args); - } + // Help and other Command Line Tools args are included in the ThumbnailCLI + var thumbnailCli = new ThumbnailCli(appSettings, console, + thumbnailService, thumbnailCleaner, selectorStorage); + await thumbnailCli.Thumbnail(args); } }