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.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 Configuration _Configuration; private readonly IConfigurationRoot _ConfigurationRoot; private readonly MetadataConfiguration _MetadataConfiguration; 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; _ConfigurationRoot = configurationRoot; MetadataConfiguration metadataConfiguration = Metadata.Models.Binder.Configuration.Get(configurationRoot); Configuration configuration = Models.Binder.Configuration.Get(configurationRoot, metadataConfiguration); _MetadataConfiguration = metadataConfiguration; _Configuration = configuration; DirectoryInfo directoryInfo = new(Path.GetFullPath(metadataConfiguration.RootDirectory)); logger?.LogInformation("{RootDirectory}", directoryInfo.FullName); MetadataConfiguration.Verify(metadataConfiguration, requireExist: false); Verify(); 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); } } private void Verify() { if (_AppSettings is null) throw new NullReferenceException(nameof(_AppSettings)); if (_Configuration is null) throw new NullReferenceException(nameof(_Configuration)); if (_ConfigurationRoot is null) throw new NullReferenceException(nameof(_ConfigurationRoot)); if (_MetadataConfiguration is null) throw new NullReferenceException(nameof(_MetadataConfiguration)); } (ReadOnlyCollection, FilePath?) IRename.ConvertAndGetFfmpegFiles(FilePath filePath) { List results = []; FilePath? result; bool isIgnoreExtension; bool isValidImageFormatExtension = _MetadataConfiguration.ValidImageFormatExtensions.Contains(filePath.ExtensionLowered); isIgnoreExtension = isValidImageFormatExtension && _MetadataConfiguration.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(_MetadataConfiguration, results[0]); if (!result.Name.EndsWith("-0001.jpg")) throw new Exception(); isValidImageFormatExtension = _MetadataConfiguration.ValidImageFormatExtensions.Contains(result.ExtensionLowered); isIgnoreExtension = isValidImageFormatExtension && _MetadataConfiguration.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(_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; fileInfo = metadata.GetFileInfo(filePath); if (filePath.Id is not null) deterministicHashCode = new(null, filePath.Id, null); else deterministicHashCode = rename.GetDeterministicHashCode(filePath); exifDirectory = metadata.GetMetadataCollection(_MetadataConfiguration, filePath, fileInfo, 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(_MetadataConfiguration, _Configuration.ForceMetadataLastWriteTimeToCreationTime, _Configuration.PropertiesChangedForMetadata); 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, _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; Record record; string paddedId; string checkFile; string seasonName; FileHolder fileHolder; string? seasonDirectory; string jsonFileDirectory; const string jpg = ".jpg"; string checkFileExtension; List distinct = []; const string jpeg = ".jpeg"; 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; (season, seasonName) = IDate.GetSeason(record.DateTime.DayOfYear); jsonFileDirectory = Path.GetDirectoryName(record.JsonFile) ?? throw new Exception(); checkFileExtension = fileHolder.ExtensionLowered == jpeg ? jpg : fileHolder.ExtensionLowered; seasonDirectory = Path.Combine(fileHolder.DirectoryName, $"{record.DateTime.Year}.{season} {seasonName}"); paddedId = IId.GetPaddedId(intMinValueLength, _MetadataConfiguration.Offset + i, record.ExifDirectory.Id.Value); checkFile = Path.Combine(seasonDirectory, $"{paddedId}{checkFileExtension}"); if (checkFile == fileHolder.FullName) continue; if (File.Exists(checkFile)) { checkFile = string.Concat(checkFile, ".del"); if (File.Exists(checkFile)) continue; } results.Add(new(null, new(record.JsonFile), Path.Combine(jsonFileDirectory, $"{record.ExifDirectory.Id.Value}{checkFileExtension}.json"), JsonFile: true)); if (distinct.Contains(checkFile)) continue; distinct.Add(checkFile); results.Add(new(seasonDirectory, 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) File.Move(toDo.FileHolder.FullName, toDo.File); else if (toDo.Directory is null) throw new NotSupportedException(); else { File.Move(toDo.FileHolder.FullName, toDo.File); results.Add($"{toDo.FileHolder.FullName}\t{toDo.File}"); } } return new(results); } }