diff --git a/.editorconfig b/.editorconfig index 3dfc1d0..a76e8b3 100644 --- a/.editorconfig +++ b/.editorconfig @@ -82,28 +82,50 @@ csharp_style_var_elsewhere = false:warning csharp_style_var_for_built_in_types = false:warning csharp_style_var_when_type_is_apparent = false:warning csharp_using_directive_placement = outside_namespace +dotnet_analyzer_diagnostic.category-Design.severity = error +dotnet_analyzer_diagnostic.category-Documentation.severity = error +dotnet_analyzer_diagnostic.category-Globalization.severity = none +dotnet_analyzer_diagnostic.category-Interoperability.severity = error +dotnet_analyzer_diagnostic.category-Maintainability.severity = error +dotnet_analyzer_diagnostic.category-Naming.severity = none +dotnet_analyzer_diagnostic.category-Performance.severity = none +dotnet_analyzer_diagnostic.category-Reliability.severity = error +dotnet_analyzer_diagnostic.category-Security.severity = error +dotnet_analyzer_diagnostic.category-SingleFile.severity = error +dotnet_analyzer_diagnostic.category-Style.severity = error +dotnet_analyzer_diagnostic.category-Usage.severity = error dotnet_code_quality_unused_parameters = all dotnet_code_quality_unused_parameters = non_public # IDE0060: Remove unused parameter dotnet_code_quality.CAXXXX.api_surface = private, internal +dotnet_diagnostic.CA1001.severity = error # CA1001: Types that own disposable fields should be disposable +dotnet_diagnostic.CA1051.severity = error # CA1051: Do not declare visible instance fields dotnet_diagnostic.CA1825.severity = warning # CA1823: Avoid zero-length array allocations dotnet_diagnostic.CA1829.severity = warning # CA1829: Use Length/Count property instead of Count() when available dotnet_diagnostic.CA1834.severity = warning # CA1834: Consider using 'StringBuilder.Append(char)' when applicable dotnet_diagnostic.CA1860.severity = error # CA1860: Prefer comparing 'Count' to 0 rather than using 'Any()', both for clarity and for performance -dotnet_diagnostic.CA1869.severity = none # CA1869: Avoid creating a new 'JsonSerializerOptions' instance for every serialization operation. Cache and reuse instances instead. -dotnet_diagnostic.CA2254.severity = none # CA2254: The logging message template should not vary between calls to 'LoggerExtensions.LogInformation(ILogger, string?, params object?[])' +dotnet_diagnostic.CA1861.severity = error # CA1861: Prefer 'static readonly' fields over constant array arguments +dotnet_diagnostic.CA1869.severity = error # CA1869: Avoid creating a new 'JsonSerializerOptions' instance for every serialization operation. Cache and reuse instances instead. +dotnet_diagnostic.CA2201.severity = none # CA2201: Exception type System.NullReferenceException is reserved by the runtime +dotnet_diagnostic.CA2254.severity = error # CA2254: The logging message template should not vary between calls to 'LoggerExtensions.LogInformation(ILogger, string?, params object?[])' dotnet_diagnostic.IDE0001.severity = warning # IDE0001: Simplify name dotnet_diagnostic.IDE0002.severity = warning # Simplify (member access) - System.Version.Equals("1", "2"); Version.Equals("1", "2"); dotnet_diagnostic.IDE0004.severity = warning # IDE0004: Cast is redundant. dotnet_diagnostic.IDE0005.severity = warning # Using directive is unnecessary +dotnet_diagnostic.IDE0010.severity = error # Add missing cases to switch statement (IDE0010) dotnet_diagnostic.IDE0028.severity = error # IDE0028: Collection initialization can be simplified dotnet_diagnostic.IDE0031.severity = warning # Use null propagation (IDE0031) dotnet_diagnostic.IDE0047.severity = warning # IDE0047: Parentheses can be removed +dotnet_diagnostic.IDE0048.severity = error # Parentheses preferences (IDE0047 and IDE0048) dotnet_diagnostic.IDE0049.severity = warning # Use language keywords instead of framework type names for type references (IDE0049) +dotnet_diagnostic.IDE0051.severity = error # Private member '' is unused [, ] dotnet_diagnostic.IDE0060.severity = warning # IDE0060: Remove unused parameter -dotnet_diagnostic.IDE0290.severity = none # Use primary constructor [Distance]csharp(IDE0290) +dotnet_diagnostic.IDE0130.severity = error # Namespace does not match folder structure (IDE0130) +dotnet_diagnostic.IDE0230.severity = warning # IDE0230: Use UTF-8 string literal +dotnet_diagnostic.IDE0290.severity = error # Use primary constructor [Distance]csharp(IDE0290) dotnet_diagnostic.IDE0300.severity = error # IDE0300: Collection initialization can be simplified dotnet_diagnostic.IDE0301.severity = error #IDE0301: Collection initialization can be simplified dotnet_diagnostic.IDE0305.severity = none # IDE0305: Collection initialization can be simplified +dotnet_diagnostic.JSON002.severity = warning # JSON002: Probable JSON string detected dotnet_naming_rule.abstract_method_should_be_pascal_case.severity = warning dotnet_naming_rule.abstract_method_should_be_pascal_case.style = pascal_case dotnet_naming_rule.abstract_method_should_be_pascal_case.symbols = abstract_method diff --git a/Metadata/Models/A_Metadata.cs b/Metadata/Models/A_Metadata.cs index 6199941..5475109 100644 --- a/Metadata/Models/A_Metadata.cs +++ b/Metadata/Models/A_Metadata.cs @@ -2,7 +2,6 @@ using System.Collections.ObjectModel; using System.Text.Json; 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.Methods; namespace View_by_Distance.Metadata.Models; @@ -132,42 +131,4 @@ public class A_Metadata return (fileInfo, result); } - public static Action SetExifDirectoryCollection(IRename rename, IRenameConfiguration renameConfiguration, A_Metadata metadata, List<(FilePath, FileInfo, ExifDirectory)> exifDirectories) - { - return file => - { - rename.Tick(); - FileInfo fileInfo; - FilePath? ffmpegFilePath; - ExifDirectory exifDirectory; - FileHolder fileHolder = new(file); - ReadOnlyCollection? ffmpegFiles; - DeterministicHashCode deterministicHashCode; - FilePath filePath = FilePath.Get(renameConfiguration.MetadataConfiguration, fileHolder, index: null); - if (!renameConfiguration.SkipIdFiles || filePath.Id is null || !filePath.IsIntelligentIdFormat && filePath.SortOrder is not null) - { - if (filePath.Id is not null) - { - ffmpegFiles = null; - deterministicHashCode = new(null, filePath.Id, null); - } - else - { - ffmpegFiles = rename.ConvertAndGetFfmpegFiles(renameConfiguration, filePath); - ffmpegFilePath = ffmpegFiles.Count == 0 ? null : FilePath.Get(renameConfiguration.MetadataConfiguration, new(ffmpegFiles[0]), index: null); - deterministicHashCode = ffmpegFilePath is null ? rename.GetDeterministicHashCode(filePath) : rename.GetDeterministicHashCode(ffmpegFilePath); - } - (fileInfo, exifDirectory) = metadata.GetMetadataCollection(renameConfiguration.MetadataConfiguration, filePath, deterministicHashCode); - lock (exifDirectories) - exifDirectories.Add(new(filePath, fileInfo, exifDirectory)); - if (ffmpegFiles is not null) - { - foreach (string ffmpegFile in ffmpegFiles) - File.Delete(ffmpegFile); - } - } - - }; - } - } \ No newline at end of file diff --git a/Metadata/Models/Stateless/Get.cs b/Metadata/Models/Stateless/Get.cs new file mode 100644 index 0000000..2a88a63 --- /dev/null +++ b/Metadata/Models/Stateless/Get.cs @@ -0,0 +1,75 @@ +using System.Collections.ObjectModel; +using View_by_Distance.Shared.Models; +using View_by_Distance.Shared.Models.Properties; +using View_by_Distance.Shared.Models.Stateless.Methods; + +namespace View_by_Distance.Metadata.Models.Stateless.Methods; + +internal static class Get +{ + + internal static ReadOnlyDictionary> GetKeyValuePairs(IEnumerable files) + { + Dictionary> results = []; + string key; + FileHolder fileHolder; + List? fileHolders; + foreach (string file in files) + { + fileHolder = FileHolder.Get(file); + if (fileHolder.DirectoryName is null) + continue; + key = $"{Path.Combine(fileHolder.DirectoryName, fileHolder.NameWithoutExtension)}"; + if (!results.TryGetValue(key, out fileHolders)) + { + results.Add(key, []); + if (!results.TryGetValue(key, out fileHolders)) + throw new NotImplementedException(); + } + fileHolders.Add(fileHolder); + } + return new(results); + } + + internal static Action SetExifDirectoryCollection(IRename rename, IRenameConfiguration renameConfiguration, A_Metadata metadata, List distinct, List<(FilePath, FileInfo, ExifDirectory, ReadOnlyCollection)> collection) + { + return file => + { + rename.Tick(); + FileInfo fileInfo; + FilePath? ffmpegFilePath; + ExifDirectory exifDirectory; + ReadOnlyCollection? ffmpegFiles; + DeterministicHashCode deterministicHashCode; + FileHolder fileHolder = FileHolder.Get(file); + FilePath filePath = FilePath.Get(renameConfiguration.MetadataConfiguration, fileHolder, index: null); + string key = $"{Path.Combine(fileHolder.DirectoryName ?? throw new NotSupportedException(), fileHolder.NameWithoutExtension)}"; + if (distinct.Contains(key)) + throw new NotSupportedException("Turn off parallelism when sidecar files are present!"); + if (!renameConfiguration.SkipIdFiles || filePath.Id is null || !filePath.IsIntelligentIdFormat && filePath.SortOrder is not null) + { + if (filePath.Id is not null) + { + ffmpegFiles = null; + deterministicHashCode = new(null, filePath.Id, null); + } + else + { + ffmpegFiles = rename.ConvertAndGetFfmpegFiles(renameConfiguration, filePath); + ffmpegFilePath = ffmpegFiles.Count == 0 ? null : FilePath.Get(renameConfiguration.MetadataConfiguration, FileHolder.Get(ffmpegFiles[0]), index: null); + deterministicHashCode = ffmpegFilePath is null ? rename.GetDeterministicHashCode(filePath) : rename.GetDeterministicHashCode(ffmpegFilePath); + } + (fileInfo, exifDirectory) = metadata.GetMetadataCollection(renameConfiguration.MetadataConfiguration, filePath, deterministicHashCode); + lock (collection) + collection.Add(new(filePath, fileInfo, exifDirectory, new([]))); + if (ffmpegFiles is not null) + { + foreach (string ffmpegFile in ffmpegFiles) + File.Delete(ffmpegFile); + } + } + + }; + } + +} \ No newline at end of file diff --git a/Metadata/Models/Stateless/Methods/IMetadata.cs b/Metadata/Models/Stateless/Methods/IMetadata.cs index d6e2a08..45f40c8 100644 --- a/Metadata/Models/Stateless/Methods/IMetadata.cs +++ b/Metadata/Models/Stateless/Methods/IMetadata.cs @@ -1,6 +1,8 @@ using MetadataExtractor; using System.Collections.ObjectModel; using View_by_Distance.Shared.Models; +using View_by_Distance.Shared.Models.Properties; +using View_by_Distance.Shared.Models.Stateless.Methods; namespace View_by_Distance.Metadata.Models.Stateless.Methods; @@ -55,4 +57,14 @@ public interface IMetadata static double? GetDistance(double originLatitude, double originLongitude, double destinationLatitude, double destinationLongitude, int decimalPlaces = 1, DistanceUnit distanceUnit = DistanceUnit.Miles) => GPS.GetDistance(originLatitude, originLongitude, destinationLatitude, destinationLongitude, decimalPlaces, distanceUnit); + Action TestStatic_SetExifDirectoryCollection(IRename rename, IRenameConfiguration renameConfiguration, A_Metadata metadata, List distinct, List<(FilePath, FileInfo, ExifDirectory, ReadOnlyCollection)> collection) => + SetExifDirectoryCollection(rename, renameConfiguration, metadata, distinct, collection); + static Action SetExifDirectoryCollection(IRename rename, IRenameConfiguration renameConfiguration, A_Metadata metadata, List distinct, List<(FilePath, FileInfo, ExifDirectory, ReadOnlyCollection)> collection) => + Get.SetExifDirectoryCollection(rename, renameConfiguration, metadata, distinct, collection); + + ReadOnlyDictionary> TestStatic_GetKeyValuePairs(IEnumerable files) => + GetKeyValuePairs(files); + static ReadOnlyDictionary> GetKeyValuePairs(IEnumerable files) => + Get.GetKeyValuePairs(files); + } \ No newline at end of file diff --git a/Rename/Models/Binder/RenameConfiguration.cs b/Rename/Models/Binder/RenameConfiguration.cs index 3868068..c972136 100644 --- a/Rename/Models/Binder/RenameConfiguration.cs +++ b/Rename/Models/Binder/RenameConfiguration.cs @@ -11,6 +11,7 @@ public class RenameConfiguration public bool? ForceNewId { get; set; } public string[]? IgnoreExtensions { get; set; } public string? RelativePropertyCollectionFile { get; set; } + public string[]? SidecarExtensions { get; set; } public bool? SkipIdFiles { get; set; } public string[]? ValidImageFormatExtensions { get; set; } public string[]? ValidVideoFormatExtensions { get; set; } @@ -53,6 +54,7 @@ public class RenameConfiguration if (configuration.ForceNewId is null) throw new NullReferenceException(nameof(configuration.ForceNewId)); if (configuration.IgnoreExtensions is null) throw new NullReferenceException(nameof(configuration.IgnoreExtensions)); if (configuration.RelativePropertyCollectionFile is null) throw new NullReferenceException(nameof(configuration.RelativePropertyCollectionFile)); + if (configuration.SidecarExtensions is null) throw new NullReferenceException(nameof(configuration.SidecarExtensions)); if (configuration.SkipIdFiles is null) throw new NullReferenceException(nameof(configuration.SkipIdFiles)); if (configuration.ValidImageFormatExtensions is null) throw new NullReferenceException(nameof(configuration.ValidImageFormatExtensions)); if (configuration.ValidVideoFormatExtensions is null) throw new NullReferenceException(nameof(configuration.ValidVideoFormatExtensions)); @@ -62,6 +64,7 @@ public class RenameConfiguration configuration.ForceNewId.Value, configuration.IgnoreExtensions, configuration.RelativePropertyCollectionFile, + configuration.SidecarExtensions, configuration.SkipIdFiles.Value, configuration.ValidImageFormatExtensions, configuration.ValidVideoFormatExtensions); diff --git a/Rename/Models/Identifier.cs b/Rename/Models/Identifier.cs index a044e42..69941f9 100644 --- a/Rename/Models/Identifier.cs +++ b/Rename/Models/Identifier.cs @@ -3,7 +3,7 @@ using System.Text.Json.Serialization; namespace View_by_Distance.Rename.Models; -internal record Identifier(int Id, string PaddedId) +internal sealed record Identifier(int Id, string PaddedId) { public override string ToString() diff --git a/Rename/Models/RenameConfiguration.cs b/Rename/Models/RenameConfiguration.cs index 16d15b7..5ddc92c 100644 --- a/Rename/Models/RenameConfiguration.cs +++ b/Rename/Models/RenameConfiguration.cs @@ -9,6 +9,7 @@ public record RenameConfiguration(Shared.Models.MetadataConfiguration MetadataCo bool ForceNewId, string[] IgnoreExtensions, string RelativePropertyCollectionFile, + string[] SidecarExtensions, bool SkipIdFiles, string[] ValidImageFormatExtensions, string[] ValidVideoFormatExtensions) : Shared.Models.Properties.IRenameConfiguration diff --git a/Rename/Rename.cs b/Rename/Rename.cs index 142726e..26511cb 100644 --- a/Rename/Rename.cs +++ b/Rename/Rename.cs @@ -17,11 +17,14 @@ using View_by_Distance.Shared.Models.Stateless.Methods; namespace View_by_Distance.Rename; -public partial class Rename : IRename +public partial class Rename : IRename, IDisposable { - private record ToDo(string? Directory, FilePath FilePath, string File, bool JsonFile); - private record Record(DateTime DateTime, ExifDirectory ExifDirectory, FilePath FilePath, bool HasDateTimeOriginal, bool HasIgnoreKeyword, string JsonFile); + private sealed record ToDo(string? Directory, FilePath FilePath, string File, bool JsonFile); + + private sealed record RecordA(ExifDirectory ExifDirectory, FileInfo FileInfo, FilePath FilePath, ReadOnlyCollection SidecarFiles); + + private sealed record RecordB(DateTime DateTime, ExifDirectory ExifDirectory, FilePath FilePath, ReadOnlyCollection SidecarFiles, bool HasDateTimeOriginal, bool HasIgnoreKeyword, string JsonFile); private ProgressBar? _ProgressBar; @@ -44,6 +47,12 @@ public partial class Rename : IRename void IRename.Tick() => _ProgressBar?.Tick(); + void IDisposable.Dispose() + { + _ProgressBar?.Dispose(); + GC.SuppressFinalize(this); + } + ReadOnlyCollection IRename.ConvertAndGetFfmpegFiles(IRenameConfiguration renameConfiguration, FilePath filePath) { List results = []; @@ -114,10 +123,11 @@ public partial class Rename : IRename private static void RenameUrl(FilePath filePath) { + FileHolder fileHolder; string[] lines = File.ReadAllLines(filePath.FullName); if (lines.Length == 1) { - FileHolder fileHolder = new(lines[0]); + fileHolder = FileHolder.Get(lines[0]); if (fileHolder.Exists) { string checkFile; @@ -130,99 +140,119 @@ public partial class Rename : IRename } } - private static List<(FilePath, FileInfo, ExifDirectory)> GetExifDirectoryCollection(IRename rename, RenameConfiguration renameConfiguration, IEnumerable files, A_Metadata metadata) + private static List GetRecordACollection(IRename rename, RenameConfiguration renameConfiguration, IEnumerable files, A_Metadata metadata) { - List<(FilePath, FileInfo, ExifDirectory)> results = []; + List results = []; int index = -1; FileInfo fileInfo; FilePath filePath; - FileHolder fileHolder; FilePath? ffmpegFilePath; ExifDirectory exifDirectory; + List sidecarFiles; ReadOnlyCollection? ffmpegFiles; DeterministicHashCode deterministicHashCode; - foreach (string file in files) + ReadOnlyDictionary> keyValuePairs = IMetadata.GetKeyValuePairs(files); + foreach (KeyValuePair> keyValuePair in keyValuePairs) { index += 1; rename.Tick(); - fileHolder = new(file); - if (renameConfiguration.IgnoreExtensions.Contains(fileHolder.ExtensionLowered)) - continue; - filePath = FilePath.Get(renameConfiguration.MetadataConfiguration, fileHolder, index); - if (filePath.ExtensionLowered == ".url" && filePath.Id is not null) + if (keyValuePair.Value.Count > 1 && !renameConfiguration.ForceNewId) + throw new NotSupportedException($"When sidecar files are present {nameof(renameConfiguration.ForceNewId)} must be true!"); + if (keyValuePair.Value.Count > 2) + throw new NotSupportedException("Too many sidecar files!"); + foreach (FileHolder fileHolder in keyValuePair.Value) { - RenameUrl(filePath); - continue; - } - if (renameConfiguration.SkipIdFiles && filePath.Id is not null && (filePath.IsIntelligentIdFormat || filePath.SortOrder is not null)) - continue; - if (!renameConfiguration.ForceNewId && filePath.Id is not null) - { - ffmpegFiles = null; - deterministicHashCode = new(null, filePath.Id, null); - } - else - { - ffmpegFiles = rename.ConvertAndGetFfmpegFiles(renameConfiguration, filePath); - ffmpegFilePath = ffmpegFiles.Count == 0 ? null : FilePath.Get(renameConfiguration.MetadataConfiguration, new(ffmpegFiles[0]), index); - deterministicHashCode = ffmpegFilePath is null ? rename.GetDeterministicHashCode(filePath) : rename.GetDeterministicHashCode(ffmpegFilePath); - } - (fileInfo, exifDirectory) = metadata.GetMetadataCollection(renameConfiguration.MetadataConfiguration, filePath, deterministicHashCode); - results.Add(new(filePath, fileInfo, exifDirectory)); - if (ffmpegFiles is not null) - { - foreach (string ffmpegFile in ffmpegFiles) - File.Delete(ffmpegFile); + if (renameConfiguration.SidecarExtensions.Contains(fileHolder.ExtensionLowered)) + continue; + if (renameConfiguration.IgnoreExtensions.Contains(fileHolder.ExtensionLowered)) + continue; + filePath = FilePath.Get(renameConfiguration.MetadataConfiguration, fileHolder, index); + if (filePath.ExtensionLowered == ".url" && filePath.Id is not null) + { + RenameUrl(filePath); + continue; + } + if (renameConfiguration.SkipIdFiles && filePath.Id is not null && (filePath.IsIntelligentIdFormat || filePath.SortOrder is not null)) + continue; + if (!renameConfiguration.ForceNewId && filePath.Id is not null) + { + ffmpegFiles = null; + deterministicHashCode = new(null, filePath.Id, null); + } + else + { + ffmpegFiles = rename.ConvertAndGetFfmpegFiles(renameConfiguration, filePath); + ffmpegFilePath = ffmpegFiles.Count == 0 ? null : FilePath.Get(renameConfiguration.MetadataConfiguration, FileHolder.Get(ffmpegFiles[0]), index); + deterministicHashCode = ffmpegFilePath is null ? rename.GetDeterministicHashCode(filePath) : rename.GetDeterministicHashCode(ffmpegFilePath); + } + sidecarFiles = []; + for (int i = 0; i < keyValuePair.Value.Count; i++) + { + if (keyValuePair.Value[i].ExtensionLowered == fileHolder.ExtensionLowered) + continue; + sidecarFiles.Add(keyValuePair.Value[i]); + } + (fileInfo, exifDirectory) = metadata.GetMetadataCollection(renameConfiguration.MetadataConfiguration, filePath, deterministicHashCode); + results.Add(new(exifDirectory, fileInfo, filePath, new(sidecarFiles))); + if (ffmpegFiles is not null) + { + foreach (string ffmpegFile in ffmpegFiles) + File.Delete(ffmpegFile); + } } } return results; } - private static ReadOnlyCollection GetExifDirectoryCollection(MetadataConfiguration metadataConfiguration, List<(FilePath, FileInfo, ExifDirectory)> exifDirectories) + private static ReadOnlyCollection GetRecordBCollection(MetadataConfiguration metadataConfiguration, List recordACollection) { - List results = []; + List results = []; DateTime? dateTime; bool hasIgnoreKeyword; bool hasDateTimeOriginal; ReadOnlyCollection keywords; - foreach ((FilePath filePath, FileInfo fileInfo, ExifDirectory exifDirectory) in exifDirectories) + foreach (RecordA recordA in recordACollection) { - dateTime = IDate.GetDateTimeOriginal(exifDirectory); + dateTime = IDate.GetDateTimeOriginal(recordA.ExifDirectory); hasDateTimeOriginal = dateTime is not null; - dateTime ??= IDate.GetMinimum(exifDirectory); - keywords = IMetadata.GetKeywords(exifDirectory); + dateTime ??= IDate.GetMinimum(recordA.ExifDirectory); + keywords = IMetadata.GetKeywords(recordA.ExifDirectory); hasIgnoreKeyword = metadataConfiguration.IgnoreRulesKeyWords.Any(l => keywords.Contains(l)); - results.Add(new(dateTime.Value, exifDirectory, filePath, hasDateTimeOriginal, hasIgnoreKeyword, fileInfo.FullName)); + results.Add(new(dateTime.Value, recordA.ExifDirectory, recordA.FilePath, recordA.SidecarFiles, hasDateTimeOriginal, hasIgnoreKeyword, recordA.FileInfo.FullName)); } return new(results); } - private ReadOnlyCollection GetExifDirectoryCollection(IRename rename, AppSettings appSettings, RenameConfiguration renameConfiguration, DirectoryInfo directoryInfo) + private ReadOnlyCollection GetRecordBCollection(IRename rename, AppSettings appSettings, RenameConfiguration renameConfiguration, DirectoryInfo directoryInfo) { - ReadOnlyCollection results; - List<(FilePath, FileInfo, ExifDirectory)> exifDirectories = []; + ReadOnlyCollection results; + List recordACollection = []; A_Metadata metadata = new(renameConfiguration.MetadataConfiguration); int appSettingsMaxDegreeOfParallelism = appSettings.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) - exifDirectories.AddRange(GetExifDirectoryCollection(rename, renameConfiguration, files, metadata)); + recordACollection.AddRange(GetRecordACollection(rename, renameConfiguration, files, metadata)); else { + List distinct = []; + List<(FilePath, FileInfo, ExifDirectory, ReadOnlyCollection)> collection = []; ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = appSettingsMaxDegreeOfParallelism }; - files.AsParallel().ForAll(A_Metadata.SetExifDirectoryCollection(rename, renameConfiguration, metadata, exifDirectories)); - if (_ProgressBar.CurrentTick != exifDirectories.Count) + files.AsParallel().ForAll(IMetadata.SetExifDirectoryCollection(rename, renameConfiguration, metadata, distinct, collection)); + if (_ProgressBar.CurrentTick != recordACollection.Count) throw new NotSupportedException(); + foreach ((FilePath filePath, FileInfo fileInfo, ExifDirectory exifDirectory, ReadOnlyCollection sidecarFiles) in collection) + recordACollection.Add(new(exifDirectory, fileInfo, filePath, sidecarFiles)); } _ProgressBar.Dispose(); - results = GetExifDirectoryCollection(renameConfiguration.MetadataConfiguration, exifDirectories); + results = GetRecordBCollection(renameConfiguration.MetadataConfiguration, recordACollection); return results; } - private static void VerifyIntMinValueLength(MetadataConfiguration metadataConfiguration, ReadOnlyCollection exifDirectories) + private static void VerifyIntMinValueLength(MetadataConfiguration metadataConfiguration, ReadOnlyCollection recordBCollection) { - foreach ((DateTime _, ExifDirectory exifDirectory, FilePath _, bool _, bool _, string _) in exifDirectories) + foreach ((DateTime _, ExifDirectory exifDirectory, FilePath _, ReadOnlyCollection _, bool _, bool _, string _) in recordBCollection) { if (exifDirectory.Id is null) continue; @@ -231,7 +261,7 @@ public partial class Rename : IRename } } - private static string? GetCheckDirectory(RenameConfiguration renameConfiguration, Record record, FilePath filePath, ReadOnlyCollection ids, bool multipleDirectoriesWithFiles) + private static string? GetCheckDirectory(RenameConfiguration renameConfiguration, RecordB record, FilePath filePath, ReadOnlyCollection ids, bool multipleDirectoriesWithFiles) { string? result; if (filePath.DirectoryName is null) @@ -252,10 +282,37 @@ public partial class Rename : IRename return result; } - private static ReadOnlyCollection GetToDoCollection(RenameConfiguration renameConfiguration, Identifier[]? identifiers, ReadOnlyCollection records) + private static List GetSidecarFiles(MetadataConfiguration metadataConfiguration, RecordB record, List distinct, string checkDirectory, string paddedId) { List results = []; - Record record; + string checkFile; + FilePath filePath; + string checkFileExtension; + foreach (FileHolder fileHolder in record.SidecarFiles) + { + checkFileExtension = fileHolder.ExtensionLowered; + filePath = FilePath.Get(metadataConfiguration, fileHolder, index: null); + checkFile = Path.Combine(checkDirectory, $"{paddedId}{checkFileExtension}"); + if (checkFile == filePath.FullName) + continue; + if (File.Exists(checkFile)) + { + checkFile = string.Concat(checkFile, ".del"); + if (File.Exists(checkFile)) + continue; + } + if (distinct.Contains(checkFile)) + continue; + distinct.Add(checkFile); + results.Add(new(checkDirectory, filePath, checkFile, JsonFile: false)); + } + return results; + } + + private static ReadOnlyCollection GetToDoCollection(RenameConfiguration renameConfiguration, Identifier[]? identifiers, ReadOnlyCollection recordBCollection) + { + List results = []; + RecordB record; string jsonFile; string paddedId; string checkFile; @@ -265,11 +322,11 @@ public partial class Rename : IRename string? checkDirectory; const string jpg = ".jpg"; string checkFileExtension; - bool multipleDirectoriesWithFiles; List distinct = []; bool? directoryCheck = null; const string jpeg = ".jpeg"; string jsonFileSubDirectory; + bool multipleDirectoriesWithFiles; foreach (string directory in Directory.GetDirectories(renameConfiguration.MetadataConfiguration.ResultConfiguration.RootDirectory, "*", SearchOption.TopDirectoryOnly)) { foreach (string _ in Directory.EnumerateFiles(directory, "*", SearchOption.AllDirectories)) @@ -283,14 +340,14 @@ public partial class Rename : IRename if (directoryCheck is not null && directoryCheck.Value) break; } - VerifyIntMinValueLength(renameConfiguration.MetadataConfiguration, records); + VerifyIntMinValueLength(renameConfiguration.MetadataConfiguration, recordBCollection); multipleDirectoriesWithFiles = directoryCheck is not null && directoryCheck.Value; - ReadOnlyCollection collection = new((from l in records orderby l.DateTime select l).ToArray()); ResultConfiguration resultConfiguration = renameConfiguration.MetadataConfiguration.ResultConfiguration; ReadOnlyCollection ids = identifiers is null ? new([]) : new((from l in identifiers select l.Id).ToArray()); - for (int i = 0; i < collection.Count; i++) + ReadOnlyCollection sorted = new((from l in recordBCollection orderby l.DateTime select l).ToArray()); + for (int i = 0; i < sorted.Count; i++) { - record = collection[i]; + record = sorted[i]; if (record.ExifDirectory.Id is null) continue; if (record.FilePath.DirectoryName is null) @@ -314,7 +371,7 @@ public partial class Rename : IRename jsonFile = Path.Combine(jsonFileSubDirectory, directoryName, $"{record.ExifDirectory.Id.Value}{checkFileExtension}.json"); if (record.JsonFile != jsonFile) { - fileHolder = new(record.JsonFile); + fileHolder = FileHolder.Get(record.JsonFile); filePath = FilePath.Get(renameConfiguration.MetadataConfiguration, fileHolder, index: null); results.Add(new(null, filePath, jsonFile, JsonFile: true)); } @@ -322,6 +379,9 @@ public partial class Rename : IRename continue; distinct.Add(checkFile); results.Add(new(checkDirectory, record.FilePath, checkFile, JsonFile: false)); + if (record.SidecarFiles.Count == 0) + continue; + results.AddRange(GetSidecarFiles(renameConfiguration.MetadataConfiguration, record, distinct, checkDirectory, paddedId)); } return new(results); } @@ -370,11 +430,11 @@ public partial class Rename : IRename return new(results); } - private static void SaveIdentifiersToDisk(long ticks, RenameConfiguration renameConfiguration, string aMetadataCollectionDirectory, ReadOnlyCollection records) + private static void SaveIdentifiersToDisk(long ticks, RenameConfiguration renameConfiguration, string aMetadataCollectionDirectory, ReadOnlyCollection recordBCollection) { string paddedId; List identifiers = []; - foreach (Record record in records) + foreach (RecordB record in recordBCollection) { if (record.ExifDirectory.Id is null) continue; @@ -395,9 +455,9 @@ public partial class Rename : IRename throw new Exception($"Invalid {nameof(renameConfiguration.RelativePropertyCollectionFile)}"); DirectoryInfo directoryInfo = new(Path.GetFullPath(renameConfiguration.MetadataConfiguration.ResultConfiguration.RootDirectory)); logger?.LogInformation("{Ticks} {RootDirectory}", ticks, directoryInfo.FullName); - ReadOnlyCollection records = GetExifDirectoryCollection(rename, appSettings, renameConfiguration, directoryInfo); - SaveIdentifiersToDisk(ticks, renameConfiguration, aMetadataCollectionDirectory, records); - ReadOnlyCollection toDoCollection = GetToDoCollection(renameConfiguration, identifiers, records); + ReadOnlyCollection recordBCollection = GetRecordBCollection(rename, appSettings, renameConfiguration, directoryInfo); + SaveIdentifiersToDisk(ticks, renameConfiguration, aMetadataCollectionDirectory, recordBCollection); + ReadOnlyCollection toDoCollection = GetToDoCollection(renameConfiguration, identifiers, recordBCollection); ReadOnlyCollection lines = RenameFilesInDirectories(toDoCollection); if (lines.Count != 0) { diff --git a/Shared/Models/FileHolder.cs b/Shared/Models/FileHolder.cs index d22c2e6..7152f6d 100644 --- a/Shared/Models/FileHolder.cs +++ b/Shared/Models/FileHolder.cs @@ -3,75 +3,97 @@ using System.Text.Json.Serialization; namespace View_by_Distance.Shared.Models; -public class FileHolder +public record FileHolder(DateTime? CreationTime, + string? DirectoryName, + bool Exists, + string ExtensionLowered, + string FullName, + int? Id, + DateTime? LastWriteTime, + long? Length, + string Name, + string NameWithoutExtension) { - protected readonly DateTime? _CreationTime; - protected readonly string? _DirectoryName; - protected readonly bool _Exists; - protected readonly string _ExtensionLowered; - protected readonly string _FullName; - protected readonly int? _Id; - protected readonly DateTime? _LastWriteTime; - protected readonly long? _Length; - protected readonly string _Name; - protected readonly string _NameWithoutExtension; - public DateTime? CreationTime => _CreationTime; - public string? DirectoryName => _DirectoryName; - public bool Exists => _Exists; - public string ExtensionLowered => _ExtensionLowered; - public string FullName => _FullName; - public int? Id => _Id; - public DateTime? LastWriteTime => _LastWriteTime; - public long? Length => _Length; - public string Name => _Name; - public string NameWithoutExtension => _NameWithoutExtension; - - public FileHolder(DateTime? creationTime, string? directoryName, bool exists, string extensionLowered, string fullName, int? id, DateTime? lastWriteTime, long? length, string name, string nameWithoutExtension) - { - _CreationTime = creationTime; - _DirectoryName = directoryName; - _Exists = exists; - _ExtensionLowered = extensionLowered; - _FullName = fullName; - _Id = id; - _LastWriteTime = lastWriteTime; - _Length = length; - _Name = name; - _NameWithoutExtension = nameWithoutExtension; - } - - public FileHolder(FileInfo fileInfo, int? id) - { - if (fileInfo.Exists) - { - _CreationTime = fileInfo.CreationTime; - _LastWriteTime = fileInfo.LastWriteTime; - _Length = fileInfo.Length; - } - _DirectoryName = fileInfo.DirectoryName; - _Exists = fileInfo.Exists; - _ExtensionLowered = fileInfo.Extension.ToLower(); - _Id = id; - _FullName = fileInfo.FullName; - _Name = fileInfo.Name; - _NameWithoutExtension = Path.GetFileNameWithoutExtension(fileInfo.FullName); - } - - public FileHolder(string fileName) : - this(new FileInfo(fileName), null) - { } - - public FileHolder(string fileName, int? id) : - this(new FileInfo(fileName), id) - { } - public override string ToString() { string result = JsonSerializer.Serialize(this, FileHolderSourceGenerationContext.Default.FileHolder); return result; } + 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)); + + 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)); + + public static FileHolder Get(FileInfo fileInfo, int? id) => + fileInfo.Exists ? GetExisting(fileInfo, id) : GetNonExisting(fileInfo, id); + + public static FileHolder Get(FilePath filePath, int? id) + { + FileHolder result; + result = new(new(filePath.CreationTicks), + filePath.DirectoryName, + true, + filePath.ExtensionLowered, + filePath.FullName, + id, + new(filePath.LastWriteTicks), + filePath.Length, + filePath.Name, + Path.GetFileNameWithoutExtension(filePath.FullName)); + return result; + } + + private static FileHolder GetExisting(FileHolder fileHolder) => + new(fileHolder.CreationTime, + fileHolder.DirectoryName, + fileHolder.Exists, + fileHolder.ExtensionLowered, + fileHolder.FullName, + Id: null, + fileHolder.LastWriteTime, + fileHolder.Length, + fileHolder.Name, + Path.GetFileNameWithoutExtension(fileHolder.FullName)); + + private static FileHolder GetNonExisting(FileHolder fileHolder) => + new(null, + fileHolder.DirectoryName, + fileHolder.Exists, + fileHolder.ExtensionLowered, + fileHolder.FullName, + Id: null, + null, + null, + fileHolder.Name, + Path.GetFileNameWithoutExtension(fileHolder.FullName)); + + public static FileHolder Get(FileHolder fileHolder) => + fileHolder.Exists ? GetExisting(fileHolder) : GetNonExisting(fileHolder); + + public static FileHolder Get(string file) => + Get(new FileInfo(file), id: null); + } [JsonSourceGenerationOptions(WriteIndented = true)] diff --git a/Shared/Models/FilePath.cs b/Shared/Models/FilePath.cs index cd4646f..284ffe7 100644 --- a/Shared/Models/FilePath.cs +++ b/Shared/Models/FilePath.cs @@ -28,7 +28,7 @@ public record FilePath(long CreationTicks, public static FilePath Get(MetadataConfiguration metadataConfiguration, FileHolder fileHolder, int? index) { if (fileHolder.CreationTime is null) - fileHolder = new(fileHolder.FullName); + fileHolder = FileHolder.Get(fileHolder); if (fileHolder.CreationTime is null) throw new NullReferenceException(nameof(fileHolder.CreationTime)); if (fileHolder.LastWriteTime is null)