From 3ea4926f5ee77cc04b8eefbc11e9230f17f87976 Mon Sep 17 00:00:00 2001 From: Mike Phares Date: Sun, 16 Feb 2025 21:30:17 -0700 Subject: [PATCH] Ready to test Windows Project --- .gitignore | 2 + .vscode/launch.json | 23 +- .vscode/mklink.md | 2 + .vscode/tasks.json | 63 +++- Distance/Models/Stateless/MappedLogicA.cs | 4 +- Face/Models/Stateless/Face.cs | 4 +- Metadata/Models/A_Metadata.cs | 60 ++- Metadata/Models/Stateless/Dimensions.cs | 6 + Metadata/Models/Stateless/Exif.cs | 21 +- Metadata/Models/Stateless/Get.cs | 80 +++- Metadata/Models/Stateless/IMetadata.cs | 18 +- Rename/Models/RenameSettings.cs | 1 + Rename/Rename.cs | 271 +++++++++----- Shared/Models/FileHolder.cs | 111 +++--- Shared/Models/FirstPass.cs | 30 ++ Shared/Models/MetadataGroup.cs | 2 +- Shared/Models/MinimumYearAndFullPath.cs | 22 ++ Shared/Models/NginxFileSystem.cs | 52 +++ Shared/Models/Properties/IWindowsSettings.cs | 10 + Shared/Models/Stateless/Age.cs | 6 +- Shared/Models/Stateless/IDate.cs | 8 +- Shared/Models/Stateless/IRename.cs | 1 + Shared/Models/Stateless/IWindows.cs | 15 + Shared/Models/Stateless/PersonBirthday.cs | 6 +- Shared/Models/Stateless/XDate.cs | 7 +- Shared/Models/Stateless/XPath.cs | 8 +- Windows/.vscode/read-me.md | 1 + Windows/AA.Windows.csproj | 57 +++ Windows/Models/AppSettings.cs | 57 +++ Windows/Models/Identifier.cs | 32 ++ Windows/Models/WindowsSettings.cs | 30 ++ Windows/Program.cs | 53 +++ Windows/Windows.cs | 368 +++++++++++++++++++ 33 files changed, 1226 insertions(+), 205 deletions(-) create mode 100644 Shared/Models/FirstPass.cs create mode 100644 Shared/Models/MinimumYearAndFullPath.cs create mode 100644 Shared/Models/NginxFileSystem.cs create mode 100644 Shared/Models/Properties/IWindowsSettings.cs create mode 100644 Shared/Models/Stateless/IWindows.cs create mode 100644 Windows/.vscode/read-me.md create mode 100644 Windows/AA.Windows.csproj create mode 100644 Windows/Models/AppSettings.cs create mode 100644 Windows/Models/Identifier.cs create mode 100644 Windows/Models/WindowsSettings.cs create mode 100644 Windows/Program.cs create mode 100644 Windows/Windows.cs diff --git a/.gitignore b/.gitignore index 7d6365a..4a180e8 100644 --- a/.gitignore +++ b/.gitignore @@ -474,3 +474,5 @@ Compare/.vscode/.UserSecrets/secrets.json Rename/.vscode/.UserSecrets/secrets.json Rename/.vscode/.UserSecretsOld/rename.json Rename/.vscode/.UserSecretsOld/secrets.json +Windows/.vscode/.UserSecrets/secrets.json +Windows/.vscode/.iCloudPhotos2025 \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 5407333..82911c2 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,6 +1,23 @@ { "version": "0.2.0", "configurations": [ + { + "name": "Compare", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "Build-Compare", + "program": "${workspaceFolder}/Compare/bin/Debug/net9.0/win-x64/AA.Compare.dll", + "args": [ + "s" + ], + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "cwd": "${workspaceFolder}", + "console": "integratedTerminal", + "stopAtEntry": false, + "requireExactSource": false + }, { "name": "Rename", "type": "coreclr", @@ -19,11 +36,11 @@ "requireExactSource": false }, { - "name": "Compare", + "name": "Windows", "type": "coreclr", "request": "launch", - "preLaunchTask": "Build-Compare", - "program": "${workspaceFolder}/Compare/bin/Debug/net9.0/win-x64/AA.Compare.dll", + "preLaunchTask": "Build-Windows", + "program": "${workspaceFolder}/Windows/bin/Debug/net9.0/win-x64/AA.Windows.dll", "args": [ "s" ], diff --git a/.vscode/mklink.md b/.vscode/mklink.md index 3f8179a..7f1e7c3 100644 --- a/.vscode/mklink.md +++ b/.vscode/mklink.md @@ -26,6 +26,8 @@ mklink /J "L:\Git\AA\Compare\.vscode\.UserSecrets" "C:\Users\mikep\AppData\Roami ```bash 1736011475498 = 638716082754980000 = Sat Jan 04 2025 10:24:35 GMT-0700 (Mountain Standard Time) mklink /J "L:\Git\AA\Rename\.vscode\.UserSecrets" "C:\Users\mikep\AppData\Roaming\Microsoft\UserSecrets\843db3e1-e18f-4cba-8b00-967529a32635" mklink /J "L:\Git\AA\Compare\.vscode\.UserSecrets" "C:\Users\mikep\AppData\Roaming\Microsoft\UserSecrets\770b6ae3-266e-4d5f-970a-173709b064de" +mklink /J "L:\Git\AA\Windows\.vscode\.UserSecrets" "C:\Users\mikep\AppData\Roaming\Microsoft\UserSecrets\076c87e8-c7f0-40a3-aba3-73eb7f9ea892" +mklink /J "L:\Git\AA\Windows\.vscode\.iCloudPhotos2025" "D:\7-Question\iCloud Photos 2025" ``` ```json 1735493575037 = 638710903750370000 = Sun Dec 29 2024 10:32:54 GMT-0700 (Mountain Standard Time) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index ee783ff..9adfc96 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -8,7 +8,7 @@ "args": [ "user-secrets", "-p", - "${workspaceFolder}/Rename/AA.Rename.csproj", + "${workspaceFolder}/Windows/AA.Windows.csproj", "init" ], "problemMatcher": "$msCompile" @@ -20,10 +20,10 @@ "args": [ "user-secrets", "-p", - "${workspaceFolder}/Rename/AA.Rename.csproj", + "${workspaceFolder}/Windows/AA.Windows.csproj", "set", "_Application", - "Rename" + "Windows" ], "problemMatcher": "$msCompile" }, @@ -123,6 +123,18 @@ ], "problemMatcher": "$msCompile" }, + { + "label": "Build-Windows", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/Windows/AA.Windows.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, { "label": "Pack-Compare", "command": "dotnet", @@ -219,6 +231,18 @@ ], "problemMatcher": "$msCompile" }, + { + "label": "Pack-Windows", + "command": "dotnet", + "type": "process", + "args": [ + "pack", + "${workspaceFolder}/Windows/AA.Windows.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, { "label": "Format-Compare-Whitespaces", "command": "dotnet", @@ -435,6 +459,22 @@ ], "problemMatcher": "$msCompile" }, + { + "label": "Format-Windows", + "command": "dotnet", + "type": "process", + "args": [ + "format", + "${workspaceFolder}/Windows/AA.Windows.csproj", + "--report", + ".vscode", + "--verbosity", + "detailed", + "--severity", + "warn" + ], + "problemMatcher": "$msCompile" + }, { "label": "AOT-Compare", "command": "dotnet", @@ -571,5 +611,22 @@ ], "problemMatcher": "$msCompile" }, + { + "label": "AOT-Windows", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "-r", + "win-x64", + "-c", + "Release", + "-p:PublishAot=true", + "${workspaceFolder}/Windows/AA.Windows.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, ] } \ No newline at end of file diff --git a/Distance/Models/Stateless/MappedLogicA.cs b/Distance/Models/Stateless/MappedLogicA.cs index 0e6586a..bafc312 100644 --- a/Distance/Models/Stateless/MappedLogicA.cs +++ b/Distance/Models/Stateless/MappedLogicA.cs @@ -145,7 +145,9 @@ internal static class MappedLogicA } if (mappedFile.FilePath.Name.EndsWith(lnk) || !File.Exists(mappedFile.FilePath.FullName)) return; - ExifDirectory exifDirectory = IMetadata.GetExifDirectory(mappedFile.FilePath, mappedFile.PersonKeyFormattedAndKeyTicksAndDisplayDirectoryName); + Stream stream = File.OpenRead(mappedFile.FilePath.FullName); + ExifDirectory exifDirectory = IMetadata.GetExifDirectory(mappedFile.FilePath, stream, mappedFile.PersonKeyFormattedAndKeyTicksAndDisplayDirectoryName); + stream.Dispose(); lock (exifDirectories) exifDirectories.Add(exifDirectory); } diff --git a/Face/Models/Stateless/Face.cs b/Face/Models/Stateless/Face.cs index 380ef9f..71bbcc2 100644 --- a/Face/Models/Stateless/Face.cs +++ b/Face/Models/Stateless/Face.cs @@ -15,7 +15,9 @@ internal static class Face FileHolder fileHolder = FileHolder.Get(fileInfo, id: null); const PersonKeyFormattedAndKeyTicksAndDisplayDirectoryName? personKeyFormattedAndKeyTicksAndDisplayDirectoryName = null; FilePath filePath = FilePath.Get(resultSettings, metadataSettings, fileHolder, index: null); - ExifDirectory exifDirectory = IMetadata.GetExifDirectory(filePath, personKeyFormattedAndKeyTicksAndDisplayDirectoryName); + Stream stream = File.OpenRead(filePath.FullName); + ExifDirectory exifDirectory = IMetadata.GetExifDirectory(filePath, stream, personKeyFormattedAndKeyTicksAndDisplayDirectoryName); + stream.Dispose(); results.Add(exifDirectory); } diff --git a/Metadata/Models/A_Metadata.cs b/Metadata/Models/A_Metadata.cs index 002c8f3..8418421 100644 --- a/Metadata/Models/A_Metadata.cs +++ b/Metadata/Models/A_Metadata.cs @@ -26,15 +26,14 @@ public class A_Metadata _FileGroups = IPath.GetKeyValuePairs(resultSettings, aResultsFullGroupDirectory, [resultSettings.ResultSingleton]); } - private (int, FileInfo) GetFileInfo(ResultSettings resultSettings, FilePath filePath) + private MinimumYearAndPathCombined GetMinimumYearAndPathCombined(ResultSettings resultSettings, FilePath filePath) { - FileInfo result; - FileInfo fileInfo = new(filePath.FullName); + MinimumYearAndPathCombined result; (_, int directoryIndex) = IPath.GetDirectoryNameAndIndex(resultSettings, filePath); - DateTime minimumDateTime = fileInfo.CreationTime < fileInfo.LastWriteTime ? fileInfo.CreationTime : fileInfo.LastWriteTime; - int fileInfoMinimumYear = minimumDateTime.Year < resultSettings.EpicYear ? resultSettings.EpicYear : minimumDateTime.Year; - result = new(Path.Combine(_FileGroups[fileInfoMinimumYear][_ResultSettings.ResultSingleton][directoryIndex], $"{filePath.NameWithoutExtension}{filePath.ExtensionLowered}.json")); - return (fileInfoMinimumYear, result); + DateTime minimumDateTime = new(filePath.CreationTicks < filePath.LastWriteTicks ? filePath.CreationTicks : filePath.LastWriteTicks); + int minimumYear = minimumDateTime.Year < resultSettings.EpicYear ? resultSettings.EpicYear : minimumDateTime.Year; + result = new(minimumYear, Path.Combine(_FileGroups[minimumYear][_ResultSettings.ResultSingleton][directoryIndex], $"{filePath.NameWithoutExtension}{filePath.ExtensionLowered}.json")); + return result; } private (int, string) GetJsonFile(ResultSettings resultSettings, FilePath filePath, ExifDirectory exifDirectory) @@ -46,7 +45,7 @@ public class A_Metadata (_, int directoryIndex) = IPath.GetDirectoryNameAndIndex(resultSettings, filePath); int exifYear = dateTime.Value.Year < resultSettings.EpicYear ? resultSettings.EpicYear : dateTime.Value.Year; result = Path.Combine(_FileGroups[exifYear][_ResultSettings.ResultSingleton][directoryIndex], $"{filePath.NameWithoutExtension}{filePath.ExtensionLowered}.json"); - return (exifYear, result); + return new(exifYear, result); } private static (string, ExifDirectory?) Get(string jsonFile) @@ -63,13 +62,14 @@ public class A_Metadata { result = null; } - return (json, result); + return new(json, result); } - public (FileInfo, ExifDirectory) GetMetadataCollection(ResultSettings resultSettings, MetadataSettings metadataSettings, FilePath filePath) + public (MinimumYearAndPathCombined, ExifDirectory) GetMetadataCollection(ResultSettings resultSettings, MetadataSettings metadataSettings, FilePath filePath) { ExifDirectory? result; - (int fileInfoMinimumYear, FileInfo fileInfo) = GetFileInfo(resultSettings, filePath); + MinimumYearAndPathCombined minimumYearAndPathCombined = GetMinimumYearAndPathCombined(resultSettings, filePath); + FileInfo fileInfo = new(minimumYearAndPathCombined.PathCombined); if (_MetadataSettings.ForceMetadataLastWriteTimeToCreationTime && !fileInfo.Exists && File.Exists(Path.ChangeExtension(fileInfo.FullName, ".delete"))) { File.Move(Path.ChangeExtension(fileInfo.FullName, ".delete"), fileInfo.FullName); @@ -103,10 +103,12 @@ public class A_Metadata if (result is null) { string json; + Stream stream = File.OpenRead(filePath.FullName); const PersonKeyFormattedAndKeyTicksAndDisplayDirectoryName? personKeyFormattedAndKeyTicksAndDisplayDirectoryName = null; - result = Exif.GetExifDirectory(filePath, personKeyFormattedAndKeyTicksAndDisplayDirectoryName); + result = Exif.GetExifDirectory(filePath, stream, personKeyFormattedAndKeyTicksAndDisplayDirectoryName); + stream.Dispose(); (int exifYear, string jsonFile) = GetJsonFile(_ResultSettings, filePath, result); - if (exifYear == fileInfoMinimumYear) + if (exifYear == minimumYearAndPathCombined.MinimumYear) json = JsonSerializer.Serialize(result, ExifDirectorySourceGenerationContext.Default.ExifDirectory); else { @@ -131,7 +133,37 @@ public class A_Metadata fileInfo.Refresh(); } } - return (fileInfo, result); + return new(minimumYearAndPathCombined, result); + } + + private static Stream GetStream(HttpClient httpClient, FilePath filePath) + { + Stream result; + Task httpResponseMessage = httpClient.GetAsync(filePath.FullName); + httpResponseMessage.Wait(); + Task task = httpResponseMessage.Result.Content.LoadIntoBufferAsync(); + task.Wait(); + Task stream = httpResponseMessage.Result.Content.ReadAsStreamAsync(); + stream.Wait(); + result = stream.Result; + return result; + } + + public (MinimumYearAndPathCombined, ExifDirectory) GetMetadataCollection(ResultSettings resultSettings, MetadataSettings metadataSettings, HttpClient? httpClient, FilePath filePath) + { + ExifDirectory result; + MinimumYearAndPathCombined minimumYearAndPathCombined; + if (httpClient is null) + (minimumYearAndPathCombined, result) = GetMetadataCollection(resultSettings, metadataSettings, filePath); + else + { + Stream stream = GetStream(httpClient, filePath); + minimumYearAndPathCombined = GetMinimumYearAndPathCombined(resultSettings, filePath); + const PersonKeyFormattedAndKeyTicksAndDisplayDirectoryName? personKeyFormattedAndKeyTicksAndDisplayDirectoryName = null; + result = Exif.GetExifDirectory(filePath, stream, personKeyFormattedAndKeyTicksAndDisplayDirectoryName); + stream.Dispose(); + } + return new(minimumYearAndPathCombined, result); } } \ No newline at end of file diff --git a/Metadata/Models/Stateless/Dimensions.cs b/Metadata/Models/Stateless/Dimensions.cs index 26522d2..96e2a70 100644 --- a/Metadata/Models/Stateless/Dimensions.cs +++ b/Metadata/Models/Stateless/Dimensions.cs @@ -123,4 +123,10 @@ internal static class Dimensions return GetDimensions(binaryReader); } + internal static Size? GetDimensions(Stream stream) + { + using BinaryReader binaryReader = new(stream); + return GetDimensions(binaryReader); + } + } \ No newline at end of file diff --git a/Metadata/Models/Stateless/Exif.cs b/Metadata/Models/Stateless/Exif.cs index 8602e55..d91ce64 100644 --- a/Metadata/Models/Stateless/Exif.cs +++ b/Metadata/Models/Stateless/Exif.cs @@ -513,7 +513,7 @@ internal abstract class Exif return results.ToArray(); } - private static Shared.Models.ExifDirectory Covert(Shared.Models.FilePath filePath, Shared.Models.PersonKeyFormattedAndKeyTicksAndDisplayDirectoryName? personKeyFormattedAndKeyTicksAndDisplayDirectoryName, System.Drawing.Size? size, IReadOnlyList directories) + private static Shared.Models.ExifDirectory Covert(Shared.Models.FilePath filePath, Shared.Models.PersonKeyFormattedAndKeyTicksAndDisplayDirectoryName? personKeyFormattedAndKeyTicksAndDisplayDirectoryName, IReadOnlyList directories, System.Drawing.Size? size) { Shared.Models.ExifDirectory result; Shared.Models.AviDirectory[] aviDirectories = GetAviDirectories(directories); @@ -548,16 +548,21 @@ internal abstract class Exif return result; } - internal static Shared.Models.ExifDirectory GetExifDirectory(Shared.Models.FilePath filePath, Shared.Models.PersonKeyFormattedAndKeyTicksAndDisplayDirectoryName? personKeyFormattedAndKeyTicksAndDisplayDirectoryName) + internal static Shared.Models.ExifDirectory GetExifDirectory(Shared.Models.FilePath filePath, Stream stream, Shared.Models.PersonKeyFormattedAndKeyTicksAndDisplayDirectoryName? personKeyFormattedAndKeyTicksAndDisplayDirectoryName) { Shared.Models.ExifDirectory result; + IReadOnlyList directories = ImageMetadataReader.ReadMetadata(stream); System.Drawing.Size? size; - try - { size = Dimensions.GetDimensions(filePath.FullName); } - catch (Exception) - { size = null; } - IReadOnlyList directories = ImageMetadataReader.ReadMetadata(filePath.FullName); - result = Covert(filePath, personKeyFormattedAndKeyTicksAndDisplayDirectoryName, size, directories); + if (!stream.CanSeek) + size = null; + else + { + try + { size = Dimensions.GetDimensions(stream); } + catch (Exception) + { size = null; } + } + result = Covert(filePath, personKeyFormattedAndKeyTicksAndDisplayDirectoryName, directories, size); return result; } diff --git a/Metadata/Models/Stateless/Get.cs b/Metadata/Models/Stateless/Get.cs index 5a149af..916837e 100644 --- a/Metadata/Models/Stateless/Get.cs +++ b/Metadata/Models/Stateless/Get.cs @@ -31,16 +31,39 @@ internal static class Get return results.AsReadOnly(); } + internal static ReadOnlyDictionary> GetKeyValuePairs(IEnumerable collection) + { + Dictionary> results = []; + string key; + FileHolder fileHolder; + List? fileHolders; + foreach (NginxFileSystem nginxFileSystem in collection) + { + fileHolder = FileHolder.Get(nginxFileSystem); + if (fileHolder.DirectoryFullPath is null) + continue; + key = $"{Path.Combine(fileHolder.DirectoryFullPath, fileHolder.NameWithoutExtension)}"; + if (!results.TryGetValue(key, out fileHolders)) + { + results.Add(key, []); + if (!results.TryGetValue(key, out fileHolders)) + throw new NotImplementedException(); + } + fileHolders.Add(fileHolder); + } + return results.AsReadOnly(); + } + internal static Action SetExifDirectoryCollection(IRename rename, ResultSettings resultSettings, MetadataSettings metadataSettings, IRenameSettings renameSettings, A_Metadata metadata, List distinct, List metadataGroups) { return file => { rename.Tick(); - FileInfo fileInfo; ExifDirectory exifDirectory; MetadataGroup metadataGroup; DeterministicHashCode deterministicHashCode; FileHolder fileHolder = FileHolder.Get(file); + MinimumYearAndPathCombined minimumYearAndPathCombined; bool fastForwardMovingPictureExpertsGroupUsed; FilePath? fastForwardMovingPictureExpertsGroupFilePath; ReadOnlyCollection? fastForwardMovingPictureExpertsGroupFiles; @@ -48,7 +71,52 @@ internal static class Get string key = $"{Path.Combine(fileHolder.DirectoryFullPath ?? throw new NotSupportedException(), fileHolder.NameWithoutExtension)}"; if (distinct.Contains(key)) throw new NotSupportedException("Turn off parallelism when sidecar files are present!"); - if (!renameSettings.SkipIdFiles || filePath.Id is null || (!filePath.IsIntelligentIdFormat && filePath.SortOrder is not null)) + if (renameSettings.SkipIdFiles && filePath.Id is not null && (filePath.IsIntelligentIdFormat || filePath.SortOrder is null)) + return; + if (filePath.Id is not null) + { + fastForwardMovingPictureExpertsGroupFiles = null; + deterministicHashCode = new(null, filePath.Id, null); + } + else + { + fastForwardMovingPictureExpertsGroupFiles = rename.ConvertAndGetFastForwardMovingPictureExpertsGroupFiles(renameSettings, filePath); + fastForwardMovingPictureExpertsGroupFilePath = fastForwardMovingPictureExpertsGroupFiles.Count == 0 ? null : FilePath.Get(resultSettings, metadataSettings, FileHolder.Get(fastForwardMovingPictureExpertsGroupFiles[0]), index: null); + deterministicHashCode = fastForwardMovingPictureExpertsGroupFilePath is null ? rename.GetDeterministicHashCode(filePath) : rename.GetDeterministicHashCode(fastForwardMovingPictureExpertsGroupFilePath); + } + filePath = FilePath.Get(filePath, deterministicHashCode); + fastForwardMovingPictureExpertsGroupUsed = fastForwardMovingPictureExpertsGroupFiles is not null && fastForwardMovingPictureExpertsGroupFiles.Count > 0; + (minimumYearAndPathCombined, exifDirectory) = metadata.GetMetadataCollection(resultSettings, metadataSettings, filePath); + metadataGroup = new(fastForwardMovingPictureExpertsGroupUsed, filePath, minimumYearAndPathCombined, exifDirectory, new([])); + lock (metadataGroups) + metadataGroups.Add(metadataGroup); + if (fastForwardMovingPictureExpertsGroupUsed && fastForwardMovingPictureExpertsGroupFiles is not null) + { + foreach (string fastForwardMovingPictureExpertsGroupFile in fastForwardMovingPictureExpertsGroupFiles) + File.Delete(fastForwardMovingPictureExpertsGroupFile); + } + }; + } + + internal static Action SetExifDirectoryCollection(IWindows windows, ResultSettings resultSettings, MetadataSettings metadataSettings, IWindowsSettings windowsSettings, A_Metadata metadata, List distinct, List metadataGroups) + { + return file => + { + windows.Tick(); + ExifDirectory exifDirectory; + MetadataGroup metadataGroup; + HttpClient? httpClient = null; + DeterministicHashCode deterministicHashCode; + FileHolder fileHolder = FileHolder.Get(file); + MinimumYearAndPathCombined minimumYearAndPathCombined; + bool fastForwardMovingPictureExpertsGroupUsed; + FilePath? fastForwardMovingPictureExpertsGroupFilePath; + ReadOnlyCollection? fastForwardMovingPictureExpertsGroupFiles; + FilePath filePath = FilePath.Get(resultSettings, metadataSettings, fileHolder, index: null); + string key = $"{Path.Combine(fileHolder.DirectoryFullPath ?? throw new NotSupportedException(), fileHolder.NameWithoutExtension)}"; + if (distinct.Contains(key)) + throw new NotSupportedException("Turn off parallelism when sidecar files are present!"); + if (filePath.Id is null || (!filePath.IsIntelligentIdFormat && filePath.SortOrder is not null)) { if (filePath.Id is not null) { @@ -57,14 +125,14 @@ internal static class Get } else { - fastForwardMovingPictureExpertsGroupFiles = rename.ConvertAndGetFastForwardMovingPictureExpertsGroupFiles(renameSettings, filePath); + fastForwardMovingPictureExpertsGroupFiles = windows.ConvertAndGetFastForwardMovingPictureExpertsGroupFiles(windowsSettings, httpClient, filePath); fastForwardMovingPictureExpertsGroupFilePath = fastForwardMovingPictureExpertsGroupFiles.Count == 0 ? null : FilePath.Get(resultSettings, metadataSettings, FileHolder.Get(fastForwardMovingPictureExpertsGroupFiles[0]), index: null); - deterministicHashCode = fastForwardMovingPictureExpertsGroupFilePath is null ? rename.GetDeterministicHashCode(filePath) : rename.GetDeterministicHashCode(fastForwardMovingPictureExpertsGroupFilePath); + deterministicHashCode = fastForwardMovingPictureExpertsGroupFilePath is null ? windows.GetDeterministicHashCode(httpClient, filePath) : windows.GetDeterministicHashCode(httpClient, fastForwardMovingPictureExpertsGroupFilePath); } filePath = FilePath.Get(filePath, deterministicHashCode); fastForwardMovingPictureExpertsGroupUsed = fastForwardMovingPictureExpertsGroupFiles is not null && fastForwardMovingPictureExpertsGroupFiles.Count > 0; - (fileInfo, exifDirectory) = metadata.GetMetadataCollection(resultSettings, metadataSettings, filePath); - metadataGroup = new(fastForwardMovingPictureExpertsGroupUsed, filePath, fileInfo, exifDirectory, new([])); + (minimumYearAndPathCombined, exifDirectory) = metadata.GetMetadataCollection(resultSettings, metadataSettings, filePath); + metadataGroup = new(fastForwardMovingPictureExpertsGroupUsed, filePath, minimumYearAndPathCombined, exifDirectory, new([])); lock (metadataGroups) metadataGroups.Add(metadataGroup); if (fastForwardMovingPictureExpertsGroupUsed && fastForwardMovingPictureExpertsGroupFiles is not null) diff --git a/Metadata/Models/Stateless/IMetadata.cs b/Metadata/Models/Stateless/IMetadata.cs index 9e81d42..6654e93 100644 --- a/Metadata/Models/Stateless/IMetadata.cs +++ b/Metadata/Models/Stateless/IMetadata.cs @@ -17,10 +17,10 @@ public interface IMetadata Meters } - ExifDirectory TestStatic_GetExifDirectory(FilePath filePath, PersonKeyFormattedAndKeyTicksAndDisplayDirectoryName? personKeyFormattedAndKeyTicksAndDisplayDirectoryName) => - GetExifDirectory(filePath, personKeyFormattedAndKeyTicksAndDisplayDirectoryName); - static ExifDirectory GetExifDirectory(FilePath filePath, PersonKeyFormattedAndKeyTicksAndDisplayDirectoryName? personKeyFormattedAndKeyTicksAndDisplayDirectoryName) => - Exif.GetExifDirectory(filePath, personKeyFormattedAndKeyTicksAndDisplayDirectoryName); + ExifDirectory TestStatic_GetExifDirectory(FilePath filePath, Stream stream, PersonKeyFormattedAndKeyTicksAndDisplayDirectoryName? personKeyFormattedAndKeyTicksAndDisplayDirectoryName) => + GetExifDirectory(filePath, stream, personKeyFormattedAndKeyTicksAndDisplayDirectoryName); + static ExifDirectory GetExifDirectory(FilePath filePath, Stream stream, PersonKeyFormattedAndKeyTicksAndDisplayDirectoryName? personKeyFormattedAndKeyTicksAndDisplayDirectoryName) => + Exif.GetExifDirectory(filePath, stream, personKeyFormattedAndKeyTicksAndDisplayDirectoryName); string? TestStatic_GetMaker(ExifDirectory? exifDirectory) => GetMaker(exifDirectory); @@ -67,4 +67,14 @@ public interface IMetadata static ReadOnlyDictionary> GetKeyValuePairs(IEnumerable files) => Get.GetKeyValuePairs(files); + ReadOnlyDictionary> TestStatic_GetKeyValuePairs(IEnumerable collection) => + GetKeyValuePairs(collection); + static ReadOnlyDictionary> GetKeyValuePairs(IEnumerable collection) => + Get.GetKeyValuePairs(collection); + + Action TestStatic_SetExifDirectoryCollection(IWindows windows, ResultSettings resultSettings, MetadataSettings metadataSettings, IWindowsSettings windowsSettings, A_Metadata metadata, List distinct, List metadataGroups) => + SetExifDirectoryCollection(windows, resultSettings, metadataSettings, windowsSettings, metadata, distinct, metadataGroups); + static Action SetExifDirectoryCollection(IWindows windows, ResultSettings resultSettings, MetadataSettings metadataSettings, IWindowsSettings windowsSettings, A_Metadata metadata, List distinct, List metadataGroups) => + Get.SetExifDirectoryCollection(windows, resultSettings, metadataSettings, windowsSettings, metadata, distinct, metadataGroups); + } \ No newline at end of file diff --git a/Rename/Models/RenameSettings.cs b/Rename/Models/RenameSettings.cs index 1aa84cc..dcc6765 100644 --- a/Rename/Models/RenameSettings.cs +++ b/Rename/Models/RenameSettings.cs @@ -6,6 +6,7 @@ namespace View_by_Distance.Rename.Models; public record RenameSettings(string Company, string DefaultMaker, + string? FirstPassFile, bool ForceNewId, string[] IgnoreExtensions, bool InPlace, diff --git a/Rename/Rename.cs b/Rename/Rename.cs index 27edd07..b2b6bb9 100644 --- a/Rename/Rename.cs +++ b/Rename/Rename.cs @@ -19,13 +19,21 @@ namespace View_by_Distance.Rename; public partial class Rename : IRename, IDisposable { - private sealed record ToDo(string? Directory, FilePath FilePath, string File, bool JsonFile); + private sealed record ToDo(string? Directory, + FileInfo FileInfo, + string File, + bool JsonFile); - private sealed record RecordA(ExifDirectory ExifDirectory, bool FastForwardMovingPictureExpertsGroupUsed, FileInfo FileInfo, FilePath FilePath, ReadOnlyCollection SidecarFiles); - - private sealed record RecordB(DateTime DateTime, ExifDirectory ExifDirectory, bool FastForwardMovingPictureExpertsGroupUsed, FilePath FilePath, ReadOnlyCollection SidecarFiles, bool HasDateTimeOriginal, bool HasIgnoreKeyword, string JsonFile); + private sealed record Record(DateTime DateTime, + ExifDirectory ExifDirectory, + bool FastForwardMovingPictureExpertsGroupUsed, + FileHolder[] SidecarFiles, + bool HasDateTimeOriginal, + bool HasIgnoreKeyword, + string JsonFile); private ProgressBar? _ProgressBar; + private readonly ProgressBarOptions _ProgressBarOptions; public Rename(List args, ILogger? logger, AppSettings appSettings, bool isSilent, IConsole console) { @@ -37,12 +45,19 @@ public partial class Rename : IRename, IDisposable throw new NullReferenceException(nameof(console)); IRename rename = this; long ticks = DateTime.Now.Ticks; + _ProgressBarOptions = new() { ProgressCharacter = '─', ProgressBarOnBottom = true, DisableBottomPercentage = true }; RenameWork(logger, appSettings, rename, ticks); } void IRename.Tick() => _ProgressBar?.Tick(); + void IRename.ConstructProgressBar(int maxTicks, string message) + { + _ProgressBar?.Dispose(); + _ProgressBar = new(maxTicks, message, _ProgressBarOptions); + } + void IDisposable.Dispose() { _ProgressBar?.Dispose(); @@ -117,7 +132,7 @@ public partial class Rename : IRename, IDisposable #pragma warning restore CA1416 - private void NonParallelismAndInPlace(AppSettings appSettings, ReadOnlyCollection ids, ExifDirectory exifDirectory, FileInfo fileInfo, FilePath filePath, bool fastForwardMovingPictureExpertsGroupUsed, ReadOnlyCollection sidecarFiles) + private void NonParallelismAndInPlace(AppSettings appSettings, IRename rename, ReadOnlyCollection ids, ExifDirectory exifDirectory, MinimumYearAndPathCombined minimumYearAndPathCombined, bool fastForwardMovingPictureExpertsGroupUsed, FileHolder[] sidecarFiles) { if (exifDirectory.FilePath.Id is null) throw new NotImplementedException(); @@ -129,12 +144,12 @@ public partial class Rename : IRename, IDisposable DateTime? dateTime = IDate.GetDateTimeOriginal(exifDirectory); ReadOnlyCollection keywords = IMetadata.GetKeywords(exifDirectory); bool hasIgnoreKeyword = appSettings.MetadataSettings.IgnoreRulesKeyWords.Any(keywords.Contains); - string checkFileExtension = filePath.ExtensionLowered == jpeg ? jpg : filePath.ExtensionLowered; + string checkFileExtension = exifDirectory.FilePath.ExtensionLowered == jpeg ? jpg : exifDirectory.FilePath.ExtensionLowered; bool hasDateTimeOriginal = dateTime is not null; string paddedId = IId.GetPaddedId(appSettings.ResultSettings, appSettings.MetadataSettings, exifDirectory.FilePath.Id.Value, hasIgnoreKeyword, hasDateTimeOriginal, i); - string checkDirectory = appSettings.RenameSettings.InPlaceWithOriginalName ? Path.Combine(filePath.DirectoryFullPath, filePath.FileNameFirstSegment) : filePath.DirectoryFullPath; + string checkDirectory = appSettings.RenameSettings.InPlaceWithOriginalName ? Path.Combine(exifDirectory.FilePath.DirectoryFullPath, exifDirectory.FilePath.FileNameFirstSegment) : exifDirectory.FilePath.DirectoryFullPath; string checkFile = Path.Combine(checkDirectory, $"{paddedId}{checkFileExtension}"); - if (checkFile != filePath.FullName) + if (checkFile != exifDirectory.FilePath.FullName) { if (File.Exists(checkFile)) { @@ -142,19 +157,28 @@ public partial class Rename : IRename, IDisposable if (File.Exists(checkFile)) throw new NotImplementedException(); } - toDo = new(checkDirectory, filePath, checkFile, JsonFile: false); + toDo = new(Directory: checkDirectory, + FileInfo: new(exifDirectory.FilePath.FullName), + File: checkFile, + JsonFile: false); toDoCollection.Add(toDo); - if (sidecarFiles.Count != 0) + if (sidecarFiles.Length != 0) { if (appSettings.RenameSettings.InPlace) throw new NotSupportedException($"Must use {nameof(appSettings.RenameSettings.InPlaceWithOriginalName)} when sidecar file(s) are present!"); dateTime ??= IDate.GetMinimum(exifDirectory); - RecordB recordB = new(dateTime.Value, exifDirectory, fastForwardMovingPictureExpertsGroupUsed, filePath, sidecarFiles, hasDateTimeOriginal, hasIgnoreKeyword, fileInfo.FullName); - toDoCollection.AddRange(GetSidecarFiles(appSettings, recordB, [], checkDirectory, paddedId)); + Record record = new(DateTime: dateTime.Value, + ExifDirectory: exifDirectory, + FastForwardMovingPictureExpertsGroupUsed: fastForwardMovingPictureExpertsGroupUsed, + SidecarFiles: sidecarFiles, + HasDateTimeOriginal: hasDateTimeOriginal, + HasIgnoreKeyword: hasIgnoreKeyword, + JsonFile: minimumYearAndPathCombined.PathCombined); + toDoCollection.AddRange(GetSidecarFiles(appSettings, record, [], checkDirectory, paddedId)); } - _ = RenameFilesInDirectories(appSettings.RenameSettings, new(toDoCollection)); + _ = RenameFilesInDirectories(appSettings.RenameSettings, rename, new(toDoCollection)); string jsonFile = Path.Combine(checkDirectory, $"{paddedId}{checkFileExtension}.json"); - File.Move(fileInfo.FullName, jsonFile, overwrite: true); + File.Move(minimumYearAndPathCombined.PathCombined, jsonFile, overwrite: true); if (appSettings.RenameSettings.InPlaceWithOriginalName && ids.Count > 0) { string contains = ids.Contains(exifDirectory.FilePath.Id.Value) ? "_ Exists _" : "_ New _"; @@ -165,19 +189,19 @@ public partial class Rename : IRename, IDisposable } } - private List GetRecordACollection(ILogger? logger, AppSettings appSettings, IRename rename, long ticks, ReadOnlyCollection ids, IEnumerable files, A_Metadata metadata) + private List GetFirstPassCollection(ILogger? logger, AppSettings appSettings, IRename rename, long ticks, ReadOnlyCollection ids, IEnumerable files, A_Metadata metadata) { - List results = []; + List results = []; int index = -1; - RecordA recordA; - FileInfo fileInfo; FilePath filePath; TimeSpan timeSpan; + FirstPass firstPass; string directoryName; ExifDirectory exifDirectory; List sidecarFiles; DeterministicHashCode deterministicHashCode; bool fastForwardMovingPictureExpertsGroupUsed; + MinimumYearAndPathCombined minimumYearAndPathCombined; FilePath? fastForwardMovingPictureExpertsGroupFilePath; ReadOnlyCollection? fastForwardMovingPictureExpertsGroupFiles; ReadOnlyDictionary> keyValuePairs = IMetadata.GetKeyValuePairs(files); @@ -225,7 +249,7 @@ public partial class Rename : IRename, IDisposable sidecarFiles.Add(keyValuePair.Value[i]); } try - { (fileInfo, exifDirectory) = metadata.GetMetadataCollection(appSettings.ResultSettings, appSettings.MetadataSettings, filePath); } + { (minimumYearAndPathCombined, exifDirectory) = metadata.GetMetadataCollection(appSettings.ResultSettings, appSettings.MetadataSettings, filePath); } catch (Exception) { logger?.LogWarning("<{filePath}>", filePath.FullName); @@ -238,11 +262,11 @@ public partial class Rename : IRename, IDisposable File.Delete(fastForwardMovingPictureExpertsGroupFile); } if (appSettings.RenameSettings.InPlace || appSettings.RenameSettings.InPlaceWithOriginalName) - NonParallelismAndInPlace(appSettings, ids, exifDirectory, fileInfo, filePath, fastForwardMovingPictureExpertsGroupUsed, new(sidecarFiles)); + NonParallelismAndInPlace(appSettings, rename, ids, exifDirectory, minimumYearAndPathCombined, fastForwardMovingPictureExpertsGroupUsed, sidecarFiles.ToArray()); if (!fastForwardMovingPictureExpertsGroupUsed && appSettings.RenameSettings.InPlaceMoveDirectory && appSettings.RenameSettings.ValidVideoFormatExtensions.Contains(filePath.ExtensionLowered)) fastForwardMovingPictureExpertsGroupUsed = true; - recordA = new(exifDirectory, fastForwardMovingPictureExpertsGroupUsed, fileInfo, filePath, new(sidecarFiles)); - results.Add(recordA); + firstPass = new(exifDirectory, fastForwardMovingPictureExpertsGroupUsed, minimumYearAndPathCombined, sidecarFiles.ToArray()); + results.Add(firstPass); } timeSpan = new(DateTime.Now.Ticks - ticks); if (timeSpan.TotalMilliseconds > appSettings.RenameSettings.MaxMilliSecondsPerCall) @@ -251,92 +275,112 @@ public partial class Rename : IRename, IDisposable return results; } - private static ReadOnlyCollection GetRecordBCollection(AppSettings appSettings, List recordACollection) + private static ReadOnlyCollection GetRecordCollection(AppSettings appSettings, List collection) { - List results = []; - RecordB recordB; + List results = []; + Record record; DateTime? dateTime; bool hasIgnoreKeyword; bool hasDateTimeOriginal; ReadOnlyCollection keywords; - foreach (RecordA recordA in recordACollection) + foreach (FirstPass firstPass in collection) { - dateTime = IDate.GetDateTimeOriginal(recordA.ExifDirectory); + dateTime = IDate.GetDateTimeOriginal(firstPass.ExifDirectory); hasDateTimeOriginal = dateTime is not null; - dateTime ??= IDate.GetMinimum(recordA.ExifDirectory); - keywords = IMetadata.GetKeywords(recordA.ExifDirectory); + dateTime ??= IDate.GetMinimum(firstPass.ExifDirectory); + keywords = IMetadata.GetKeywords(firstPass.ExifDirectory); hasIgnoreKeyword = appSettings.MetadataSettings.IgnoreRulesKeyWords.Any(l => keywords.Contains(l)); - recordB = new(dateTime.Value, recordA.ExifDirectory, recordA.FastForwardMovingPictureExpertsGroupUsed, recordA.FilePath, recordA.SidecarFiles, hasDateTimeOriginal, hasIgnoreKeyword, recordA.FileInfo.FullName); - results.Add(recordB); + record = new(DateTime: dateTime.Value, + ExifDirectory: firstPass.ExifDirectory, + FastForwardMovingPictureExpertsGroupUsed: firstPass.FastForwardMovingPictureExpertsGroupUsed, + SidecarFiles: firstPass.SidecarFiles, + HasDateTimeOriginal: hasDateTimeOriginal, + HasIgnoreKeyword: hasIgnoreKeyword, + JsonFile: firstPass.MinimumYearAndPathCombined.PathCombined); + results.Add(record); } return results.AsReadOnly(); } - private ReadOnlyCollection GetRecordBCollection(ILogger? logger, AppSettings appSettings, IRename rename, long ticks, ReadOnlyCollection ids, DirectoryInfo directoryInfo) + private ReadOnlyCollection GetRecordCollection(ILogger? logger, AppSettings appSettings, IRename rename, long ticks, ReadOnlyCollection ids, string sourceDirectory, ReadOnlyCollection files) { - ReadOnlyCollection results; - RecordA recordA; - List recordACollection = []; - A_Metadata metadata = new(appSettings.ResultSettings, appSettings.MetadataSettings); - int appSettingsMaxDegreeOfParallelism = appSettings.RenameSettings.MaxDegreeOfParallelism; - IEnumerable files = appSettingsMaxDegreeOfParallelism == 1 ? Directory.GetFiles(directoryInfo.FullName, "*", SearchOption.AllDirectories) : Directory.EnumerateFiles(directoryInfo.FullName, "*", SearchOption.AllDirectories); - int filesCount = appSettingsMaxDegreeOfParallelism == 1 ? files.Count() : 123000; - _ProgressBar = new(filesCount, "EnumerateFiles load", new ProgressBarOptions() { ProgressCharacter = '─', ProgressBarOnBottom = true, DisableBottomPercentage = true }); - if (appSettingsMaxDegreeOfParallelism == 1) - recordACollection.AddRange(GetRecordACollection(logger, appSettings, rename, ticks, ids, files, metadata)); + ReadOnlyCollection results; + FirstPass firstPass; + List collection; + string? checkFile = string.IsNullOrEmpty(appSettings.RenameSettings.FirstPassFile) ? null : Path.Combine(sourceDirectory, appSettings.RenameSettings.FirstPassFile); + if (!string.IsNullOrEmpty(checkFile) && File.Exists(checkFile)) + { + string json = File.ReadAllText(checkFile); + collection = JsonSerializer.Deserialize(json, FirstPassCollectionSourceGenerationContext.Default.ListFirstPass) ?? throw new Exception(); + } else { - List distinct = []; - List metadataGroups = []; - ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = appSettingsMaxDegreeOfParallelism }; - files.AsParallel().ForAll(IMetadata.SetExifDirectoryCollection(rename, appSettings.ResultSettings, appSettings.MetadataSettings, appSettings.RenameSettings, metadata, distinct, metadataGroups)); - if (_ProgressBar.CurrentTick != recordACollection.Count) - throw new NotSupportedException(); - foreach (MetadataGroup metadataGroup in metadataGroups) + collection = []; + A_Metadata metadata = new(appSettings.ResultSettings, appSettings.MetadataSettings); + int appSettingsMaxDegreeOfParallelism = appSettings.RenameSettings.MaxDegreeOfParallelism; + int filesCount = appSettingsMaxDegreeOfParallelism == 1 ? files.Count : 123000; + rename.ConstructProgressBar(filesCount, "EnumerateFiles load"); + if (appSettingsMaxDegreeOfParallelism == 1) + collection.AddRange(GetFirstPassCollection(logger, appSettings, rename, ticks, ids, files, metadata)); + else { - if (metadataGroup.FastForwardMovingPictureExpertsGroupUsed || !appSettings.RenameSettings.InPlaceMoveDirectory || !appSettings.RenameSettings.ValidVideoFormatExtensions.Contains(metadataGroup.FilePath.ExtensionLowered)) - recordA = new(metadataGroup.ExifDirectory, metadataGroup.FastForwardMovingPictureExpertsGroupUsed, metadataGroup.FileInfo, metadataGroup.FilePath, metadataGroup.SidecarFiles); - else - recordA = new(metadataGroup.ExifDirectory, FastForwardMovingPictureExpertsGroupUsed: true, metadataGroup.FileInfo, metadataGroup.FilePath, metadataGroup.SidecarFiles); - recordACollection.Add(recordA); + List distinct = []; + List metadataGroups = []; + ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = appSettingsMaxDegreeOfParallelism }; + files.AsParallel().ForAll(IMetadata.SetExifDirectoryCollection(rename, appSettings.ResultSettings, appSettings.MetadataSettings, appSettings.RenameSettings, metadata, distinct, metadataGroups)); + Thread.Sleep(500); + foreach (MetadataGroup metadataGroup in metadataGroups) + { + if (metadataGroup.FastForwardMovingPictureExpertsGroupUsed || !appSettings.RenameSettings.InPlaceMoveDirectory || !appSettings.RenameSettings.ValidVideoFormatExtensions.Contains(metadataGroup.FilePath.ExtensionLowered)) + firstPass = new(metadataGroup.ExifDirectory, metadataGroup.FastForwardMovingPictureExpertsGroupUsed, metadataGroup.MinimumYearAndPathCombined, metadataGroup.SidecarFiles.ToArray()); + else + firstPass = new(metadataGroup.ExifDirectory, FastForwardMovingPictureExpertsGroupUsed: true, metadataGroup.MinimumYearAndPathCombined, metadataGroup.SidecarFiles.ToArray()); + collection.Add(firstPass); + } + } + if (!string.IsNullOrEmpty(checkFile)) + { + string json = JsonSerializer.Serialize(collection, FirstPassCollectionSourceGenerationContext.Default.ListFirstPass); + File.WriteAllText(Path.Combine(sourceDirectory, $"{ticks}.json"), json); } } - _ProgressBar.Dispose(); - results = GetRecordBCollection(appSettings, recordACollection); + results = GetRecordCollection(appSettings, collection); return results; } - private static void VerifyIntMinValueLength(MetadataSettings metadataSettings, ReadOnlyCollection recordBCollection) + private static void VerifyIntMinValueLength(MetadataSettings metadataSettings, ReadOnlyCollection recordCollection) { - foreach (RecordB recordB in recordBCollection) + foreach (Record record in recordCollection) { - if (recordB.ExifDirectory.FilePath.Id is null) + if (record.ExifDirectory.FilePath.Id is null) continue; - if (metadataSettings.IntMinValueLength < recordB.ExifDirectory.FilePath.Id.Value.ToString().Length) + if (metadataSettings.IntMinValueLength < record.ExifDirectory.FilePath.Id.Value.ToString().Length) throw new NotSupportedException(); } } - private static string GetTFW(RecordB record, bool? isWrongYear) => + private static string GetTFW(Record record, bool? isWrongYear) => string.Concat(record.HasDateTimeOriginal ? "T" : "F", isWrongYear is not null && isWrongYear.Value ? "W" : record.FastForwardMovingPictureExpertsGroupUsed ? "V" : "I"); private static string GetDirectoryName(string year, string tfw, string prefix, string? splat, int seasonValue, string seasonName, string makerSplit) => splat is null ? $"{prefix}{year} {tfw}{year}.{seasonValue} {seasonName}{makerSplit}" : $"{prefix}{year} {tfw}{year}{splat}"; - private static string? GetCheckDirectory(AppSettings appSettings, RecordB record, ReadOnlyCollection ids, bool multipleDirectoriesWithFiles, string paddedId) + private static string? GetCheckDirectory(AppSettings appSettings, DirectoryInfo directoryInfo, Record record, ReadOnlyCollection ids, bool multipleDirectoriesWithFiles, string paddedId) { string? result; string year = record.DateTime.Year.ToString(); - string checkDirectoryName = Path.GetFileName(record.FilePath.DirectoryFullPath); + string checkDirectoryName = Path.GetFileName(record.ExifDirectory.FilePath.DirectoryFullPath); if (multipleDirectoriesWithFiles && !checkDirectoryName.Contains(year)) result = null; else { - (bool? isWrongYear, string[] years) = IDate.IsWrongYear(record.FilePath, record.ExifDirectory); - if (appSettings.RenameSettings.InPlaceMoveDirectory && !record.FilePath.FileNameFirstSegment.Contains(paddedId)) + (bool? isWrongYear, string[] years) = IDate.IsWrongYear(directoryInfo, record.ExifDirectory.FilePath, record.ExifDirectory); + if (appSettings.RenameSettings.InPlaceMoveDirectory && !record.ExifDirectory.FilePath.FileNameFirstSegment.Contains(paddedId)) result = null; else { + if (record.ExifDirectory.FilePath.FullName[..2] != directoryInfo.FullName[..2]) + isWrongYear = null; string tfw = GetTFW(record, isWrongYear); string? maker = IMetadata.GetMaker(record.ExifDirectory); string rootDirectory = appSettings.ResultSettings.RootDirectory; @@ -352,7 +396,7 @@ public partial class Rename : IRename, IDisposable return result; } - private static List GetSidecarFiles(AppSettings appSettings, RecordB record, List distinct, string checkDirectory, string paddedId) + private static List GetSidecarFiles(AppSettings appSettings, Record record, List distinct, string checkDirectory, string paddedId) { List results = []; ToDo toDo; @@ -375,7 +419,10 @@ public partial class Rename : IRename, IDisposable if (distinct.Contains(checkFile)) continue; distinct.Add(checkFile); - toDo = new(checkDirectory, filePath, checkFile, JsonFile: false); + toDo = new(Directory: checkDirectory, + FileInfo: new(filePath.FullName), + File: checkFile, + JsonFile: false); results.Add(toDo); } return results; @@ -403,15 +450,16 @@ public partial class Rename : IRename, IDisposable return result; } - private static ReadOnlyCollection GetToDoCollection(AppSettings appSettings, ReadOnlyCollection ids, ReadOnlyCollection recordBCollection) + private static ReadOnlyCollection GetToDoCollection(AppSettings appSettings, DirectoryInfo directoryInfo, ReadOnlyCollection ids, ReadOnlyCollection files, ReadOnlyCollection recordCollection) { List results = []; - ToDo toDo; - RecordB record; + ToDo? toDo; + Record record; string jsonFile; string paddedId; string checkFile; FilePath filePath; + FileInfo[] matches; string directoryName; FileHolder fileHolder; string? checkDirectory; @@ -421,22 +469,23 @@ public partial class Rename : IRename, IDisposable const string jpeg = ".jpeg"; string jsonFileSubDirectory; bool? directoryCheck = GetDirectoryCheck(appSettings.ResultSettings); - VerifyIntMinValueLength(appSettings.MetadataSettings, recordBCollection); + VerifyIntMinValueLength(appSettings.MetadataSettings, recordCollection); bool multipleDirectoriesWithFiles = directoryCheck is not null && directoryCheck.Value; - ReadOnlyCollection sorted = (from l in recordBCollection orderby l.DateTime select l).ToArray().AsReadOnly(); + ReadOnlyCollection collection = (from l in files select new FileInfo(l)).ToArray().AsReadOnly(); + ReadOnlyCollection sorted = (from l in recordCollection orderby l.DateTime select l).ToArray().AsReadOnly(); for (int i = 0; i < sorted.Count; i++) { record = sorted[i]; if (record.ExifDirectory.FilePath.Id is null) continue; paddedId = IId.GetPaddedId(appSettings.ResultSettings, appSettings.MetadataSettings, record.ExifDirectory.FilePath.Id.Value, record.HasIgnoreKeyword, record.HasDateTimeOriginal, i); - checkDirectory = GetCheckDirectory(appSettings, record, ids, multipleDirectoriesWithFiles, paddedId); + checkDirectory = GetCheckDirectory(appSettings, directoryInfo, record, ids, multipleDirectoriesWithFiles, paddedId); if (string.IsNullOrEmpty(checkDirectory)) continue; - checkFileExtension = record.FilePath.ExtensionLowered == jpeg ? jpg : record.FilePath.ExtensionLowered; + checkFileExtension = record.ExifDirectory.FilePath.ExtensionLowered == jpeg ? jpg : record.ExifDirectory.FilePath.ExtensionLowered; jsonFileSubDirectory = Path.GetDirectoryName(Path.GetDirectoryName(record.JsonFile)) ?? throw new Exception(); checkFile = Path.Combine(checkDirectory, $"{paddedId}{checkFileExtension}"); - if (checkFile == record.FilePath.FullName) + if (checkFile == record.ExifDirectory.FilePath.FullName) continue; if (File.Exists(checkFile)) { @@ -449,16 +498,37 @@ public partial class Rename : IRename, IDisposable if (record.JsonFile != jsonFile) { fileHolder = FileHolder.Get(record.JsonFile); - filePath = FilePath.Get(appSettings.ResultSettings, appSettings.MetadataSettings, fileHolder, index: null); - toDo = new(null, filePath, jsonFile, JsonFile: true); - results.Add(toDo); + if (fileHolder.Exists) + { + filePath = FilePath.Get(appSettings.ResultSettings, appSettings.MetadataSettings, fileHolder, index: null); + toDo = new(Directory: null, + FileInfo: new(filePath.FullName), + File: jsonFile, + JsonFile: true); + results.Add(toDo); + } } if (distinct.Contains(checkFile)) continue; + toDo = null; distinct.Add(checkFile); - toDo = new(checkDirectory, record.FilePath, checkFile, JsonFile: false); + if (files.Contains(record.ExifDirectory.FilePath.FullName)) + toDo = new(Directory: checkDirectory, + FileInfo: new(record.ExifDirectory.FilePath.FullName), + File: checkFile, + JsonFile: false); + else + { + matches = (from l in collection where record.ExifDirectory.FilePath.Name == l.Name select l).ToArray(); + toDo = matches.Length != 1 ? null : new(Directory: checkDirectory, + FileInfo: matches[0], + File: checkFile, + JsonFile: false); + } + if (toDo is null) + continue; results.Add(toDo); - if (record.SidecarFiles.Count == 0) + if (record.SidecarFiles.Length == 0) continue; results.AddRange(GetSidecarFiles(appSettings, record, distinct, checkDirectory, paddedId)); } @@ -478,23 +548,25 @@ public partial class Rename : IRename, IDisposable } } - private ReadOnlyCollection RenameFilesInDirectories(RenameSettings renameSettings, ReadOnlyCollection toDoCollection) + private ReadOnlyCollection RenameFilesInDirectories(RenameSettings renameSettings, IRename rename, ReadOnlyCollection toDoCollection) { List results = []; VerifyDirectories(toDoCollection); bool useProgressBar = !renameSettings.InPlace && !renameSettings.InPlaceWithOriginalName; if (useProgressBar) - _ProgressBar = new(toDoCollection.Count, "Move Files", new ProgressBarOptions() { ProgressCharacter = '─', ProgressBarOnBottom = true, DisableBottomPercentage = true }); + rename.ConstructProgressBar(toDoCollection.Count, "Move Files"); foreach (ToDo toDo in toDoCollection) { if (useProgressBar) _ProgressBar?.Tick(); + if (!toDo.FileInfo.Exists) + continue; if (toDo.JsonFile) { if (File.Exists(toDo.File)) File.Delete(toDo.File); try - { File.Move(toDo.FilePath.FullName, toDo.File); } + { File.Move(toDo.FileInfo.FullName, toDo.File); } catch (Exception) { continue; } } @@ -505,10 +577,10 @@ public partial class Rename : IRename, IDisposable if (File.Exists(toDo.File)) File.Delete(toDo.File); try - { File.Move(toDo.FilePath.FullName, toDo.File); } + { File.Move(toDo.FileInfo.FullName, toDo.File); } catch (Exception) { continue; } - results.Add($"{toDo.FilePath.FullName}\t{toDo.File}"); + results.Add($"{toDo.FileInfo.FullName}\t{toDo.File}"); } } if (useProgressBar) @@ -516,18 +588,18 @@ public partial class Rename : IRename, IDisposable return results.AsReadOnly(); } - private static void SaveIdentifiersToDisk(long ticks, AppSettings appSettings, ReadOnlyCollection recordBCollection) + private static void SaveIdentifiersToDisk(long ticks, AppSettings appSettings, ReadOnlyCollection recordCollection) { string paddedId; Identifier identifier; List identifiers = []; string aMetadataCollectionDirectory = IResult.GetResultsDateGroupDirectory(appSettings.ResultSettings, nameof(A_Metadata), appSettings.ResultSettings.ResultCollection); - foreach (RecordB record in recordBCollection) + foreach (Record record in recordCollection) { if (record.ExifDirectory.FilePath.Id is null) continue; paddedId = IId.GetPaddedId(appSettings.ResultSettings, appSettings.MetadataSettings, record.ExifDirectory.FilePath.Id.Value, record.HasIgnoreKeyword, record.HasDateTimeOriginal, index: null); - identifier = new([], record.HasDateTimeOriginal, record.ExifDirectory.FilePath.Id.Value, record.FilePath.Length, paddedId, record.DateTime.Ticks); + identifier = new([], record.HasDateTimeOriginal, record.ExifDirectory.FilePath.Id.Value, record.ExifDirectory.FilePath.Length, paddedId, record.DateTime.Ticks); identifiers.Add(identifier); } string json = JsonSerializer.Serialize(identifiers.OrderBy(l => l.PaddedId).ToArray(), IdentifierCollectionSourceGenerationContext.Default.IdentifierArray); @@ -549,22 +621,27 @@ public partial class Rename : IRename, IDisposable private void RenameWork(ILogger? logger, AppSettings appSettings, IRename rename, long ticks) { ReadOnlyCollection ids = GetIds(appSettings.RenameSettings); - _ = IPath.DeleteEmptyDirectories(appSettings.ResultSettings.RootDirectory); - DirectoryInfo directoryInfo = new(Path.GetFullPath(appSettings.ResultSettings.RootDirectory)); - logger?.LogInformation("{Ticks} {RootDirectory}", ticks, directoryInfo.FullName); - ReadOnlyCollection recordBCollection = GetRecordBCollection(logger, appSettings, rename, ticks, ids, directoryInfo); - SaveIdentifiersToDisk(ticks, appSettings, recordBCollection); + string sourceDirectory = Path.GetFullPath(appSettings.ResultSettings.RootDirectory); + if (!Directory.Exists(sourceDirectory)) + _ = Directory.CreateDirectory(sourceDirectory); + logger?.LogInformation("{Ticks} {RootDirectory}", ticks, sourceDirectory); + ReadOnlyCollection files = Directory.GetFiles(sourceDirectory, "*", SearchOption.AllDirectories).ToArray().AsReadOnly(); + if (files.Count > 0) + _ = IPath.DeleteEmptyDirectories(appSettings.ResultSettings.RootDirectory); + ReadOnlyCollection recordCollection = GetRecordCollection(logger, appSettings, rename, ticks, ids, sourceDirectory, files); + SaveIdentifiersToDisk(ticks, appSettings, recordCollection); if (appSettings.RenameSettings.InPlace || appSettings.RenameSettings.InPlaceWithOriginalName) { - if (recordBCollection.Count > 0) - recordBCollection = new([]); + if (recordCollection.Count > 0) + recordCollection = new([]); string aMetadataSingletonDirectory = IResult.GetResultsGroupDirectory(appSettings.ResultSettings, nameof(A_Metadata)); _ = IPath.DeleteEmptyDirectories(aMetadataSingletonDirectory); } if (!appSettings.RenameSettings.OnlySaveIdentifiersToDisk) { - ReadOnlyCollection toDoCollection = GetToDoCollection(appSettings, ids, recordBCollection); - ReadOnlyCollection lines = RenameFilesInDirectories(appSettings.RenameSettings, toDoCollection); + DirectoryInfo directoryInfo = new(sourceDirectory); + ReadOnlyCollection toDoCollection = GetToDoCollection(appSettings, directoryInfo, ids, files, recordCollection); + ReadOnlyCollection lines = RenameFilesInDirectories(appSettings.RenameSettings, rename, toDoCollection); if (lines.Count != 0) { File.WriteAllLines($"D:/Tmp/Phares/{DateTime.Now.Ticks}.tsv", lines); diff --git a/Shared/Models/FileHolder.cs b/Shared/Models/FileHolder.cs index e777014..fdf9a0d 100644 --- a/Shared/Models/FileHolder.cs +++ b/Shared/Models/FileHolder.cs @@ -21,29 +21,44 @@ public record FileHolder(DateTime? CreationTime, return result; } + private static FileHolder GetExisting(NginxFileSystem nginxFileSystem, int? id) => + new(CreationTime: nginxFileSystem.LastModified, + DirectoryFullPath: Path.GetDirectoryName(nginxFileSystem.URI?.OriginalString ?? throw new Exception()), + Exists: true, + ExtensionLowered: Path.GetExtension(nginxFileSystem.Name).ToLower(), + FullName: nginxFileSystem.URI?.OriginalString ?? throw new Exception(), + Id: id, + LastWriteTime: nginxFileSystem.LastModified, + Length: nginxFileSystem.Length is null ? null : (long)nginxFileSystem.Length.Value, + Name: nginxFileSystem.Name, + NameWithoutExtension: Path.GetFileNameWithoutExtension(nginxFileSystem.Name)); + + public static FileHolder Get(NginxFileSystem nginxFileSystem) => + GetExisting(nginxFileSystem, id: null); + private static FileHolder GetExisting(FileInfo fileInfo, int? id) => - new(fileInfo.CreationTime, - fileInfo.DirectoryName, - fileInfo.Exists, - fileInfo.Extension.ToLower(), - fileInfo.FullName, - id, - fileInfo.LastWriteTime, - fileInfo.Length, - fileInfo.Name, - Path.GetFileNameWithoutExtension(fileInfo.FullName)); + new(CreationTime: fileInfo.CreationTime, + DirectoryFullPath: fileInfo.DirectoryName, + Exists: fileInfo.Exists, + ExtensionLowered: fileInfo.Extension.ToLower(), + FullName: fileInfo.FullName, + Id: id, + LastWriteTime: fileInfo.LastWriteTime, + Length: fileInfo.Length, + Name: fileInfo.Name, + NameWithoutExtension: Path.GetFileNameWithoutExtension(fileInfo.FullName)); private static FileHolder GetNonExisting(FileInfo fileInfo, int? id) => - new(null, - fileInfo.DirectoryName, - fileInfo.Exists, - fileInfo.Extension.ToLower(), - fileInfo.FullName, - id, - null, - null, - fileInfo.Name, - Path.GetFileNameWithoutExtension(fileInfo.FullName)); + new(CreationTime: null, + DirectoryFullPath: fileInfo.DirectoryName, + Exists: fileInfo.Exists, + ExtensionLowered: fileInfo.Extension.ToLower(), + FullName: fileInfo.FullName, + Id: id, + LastWriteTime: null, + Length: null, + Name: fileInfo.Name, + NameWithoutExtension: Path.GetFileNameWithoutExtension(fileInfo.FullName)); public static FileHolder Get(FileInfo fileInfo, int? id) => fileInfo.Exists ? GetExisting(fileInfo, id) : GetNonExisting(fileInfo, id); @@ -52,42 +67,42 @@ public record FileHolder(DateTime? CreationTime, { FileHolder result; DateTime dateTime = new(filePath.CreationTicks); - result = new(dateTime, - filePath.DirectoryFullPath, - true, - filePath.ExtensionLowered, - filePath.FullName, - id, - new(filePath.LastWriteTicks), - filePath.Length, - filePath.Name, - Path.GetFileNameWithoutExtension(filePath.FullName)); + result = new(CreationTime: dateTime, + DirectoryFullPath: filePath.DirectoryFullPath, + Exists: true, + ExtensionLowered: filePath.ExtensionLowered, + FullName: filePath.FullName, + Id: id, + LastWriteTime: new(filePath.LastWriteTicks), + Length: filePath.Length, + Name: filePath.Name, + NameWithoutExtension: Path.GetFileNameWithoutExtension(filePath.FullName)); return result; } private static FileHolder GetExisting(FileHolder fileHolder) => - new(fileHolder.CreationTime, - fileHolder.DirectoryFullPath, - fileHolder.Exists, - fileHolder.ExtensionLowered, - fileHolder.FullName, + new(CreationTime: fileHolder.CreationTime, + DirectoryFullPath: fileHolder.DirectoryFullPath, + Exists: fileHolder.Exists, + ExtensionLowered: fileHolder.ExtensionLowered, + FullName: fileHolder.FullName, Id: null, - fileHolder.LastWriteTime, - fileHolder.Length, - fileHolder.Name, - Path.GetFileNameWithoutExtension(fileHolder.FullName)); + LastWriteTime: fileHolder.LastWriteTime, + Length: fileHolder.Length, + Name: fileHolder.Name, + NameWithoutExtension: Path.GetFileNameWithoutExtension(fileHolder.FullName)); private static FileHolder GetNonExisting(FileHolder fileHolder) => - new(null, - fileHolder.DirectoryFullPath, - fileHolder.Exists, - fileHolder.ExtensionLowered, - fileHolder.FullName, + new(CreationTime: null, + DirectoryFullPath: fileHolder.DirectoryFullPath, + Exists: fileHolder.Exists, + ExtensionLowered: fileHolder.ExtensionLowered, + FullName: fileHolder.FullName, Id: null, - null, - null, - fileHolder.Name, - Path.GetFileNameWithoutExtension(fileHolder.FullName)); + LastWriteTime: null, + Length: null, + Name: fileHolder.Name, + NameWithoutExtension: Path.GetFileNameWithoutExtension(fileHolder.FullName)); public static FileHolder Get(FileHolder fileHolder) => fileHolder.Exists ? GetExisting(fileHolder) : GetNonExisting(fileHolder); diff --git a/Shared/Models/FirstPass.cs b/Shared/Models/FirstPass.cs new file mode 100644 index 0000000..a4f4101 --- /dev/null +++ b/Shared/Models/FirstPass.cs @@ -0,0 +1,30 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace View_by_Distance.Shared.Models; + +public record FirstPass(ExifDirectory ExifDirectory, + bool FastForwardMovingPictureExpertsGroupUsed, + MinimumYearAndPathCombined MinimumYearAndPathCombined, + FileHolder[] SidecarFiles) +{ + + public override string ToString() + { + string result = JsonSerializer.Serialize(this, FirstPassSourceGenerationContext.Default.FilePath); + return result; + } + +} + +[JsonSourceGenerationOptions(WriteIndented = true)] +[JsonSerializable(typeof(FirstPass))] +public partial class FirstPassSourceGenerationContext : JsonSerializerContext +{ +} + +[JsonSourceGenerationOptions(WriteIndented = true)] +[JsonSerializable(typeof(List))] +public partial class FirstPassCollectionSourceGenerationContext : JsonSerializerContext +{ +} \ No newline at end of file diff --git a/Shared/Models/MetadataGroup.cs b/Shared/Models/MetadataGroup.cs index ddf9a89..1bed256 100644 --- a/Shared/Models/MetadataGroup.cs +++ b/Shared/Models/MetadataGroup.cs @@ -4,7 +4,7 @@ using System.Text.Json.Serialization; namespace View_by_Distance.Shared.Models; -public record MetadataGroup(bool FastForwardMovingPictureExpertsGroupUsed, FilePath FilePath, FileInfo FileInfo, ExifDirectory ExifDirectory, ReadOnlyCollection SidecarFiles) +public record MetadataGroup(bool FastForwardMovingPictureExpertsGroupUsed, FilePath FilePath, MinimumYearAndPathCombined MinimumYearAndPathCombined, ExifDirectory ExifDirectory, ReadOnlyCollection SidecarFiles) { public override string ToString() diff --git a/Shared/Models/MinimumYearAndFullPath.cs b/Shared/Models/MinimumYearAndFullPath.cs new file mode 100644 index 0000000..2bef02d --- /dev/null +++ b/Shared/Models/MinimumYearAndFullPath.cs @@ -0,0 +1,22 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace View_by_Distance.Shared.Models; + +public record MinimumYearAndPathCombined(int MinimumYear, + string PathCombined) +{ + + public override string ToString() + { + string result = JsonSerializer.Serialize(this, MinimumYearAndPathCombinedSourceGenerationContext.Default.AviDirectory); + return result; + } + +} + +[JsonSourceGenerationOptions(WriteIndented = true)] +[JsonSerializable(typeof(AviDirectory))] +public partial class MinimumYearAndPathCombinedSourceGenerationContext : JsonSerializerContext +{ +} \ No newline at end of file diff --git a/Shared/Models/NginxFileSystem.cs b/Shared/Models/NginxFileSystem.cs new file mode 100644 index 0000000..93200d8 --- /dev/null +++ b/Shared/Models/NginxFileSystem.cs @@ -0,0 +1,52 @@ +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace View_by_Distance.Shared.Models; + +public record NginxFileSystem([property: JsonPropertyName("name")] string Name, + DateTime? LastModified, + [property: JsonPropertyName("mtime")] string MTime, + Uri? URI, + [property: JsonPropertyName("type")] string Type, + [property: JsonPropertyName("size")] float? Length) +{ + + public override string ToString() + { + string result = JsonSerializer.Serialize(this, NginxFileSystemSourceGenerationContext.Default.NginxFileSystem); + return result; + } + + public static NginxFileSystem Get(string format, TimeZoneInfo timeZoneInfo, string name, string mTime, Uri uri, string type, float? size) + { + NginxFileSystem result; + DateTime dateTime; + DateTime? nullableDateTime; + if (mTime.Length != format.Length + 4 || !DateTime.TryParseExact(mTime[..format.Length], format, CultureInfo.InvariantCulture, DateTimeStyles.None, out dateTime)) + nullableDateTime = null; + else + nullableDateTime = TimeZoneInfo.ConvertTimeBySystemTimeZoneId(dateTime, mTime[(format.Length + 1)..], timeZoneInfo.Id); + result = new(name, nullableDateTime, mTime, new($"{uri.OriginalString}/{name}"), type, size); + return result; + } + + public static string GetFormat() => + "ddd, dd MMM yyyy HH:mm:ss"; + + public static NginxFileSystem Get(string format, TimeZoneInfo timeZoneInfo, Uri uri, NginxFileSystem nginxFileSystem) => + Get(format, timeZoneInfo, nginxFileSystem.Name, nginxFileSystem.MTime, uri, nginxFileSystem.Type, nginxFileSystem.Length); + +} + +[JsonSourceGenerationOptions(WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)] +[JsonSerializable(typeof(NginxFileSystem))] +public partial class NginxFileSystemSourceGenerationContext : JsonSerializerContext +{ +} + +[JsonSourceGenerationOptions(WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)] +[JsonSerializable(typeof(NginxFileSystem[]))] +public partial class NginxFileSystemCollectionSourceGenerationContext : JsonSerializerContext +{ +} \ No newline at end of file diff --git a/Shared/Models/Properties/IWindowsSettings.cs b/Shared/Models/Properties/IWindowsSettings.cs new file mode 100644 index 0000000..0d63d8e --- /dev/null +++ b/Shared/Models/Properties/IWindowsSettings.cs @@ -0,0 +1,10 @@ +namespace View_by_Distance.Shared.Models.Properties; + +public interface IWindowsSettings +{ + + public string[] IgnoreExtensions { init; get; } + public string[] ValidImageFormatExtensions { init; get; } + public string[] ValidVideoFormatExtensions { init; get; } + +} \ No newline at end of file diff --git a/Shared/Models/Stateless/Age.cs b/Shared/Models/Stateless/Age.cs index e71380b..8de02b3 100644 --- a/Shared/Models/Stateless/Age.cs +++ b/Shared/Models/Stateless/Age.cs @@ -16,19 +16,19 @@ internal abstract class Age years += 1; } result = new(minuendTicks - check.AddYears(-1).Ticks); - return (years, result); + return new(years, result); } internal static (int, TimeSpan) GetAge(long minuendTicks, DateTime subtrahend) { (int years, TimeSpan result) = GetAge(minuendTicks, subtrahend.Ticks); - return (years, result); + return new(years, result); } internal static (int, TimeSpan) GetAge(DateTime minuend, DateTime subtrahend) { (int years, TimeSpan result) = GetAge(minuend.Ticks, subtrahend.Ticks); - return (years, result); + return new(years, result); } internal static int? GetApproximateYears(char[] personCharacters, string personDisplayDirectoryName) diff --git a/Shared/Models/Stateless/IDate.cs b/Shared/Models/Stateless/IDate.cs index c7acfd0..fc07b9d 100644 --- a/Shared/Models/Stateless/IDate.cs +++ b/Shared/Models/Stateless/IDate.cs @@ -3,10 +3,10 @@ namespace View_by_Distance.Shared.Models.Stateless; public interface IDate { - (bool?, string[]) TestStatic_IsWrongYear(FilePath filePath, ExifDirectory exifDirectory) => - IsWrongYear(filePath, exifDirectory); - static (bool?, string[]) IsWrongYear(FilePath filePath, ExifDirectory exifDirectory) => - XDate.IsWrongYear(filePath, exifDirectory); + (bool?, string[]) TestStatic_IsWrongYear(DirectoryInfo directoryInfo, FilePath filePath, ExifDirectory exifDirectory) => + IsWrongYear(directoryInfo, filePath, exifDirectory); + static (bool?, string[]) IsWrongYear(DirectoryInfo directoryInfo, FilePath filePath, ExifDirectory exifDirectory) => + XDate.IsWrongYear(directoryInfo, filePath, exifDirectory); (int Season, string seasonName) TestStatic_GetSeason(int dayOfYear) => GetSeason(dayOfYear); diff --git a/Shared/Models/Stateless/IRename.cs b/Shared/Models/Stateless/IRename.cs index 2b666de..0228dd6 100644 --- a/Shared/Models/Stateless/IRename.cs +++ b/Shared/Models/Stateless/IRename.cs @@ -8,6 +8,7 @@ public interface IRename ReadOnlyCollection ConvertAndGetFastForwardMovingPictureExpertsGroupFiles(IRenameSettings renameSettings, FilePath filePath); DeterministicHashCode GetDeterministicHashCode(FilePath filePath); + void ConstructProgressBar(int maxTicks, string message); void Tick(); } \ No newline at end of file diff --git a/Shared/Models/Stateless/IWindows.cs b/Shared/Models/Stateless/IWindows.cs new file mode 100644 index 0000000..971b1ed --- /dev/null +++ b/Shared/Models/Stateless/IWindows.cs @@ -0,0 +1,15 @@ +using System.Collections.ObjectModel; +using View_by_Distance.Shared.Models.Properties; + +namespace View_by_Distance.Shared.Models.Stateless; + +public interface IWindows +{ + + ReadOnlyCollection ConvertAndGetFastForwardMovingPictureExpertsGroupFiles(IWindowsSettings windowsSettings, HttpClient? httpClient, FilePath filePath); + DeterministicHashCode GetDeterministicHashCode(HttpClient? httpClient, FilePath filePath); + DeterministicHashCode GetDeterministicHashCode(HttpClient httpClient, Uri uri); + void ConstructProgressBar(int maxTicks, string message); + void Tick(); + +} \ No newline at end of file diff --git a/Shared/Models/Stateless/PersonBirthday.cs b/Shared/Models/Stateless/PersonBirthday.cs index 971861a..bd61c5a 100644 --- a/Shared/Models/Stateless/PersonBirthday.cs +++ b/Shared/Models/Stateless/PersonBirthday.cs @@ -48,7 +48,7 @@ internal abstract class PersonBirthday if (birthday?.Value is null) throw new NullReferenceException(nameof(birthday.Value)); (years, result) = Age.GetAge(dateTimeTicks, birthday.Value); - return (years, result); + return new(years, result); } internal static (int, TimeSpan) GetAge(DateTime dateTime, Models.PersonBirthday birthday) @@ -58,7 +58,7 @@ internal abstract class PersonBirthday if (birthday?.Value is null) throw new NullReferenceException(nameof(birthday.Value)); (years, result) = Age.GetAge(dateTime, birthday.Value); - return (years, result); + return new(years, result); } internal static (int, double) GetAge(DateTime dateTime, DateTime dayBeforeLeapDate, Models.PersonBirthday birthday) @@ -69,7 +69,7 @@ internal abstract class PersonBirthday result = timeSpan.TotalDays / 365; else result = timeSpan.TotalDays / 366; - return (years, result); + return new(years, result); } internal static double? GetAge(Models.PersonBirthday birthday) diff --git a/Shared/Models/Stateless/XDate.cs b/Shared/Models/Stateless/XDate.cs index 3ef8a4f..7f98a79 100644 --- a/Shared/Models/Stateless/XDate.cs +++ b/Shared/Models/Stateless/XDate.cs @@ -58,7 +58,7 @@ internal abstract class XDate return result; } - internal static (bool?, string[]) IsWrongYear(FilePath filePath, ExifDirectory exifDirectory) + internal static (bool?, string[]) IsWrongYear(DirectoryInfo directoryInfo, FilePath filePath, ExifDirectory exifDirectory) { string[] results = []; bool? result = null; @@ -67,9 +67,6 @@ internal abstract class XDate string[] directorySegments; List collection = []; string? check = Path.GetFullPath(filePath.FullName); - string? pathRoot = Path.GetPathRoot(filePath.FullName); - if (string.IsNullOrEmpty(pathRoot)) - throw new Exception(); DateTime? dateTimeOriginal = GetDateTimeOriginal(exifDirectory); if (dateTimeOriginal is not null) collection.Add(dateTimeOriginal.Value); @@ -85,7 +82,7 @@ internal abstract class XDate for (int i = 0; i < int.MaxValue; i++) { check = Path.GetDirectoryName(check); - if (string.IsNullOrEmpty(check) || check == pathRoot) + if (string.IsNullOrEmpty(check) || check == directoryInfo.FullName) break; directoryName = Path.GetFileName(check); directorySegments = directoryName.Split(' '); diff --git a/Shared/Models/Stateless/XPath.cs b/Shared/Models/Stateless/XPath.cs index 615aab9..2f3fad0 100644 --- a/Shared/Models/Stateless/XPath.cs +++ b/Shared/Models/Stateless/XPath.cs @@ -281,19 +281,19 @@ internal abstract class XPath result = check; converted = int.Parse(check); } - return (result, converted); + return new(result, converted); } internal static (string, int) GetDirectoryNameAndIndex(ResultSettings resultSettings, int id) { (string result, int converted) = GetDirectoryNameAndIndex(resultSettings.ResultAllInOneSubdirectoryLength, id.ToString()); - return (result, converted); + return new(result, converted); } internal static (string, int) GetDirectoryNameAndIndex(ResultSettings resultSettings, FileHolder fileHolder) { (string result, int converted) = GetDirectoryNameAndIndex(resultSettings.ResultAllInOneSubdirectoryLength, fileHolder.NameWithoutExtension); - return (result, converted); + return new(result, converted); } internal static (string, int) GetDirectoryNameAndIndex(ResultSettings resultSettings, FilePath filePath) @@ -304,7 +304,7 @@ internal abstract class XPath (result, converted) = GetDirectoryNameAndIndex(resultSettings.ResultAllInOneSubdirectoryLength, filePath.Id.Value.ToString()); else (result, converted) = GetDirectoryNameAndIndex(resultSettings.ResultAllInOneSubdirectoryLength, filePath.NameWithoutExtension); - return (result, converted); + return new(result, converted); } private static ReadOnlyCollection GetYears(ResultSettings resultSettings) diff --git a/Windows/.vscode/read-me.md b/Windows/.vscode/read-me.md new file mode 100644 index 0000000..bbb628c --- /dev/null +++ b/Windows/.vscode/read-me.md @@ -0,0 +1 @@ +# Read Me diff --git a/Windows/AA.Windows.csproj b/Windows/AA.Windows.csproj new file mode 100644 index 0000000..9f38eaf --- /dev/null +++ b/Windows/AA.Windows.csproj @@ -0,0 +1,57 @@ + + + enable + enable + Exe + win-x64 + net9.0 + 076c87e8-c7f0-40a3-aba3-73eb7f9ea892 + + + Phares.View.by.Distance.Windows + 8.0.112.0 + Phares + Mike Phares + true + read-me.md + snupkg + false + MIT + + + true + true + true + + + Windows + + + OSX + + + Linux + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Windows/Models/AppSettings.cs b/Windows/Models/AppSettings.cs new file mode 100644 index 0000000..d167580 --- /dev/null +++ b/Windows/Models/AppSettings.cs @@ -0,0 +1,57 @@ +using Microsoft.Extensions.Configuration; +using System.Text.Json; +using System.Text.Json.Serialization; +using View_by_Distance.Shared.Models; + +namespace View_by_Distance.Windows.Models; + +public record AppSettings(ResultSettings ResultSettings, + MetadataSettings MetadataSettings, + WindowsSettings WindowsSettings) +{ + + public override string ToString() + { + string result = JsonSerializer.Serialize(this, AppSettingsSourceGenerationContext.Default.AppSettings); + return result; + } + + private static void Verify(AppSettings appSettings) + { + if (appSettings.WindowsSettings.MaxDegreeOfParallelism > Environment.ProcessorCount) + throw new Exception("MaxDegreeOfParallelism must be =< Environment.ProcessorCount!"); + } + + public static AppSettings Get(IConfigurationRoot configurationRoot) + { + AppSettings result; +#pragma warning disable IL3050, IL2026 + ResultSettings? resultSettings = configurationRoot.GetSection(nameof(ResultSettings)).Get(); + MetadataSettings? metadataSettings = configurationRoot.GetSection(nameof(MetadataSettings)).Get(); + WindowsSettings? WindowsSettings = configurationRoot.GetSection(nameof(WindowsSettings)).Get(); +#pragma warning restore IL3050, IL2026 + if (resultSettings is null || metadataSettings is null || WindowsSettings?.Company is null) + { + List paths = []; + foreach (IConfigurationProvider configurationProvider in configurationRoot.Providers) + { + if (configurationProvider is not Microsoft.Extensions.Configuration.Json.JsonConfigurationProvider jsonConfigurationProvider) + continue; + if (jsonConfigurationProvider.Source.FileProvider is not Microsoft.Extensions.FileProviders.PhysicalFileProvider physicalFileProvider) + continue; + paths.Add(physicalFileProvider.Root); + } + throw new NotSupportedException($"Not found!{Environment.NewLine}{string.Join(Environment.NewLine, paths.Distinct())}"); + } + result = new(resultSettings, metadataSettings, WindowsSettings); + Verify(result); + return result; + } + +} + +[JsonSourceGenerationOptions(WriteIndented = true)] +[JsonSerializable(typeof(AppSettings))] +internal partial class AppSettingsSourceGenerationContext : JsonSerializerContext +{ +} \ No newline at end of file diff --git a/Windows/Models/Identifier.cs b/Windows/Models/Identifier.cs new file mode 100644 index 0000000..b5d3286 --- /dev/null +++ b/Windows/Models/Identifier.cs @@ -0,0 +1,32 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace View_by_Distance.Windows.Models; + +internal sealed record Identifier(string[] DirectoryNames, + bool? HasDateTimeOriginal, + int Id, + long Length, + string PaddedId, + long Ticks) +{ + + public override string ToString() + { + string result = JsonSerializer.Serialize(this, IdentifierSourceGenerationContext.Default.Identifier); + return result; + } + +} + +[JsonSourceGenerationOptions(WriteIndented = true)] +[JsonSerializable(typeof(Identifier))] +internal partial class IdentifierSourceGenerationContext : JsonSerializerContext +{ +} + +[JsonSourceGenerationOptions(WriteIndented = true)] +[JsonSerializable(typeof(Identifier[]))] +internal partial class IdentifierCollectionSourceGenerationContext : JsonSerializerContext +{ +} \ No newline at end of file diff --git a/Windows/Models/WindowsSettings.cs b/Windows/Models/WindowsSettings.cs new file mode 100644 index 0000000..da4d042 --- /dev/null +++ b/Windows/Models/WindowsSettings.cs @@ -0,0 +1,30 @@ + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace View_by_Distance.Windows.Models; + +public record WindowsSettings(string Company, + string? Host, + string[] IgnoreExtensions, + int MaxDegreeOfParallelism, + string? Page, + string[] SidecarExtensions, + string[] ValidImageFormatExtensions, + string[] ValidVideoFormatExtensions, + bool VerifyOnly) : Shared.Models.Properties.IWindowsSettings +{ + + public override string ToString() + { + string result = JsonSerializer.Serialize(this, WindowsSettingsSourceGenerationContext.Default.WindowsSettings); + return result; + } + +} + +[JsonSourceGenerationOptions(WriteIndented = true)] +[JsonSerializable(typeof(WindowsSettings))] +internal partial class WindowsSettingsSourceGenerationContext : JsonSerializerContext +{ +} \ No newline at end of file diff --git a/Windows/Program.cs b/Windows/Program.cs new file mode 100644 index 0000000..433b011 --- /dev/null +++ b/Windows/Program.cs @@ -0,0 +1,53 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using View_by_Distance.Windows.Models; + +namespace View_by_Distance.Windows; + +public class Program +{ + + public static void Secondary(ILogger logger, List args) + { + IConfigurationBuilder configurationBuilder = new ConfigurationBuilder() + .AddEnvironmentVariables() + .AddUserSecrets(); + IConfigurationRoot configurationRoot = configurationBuilder.Build(); + AppSettings appSettings = AppSettings.Get(configurationRoot); + int silentIndex = args.IndexOf("s"); + if (silentIndex > -1) + args.RemoveAt(silentIndex); + try + { + if (args is null) + throw new Exception("args is null!"); + Shared.Models.Console console = new(); + _ = new Windows(args, logger, appSettings, silentIndex > -1, console); + } + catch (Exception ex) + { + logger?.LogError(ex, "Error!"); + } + if (silentIndex > -1) + logger?.LogInformation("Done. Bye"); + else + { + logger?.LogInformation("Done. Press 'Enter' to end"); + _ = Console.ReadLine(); + } + } + + public static void Main(string[] args) + { +#pragma warning disable IL3050 + ILogger? logger = Host.CreateDefaultBuilder(args).Build().Services.GetRequiredService>(); +#pragma warning restore IL3050 + if (args is not null) + Secondary(logger, args.ToList()); + else + Secondary(logger, []); + } + +} \ No newline at end of file diff --git a/Windows/Windows.cs b/Windows/Windows.cs new file mode 100644 index 0000000..1cf8739 --- /dev/null +++ b/Windows/Windows.cs @@ -0,0 +1,368 @@ +using CliWrap; +using Microsoft.Extensions.Logging; +using ShellProgressBar; +using System.Collections.ObjectModel; +using System.Drawing; +using System.Drawing.Imaging; +using System.Runtime.InteropServices; +using System.Text.Json; +using View_by_Distance.Metadata.Models; +using View_by_Distance.Metadata.Models.Stateless; +using View_by_Distance.Shared.Models; +using View_by_Distance.Shared.Models.Properties; +using View_by_Distance.Shared.Models.Stateless; +using View_by_Distance.Windows.Models; + +namespace View_by_Distance.Windows; + +public partial class Windows : IWindows, IDisposable +{ + + private ProgressBar? _ProgressBar; + private readonly ProgressBarOptions _ProgressBarOptions; + + public Windows(List args, ILogger? logger, AppSettings appSettings, bool isSilent, IConsole console) + { + if (isSilent) + { } + if (args is null) + throw new NullReferenceException(nameof(args)); + if (console is null) + throw new NullReferenceException(nameof(console)); + IWindows windows = this; + long ticks = DateTime.Now.Ticks; + _ProgressBarOptions = new() { ProgressCharacter = '─', ProgressBarOnBottom = true, DisableBottomPercentage = true }; + WindowsWork(logger, appSettings, windows, ticks); + } + + void IWindows.Tick() => + _ProgressBar?.Tick(); + + void IWindows.ConstructProgressBar(int maxTicks, string message) + { + _ProgressBar?.Dispose(); + _ProgressBar = new(maxTicks, message, _ProgressBarOptions); + } + + void IDisposable.Dispose() + { + _ProgressBar?.Dispose(); + GC.SuppressFinalize(this); + } + + ReadOnlyCollection IWindows.ConvertAndGetFastForwardMovingPictureExpertsGroupFiles(IWindowsSettings WindowsSettings, HttpClient? httpClient, FilePath filePath) + { + List results = []; + bool isValidVideoFormatExtensions = WindowsSettings.ValidVideoFormatExtensions.Contains(filePath.ExtensionLowered); + if (isValidVideoFormatExtensions) + { + bool check; + try + { + CommandTask commandTask = Cli.Wrap("L:/Git/ffmpeg-2024-10-02-git-358fdf3083-full_build/bin/ffmpeg.exe") + .WithArguments(["-i", filePath.FullName, "-vf", "select=eq(n\\,0)", "-q:v", "1", $"{filePath.Name}-%4d.jpg"]) + .WithWorkingDirectory(filePath.DirectoryFullPath) + .ExecuteAsync(); + commandTask.Task.Wait(); + check = true; + } + catch (Exception) + { + check = false; + } + if (check) + { + results.AddRange(Directory.GetFiles(filePath.DirectoryFullPath, $"{filePath.Name}-*.jpg", SearchOption.TopDirectoryOnly)); + if (results.Count == 0) + throw new Exception(); + File.SetCreationTime(results[0], new(filePath.CreationTicks)); + File.SetLastWriteTime(results[0], new(filePath.LastWriteTicks)); + Thread.Sleep(100); + } + } + return results.AsReadOnly(); + } + +#pragma warning disable CA1416 + + private static DeterministicHashCode GetDeterministicHashCode(Stream stream) + { + DeterministicHashCode result; + int? id; + int? width; + int? height; + try + { + using Image image = Image.FromStream(stream); + width = image.Width; + height = image.Height; + using Bitmap bitmap = new(image); + Rectangle rectangle = new(0, 0, image.Width, image.Height); + BitmapData bitmapData = bitmap.LockBits(rectangle, ImageLockMode.ReadOnly, bitmap.PixelFormat); + IntPtr intPtr = bitmapData.Scan0; + int length = bitmapData.Stride * bitmap.Height; + byte[] bytes = new byte[length]; + Marshal.Copy(intPtr, bytes, 0, length); + bitmap.UnlockBits(bitmapData); + id = IId.GetDeterministicHashCode(bytes); + } + catch (Exception) + { + id = null; + width = null; + height = null; + } + result = new(height, id, width); + return result; + } + + private static Stream GetStream(HttpClient httpClient, Uri uri) + { + Stream result; + Task task = httpClient.GetStreamAsync(uri); + task.Wait(); + result = task.Result; + return result; + } + + private static DeterministicHashCode GetDeterministicHashCode(HttpClient httpClient, Uri uri) + { + DeterministicHashCode result; + Stream stream = GetStream(httpClient, uri); + result = GetDeterministicHashCode(stream); + stream.Dispose(); + return result; + } + + DeterministicHashCode IWindows.GetDeterministicHashCode(HttpClient httpClient, Uri uri) => + GetDeterministicHashCode(httpClient, uri); + + DeterministicHashCode IWindows.GetDeterministicHashCode(HttpClient? httpClient, FilePath filePath) + { + DeterministicHashCode result; + if (httpClient is not null) + result = GetDeterministicHashCode(httpClient, new Uri(filePath.FullName)); + else + { + Stream stream = File.OpenRead(filePath.FullName); + result = GetDeterministicHashCode(stream); + stream.Dispose(); + } + return result; + } + +#pragma warning restore CA1416 + + private static ReadOnlyCollection? GetRecursiveCollection(HttpClient httpClient, string host, string page) + { + List? results; + Uri uri = new($"http://{host}/{page}"); + string format = NginxFileSystem.GetFormat(); + TimeZoneInfo timeZoneInfo = TimeZoneInfo.Local; + Task taskHttpResponseMessage = httpClient.GetAsync(uri); + taskHttpResponseMessage.Wait(); + if (!taskHttpResponseMessage.Result.IsSuccessStatusCode) + results = null; + else + { + Task taskString = taskHttpResponseMessage.Result.Content.ReadAsStringAsync(); + taskString.Wait(); + NginxFileSystem[]? nginxFileSystems = JsonSerializer.Deserialize(taskString.Result, NginxFileSystemCollectionSourceGenerationContext.Default.NginxFileSystemArray); + if (nginxFileSystems is null) + results = null; + else + { + results = []; + NginxFileSystem nginxFileSystem; + ReadOnlyCollection? directory; + for (int i = 0; i < nginxFileSystems.Length; i++) + { + nginxFileSystem = NginxFileSystem.Get(format, timeZoneInfo, uri, nginxFileSystems[i]); + if (nginxFileSystem.Type == "file") + results.Add(nginxFileSystem); + else + { + directory = GetRecursiveCollection(httpClient, host, $"{page}/{nginxFileSystem.Name}"); + if (directory is null) + continue; + results.AddRange(directory); + } + } + } + } + return results?.AsReadOnly(); + } + + private static void VerifyParallelFor(AppSettings appSettings, IWindows windows, HttpClient httpClient, NginxFileSystem nginxFileSystem, List messages) + { + windows.Tick(); + if (nginxFileSystem.URI is null) + return; + if (!nginxFileSystem.Name.EndsWith(".jpg")) + return; + DeterministicHashCode deterministicHashCode = windows.GetDeterministicHashCode(httpClient, nginxFileSystem.URI); + if (deterministicHashCode.Id is null) + { + messages.Add($"{nginxFileSystem.URI.OriginalString}"); + return; + } + string paddedId = IId.GetPaddedId(resultSettings: appSettings.ResultSettings, + metadataSettings: appSettings.MetadataSettings, + id: deterministicHashCode.Id.Value, + hasIgnoreKeyword: null, + hasDateTimeOriginal: null, + index: null); + if (!nginxFileSystem.Name.StartsWith(paddedId)) + messages.Add($"!{nginxFileSystem.Name}.StartsWith({paddedId})"); + } + + private static void Verify(ILogger? logger, AppSettings appSettings, IWindows windows, string host, string page) + { + List messages = []; + HttpClient httpClient = new(); + int appSettingsMaxDegreeOfParallelism = appSettings.WindowsSettings.MaxDegreeOfParallelism; + ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = appSettingsMaxDegreeOfParallelism }; + ReadOnlyCollection collection = GetRecursiveCollection(httpClient, host, page) ?? throw new Exception(); + windows.ConstructProgressBar(collection.Count, nameof(Verify)); + _ = Parallel.For(0, collection.Count, parallelOptions, (i, state) => + VerifyParallelFor(appSettings, windows, httpClient, collection[i], messages)); + httpClient.Dispose(); + foreach (string message in messages) + logger?.LogWarning("{message}", message); + } + + private static ReadOnlyCollection GetRecursiveCollection(string host, string page) + { + ReadOnlyCollection results; + HttpClient httpClient = new(); + results = GetRecursiveCollection(httpClient, host, page) ?? throw new Exception(); + httpClient.Dispose(); + return results; + } + + private List GetCollection(ILogger? logger, AppSettings appSettings, IWindows windows, IEnumerable files, A_Metadata metadata) + { + List results = []; + int index = -1; + FilePath filePath; + FirstPass firstPass; + string directoryName; + ExifDirectory exifDirectory; + List sidecarFiles; + DeterministicHashCode deterministicHashCode; + bool fastForwardMovingPictureExpertsGroupUsed; + MinimumYearAndPathCombined minimumYearAndPathCombined; + FilePath? fastForwardMovingPictureExpertsGroupFilePath; + ReadOnlyDictionary> keyValuePairs; + ReadOnlyCollection? fastForwardMovingPictureExpertsGroupFiles; + HttpClient? httpClient = string.IsNullOrEmpty(appSettings.WindowsSettings.Host) || string.IsNullOrEmpty(appSettings.WindowsSettings.Page) ? null : new(); + if (string.IsNullOrEmpty(appSettings.WindowsSettings.Host) || string.IsNullOrEmpty(appSettings.WindowsSettings.Page)) + keyValuePairs = IMetadata.GetKeyValuePairs(files); + else + { + ReadOnlyCollection collection = GetRecursiveCollection(appSettings.WindowsSettings.Host, appSettings.WindowsSettings.Page); + keyValuePairs = IMetadata.GetKeyValuePairs(collection); + } + foreach (KeyValuePair> keyValuePair in keyValuePairs) + { + index += 1; + windows.Tick(); + if (keyValuePair.Value.Count > 2) + throw new NotSupportedException("Too many sidecar files!"); + foreach (FileHolder fileHolder in keyValuePair.Value) + { + if (appSettings.WindowsSettings.SidecarExtensions.Contains(fileHolder.ExtensionLowered)) + continue; + if (appSettings.WindowsSettings.IgnoreExtensions.Contains(fileHolder.ExtensionLowered)) + continue; + filePath = FilePath.Get(appSettings.ResultSettings, appSettings.MetadataSettings, fileHolder, index); + if (filePath.Id is not null && (filePath.IsIntelligentIdFormat || filePath.SortOrder is not null)) + continue; + if (filePath.Id is not null) + { + fastForwardMovingPictureExpertsGroupFiles = null; + deterministicHashCode = new(null, filePath.Id, null); + directoryName = Path.GetFileName(filePath.DirectoryFullPath); + if (directoryName.EndsWith(filePath.Id.Value.ToString())) + continue; + } + else + { + fastForwardMovingPictureExpertsGroupFiles = windows.ConvertAndGetFastForwardMovingPictureExpertsGroupFiles(appSettings.WindowsSettings, httpClient, filePath); + fastForwardMovingPictureExpertsGroupFilePath = fastForwardMovingPictureExpertsGroupFiles.Count == 0 ? null : FilePath.Get(appSettings.ResultSettings, appSettings.MetadataSettings, FileHolder.Get(fastForwardMovingPictureExpertsGroupFiles[0]), index); + deterministicHashCode = fastForwardMovingPictureExpertsGroupFilePath is null ? windows.GetDeterministicHashCode(httpClient, filePath) : windows.GetDeterministicHashCode(httpClient, fastForwardMovingPictureExpertsGroupFilePath); + } + sidecarFiles = []; + filePath = FilePath.Get(filePath, deterministicHashCode); + for (int i = 0; i < keyValuePair.Value.Count; i++) + { + if (keyValuePair.Value[i].ExtensionLowered == fileHolder.ExtensionLowered) + continue; + sidecarFiles.Add(keyValuePair.Value[i]); + } + try + { (minimumYearAndPathCombined, exifDirectory) = metadata.GetMetadataCollection(appSettings.ResultSettings, appSettings.MetadataSettings, httpClient, filePath); } + catch (Exception) + { + logger?.LogWarning("<{filePath}>", filePath.FullName); + continue; + } + fastForwardMovingPictureExpertsGroupUsed = fastForwardMovingPictureExpertsGroupFiles is not null && fastForwardMovingPictureExpertsGroupFiles.Count > 0; + if (fastForwardMovingPictureExpertsGroupUsed && fastForwardMovingPictureExpertsGroupFiles is not null) + { + foreach (string fastForwardMovingPictureExpertsGroupFile in fastForwardMovingPictureExpertsGroupFiles) + File.Delete(fastForwardMovingPictureExpertsGroupFile); + } + if (!fastForwardMovingPictureExpertsGroupUsed && appSettings.WindowsSettings.ValidVideoFormatExtensions.Contains(filePath.ExtensionLowered)) + fastForwardMovingPictureExpertsGroupUsed = true; + firstPass = new(exifDirectory, fastForwardMovingPictureExpertsGroupUsed, minimumYearAndPathCombined, sidecarFiles.ToArray()); + results.Add(firstPass); + } + } + return results; + } + + private void WindowsWork(ILogger? logger, AppSettings appSettings, IWindows windows, long ticks) + { + if (appSettings.WindowsSettings.VerifyOnly && !string.IsNullOrEmpty(appSettings.WindowsSettings.Host) && !string.IsNullOrEmpty(appSettings.WindowsSettings.Page)) + Verify(logger, appSettings, windows, appSettings.WindowsSettings.Host, appSettings.WindowsSettings.Page); + else + { + FirstPass firstPass; + List results = []; + string sourceDirectory = Path.GetFullPath(appSettings.ResultSettings.RootDirectory); + if (!Directory.Exists(sourceDirectory)) + _ = Directory.CreateDirectory(sourceDirectory); + logger?.LogInformation("{Ticks} {RootDirectory}", ticks, sourceDirectory); + ReadOnlyCollection files = Directory.GetFiles(sourceDirectory, "*", SearchOption.AllDirectories).ToArray().AsReadOnly(); + if (files.Count > 0) + _ = IPath.DeleteEmptyDirectories(appSettings.ResultSettings.RootDirectory); + A_Metadata metadata = new(appSettings.ResultSettings, appSettings.MetadataSettings); + int appSettingsMaxDegreeOfParallelism = appSettings.WindowsSettings.MaxDegreeOfParallelism; + int filesCount = appSettingsMaxDegreeOfParallelism == 1 ? files.Count : 123000; + windows.ConstructProgressBar(filesCount, "EnumerateFiles load"); + if (appSettingsMaxDegreeOfParallelism == 1) + results.AddRange(GetCollection(logger, appSettings, windows, files, metadata)); + else + { + List distinct = []; + List metadataGroups = []; + ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = appSettingsMaxDegreeOfParallelism }; + files.AsParallel().ForAll(IMetadata.SetExifDirectoryCollection(windows, appSettings.ResultSettings, appSettings.MetadataSettings, appSettings.WindowsSettings, metadata, distinct, metadataGroups)); + if (_ProgressBar?.CurrentTick != results.Count) + throw new NotSupportedException(); + foreach (MetadataGroup metadataGroup in metadataGroups) + { + if (metadataGroup.FastForwardMovingPictureExpertsGroupUsed || !appSettings.WindowsSettings.ValidVideoFormatExtensions.Contains(metadataGroup.FilePath.ExtensionLowered)) + firstPass = new(metadataGroup.ExifDirectory, metadataGroup.FastForwardMovingPictureExpertsGroupUsed, metadataGroup.MinimumYearAndPathCombined, metadataGroup.SidecarFiles.ToArray()); + else + firstPass = new(metadataGroup.ExifDirectory, FastForwardMovingPictureExpertsGroupUsed: true, metadataGroup.MinimumYearAndPathCombined, metadataGroup.SidecarFiles.ToArray()); + results.Add(firstPass); + } + } + string json = JsonSerializer.Serialize(results, FirstPassCollectionSourceGenerationContext.Default.ListFirstPass); + File.WriteAllText(Path.Combine(sourceDirectory, $"{ticks}.json"), json); + } + } + +} \ No newline at end of file