using CliWrap; using Microsoft.Extensions.Configuration; using Phares.Shared; using Serilog; using ShellProgressBar; using View_by_Distance.Rename.Models; using View_by_Distance.Shared.Models; using View_by_Distance.Shared.Models.Methods; namespace View_by_Distance.Rename; public class Rename { private record Record(int Index, bool IsIgnoreExtension, bool IsValidImageFormatExtension, List FileHolders, bool FfmpegFilesPresent, DateTime? DateTimeOriginal, DateTime?[] DateTimes, int? Id); private readonly AppSettings _AppSettings; private readonly string _WorkingDirectory; private readonly Configuration _Configuration; private readonly IsEnvironment _IsEnvironment; private readonly IConfigurationRoot _ConfigurationRoot; private readonly Property.Models.Configuration _PropertyConfiguration; public Rename(List args, IsEnvironment isEnvironment, IConfigurationRoot configurationRoot, AppSettings appSettings, string workingDirectory, bool isSilent, IConsole console) { if (isSilent) { } if (console is null) throw new NullReferenceException(nameof(console)); _AppSettings = appSettings; _IsEnvironment = isEnvironment; _WorkingDirectory = workingDirectory; _ConfigurationRoot = configurationRoot; ILogger? log = Log.ForContext(); Property.Models.Configuration propertyConfiguration = Property.Models.Binder.Configuration.Get(isEnvironment, configurationRoot); Configuration configuration = Models.Binder.Configuration.Get(isEnvironment, configurationRoot, propertyConfiguration); _PropertyConfiguration = propertyConfiguration; _Configuration = configuration; propertyConfiguration.Update(); log.Information(propertyConfiguration.RootDirectory); Verify(); List lines = RenameFilesInDirectories(log); if (lines.Any()) { File.WriteAllLines($"D:/Tmp/Phares/{DateTime.Now.Ticks}.tsv", lines); _ = Shared.Models.Stateless.Methods.IPath.DeleteEmptyDirectories(propertyConfiguration.RootDirectory); } } private void Verify() { if (_AppSettings is null) throw new NullReferenceException(nameof(_AppSettings)); if (_IsEnvironment is null) throw new NullReferenceException(nameof(_IsEnvironment)); if (_Configuration is null) throw new NullReferenceException(nameof(_Configuration)); if (_ConfigurationRoot is null) throw new NullReferenceException(nameof(_ConfigurationRoot)); if (_WorkingDirectory is null) throw new NullReferenceException(nameof(_WorkingDirectory)); if (_PropertyConfiguration is null) throw new NullReferenceException(nameof(_PropertyConfiguration)); } private static List<(FileHolder, string, string)> GetRenameUndoToDoCollection(ProgressBar progressBar, string[] files) { List<(FileHolder, string, string)> results = new(); string[] lines; string fileName; string? directory; FileHolder fileHolder; List distinct = new(); foreach (string file in files) { progressBar.Tick(); directory = Path.GetDirectoryName(file); if (string.IsNullOrEmpty(directory)) continue; fileName = Path.GetFileName(file); if (!fileName.EndsWith(".id")) continue; lines = File.ReadAllLines(file); if (lines.Length < 2) continue; if (distinct.Contains(lines[1])) continue; distinct.Add(lines[1]); fileHolder = new(lines[0]); results.Add(new(fileHolder, directory, lines[1])); } return results; } private List GetRecords(int offset, ProgressBar progressBar, string[] files) { List results = new(); int? id; string? message; string? directory; DateTime?[] dateTimes; FileHolder fileHolder; string[]? ffmpegFiles; bool isIgnoreExtension; DateTime? dateTimeOriginal; bool isValidImageFormatExtension; bool nameWithoutExtensionIsIdFormat; IReadOnlyList directories; for (int i = 0; i < files.Length; i++) { progressBar.Tick(); fileHolder = new(files[i]); if (!fileHolder.Exists) continue; directory = Path.GetDirectoryName(files[i]); if (string.IsNullOrEmpty(directory)) continue; if (fileHolder.ExtensionLowered == ".id" || fileHolder.ExtensionLowered == ".lsv" || fileHolder.DirectoryName is null) continue; if (files.Contains($"{fileHolder.FullName}.id")) continue; nameWithoutExtensionIsIdFormat = Shared.Models.Stateless.Methods.IProperty.NameWithoutExtensionIsIdFormat(fileHolder); if (nameWithoutExtensionIsIdFormat) continue; isValidImageFormatExtension = _PropertyConfiguration.ValidImageFormatExtensions.Contains(fileHolder.ExtensionLowered); isIgnoreExtension = isValidImageFormatExtension && _PropertyConfiguration.IgnoreExtensions.Contains(fileHolder.ExtensionLowered); if (!isIgnoreExtension && isValidImageFormatExtension) ffmpegFiles = null; else { try { directories = MetadataExtractor.ImageMetadataReader.ReadMetadata(files[i]); } catch (Exception) { continue; } CommandTask result = 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", files[i], "-vframes", "1", $"{fileHolder.Name}-%4d.jpg" }) .WithWorkingDirectory(fileHolder.DirectoryName) .ExecuteAsync(); result.Task.Wait(); ffmpegFiles = Directory.GetFiles(fileHolder.DirectoryName, $"{fileHolder.Name}-*.jpg", SearchOption.TopDirectoryOnly); if (!ffmpegFiles.Any()) continue; fileHolder = new(ffmpegFiles.First()); if (!fileHolder.Name.EndsWith("-0001.jpg")) throw new Exception(); isValidImageFormatExtension = _PropertyConfiguration.ValidImageFormatExtensions.Contains(fileHolder.ExtensionLowered); isIgnoreExtension = isValidImageFormatExtension && _PropertyConfiguration.IgnoreExtensions.Contains(fileHolder.ExtensionLowered); if (isIgnoreExtension || !isValidImageFormatExtension) continue; if (fileHolder.DirectoryName is null) continue; } (dateTimeOriginal, dateTimes, id, message) = Shared.Models.Stateless.Methods.IProperty.Get(fileHolder, isIgnoreExtension, isValidImageFormatExtension, _PropertyConfiguration.PopulatePropertyId); if (ffmpegFiles is not null) { fileHolder = new(files[i]); foreach (string ffmpegFile in ffmpegFiles) File.Delete(ffmpegFile); } if (message is not null) throw new Exception(message); results.Add(new(i + offset, isIgnoreExtension, isValidImageFormatExtension, new() { fileHolder }, ffmpegFiles is null, dateTimeOriginal, dateTimes, id)); } return results; } private List<(FileHolder, string, string)> GetToDoCollection(ProgressBar progressBar, bool nefPresent, List records, int length) { List<(FileHolder, string, string)> results = new(); string id; int season; string checkFile; bool? isWrongYear; DateTime dateTime; string seasonName; TimeSpan? timeSpan; string directoryName; FileHolder fileHolder; string? seasonDirectory; const string jpg = ".jpg"; DateTime? minimumDateTime; string checkFileExtension; DateTime? dateTimeFromName; const string jpeg = ".jpeg"; DateTime?[] metadataDateTimes; List distinct = new(); string[] directoryNameSegments; DateTime? dateTimeOriginalByLogic; DateTime? metadataDateTimeOriginal; IReadOnlyList directories; foreach (Record record in records) { progressBar.Tick(); if (record.FileHolders.Count != 1) continue; fileHolder = record.FileHolders.First(); if (!fileHolder.Exists) continue; if (string.IsNullOrEmpty(fileHolder.DirectoryName)) continue; if (fileHolder.ExtensionLowered == jpeg) { if (!record.IsIgnoreExtension && record.IsValidImageFormatExtension) { if (File.Exists($"{fileHolder.FullName}.id")) { checkFile = Path.Combine(fileHolder.DirectoryName, $"{fileHolder.NameWithoutExtension}{jpg}.id"); if (File.Exists(checkFile)) continue; if (distinct.Contains(checkFile)) continue; distinct.Add(checkFile); results.Add(new(new($"{fileHolder.FullName}.id"), fileHolder.DirectoryName, checkFile)); } checkFile = Path.Combine(fileHolder.DirectoryName, $"{fileHolder.NameWithoutExtension}{jpg}"); if (File.Exists(checkFile)) continue; if (distinct.Contains(checkFile)) continue; distinct.Add(checkFile); results.Add(new(fileHolder, fileHolder.DirectoryName, checkFile)); if (nefPresent) results.Add(new(new($"{fileHolder.FullName[..^4]}.tif"), fileHolder.DirectoryName, $"{checkFile[..^4]}.tif")); if (nefPresent) results.Add(new(new($"{fileHolder.FullName[..^4]}.nef"), fileHolder.DirectoryName, $"{checkFile[..^4]}.nef")); if (File.Exists(checkFile)) continue; File.Move(fileHolder.FullName, checkFile); fileHolder = new(checkFile); if (fileHolder.DirectoryName is null) continue; } } dateTimeFromName = Shared.Models.Stateless.Methods.IProperty.GetDateTimeFromName(fileHolder); minimumDateTime = record.DateTimes.Min(); if (minimumDateTime is null) throw new NotSupportedException(); if (record.DateTimeOriginal is null) timeSpan = null; else timeSpan = new(Math.Abs(minimumDateTime.Value.Ticks - record.DateTimeOriginal.Value.Ticks)); if (timeSpan is not null && timeSpan.Value.TotalMinutes < _AppSettings.MaxMinutesDelta) (dateTimeOriginalByLogic, metadataDateTimeOriginal, metadataDateTimes) = (record.DateTimeOriginal, null, Array.Empty()); else { if (_PropertyConfiguration.IgnoreExtensions.Contains(fileHolder.ExtensionLowered)) continue; try { directories = MetadataExtractor.ImageMetadataReader.ReadMetadata(fileHolder.FullName); } catch (Exception) { continue; } (metadataDateTimeOriginal, metadataDateTimes) = Metadata.Models.Stateless.Methods.IMetadata.GetDateTimes(fileHolder, directories); dateTimeOriginalByLogic = record.DateTimeOriginal is not null ? record.DateTimeOriginal : metadataDateTimeOriginal; if (record.FfmpegFilesPresent && dateTimeOriginalByLogic is not null) minimumDateTime = dateTimeOriginalByLogic.Value; if (dateTimeOriginalByLogic is null) timeSpan = null; else timeSpan = new(Math.Abs(minimumDateTime.Value.Ticks - dateTimeOriginalByLogic.Value.Ticks)); } if (timeSpan is null || timeSpan.Value.TotalMinutes > _AppSettings.MaxMinutesDelta) { if (string.IsNullOrEmpty(_AppSettings.DefaultUnknownDirectoryName)) (isWrongYear, seasonDirectory) = (null, !_AppSettings.ForceIdName ? null : fileHolder.DirectoryName); else (isWrongYear, seasonDirectory) = (null, !_AppSettings.ForceIdName ? null : Path.Combine(fileHolder.DirectoryName, _AppSettings.DefaultUnknownDirectoryName)); } else { directoryName = Path.GetFileName(fileHolder.DirectoryName); directoryNameSegments = directoryName.Split(' '); if (dateTimeFromName is null) isWrongYear = null; else (isWrongYear, _) = Shared.Models.Stateless.Methods.IProperty.IsWrongYear(directoryNameSegments, dateTimeFromName.Value.ToString("yyyy")); dateTime = minimumDateTime.Value.AddTicks(timeSpan.Value.Ticks); (season, seasonName) = Shared.Models.Stateless.Methods.IProperty.GetSeason(dateTime.DayOfYear); seasonDirectory = Path.Combine(fileHolder.DirectoryName, $"{dateTime.Year}.{season} {seasonName}"); } if (seasonDirectory is null || (isWrongYear is not null && isWrongYear.Value)) { if (dateTimeFromName is not null && isWrongYear is not null && isWrongYear.Value) minimumDateTime = dateTimeFromName.Value; else if (dateTimeOriginalByLogic is not null) minimumDateTime = dateTimeOriginalByLogic.Value; else minimumDateTime = new DateTime?[] { record.DateTimes.Where(l => l is not null).Min(), metadataDateTimes.Where(l => l is not null).Min() }.Min(); if (minimumDateTime is null) continue; checkFileExtension = fileHolder.ExtensionLowered == jpeg ? jpg : fileHolder.ExtensionLowered; checkFile = Path.Combine(fileHolder.DirectoryName, $"{minimumDateTime.Value:yyyy-MM-dd}.{minimumDateTime.Value.Ticks}.{fileHolder.Length}{checkFileExtension}"); if (checkFile == fileHolder.FullName) continue; if (distinct.Contains(checkFile)) continue; distinct.Add(checkFile); results.Add(new(fileHolder, fileHolder.DirectoryName, checkFile)); if (nefPresent) results.Add(new(new($"{fileHolder.FullName[..^4]}.tif"), fileHolder.DirectoryName, $"{checkFile[..^4]}.tif")); if (nefPresent) results.Add(new(new($"{fileHolder.FullName[..^4]}.nef"), fileHolder.DirectoryName, $"{checkFile[..^4]}.nef")); } else { if (record.Id is null) continue; id = Shared.Models.Stateless.Methods.IDirectory.GetPaddedId(length, record.Index, record.Id.Value); checkFileExtension = fileHolder.ExtensionLowered == jpeg ? jpg : fileHolder.ExtensionLowered; checkFile = Path.Combine(seasonDirectory, $"{id}{checkFileExtension}"); if (checkFile == fileHolder.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(fileHolder, seasonDirectory, checkFile)); if (nefPresent) results.Add(new(new($"{fileHolder.FullName[..^4]}.tif"), seasonDirectory, $"{checkFile[..^4]}.tif")); if (nefPresent) results.Add(new(new($"{fileHolder.FullName[..^4]}.nef"), seasonDirectory, $"{checkFile[..^4]}.nef")); } } return results; } private static List Move(ProgressBar progressBar, List<(FileHolder, string)> verifiedToDoCollection) { List results = new(); foreach ((FileHolder fileHolder, string to) in verifiedToDoCollection) { progressBar.Tick(); results.Add(fileHolder.NameWithoutExtension); try { File.Move(fileHolder.FullName, to); } catch (Exception) { } } return results; } private List RenameFilesInDirectories(ILogger log) { List results = new(); string message; bool nefPresentCheck; bool nefPresent = false; ProgressBar progressBar; List records = new(); const string fileSearchFilter = "*"; const string directorySearchFilter = "*"; List distinctDirectories = new(); List<(FileHolder, string)> verifiedToDoCollection = new(); List<(FileHolder, string, string)> toDoCollection = new(); int offset = Shared.Models.Stateless.Methods.IDirectory.GetOffset(); ProgressBarOptions options = new() { ProgressCharacter = '─', ProgressBarOnBottom = true, DisableBottomPercentage = true }; List filesCollection = Shared.Models.Stateless.Methods.IDirectory.GetFilesCollection(_PropertyConfiguration.RootDirectory, directorySearchFilter, fileSearchFilter); int count = filesCollection.Select(l => l.Length).Sum(); foreach (string[] files in filesCollection) { if (!files.Any()) continue; // foreach (string files[i] in files) // { // if (!files[i].EndsWith(".del")) // continue; // File.Move(files[i], files[i][..^4]); // } // continue; distinctDirectories.Clear(); if (_AppSettings.RenameUndo) { message = $") Undo renaming files for <{files.FirstOrDefault()}>"; progressBar = new(files.Length, message, options); toDoCollection.AddRange(GetRenameUndoToDoCollection(progressBar, files)); } else { message = $"{records.Count:00000}) Gathering records for files next to <{files.FirstOrDefault()}>"; progressBar = new(files.Length, message, options); nefPresentCheck = files.Any(l => l.EndsWith(".NEF")); if (!nefPresentCheck) records.AddRange(GetRecords(offset + records.Count, progressBar, files)); else { if (!nefPresent) nefPresent = true; records.AddRange(GetRecords(offset + records.Count, progressBar, (from l in files where l.EndsWith(".JPG") select l).ToArray())); } } progressBar.Dispose(); } if (records.Any()) { int length = 0; foreach (Record record in records) { if (record.Id is null) continue; if (length > record.Id.Value.ToString().Length) continue; length = record.Id.Value.ToString().Length; } message = $"{length}) comparing records"; progressBar = new(records.Count, message, options); toDoCollection.AddRange(GetToDoCollection(progressBar, nefPresent, records, length)); progressBar.Dispose(); } foreach ((FileHolder fileHolder, string directory, string to) in toDoCollection) { if (distinctDirectories.Contains(directory)) continue; distinctDirectories.Add(directory); } foreach (string distinctDirectory in distinctDirectories) { if (!Directory.Exists(distinctDirectory)) _ = Directory.CreateDirectory(distinctDirectory); } foreach ((FileHolder fileHolder, string directory, string to) in toDoCollection) { if (File.Exists(to)) continue; verifiedToDoCollection.Add(new(fileHolder, to)); File.WriteAllText($"{to}.id", $"{to}{Environment.NewLine}{fileHolder.FullName}"); } ConsoleKey? consoleKey = null; log.Information($"Ready to Move {verifiedToDoCollection.Count} files[i](s)?"); for (int y = 0; y < int.MaxValue; y++) { log.Information("Press \"Y\" key to move files[i](s), \"N\" key to log files[i](s) or close console to not move files"); consoleKey = System.Console.ReadKey().Key; if (consoleKey is ConsoleKey.Y or ConsoleKey.N) break; } log.Information(". . ."); if (consoleKey is null || consoleKey.Value != ConsoleKey.Y) log.Information("Nothing moved!"); else { message = ") Renaming files"; progressBar = new(count, message, options); results.AddRange(Move(progressBar, verifiedToDoCollection)); progressBar.Dispose(); log.Information("Done Moving"); } return results; } }