using CliWrap; using Microsoft.Extensions.Logging; using ShellProgressBar; using System.Collections.ObjectModel; 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; using View_by_Distance.Shared.Models; using View_by_Distance.Shared.Models.Properties; using View_by_Distance.Shared.Models.Stateless; using View_by_Distance.Windows.Models; namespace View_by_Distance.Windows; public partial class Windows : IWindows, IDisposable { private ProgressBar? _ProgressBar; private readonly ProgressBarOptions _ProgressBarOptions; public Windows(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)); IWindows windows = this; long ticks = DateTime.Now.Ticks; _ProgressBarOptions = new() { ProgressCharacter = '─', ProgressBarOnBottom = true, DisableBottomPercentage = true }; WindowsWork(logger, appSettings, windows, ticks); } void IWindows.Tick() => _ProgressBar?.Tick(); void IWindows.ConstructProgressBar(int maxTicks, string message) { _ProgressBar?.Dispose(); _ProgressBar = new(maxTicks, message, _ProgressBarOptions); } void IDisposable.Dispose() { _ProgressBar?.Dispose(); GC.SuppressFinalize(this); } ReadOnlyCollection IWindows.ConvertAndGetFastForwardMovingPictureExpertsGroupFiles(IWindowsSettings WindowsSettings, HttpClient? httpClient, FilePath filePath) { List results = []; bool isValidVideoFormatExtensions = WindowsSettings.ValidVideoFormatExtensions.Contains(filePath.ExtensionLowered); if (isValidVideoFormatExtensions) { bool check; try { CommandTask commandTask = Cli.Wrap("L:/Git/ffmpeg-2024-10-02-git-358fdf3083-full_build/bin/ffmpeg.exe") .WithArguments(["-i", filePath.FullName, "-vf", "select=eq(n\\,0)", "-q:v", "1", $"{filePath.Name}-%4d.jpg"]) .WithWorkingDirectory(filePath.DirectoryFullPath) .ExecuteAsync(); commandTask.Task.Wait(); check = true; } catch (Exception) { check = false; } if (check) { results.AddRange(Directory.GetFiles(filePath.DirectoryFullPath, $"{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 results.AsReadOnly(); } #pragma warning disable CA1416 private static DeterministicHashCode GetDeterministicHashCode(Stream stream) { DeterministicHashCode result; int? id; int? width; int? height; try { using Image image = Image.FromStream(stream); 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; } private static Stream GetStream(HttpClient httpClient, Uri uri) { Stream result; Task task = httpClient.GetStreamAsync(uri); task.Wait(); result = task.Result; return result; } private static DeterministicHashCode GetDeterministicHashCode(HttpClient httpClient, Uri uri) { DeterministicHashCode result; Stream stream = GetStream(httpClient, uri); result = GetDeterministicHashCode(stream); stream.Dispose(); return result; } DeterministicHashCode IWindows.GetDeterministicHashCode(HttpClient httpClient, Uri uri) => GetDeterministicHashCode(httpClient, uri); DeterministicHashCode IWindows.GetDeterministicHashCode(HttpClient? httpClient, FilePath filePath) { DeterministicHashCode result; if (httpClient is not null) result = GetDeterministicHashCode(httpClient, new Uri(filePath.FullName)); else { Stream stream = File.OpenRead(filePath.FullName); result = GetDeterministicHashCode(stream); stream.Dispose(); } return result; } #pragma warning restore CA1416 private static ReadOnlyCollection? GetRecursiveCollection(HttpClient httpClient, string host, string page) { List? results; Uri uri = new($"http://{host}/{page}"); string format = NginxFileSystem.GetFormat(); TimeZoneInfo timeZoneInfo = TimeZoneInfo.Local; Task taskHttpResponseMessage = httpClient.GetAsync(uri); taskHttpResponseMessage.Wait(); if (!taskHttpResponseMessage.Result.IsSuccessStatusCode) results = null; else { Task taskString = taskHttpResponseMessage.Result.Content.ReadAsStringAsync(); taskString.Wait(); NginxFileSystem[]? nginxFileSystems = JsonSerializer.Deserialize(taskString.Result, NginxFileSystemCollectionSourceGenerationContext.Default.NginxFileSystemArray); if (nginxFileSystems is null) results = null; else { results = []; NginxFileSystem nginxFileSystem; ReadOnlyCollection? directory; for (int i = 0; i < nginxFileSystems.Length; i++) { nginxFileSystem = NginxFileSystem.Get(format, timeZoneInfo, uri, nginxFileSystems[i]); if (nginxFileSystem.Type == "file") results.Add(nginxFileSystem); else { directory = GetRecursiveCollection(httpClient, host, $"{page}/{nginxFileSystem.Name}"); if (directory is null) continue; results.AddRange(directory); } } } } return results?.AsReadOnly(); } private static void VerifyParallelFor(AppSettings appSettings, IWindows windows, HttpClient httpClient, NginxFileSystem nginxFileSystem, List messages) { windows.Tick(); if (nginxFileSystem.URI is null) return; if (!nginxFileSystem.Name.EndsWith(".jpg")) return; DeterministicHashCode deterministicHashCode = windows.GetDeterministicHashCode(httpClient, nginxFileSystem.URI); if (deterministicHashCode.Id is null) { messages.Add($"{nginxFileSystem.URI.OriginalString}"); return; } string paddedId = IId.GetPaddedId(resultSettings: appSettings.ResultSettings, metadataSettings: appSettings.MetadataSettings, id: deterministicHashCode.Id.Value, hasIgnoreKeyword: null, hasDateTimeOriginal: null, index: null); if (!nginxFileSystem.Name.StartsWith(paddedId)) messages.Add($"!{nginxFileSystem.Name}.StartsWith({paddedId})"); } private static void Verify(ILogger? logger, AppSettings appSettings, IWindows windows, string host, string page) { List messages = []; HttpClient httpClient = new(); int appSettingsMaxDegreeOfParallelism = appSettings.WindowsSettings.MaxDegreeOfParallelism; ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = appSettingsMaxDegreeOfParallelism }; ReadOnlyCollection collection = GetRecursiveCollection(httpClient, host, page) ?? throw new Exception(); windows.ConstructProgressBar(collection.Count, nameof(Verify)); _ = Parallel.For(0, collection.Count, parallelOptions, (i, state) => VerifyParallelFor(appSettings, windows, httpClient, collection[i], messages)); httpClient.Dispose(); foreach (string message in messages) logger?.LogWarning("{message}", message); } private static ReadOnlyCollection GetRecursiveCollection(string host, string page) { ReadOnlyCollection results; HttpClient httpClient = new(); results = GetRecursiveCollection(httpClient, host, page) ?? throw new Exception(); httpClient.Dispose(); return results; } private List GetCollection(ILogger? logger, AppSettings appSettings, IWindows windows, IEnumerable files, A_Metadata metadata) { List results = []; int index = -1; FilePath filePath; FirstPass firstPass; string directoryName; ExifDirectory exifDirectory; List sidecarFiles; DeterministicHashCode deterministicHashCode; bool fastForwardMovingPictureExpertsGroupUsed; MinimumYearAndPathCombined minimumYearAndPathCombined; FilePath? fastForwardMovingPictureExpertsGroupFilePath; ReadOnlyDictionary> keyValuePairs; ReadOnlyCollection? fastForwardMovingPictureExpertsGroupFiles; HttpClient? httpClient = string.IsNullOrEmpty(appSettings.WindowsSettings.Host) || string.IsNullOrEmpty(appSettings.WindowsSettings.Page) ? null : new(); if (string.IsNullOrEmpty(appSettings.WindowsSettings.Host) || string.IsNullOrEmpty(appSettings.WindowsSettings.Page)) keyValuePairs = IMetadata.GetKeyValuePairs(files); else { ReadOnlyCollection collection = GetRecursiveCollection(appSettings.WindowsSettings.Host, appSettings.WindowsSettings.Page); keyValuePairs = IMetadata.GetKeyValuePairs(collection); } foreach (KeyValuePair> keyValuePair in keyValuePairs) { index += 1; windows.Tick(); if (keyValuePair.Value.Count > 2) throw new NotSupportedException("Too many sidecar files!"); foreach (FileHolder fileHolder in keyValuePair.Value) { if (appSettings.WindowsSettings.SidecarExtensions.Contains(fileHolder.ExtensionLowered)) continue; if (appSettings.WindowsSettings.IgnoreExtensions.Contains(fileHolder.ExtensionLowered)) continue; filePath = FilePath.Get(appSettings.ResultSettings, appSettings.MetadataSettings, fileHolder, index); if (filePath.Id is not null && (filePath.IsIntelligentIdFormat || filePath.SortOrder is not null)) continue; if (filePath.Id is not null) { fastForwardMovingPictureExpertsGroupFiles = null; deterministicHashCode = new(null, filePath.Id, null); directoryName = Path.GetFileName(filePath.DirectoryFullPath); if (directoryName.EndsWith(filePath.Id.Value.ToString())) continue; } else { fastForwardMovingPictureExpertsGroupFiles = windows.ConvertAndGetFastForwardMovingPictureExpertsGroupFiles(appSettings.WindowsSettings, httpClient, filePath); fastForwardMovingPictureExpertsGroupFilePath = fastForwardMovingPictureExpertsGroupFiles.Count == 0 ? null : FilePath.Get(appSettings.ResultSettings, appSettings.MetadataSettings, FileHolder.Get(fastForwardMovingPictureExpertsGroupFiles[0]), index); deterministicHashCode = fastForwardMovingPictureExpertsGroupFilePath is null ? windows.GetDeterministicHashCode(httpClient, filePath) : windows.GetDeterministicHashCode(httpClient, fastForwardMovingPictureExpertsGroupFilePath); } sidecarFiles = []; filePath = FilePath.Get(filePath, deterministicHashCode); for (int i = 0; i < keyValuePair.Value.Count; i++) { if (keyValuePair.Value[i].ExtensionLowered == fileHolder.ExtensionLowered) continue; sidecarFiles.Add(keyValuePair.Value[i]); } try { (minimumYearAndPathCombined, exifDirectory) = metadata.GetMetadataCollection(appSettings.ResultSettings, appSettings.MetadataSettings, httpClient, filePath); } catch (Exception) { logger?.LogWarning("<{filePath}>", filePath.FullName); continue; } fastForwardMovingPictureExpertsGroupUsed = fastForwardMovingPictureExpertsGroupFiles is not null && fastForwardMovingPictureExpertsGroupFiles.Count > 0; if (fastForwardMovingPictureExpertsGroupUsed && fastForwardMovingPictureExpertsGroupFiles is not null) { foreach (string fastForwardMovingPictureExpertsGroupFile in fastForwardMovingPictureExpertsGroupFiles) File.Delete(fastForwardMovingPictureExpertsGroupFile); } if (!fastForwardMovingPictureExpertsGroupUsed && appSettings.WindowsSettings.ValidVideoFormatExtensions.Contains(filePath.ExtensionLowered)) fastForwardMovingPictureExpertsGroupUsed = true; firstPass = new(exifDirectory, fastForwardMovingPictureExpertsGroupUsed, minimumYearAndPathCombined, sidecarFiles.ToArray()); results.Add(firstPass); } } return results; } private void WindowsWork(ILogger? logger, AppSettings appSettings, IWindows windows, long ticks) { if (appSettings.WindowsSettings.VerifyOnly && !string.IsNullOrEmpty(appSettings.WindowsSettings.Host) && !string.IsNullOrEmpty(appSettings.WindowsSettings.Page)) Verify(logger, appSettings, windows, appSettings.WindowsSettings.Host, appSettings.WindowsSettings.Page); else { FirstPass firstPass; List results = []; string sourceDirectory = Path.GetFullPath(appSettings.ResultSettings.RootDirectory); if (!Directory.Exists(sourceDirectory)) _ = Directory.CreateDirectory(sourceDirectory); logger?.LogInformation("{Ticks} {RootDirectory}", ticks, sourceDirectory); ReadOnlyCollection files = Directory.GetFiles(sourceDirectory, "*", SearchOption.AllDirectories).ToArray().AsReadOnly(); if (files.Count > 0) _ = IPath.DeleteEmptyDirectories(appSettings.ResultSettings.RootDirectory); A_Metadata metadata = new(appSettings.ResultSettings, appSettings.MetadataSettings); int appSettingsMaxDegreeOfParallelism = appSettings.WindowsSettings.MaxDegreeOfParallelism; int filesCount = appSettingsMaxDegreeOfParallelism == 1 ? files.Count : 123000; windows.ConstructProgressBar(filesCount, "EnumerateFiles load"); if (appSettingsMaxDegreeOfParallelism == 1) results.AddRange(GetCollection(logger, appSettings, windows, files, metadata)); else { List distinct = []; List metadataGroups = []; ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = appSettingsMaxDegreeOfParallelism }; files.AsParallel().ForAll(IMetadata.SetExifDirectoryCollection(windows, appSettings.ResultSettings, appSettings.MetadataSettings, appSettings.WindowsSettings, metadata, distinct, metadataGroups)); if (_ProgressBar?.CurrentTick != results.Count) throw new NotSupportedException(); foreach (MetadataGroup metadataGroup in metadataGroups) { if (metadataGroup.FastForwardMovingPictureExpertsGroupUsed || !appSettings.WindowsSettings.ValidVideoFormatExtensions.Contains(metadataGroup.FilePath.ExtensionLowered)) firstPass = new(metadataGroup.ExifDirectory, metadataGroup.FastForwardMovingPictureExpertsGroupUsed, metadataGroup.MinimumYearAndPathCombined, metadataGroup.SidecarFiles.ToArray()); else firstPass = new(metadataGroup.ExifDirectory, FastForwardMovingPictureExpertsGroupUsed: true, metadataGroup.MinimumYearAndPathCombined, metadataGroup.SidecarFiles.ToArray()); results.Add(firstPass); } } string json = JsonSerializer.Serialize(results, FirstPassCollectionSourceGenerationContext.Default.ListFirstPass); File.WriteAllText(Path.Combine(sourceDirectory, $"{ticks}.json"), json); } } }