diff --git a/Metadata/Models/A_Metadata.cs b/Metadata/Models/A_Metadata.cs index 2e93269..59068e2 100644 --- a/Metadata/Models/A_Metadata.cs +++ b/Metadata/Models/A_Metadata.cs @@ -4,6 +4,7 @@ using View_by_Distance.Metadata.Models.Stateless; using View_by_Distance.Metadata.Models.Stateless.Methods; 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; @@ -29,13 +30,11 @@ public class A_Metadata _FileGroups = Shared.Models.Stateless.Methods.IPath.GetKeyValuePairs(aAConfiguration, bResultsFullGroupDirectory, [aAConfiguration.ResultSingleton]); } - public ExifDirectory GetMetadataCollection(IMetadataConfiguration metadataConfiguration, string file, NameWithoutExtension nameWithoutExtension) + public ExifDirectory GetMetadataCollection(IMetadataConfiguration metadataConfiguration, FilePath filePath, DeterministicHashCode deterministicHashCode) { ExifDirectory? results; - string fileName = Path.GetFileName(file); - string fileExtensionLowered = Path.GetExtension(file).ToLower(); - (_, int directoryIndex) = Shared.Models.Stateless.Methods.IPath.GetDirectoryNameAndIndex(_AAConfiguration.ResultAllInOneSubdirectoryLength, fileName); - FileInfo fileInfo = new(Path.Combine(_FileGroups[_AAConfiguration.ResultSingleton][directoryIndex], $"{nameWithoutExtension.FileNameWithoutExtension}{fileExtensionLowered}.json")); + (_, int directoryIndex) = Shared.Models.Stateless.Methods.IPath.GetDirectoryNameAndIndex(_AAConfiguration.ResultAllInOneSubdirectoryLength, filePath.Name); + FileInfo fileInfo = new(Path.Combine(_FileGroups[_AAConfiguration.ResultSingleton][directoryIndex], $"{filePath.NameWithoutExtension}{filePath.ExtensionLowered}.json")); if (_ForceMetadataLastWriteTimeToCreationTime && !fileInfo.Exists && File.Exists(Path.ChangeExtension(fileInfo.FullName, ".delete"))) { File.Move(Path.ChangeExtension(fileInfo.FullName, ".delete"), fileInfo.FullName); @@ -70,12 +69,12 @@ public class A_Metadata { System.Drawing.Size? size; try - { size = Dimensions.GetDimensions(file); } + { size = Dimensions.GetDimensions(filePath.FullName); } catch (Exception) { size = null; } - IReadOnlyList directories = ImageMetadataReader.ReadMetadata(file); - results = Exif.Covert(file, fileInfo, size, directories); + IReadOnlyList directories = ImageMetadataReader.ReadMetadata(filePath.FullName); + results = Exif.Covert(filePath, deterministicHashCode, fileInfo, size, directories); string json = JsonSerializer.Serialize(results, ExifDirectorySourceGenerationContext.Default.ExifDirectory); if (Shared.Models.Stateless.Methods.IPath.WriteAllText(fileInfo.FullName, json, updateDateWhenMatches: false, compareBeforeWrite: true, updateToWhenMatches: null) && _ForceMetadataLastWriteTimeToCreationTime) { @@ -86,15 +85,28 @@ public class A_Metadata return results; } - public static Action GetResultCollection(IMetadataConfiguration metadataConfiguration, A_Metadata metadata, List exifDirectories, Action tick) + public static Action GetResultCollection(IRename rename, IMetadataConfiguration metadataConfiguration, A_Metadata metadata, List exifDirectories, Action tick) { return file => { tick.Invoke(); - string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(file); - NameWithoutExtension nameWithoutExtension = Shared.Models.Stateless.Methods.IId.GetNameWithoutExtension(metadataConfiguration, fileNameWithoutExtension); - lock (exifDirectories) - exifDirectories.Add(metadata.GetMetadataCollection(metadataConfiguration, file, nameWithoutExtension)); + FilePath filePath = Shared.Models.Stateless.Methods.IId.GetFilePath(metadataConfiguration, file); + if (filePath.ExtensionLowered is not ".paddedId" and not ".lsv") + { + if (filePath.Id is null || (!filePath.IsIdFormat && !filePath.IsPaddedIdFormat)) + { + string[]? ffmpegFiles = rename.ConvertAndGetFfmpegFiles(filePath); + filePath = ffmpegFiles is null || ffmpegFiles.Length < 0 ? filePath : IId.GetFilePath(filePath, ffmpegFiles[0]); + DeterministicHashCode deterministicHashCode = filePath.Id is not null ? deterministicHashCode = new(null, filePath.Id, null) : deterministicHashCode = rename.GetDeterministicHashCode(filePath); + if (ffmpegFiles is not null) + { + foreach (string ffmpegFile in ffmpegFiles) + File.Delete(ffmpegFile); + } + lock (exifDirectories) + exifDirectories.Add(metadata.GetMetadataCollection(metadataConfiguration, filePath, deterministicHashCode)); + } + } }; } diff --git a/Metadata/Models/Stateless/Exif.cs b/Metadata/Models/Stateless/Exif.cs index edb7c6a..a01855c 100644 --- a/Metadata/Models/Stateless/Exif.cs +++ b/Metadata/Models/Stateless/Exif.cs @@ -322,7 +322,7 @@ internal abstract class Exif return result; } - internal static Shared.Models.ExifDirectory Covert(string file, FileInfo fileInfo, System.Drawing.Size? size, IReadOnlyList directories) + internal static Shared.Models.ExifDirectory Covert(Shared.Models.FilePath filePath, Shared.Models.DeterministicHashCode deterministicHashCode, FileInfo fileInfo, System.Drawing.Size? size, IReadOnlyList directories) { Shared.Models.ExifDirectory results; Shared.Models.AviDirectory aviDirectory = GetAviDirectory(directories); @@ -333,16 +333,17 @@ internal abstract class Exif Shared.Models.ExifDirectoryBase exifDirectoryBase = GetExifDirectoryBase(directories); Shared.Models.GifHeaderDirectory gifHeaderDirectory = GetGifHeaderDirectory(directories); Shared.Models.PhotoshopDirectory photoshopDirectory = GetPhotoshopDirectory(directories); - Shared.Models.FileMetadataDirectory fileMetadataDirectory = GetFileMetadataDirectory(file, directories); + Shared.Models.FileMetadataDirectory fileMetadataDirectory = GetFileMetadataDirectory(filePath.FullName, directories); Shared.Models.QuickTimeMovieHeaderDirectory quickTimeMovieHeaderDirectory = GetQuickTimeMovieHeaderDirectoryDirectory(directories); Shared.Models.QuickTimeTrackHeaderDirectory quickTimeTrackHeaderDirectory = GetQuickTimeTrackHeaderDirectoryDirectory(directories); results = new(aviDirectory, exifDirectoryBase, - file, + filePath.FullName, fileMetadataDirectory, gifHeaderDirectory, gpsDirectory, size?.Height, + deterministicHashCode.Id ?? filePath.Id, fileInfo.FullName, jpegDirectory, photoshopDirectory, diff --git a/Metadata/Models/Stateless/Methods/IMetadata.cs b/Metadata/Models/Stateless/Methods/IMetadata.cs index 3b5894a..c6c4f85 100644 --- a/Metadata/Models/Stateless/Methods/IMetadata.cs +++ b/Metadata/Models/Stateless/Methods/IMetadata.cs @@ -11,11 +11,6 @@ public interface IMetadata Meters } - Shared.Models.ExifDirectory TestStatic_Convert(string file, FileInfo fileInfo, System.Drawing.Size? size, IReadOnlyList directories) => - Convert(file, fileInfo, size, directories); - static Shared.Models.ExifDirectory Convert(string file, FileInfo fileInfo, System.Drawing.Size? size, IReadOnlyList directories) => - Exif.Covert(file, fileInfo, size, directories); - double? TestStatic_GetDistance(double originLatitude, double originLongitude, double destinationLatitude, double destinationLongitude, int decimalPlaces = 1, DistanceUnit distanceUnit = DistanceUnit.Miles) => GetDistance(originLatitude, originLongitude, destinationLatitude, destinationLongitude, decimalPlaces, distanceUnit); static double? GetDistance(double originLatitude, double originLongitude, double destinationLatitude, double destinationLongitude, int decimalPlaces = 1, DistanceUnit distanceUnit = DistanceUnit.Miles) => diff --git a/Rename/Rename.cs b/Rename/Rename.cs index 6f84db2..441c7ae 100644 --- a/Rename/Rename.cs +++ b/Rename/Rename.cs @@ -1,6 +1,10 @@ +using CliWrap; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using ShellProgressBar; +using System.Drawing; +using System.Drawing.Imaging; +using System.Runtime.InteropServices; using View_by_Distance.Metadata.Models; using View_by_Distance.Rename.Models; using View_by_Distance.Shared.Models; @@ -8,7 +12,7 @@ using View_by_Distance.Shared.Models.Stateless.Methods; namespace View_by_Distance.Rename; -public class Rename +public class Rename : IRename { private readonly AppSettings _AppSettings; @@ -94,28 +98,115 @@ public class Rename return result; } + string[]? IRename.ConvertAndGetFfmpegFiles(FilePath filePath) + { + string[]? results; + bool isIgnoreExtension; + bool isValidImageFormatExtension = _MetadataConfiguration.ValidImageFormatExtensions.Contains(filePath.ExtensionLowered); + isIgnoreExtension = isValidImageFormatExtension && _MetadataConfiguration.IgnoreExtensions.Contains(filePath.ExtensionLowered); + if (!isIgnoreExtension && isValidImageFormatExtension) + results = null; + else + { + CommandTask commandTask = Cli.Wrap("ffmpeg.exe") + // .WithArguments(new[] { "-ss", "00:00:00", "-t", "00:00:00", "-i", files[i], "-qscale:v", "2", "-r", "0.01", $"{fileHolder.Name}-%4d.jpg" }) + .WithArguments(new[] { "-i", filePath.FullName, "-vframes", "1", $"{filePath.Name}-%4d.jpg" }) + .WithWorkingDirectory(filePath.DirectoryName) + .ExecuteAsync(); + commandTask.Task.Wait(); + results = Directory.GetFiles(filePath.DirectoryName, $"{filePath.Name}-*.jpg", SearchOption.TopDirectoryOnly); + if (results.Length == 0) + throw new Exception(); + if (!filePath.Name.EndsWith("-0001.jpg")) + throw new Exception(); + isValidImageFormatExtension = _MetadataConfiguration.ValidImageFormatExtensions.Contains(filePath.ExtensionLowered); + isIgnoreExtension = isValidImageFormatExtension && _MetadataConfiguration.IgnoreExtensions.Contains(filePath.ExtensionLowered); + if (isIgnoreExtension || !isValidImageFormatExtension) + throw new Exception(); + if (filePath.DirectoryName is null) + throw new NullReferenceException(nameof(filePath.DirectoryName)); + } + return results; + } + +#pragma warning disable CA1416 + + DeterministicHashCode IRename.GetDeterministicHashCode(FilePath filePath) + { + DeterministicHashCode result; + int? id; + int? width; + int? height; + try + { + using Image image = Image.FromFile(filePath.FullName); + 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; + } + +#pragma warning restore CA1416 + + private void GetResultCollection(IRename rename, List exifDirectories, IEnumerable files, A_Metadata metadata) + { + string[]? ffmpegFiles; + DeterministicHashCode deterministicHashCode; + foreach (string file in files) + { + FilePath filePath = IId.GetFilePath(_MetadataConfiguration, file); + if (filePath.ExtensionLowered is ".paddedId" or ".lsv") + continue; + if (files.Contains($"{filePath.FullName}.paddedId")) + continue; + if (filePath.Id is not null && (filePath.IsIdFormat || filePath.IsPaddedIdFormat)) + continue; + ffmpegFiles = rename.ConvertAndGetFfmpegFiles(filePath); + filePath = ffmpegFiles is null || ffmpegFiles.Length < 0 ? filePath : IId.GetFilePath(filePath, ffmpegFiles[0]); + if (filePath.Id is not null) + deterministicHashCode = new(null, filePath.Id, null); + else + deterministicHashCode = rename.GetDeterministicHashCode(filePath); + if (ffmpegFiles is not null) + { + foreach (string ffmpegFile in ffmpegFiles) + File.Delete(ffmpegFile); + } + exifDirectories.Add(metadata.GetMetadataCollection(_MetadataConfiguration, filePath, deterministicHashCode)); + } + } + private List RenameFilesInDirectories(ILogger? logger, long ticks, DirectoryInfo directoryInfo) { List old = []; + IRename rename = this; List exifDirectories = []; bool runToDoCollectionFirst = GetRunToDoCollectionFirst(ticks, directoryInfo); IEnumerable files = Directory.EnumerateFiles(directoryInfo.FullName, "*", SearchOption.AllDirectories); A_Metadata metadata = new(_MetadataConfiguration, _Configuration.ForceMetadataLastWriteTimeToCreationTime, _Configuration.PropertiesChangedForMetadata); if (runToDoCollectionFirst) - { - foreach (string file in files) - { - NameWithoutExtension nameWithoutExtension = IId.GetNameWithoutExtension(_MetadataConfiguration, file); - exifDirectories.Add(metadata.GetMetadataCollection(_MetadataConfiguration, file, nameWithoutExtension)); - if (nameWithoutExtension.Id is null || (!nameWithoutExtension.IsIdFormat && !nameWithoutExtension.IsPaddedIdFormat)) - ; - } - } + GetResultCollection(rename, exifDirectories, files, metadata); else { ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = _AppSettings.MaxDegreeOfParallelism }; ProgressBar progressBar = new(123000, "EnumerateFiles load", new ProgressBarOptions() { ProgressCharacter = '─', ProgressBarOnBottom = true, DisableBottomPercentage = true }); - files.AsParallel().ForAll(A_Metadata.GetResultCollection(_MetadataConfiguration, metadata, exifDirectories, () => progressBar.Tick())); + files.AsParallel().ForAll(A_Metadata.GetResultCollection(rename, _MetadataConfiguration, metadata, exifDirectories, () => progressBar.Tick())); if (progressBar.CurrentTick != exifDirectories.Count) throw new NotSupportedException(); } diff --git a/Shared/Models/DeterministicHashCode.cs b/Shared/Models/DeterministicHashCode.cs new file mode 100644 index 0000000..5f4da31 --- /dev/null +++ b/Shared/Models/DeterministicHashCode.cs @@ -0,0 +1,23 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace View_by_Distance.Shared.Models; + +public record DeterministicHashCode(int? Height, + int? Id, + int? Width) +{ + + public override string ToString() + { + string result = JsonSerializer.Serialize(this, DeterministicHashCodeSourceGenerationContext.Default.DeterministicHashCode); + return result; + } + +} + +[JsonSourceGenerationOptions(WriteIndented = true)] +[JsonSerializable(typeof(DeterministicHashCode))] +public partial class DeterministicHashCodeSourceGenerationContext : JsonSerializerContext +{ +} \ No newline at end of file diff --git a/Shared/Models/ExifDirectory.cs b/Shared/Models/ExifDirectory.cs index 45c5dc6..802523b 100644 --- a/Shared/Models/ExifDirectory.cs +++ b/Shared/Models/ExifDirectory.cs @@ -10,6 +10,7 @@ public record ExifDirectory(AviDirectory AviDirectory, GifHeaderDirectory GifHeaderDirectory, GpsDirectory GpsDirectory, int? Height, + int? Id, string JsonFile, JpegDirectory JpegDirectory, PhotoshopDirectory PhotoshopDirectory, diff --git a/Shared/Models/FilePath.cs b/Shared/Models/FilePath.cs new file mode 100644 index 0000000..ef6d92a --- /dev/null +++ b/Shared/Models/FilePath.cs @@ -0,0 +1,28 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace View_by_Distance.Shared.Models; + +public record FilePath(string DirectoryName, + string ExtensionLowered, + string FullName, + int? Id, + bool IsIdFormat, + bool IsPaddedIdFormat, + string Name, + string NameWithoutExtension) +{ + + public override string ToString() + { + string result = JsonSerializer.Serialize(this, FilePathSourceGenerationContext.Default.FilePath); + return result; + } + +} + +[JsonSourceGenerationOptions(WriteIndented = true)] +[JsonSerializable(typeof(FilePath))] +public partial class FilePathSourceGenerationContext : JsonSerializerContext +{ +} \ No newline at end of file diff --git a/Shared/Models/NameWithoutExtension.cs b/Shared/Models/NameWithoutExtension.cs deleted file mode 100644 index 3e42822..0000000 --- a/Shared/Models/NameWithoutExtension.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace View_by_Distance.Shared.Models; - -public record NameWithoutExtension(string FileNameWithoutExtension, - int? Id, - bool IsIdFormat, - bool IsPaddedIdFormat) -{ - - public override string ToString() - { - string result = JsonSerializer.Serialize(this, NameWithoutExtensionSourceGenerationContext.Default.NameWithoutExtension); - return result; - } - -} - -[JsonSourceGenerationOptions(WriteIndented = true)] -[JsonSerializable(typeof(NameWithoutExtension))] -public partial class NameWithoutExtensionSourceGenerationContext : JsonSerializerContext -{ -} \ No newline at end of file diff --git a/Shared/Models/Stateless/Id.cs b/Shared/Models/Stateless/Id.cs index 406d5f0..77a3ead 100644 --- a/Shared/Models/Stateless/Id.cs +++ b/Shared/Models/Stateless/Id.cs @@ -20,15 +20,27 @@ internal abstract class Id return result; } - internal static NameWithoutExtension GetNameWithoutExtension(IMetadataConfiguration configuration, string file) + internal static FilePath GetFilePath(FilePath filePath, string file) { - NameWithoutExtension result; + FilePath result; + string fileName = Path.GetFileName(file); + string fileExtensionLowered = Path.GetExtension(file).ToLower(); + result = new(filePath.DirectoryName, fileExtensionLowered, file, filePath.Id, filePath.IsIdFormat, filePath.IsPaddedIdFormat, fileName, filePath.NameWithoutExtension); + return result; + } + + internal static FilePath GetFilePath(IMetadataConfiguration configuration, string file) + { + FilePath result; int? id; short? multiplier; char negativeMarker; int absoluteValueOfId; + string fileName = Path.GetFileName(file); + string fileExtensionLowered = Path.GetExtension(file).ToLower(); string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(file); short sortOrderOnlyLengthIndex = IId.GetSortOrderOnlyLengthIndex(configuration.Offset); + string fileDirectoryName = Path.GetDirectoryName(file) ?? throw new NullReferenceException(); bool nameWithoutExtensionIsIdFormat = IId.NameWithoutExtensionIsIdFormat(fileNameWithoutExtension); bool nameWithoutExtensionIsPaddedIdFormat = IId.NameWithoutExtensionIsPaddedIdFormat(fileNameWithoutExtension, sortOrderOnlyLengthIndex); if (!nameWithoutExtensionIsIdFormat && !nameWithoutExtensionIsPaddedIdFormat) @@ -58,7 +70,26 @@ internal abstract class Id id = null; } } - result = new(fileNameWithoutExtension, id, nameWithoutExtensionIsIdFormat, nameWithoutExtensionIsPaddedIdFormat); + result = new(fileDirectoryName, fileExtensionLowered, file, id, nameWithoutExtensionIsIdFormat, nameWithoutExtensionIsPaddedIdFormat, fileName, fileNameWithoutExtension); + return result; + } + + internal static int GetDeterministicHashCode(byte[] value) + { + int result; + unchecked + { + int hash1 = (5381 << 16) + 5381; + int hash2 = hash1; + for (int i = 0; i < value.Length; i += 2) + { + hash1 = ((hash1 << 5) + hash1) ^ value[i]; + if (i == value.Length - 1) + break; + hash2 = ((hash2 << 5) + hash2) ^ value[i + 1]; + } + result = hash1 + (hash2 * 1566083941); + } return result; } diff --git a/Shared/Models/Stateless/Methods/IId.cs b/Shared/Models/Stateless/Methods/IId.cs index e09ac54..4999f7f 100644 --- a/Shared/Models/Stateless/Methods/IId.cs +++ b/Shared/Models/Stateless/Methods/IId.cs @@ -33,9 +33,19 @@ public interface IId static short GetSortOrderOnlyLengthIndex(int offset) => (short)(offset.ToString().Length + 3); - NameWithoutExtension TestStatic_GetNameWithoutExtension(IMetadataConfiguration configuration, string file) => - GetNameWithoutExtension(configuration, file); - static NameWithoutExtension GetNameWithoutExtension(IMetadataConfiguration configuration, string file) => - Id.GetNameWithoutExtension(configuration, file); + FilePath TestStatic_GetFilePath(FilePath filePath, string file) => + GetFilePath(filePath, file); + static FilePath GetFilePath(FilePath filePath, string file) => + Id.GetFilePath(filePath, file); + + FilePath TestStatic_GetFilePath(IMetadataConfiguration configuration, string file) => + GetFilePath(configuration, file); + static FilePath GetFilePath(IMetadataConfiguration configuration, string file) => + Id.GetFilePath(configuration, file); + + int TestStatic_GetDeterministicHashCode(byte[] value) => + GetDeterministicHashCode(value); + static int GetDeterministicHashCode(byte[] value) => + Id.GetDeterministicHashCode(value); } \ No newline at end of file diff --git a/Shared/Models/Stateless/Methods/IRename.cs b/Shared/Models/Stateless/Methods/IRename.cs new file mode 100644 index 0000000..0562cd6 --- /dev/null +++ b/Shared/Models/Stateless/Methods/IRename.cs @@ -0,0 +1,9 @@ +namespace View_by_Distance.Shared.Models.Stateless.Methods; + +public interface IRename +{ + + string[]? ConvertAndGetFfmpegFiles(FilePath filePath); + DeterministicHashCode GetDeterministicHashCode(FilePath filePath); + +} \ No newline at end of file