using CliWrap; using Microsoft.Extensions.Logging; using ShellProgressBar; using System.Collections.ObjectModel; using System.Data; 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.Methods; using View_by_Distance.Rename.Models; 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.Rename; public partial class Rename : IRename, IDisposable { 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; public Rename(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)); IRename rename = this; long ticks = DateTime.Now.Ticks; RenameWork(logger, appSettings, rename, ticks); } void IRename.Tick() => _ProgressBar?.Tick(); void IDisposable.Dispose() { _ProgressBar?.Dispose(); GC.SuppressFinalize(this); } ReadOnlyCollection IRename.ConvertAndGetFfmpegFiles(IRenameConfiguration renameConfiguration, FilePath filePath) { List results = []; bool isValidVideoFormatExtensions = renameConfiguration.ValidVideoFormatExtensions.Contains(filePath.ExtensionLowered); if (isValidVideoFormatExtensions) { bool check; try { CommandTask commandTask = Cli.Wrap("ffmpeg.exe") .WithArguments(new[] { "-i", filePath.FullName, "-vf", "select=eq(n\\,0)", "-q:v", "1", $"{filePath.Name}-%4d.jpg" }) .WithWorkingDirectory(filePath.DirectoryName) .ExecuteAsync(); commandTask.Task.Wait(); check = true; } catch (Exception) { check = false; } if (check) { results.AddRange(Directory.GetFiles(filePath.DirectoryName, $"{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 new(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 static void RenameUrl(FilePath filePath) { FileHolder fileHolder; string[] lines = File.ReadAllLines(filePath.FullName); if (lines.Length == 1) { fileHolder = FileHolder.Get(lines[0]); if (fileHolder.Exists) { string checkFile; checkFile = IId.GetIgnoreFullPath(filePath, fileHolder); if (lines[0] == checkFile || lines[0].Length != checkFile.Length) throw new NotSupportedException(); File.Move(lines[0], checkFile); } File.Delete(filePath.FullName); } } private static List GetRecordACollection(ILogger? logger, IRename rename, RenameConfiguration renameConfiguration, IEnumerable files, A_Metadata metadata) { List results = []; int index = -1; FileInfo fileInfo; FilePath filePath; FilePath? ffmpegFilePath; ExifDirectory exifDirectory; List sidecarFiles; ReadOnlyCollection? ffmpegFiles; DeterministicHashCode deterministicHashCode; ReadOnlyDictionary> keyValuePairs = IMetadata.GetKeyValuePairs(files); foreach (KeyValuePair> keyValuePair in keyValuePairs) { index += 1; rename.Tick(); 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) { 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]); } try { (fileInfo, exifDirectory) = metadata.GetMetadataCollection(renameConfiguration.MetadataConfiguration, filePath, deterministicHashCode); } catch (Exception) { logger?.LogWarning("<{filePath}>", filePath.FullName); continue; } 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 GetRecordBCollection(MetadataConfiguration metadataConfiguration, List recordACollection) { List results = []; DateTime? dateTime; bool hasIgnoreKeyword; bool hasDateTimeOriginal; ReadOnlyCollection keywords; foreach (RecordA recordA in recordACollection) { dateTime = IDate.GetDateTimeOriginal(recordA.ExifDirectory); hasDateTimeOriginal = dateTime is not null; dateTime ??= IDate.GetMinimum(recordA.ExifDirectory); keywords = IMetadata.GetKeywords(recordA.ExifDirectory); hasIgnoreKeyword = metadataConfiguration.IgnoreRulesKeyWords.Any(l => keywords.Contains(l)); results.Add(new(dateTime.Value, recordA.ExifDirectory, recordA.FilePath, recordA.SidecarFiles, hasDateTimeOriginal, hasIgnoreKeyword, recordA.FileInfo.FullName)); } return new(results); } private ReadOnlyCollection GetRecordBCollection(ILogger? logger, IRename rename, AppSettings appSettings, RenameConfiguration renameConfiguration, DirectoryInfo directoryInfo) { 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) recordACollection.AddRange(GetRecordACollection(logger, rename, renameConfiguration, files, metadata)); else { List distinct = []; List<(FilePath, FileInfo, ExifDirectory, ReadOnlyCollection)> collection = []; ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = appSettingsMaxDegreeOfParallelism }; 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 = GetRecordBCollection(renameConfiguration.MetadataConfiguration, recordACollection); return results; } private static void VerifyIntMinValueLength(MetadataConfiguration metadataConfiguration, ReadOnlyCollection recordBCollection) { foreach ((DateTime _, ExifDirectory exifDirectory, FilePath _, ReadOnlyCollection _, bool _, bool _, string _) in recordBCollection) { if (exifDirectory.Id is null) continue; if (metadataConfiguration.IntMinValueLength < exifDirectory.Id.Value.ToString().Length) throw new NotSupportedException(); } } private static string? GetCheckDirectory(RenameConfiguration renameConfiguration, RecordB record, FilePath filePath, ReadOnlyCollection ids, bool multipleDirectoriesWithFiles) { string? result; if (filePath.DirectoryName is null) throw new NullReferenceException(nameof(filePath.DirectoryName)); string year = record.DateTime.Year.ToString(); string checkDirectoryName = Path.GetFileName(filePath.DirectoryName); if (multipleDirectoriesWithFiles && !checkDirectoryName.Contains(year)) result = null; else { string? maker = IMetadata.GetMaker(record.ExifDirectory); (int seasonValue, string seasonName) = IDate.GetSeason(record.DateTime.DayOfYear); string splat = filePath.DirectoryName[^3..][1] == '!' ? filePath.DirectoryName[^3..] : string.Empty; string makerSplit = string.IsNullOrEmpty(maker) ? string.IsNullOrEmpty(renameConfiguration.DefaultMaker) ? string.Empty : renameConfiguration.DefaultMaker : $" {maker.Split(' ')[0]}"; string directoryName = $"{year}.{seasonValue} {seasonName}{makerSplit}{splat}"; result = Path.Combine(renameConfiguration.MetadataConfiguration.ResultConfiguration.RootDirectory, record.ExifDirectory.Id is null || !ids.Contains(record.ExifDirectory.Id.Value) ? "_ Destination _" : "_ Exists _", record.HasDateTimeOriginal ? "Has" : "Not", directoryName); } return result; } private static List GetSidecarFiles(MetadataConfiguration metadataConfiguration, RecordB record, List distinct, string checkDirectory, string paddedId) { List results = []; 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 bool? GetDirectoryCheck(RenameConfiguration renameConfiguration) { bool? result = null; foreach (string directory in Directory.GetDirectories(renameConfiguration.MetadataConfiguration.ResultConfiguration.RootDirectory, "*", SearchOption.TopDirectoryOnly)) { foreach (string _ in Directory.EnumerateFiles(directory, "*", SearchOption.AllDirectories)) { if (result is null) result = false; else if (result.Value) result = true; break; } if (result is not null && result.Value) break; } return result; } private static ReadOnlyCollection GetToDoCollection(RenameConfiguration renameConfiguration, Identifier[]? identifiers, ReadOnlyCollection recordBCollection) { List results = []; RecordB record; string jsonFile; string paddedId; string checkFile; FilePath filePath; string directoryName; FileHolder fileHolder; string? checkDirectory; const string jpg = ".jpg"; string checkFileExtension; List distinct = []; const string jpeg = ".jpeg"; string jsonFileSubDirectory; bool? directoryCheck = GetDirectoryCheck(renameConfiguration); VerifyIntMinValueLength(renameConfiguration.MetadataConfiguration, recordBCollection); bool multipleDirectoriesWithFiles = directoryCheck is not null && directoryCheck.Value; ResultConfiguration resultConfiguration = renameConfiguration.MetadataConfiguration.ResultConfiguration; ReadOnlyCollection ids = identifiers is null ? new([]) : new((from l in identifiers select l.Id).ToArray()); ReadOnlyCollection sorted = new((from l in recordBCollection orderby l.DateTime select l).ToArray()); for (int i = 0; i < sorted.Count; i++) { record = sorted[i]; if (record.ExifDirectory.Id is null) continue; if (record.FilePath.DirectoryName is null) continue; checkDirectory = GetCheckDirectory(renameConfiguration, record, record.FilePath, ids, multipleDirectoriesWithFiles); if (string.IsNullOrEmpty(checkDirectory)) continue; checkFileExtension = record.FilePath.ExtensionLowered == jpeg ? jpg : record.FilePath.ExtensionLowered; paddedId = IId.GetPaddedId(renameConfiguration.MetadataConfiguration, record.ExifDirectory.Id.Value, record.HasIgnoreKeyword, i); jsonFileSubDirectory = Path.GetDirectoryName(Path.GetDirectoryName(record.JsonFile)) ?? throw new Exception(); checkFile = Path.Combine(checkDirectory, $"{paddedId}{checkFileExtension}"); if (checkFile == record.FilePath.FullName) continue; if (File.Exists(checkFile)) { checkFile = string.Concat(checkFile, ".del"); if (File.Exists(checkFile)) continue; } (directoryName, _) = IPath.GetDirectoryNameAndIndex(resultConfiguration, record.ExifDirectory.Id.Value); jsonFile = Path.Combine(jsonFileSubDirectory, directoryName, $"{record.ExifDirectory.Id.Value}{checkFileExtension}.json"); if (record.JsonFile != jsonFile) { fileHolder = FileHolder.Get(record.JsonFile); filePath = FilePath.Get(renameConfiguration.MetadataConfiguration, fileHolder, index: null); results.Add(new(null, filePath, jsonFile, JsonFile: true)); } if (distinct.Contains(checkFile)) 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); } private static void VerifyDirectories(ReadOnlyCollection toDoCollection) { List distinct = []; foreach (ToDo toDo in toDoCollection) { if (toDo.Directory is null || distinct.Contains(toDo.Directory)) continue; if (!Directory.Exists(toDo.Directory)) _ = Directory.CreateDirectory(toDo.Directory); distinct.Add(toDo.Directory); } } private ReadOnlyCollection RenameFilesInDirectories(ReadOnlyCollection toDoCollection) { List results = []; VerifyDirectories(toDoCollection); _ProgressBar = new(toDoCollection.Count, "Move Files", new ProgressBarOptions() { ProgressCharacter = '─', ProgressBarOnBottom = true, DisableBottomPercentage = true }); foreach (ToDo toDo in toDoCollection) { _ProgressBar.Tick(); if (toDo.JsonFile) { if (File.Exists(toDo.File)) File.Delete(toDo.File); File.Move(toDo.FilePath.FullName, toDo.File); } else if (toDo.Directory is null) throw new NotSupportedException(); else { if (File.Exists(toDo.File)) File.Delete(toDo.File); try { File.Move(toDo.FilePath.FullName, toDo.File); } catch (Exception) { continue; } results.Add($"{toDo.FilePath.FullName}\t{toDo.File}"); } } _ProgressBar.Dispose(); return new(results); } private static void SaveIdentifiersToDisk(long ticks, RenameConfiguration renameConfiguration, string aMetadataCollectionDirectory, ReadOnlyCollection recordBCollection) { string paddedId; List identifiers = []; foreach (RecordB record in recordBCollection) { if (record.ExifDirectory.Id is null) continue; paddedId = IId.GetPaddedId(renameConfiguration.MetadataConfiguration, record.ExifDirectory.Id.Value, record.FilePath.IsIgnore, index: null); identifiers.Add(new(record.ExifDirectory.Id.Value, record.FilePath.Length, paddedId, record.DateTime.Ticks)); } string json = JsonSerializer.Serialize(identifiers.OrderBy(l => l.PaddedId).ToArray(), IdentifierCollectionSourceGenerationContext.Default.IdentifierArray); _ = IPath.WriteAllText(Path.Combine(aMetadataCollectionDirectory, $"{ticks}.json"), json, updateDateWhenMatches: false, compareBeforeWrite: true, updateToWhenMatches: null); } private void RenameWork(ILogger? logger, AppSettings appSettings, IRename rename, long ticks) { RenameConfiguration renameConfiguration = appSettings.RenameConfiguration; MetadataConfiguration metadataConfiguration = renameConfiguration.MetadataConfiguration; string aMetadataCollectionDirectory = IResult.GetResultsDateGroupDirectory(metadataConfiguration.ResultConfiguration, nameof(A_Metadata), metadataConfiguration.ResultConfiguration.ResultCollection); string? propertyCollectionFile = string.IsNullOrEmpty(renameConfiguration.RelativePropertyCollectionFile) ? null : Path.GetFullPath(Path.Combine(aMetadataCollectionDirectory, renameConfiguration.RelativePropertyCollectionFile)); string? json = !File.Exists(propertyCollectionFile) ? null : File.ReadAllText(propertyCollectionFile); Identifier[]? identifiers = json is null ? null : JsonSerializer.Deserialize(json, IdentifierCollectionSourceGenerationContext.Default.IdentifierArray); if (identifiers is null && !string.IsNullOrEmpty(renameConfiguration.RelativePropertyCollectionFile)) throw new Exception($"Invalid {nameof(renameConfiguration.RelativePropertyCollectionFile)}"); DirectoryInfo directoryInfo = new(Path.GetFullPath(metadataConfiguration.ResultConfiguration.RootDirectory)); logger?.LogInformation("{Ticks} {RootDirectory}", ticks, directoryInfo.FullName); ReadOnlyCollection recordBCollection = GetRecordBCollection(logger, rename, appSettings, renameConfiguration, directoryInfo); SaveIdentifiersToDisk(ticks, renameConfiguration, aMetadataCollectionDirectory, recordBCollection); if (!renameConfiguration.OnlySaveIdentifiersToDisk) { ReadOnlyCollection toDoCollection = GetToDoCollection(renameConfiguration, identifiers, recordBCollection); ReadOnlyCollection lines = RenameFilesInDirectories(toDoCollection); if (lines.Count != 0) { File.WriteAllLines($"D:/Tmp/Phares/{DateTime.Now.Ticks}.tsv", lines); _ = IPath.DeleteEmptyDirectories(directoryInfo.FullName); } } } }