using CliWrap; using Microsoft.Extensions.Configuration; 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 { 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 ProgressBar? _ProgressBar; public Rename(List args, ILogger? logger, IConfigurationRoot configurationRoot, 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; ResultConfiguration resultConfiguration = Metadata.Models.Binder.ResultConfiguration.Get(configurationRoot, appSettings.RequireRootDirectoryExists); MetadataConfiguration metadataConfiguration = Metadata.Models.Binder.MetadataConfiguration.Get(configurationRoot, resultConfiguration); RenameConfiguration renameConfiguration = Models.Binder.RenameConfiguration.Get(configurationRoot, metadataConfiguration); RenameWork(logger, appSettings, rename, ticks, renameConfiguration); } void IRename.Tick() => _ProgressBar?.Tick(); 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) { string[] lines = File.ReadAllLines(filePath.FullName); if (lines.Length == 1) { FileHolder fileHolder = new(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<(FilePath, FileInfo, ExifDirectory)> GetExifDirectoryCollection(IRename rename, RenameConfiguration renameConfiguration, IEnumerable files, A_Metadata metadata) { List<(FilePath, FileInfo, ExifDirectory)> results = []; int index = -1; FileInfo fileInfo; FilePath filePath; FileHolder fileHolder; FilePath? ffmpegFilePath; ExifDirectory exifDirectory; ReadOnlyCollection? ffmpegFiles; DeterministicHashCode deterministicHashCode; foreach (string file in files) { 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) { 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); } } return results; } private static ReadOnlyCollection GetExifDirectoryCollection(MetadataConfiguration metadataConfiguration, List<(FilePath, FileInfo, ExifDirectory)> exifDirectories) { List results = []; DateTime? dateTime; bool hasIgnoreKeyword; bool hasDateTimeOriginal; ReadOnlyCollection keywords; foreach ((FilePath filePath, FileInfo fileInfo, ExifDirectory exifDirectory) in exifDirectories) { dateTime = IDate.GetDateTimeOriginal(exifDirectory); hasDateTimeOriginal = dateTime is not null; dateTime ??= IDate.GetMinimum(exifDirectory); keywords = IMetadata.GetKeywords(exifDirectory); hasIgnoreKeyword = metadataConfiguration.IgnoreRulesKeyWords.Any(l => keywords.Contains(l)); results.Add(new(dateTime.Value, exifDirectory, filePath, hasDateTimeOriginal, hasIgnoreKeyword, fileInfo.FullName)); } return new(results); } private ReadOnlyCollection GetExifDirectoryCollection(IRename rename, AppSettings appSettings, RenameConfiguration renameConfiguration, DirectoryInfo directoryInfo) { ReadOnlyCollection results; List<(FilePath, FileInfo, ExifDirectory)> exifDirectories = []; 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)); else { ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = appSettingsMaxDegreeOfParallelism }; files.AsParallel().ForAll(A_Metadata.SetExifDirectoryCollection(rename, renameConfiguration, metadata, exifDirectories)); if (_ProgressBar.CurrentTick != exifDirectories.Count) throw new NotSupportedException(); } _ProgressBar.Dispose(); results = GetExifDirectoryCollection(renameConfiguration.MetadataConfiguration, exifDirectories); return results; } private static void VerifyIntMinValueLength(MetadataConfiguration metadataConfiguration, ReadOnlyCollection exifDirectories) { foreach ((DateTime _, ExifDirectory exifDirectory, FilePath _, bool _, bool _, string _) in exifDirectories) { if (exifDirectory.Id is null) continue; if (metadataConfiguration.IntMinValueLength < exifDirectory.Id.Value.ToString().Length) throw new NotSupportedException(); } } private static string? GetCheckDirectory(RenameConfiguration renameConfiguration, Record 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 ReadOnlyCollection GetToDoCollection(RenameConfiguration renameConfiguration, Identifier[]? identifiers, ReadOnlyCollection records) { List results = []; Record record; string jsonFile; string paddedId; string checkFile; FilePath filePath; string directoryName; FileHolder fileHolder; string? checkDirectory; const string jpg = ".jpg"; string checkFileExtension; bool multipleDirectoriesWithFiles; List distinct = []; bool? directoryCheck = null; const string jpeg = ".jpeg"; string jsonFileSubDirectory; foreach (string directory in Directory.GetDirectories(renameConfiguration.MetadataConfiguration.ResultConfiguration.RootDirectory, "*", SearchOption.TopDirectoryOnly)) { foreach (string _ in Directory.EnumerateFiles(directory, "*", SearchOption.AllDirectories)) { if (directoryCheck is null) directoryCheck = false; else if (directoryCheck.Value) directoryCheck = true; break; } if (directoryCheck is not null && directoryCheck.Value) break; } VerifyIntMinValueLength(renameConfiguration.MetadataConfiguration, records); 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++) { record = collection[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 = new(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)); } 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 records) { string paddedId; List identifiers = []; foreach (Record record in records) { 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, paddedId)); } 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) { string aMetadataCollectionDirectory = IResult.GetResultsDateGroupDirectory(renameConfiguration.MetadataConfiguration.ResultConfiguration, nameof(A_Metadata), renameConfiguration.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(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 lines = RenameFilesInDirectories(toDoCollection); if (lines.Count != 0) { File.WriteAllLines($"D:/Tmp/Phares/{DateTime.Now.Ticks}.tsv", lines); _ = IPath.DeleteEmptyDirectories(directoryInfo.FullName); } } }