using CliWrap; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using ShellProgressBar; using System.Collections.ObjectModel; using System.Drawing; using System.Drawing.Imaging; using System.Runtime.InteropServices; 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.Stateless.Methods; namespace View_by_Distance.Rename; public class Rename : IRename { private record ToDo(string? Directory, FileHolder FileHolder, string File, bool JsonFile); private record Record(DateTime DateTime, ExifDirectory ExifDirectory, string File, string JsonFile); private readonly AppSettings _AppSettings; private readonly RenameConfiguration _RenameConfiguration; 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)); _AppSettings = appSettings; 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); _RenameConfiguration = renameConfiguration; DirectoryInfo directoryInfo = new(Path.GetFullPath(resultConfiguration.RootDirectory)); logger?.LogInformation("{RootDirectory}", directoryInfo.FullName); ReadOnlyCollection exifDirectories = GetExifDirectoryCollection(directoryInfo); ReadOnlyCollection toDoCollection = GetToDoCollection(logger, ticks, exifDirectories); ReadOnlyCollection lines = RenameFilesInDirectories(toDoCollection); if (lines.Count != 0) { File.WriteAllLines($"D:/Tmp/Phares/{DateTime.Now.Ticks}.tsv", lines); _ = IPath.DeleteEmptyDirectories(directoryInfo.FullName); } } (ReadOnlyCollection, FilePath?) IRename.ConvertAndGetFfmpegFiles(FilePath filePath) { List results = []; FilePath? result; bool isIgnoreExtension; bool isValidImageFormatExtension = _RenameConfiguration.ValidImageFormatExtensions.Contains(filePath.ExtensionLowered); isIgnoreExtension = isValidImageFormatExtension && _RenameConfiguration.IgnoreExtensions.Contains(filePath.ExtensionLowered); if (!isIgnoreExtension && isValidImageFormatExtension) result = 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.AddRange(Directory.GetFiles(filePath.DirectoryName, $"{filePath.Name}-*.jpg", SearchOption.TopDirectoryOnly)); if (results.Count == 0) throw new Exception(); result = IId.GetFilePath(_RenameConfiguration.MetadataConfiguration, results[0]); if (!result.Name.EndsWith("-0001.jpg")) throw new Exception(); isValidImageFormatExtension = _RenameConfiguration.ValidImageFormatExtensions.Contains(result.ExtensionLowered); isIgnoreExtension = isValidImageFormatExtension && _RenameConfiguration.IgnoreExtensions.Contains(result.ExtensionLowered); if (isIgnoreExtension || !isValidImageFormatExtension) throw new Exception(); if (result.DirectoryName is null) throw new NullReferenceException(nameof(result.DirectoryName)); } return new(new(results), result); } #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 GetExifDirectoryCollection(IRename rename, List<(string, FileInfo, ExifDirectory)> exifDirectories, IEnumerable files, A_Metadata metadata) { FileInfo fileInfo; FilePath filePath; FilePath? ffmpegFilePath; ExifDirectory exifDirectory; ReadOnlyCollection ffmpegFiles; DeterministicHashCode deterministicHashCode; foreach (string file in files) { filePath = IId.GetFilePath(_RenameConfiguration.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, ffmpegFilePath) = rename.ConvertAndGetFfmpegFiles(filePath); if (ffmpegFilePath is not null) filePath = ffmpegFilePath; if (filePath.Id is not null) deterministicHashCode = new(null, filePath.Id, null); else deterministicHashCode = rename.GetDeterministicHashCode(filePath); (fileInfo, exifDirectory) = metadata.GetMetadataCollection(_RenameConfiguration.MetadataConfiguration, filePath, deterministicHashCode); exifDirectories.Add(new(file, fileInfo, exifDirectory)); foreach (string ffmpegFile in ffmpegFiles) File.Delete(ffmpegFile); } } private static ReadOnlyCollection GetExifDirectoryCollection(List<(string, FileInfo, ExifDirectory)> exifDirectories) { List results = []; DateTime? dateTime; foreach ((string file, FileInfo fileInfo, ExifDirectory exifDirectory) in exifDirectories) { dateTime = IDate.GetDateTimeOriginal(exifDirectory); dateTime ??= IDate.GetMinimum(exifDirectory); results.Add(new(dateTime.Value, exifDirectory, file, fileInfo.FullName)); } return new(results); } private ReadOnlyCollection GetExifDirectoryCollection(DirectoryInfo directoryInfo) { ReadOnlyCollection results; IRename rename = this; List<(string, FileInfo, ExifDirectory)> exifDirectories = []; int appSettingsMaxDegreeOfParallelism = _AppSettings.MaxDegreeOfParallelism; IEnumerable files = Directory.EnumerateFiles(directoryInfo.FullName, "*", SearchOption.AllDirectories); A_Metadata metadata = new(_RenameConfiguration.MetadataConfiguration); if (appSettingsMaxDegreeOfParallelism == 1) GetExifDirectoryCollection(rename, exifDirectories, files, metadata); else { ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = appSettingsMaxDegreeOfParallelism }; ProgressBar progressBar = new(123000, "EnumerateFiles load", new ProgressBarOptions() { ProgressCharacter = '─', ProgressBarOnBottom = true, DisableBottomPercentage = true }); files.AsParallel().ForAll(A_Metadata.SetExifDirectoryCollection(rename, _RenameConfiguration.MetadataConfiguration, metadata, exifDirectories, () => progressBar.Tick())); if (progressBar.CurrentTick != exifDirectories.Count) throw new NotSupportedException(); } results = GetExifDirectoryCollection(exifDirectories); return results; } private static void VerifyIntMinValueLength(ReadOnlyCollection exifDirectories, int intMinValueLength) { foreach ((DateTime _, ExifDirectory exifDirectory, string _, string _) in exifDirectories) { if (exifDirectory.Id is null) continue; if (intMinValueLength < exifDirectory.Id.Value.ToString().Length) throw new NotSupportedException(); } } private ReadOnlyCollection GetToDoCollection(ILogger? logger, long ticks, ReadOnlyCollection exifDirectories) { List results = []; int season; string maker; Record record; string jsonFile; string paddedId; string checkFile; string seasonName; string directoryName; FileHolder fileHolder; string? checkDirectory; const string jpg = ".jpg"; string checkFileExtension; List distinct = []; const string jpeg = ".jpeg"; string jsonFileSubDirectory; int intMinValueLength = int.MinValue.ToString().Length; VerifyIntMinValueLength(exifDirectories, intMinValueLength); ReadOnlyCollection records = new((from l in exifDirectories orderby l.DateTime select l).ToArray()); for (int i = 0; i < records.Count; i++) { record = records[i]; if (record.ExifDirectory.Id is null) continue; fileHolder = new(record.File); if (fileHolder.DirectoryName is null) continue; maker = IMetadata.GetMaker(record.ExifDirectory.ExifDirectoryBase); (season, seasonName) = IDate.GetSeason(record.DateTime.DayOfYear); checkFileExtension = fileHolder.ExtensionLowered == jpeg ? jpg : fileHolder.ExtensionLowered; checkDirectory = Path.Combine(fileHolder.DirectoryName, $"{record.DateTime.Year}.{season} {seasonName}{maker}"); jsonFileSubDirectory = Path.GetDirectoryName(Path.GetDirectoryName(record.JsonFile)) ?? throw new Exception(); paddedId = IId.GetPaddedId(intMinValueLength, _RenameConfiguration.MetadataConfiguration.Offset + i, record.ExifDirectory.Id.Value); checkFile = Path.Combine(checkDirectory, $"{paddedId}{checkFileExtension}"); if (checkFile == fileHolder.FullName) continue; if (File.Exists(checkFile)) { checkFile = string.Concat(checkFile, ".del"); if (File.Exists(checkFile)) continue; } (directoryName, _) = IPath.GetDirectoryNameAndIndex(_RenameConfiguration.MetadataConfiguration.ResultConfiguration, record.ExifDirectory.Id.Value); jsonFile = Path.Combine(jsonFileSubDirectory, directoryName, $"{record.ExifDirectory.Id.Value}{checkFileExtension}.json"); if (record.JsonFile != jsonFile) results.Add(new(null, new(record.JsonFile), jsonFile, JsonFile: true)); if (distinct.Contains(checkFile)) continue; distinct.Add(checkFile); results.Add(new(checkDirectory, fileHolder, 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); foreach (ToDo toDo in toDoCollection) { if (toDo.JsonFile) { if (File.Exists(toDo.File)) File.Delete(toDo.File); File.Move(toDo.FileHolder.FullName, toDo.File); } else if (toDo.Directory is null) throw new NotSupportedException(); else { if (File.Exists(toDo.File)) File.Delete(toDo.File); File.Move(toDo.FileHolder.FullName, toDo.File); results.Add($"{toDo.FileHolder.FullName}\t{toDo.File}"); } } return new(results); } }