From 3e40ab9eecb4d101e8c30e412e697c009dc1ca1b Mon Sep 17 00:00:00 2001 From: holger Date: Sat, 4 Nov 2023 12:08:53 +0100 Subject: [PATCH] Working on the gui --- PhotoRenamer.Test/PhotoRenamer.Test.csproj | 2 +- PhotoRenamer/FilesHelper.cs | 7 +- PhotoRenamer/FolderExtension.cs | 25 ++++++ PhotoRenamer/MediaFileExtension.cs | 61 ++++++++++++++ PhotoRenamer/PhotoRenamer.csproj | 4 +- PhotoRenamer/Program.cs | 6 +- PhotoRenamer/Renamer.cs | 92 +++++++++------------- PhotoRenamer/StreamExtensions.cs | 17 +++- PhotoRenamer/appsettings.json | 4 +- PhotoRenamerGui/App.axaml | 12 +-- PhotoRenamerGui/MainWindow.axaml | 18 +++++ PhotoRenamerGui/MainWindowViewModel.cs | 66 +++++++++++++--- PhotoRenamerGui/PhotoRenamerGui.csproj | 20 +++-- 13 files changed, 248 insertions(+), 86 deletions(-) create mode 100644 PhotoRenamer/FolderExtension.cs create mode 100644 PhotoRenamer/MediaFileExtension.cs diff --git a/PhotoRenamer.Test/PhotoRenamer.Test.csproj b/PhotoRenamer.Test/PhotoRenamer.Test.csproj index 898177c..28ce517 100644 --- a/PhotoRenamer.Test/PhotoRenamer.Test.csproj +++ b/PhotoRenamer.Test/PhotoRenamer.Test.csproj @@ -1,7 +1,7 @@  - net7.0 + net8.0 false diff --git a/PhotoRenamer/FilesHelper.cs b/PhotoRenamer/FilesHelper.cs index 2f03242..21d09d6 100644 --- a/PhotoRenamer/FilesHelper.cs +++ b/PhotoRenamer/FilesHelper.cs @@ -12,7 +12,12 @@ public static class FilesHelper var fileExt = Path.GetExtension(file); foreach (var supportedFileExtension in SupportedFileExtensions) { - if (fileExt.Equals(supportedFileExtension, StringComparison.InvariantCultureIgnoreCase)) + if ( + fileExt.Equals( + supportedFileExtension, + StringComparison.InvariantCultureIgnoreCase + ) + ) yield return file; } } diff --git a/PhotoRenamer/FolderExtension.cs b/PhotoRenamer/FolderExtension.cs new file mode 100644 index 0000000..b140342 --- /dev/null +++ b/PhotoRenamer/FolderExtension.cs @@ -0,0 +1,25 @@ +namespace PhotoRenamer; + +public static class FolderExtension +{ + public static string CreateFolderStructureByDate(string targetRoot, DateOnly dateTime) + { + var folder = BuildTargetDirectoryByDate(targetRoot, dateTime); + if (!Directory.Exists(folder)) + { + Directory.CreateDirectory(folder); + } + + return folder; + } + + public static string BuildTargetDirectoryByDate(string targetRoot, DateOnly dateTime) + { + return Path.Combine(targetRoot, dateTime.Year.ToString(), dateTime.Month.ToString("D2")); + } + + public static string[] GetAllFiles(string sourceRoot) + { + return Directory.GetFiles(sourceRoot, "*", SearchOption.AllDirectories); + } +} diff --git a/PhotoRenamer/MediaFileExtension.cs b/PhotoRenamer/MediaFileExtension.cs new file mode 100644 index 0000000..c830fd4 --- /dev/null +++ b/PhotoRenamer/MediaFileExtension.cs @@ -0,0 +1,61 @@ +using MetadataExtractor; +using MetadataExtractor.Formats.Exif; +using MetadataExtractor.Formats.QuickTime; +using Serilog; + +namespace PhotoRenamer; + +public static class MediaFileExtension +{ + public static DateTime GetDateTimeFromSourceFile(string file) + { + DateTime dateTime; + try + { + var metadata = ImageMetadataReader.ReadMetadata(file); + dateTime = + GetDateTimeFromExif(metadata) + ?? GetDateTimeFromMp4(metadata) + ?? GetDateTimeFromLastWrite(file); + } + catch (ImageProcessingException e) + { + Log.Error(e, $"Error reading file information from {file}"); + dateTime = GetDateTimeFromLastWrite(file); + } + + return dateTime; + } + + private static DateTime GetDateTimeFromLastWrite(string file) + { + var creationTime = File.GetLastWriteTime(file); + return creationTime; + } + + private static DateTime? GetDateTimeFromExif( + IEnumerable directories + ) + { + return + directories + .OfType() + .FirstOrDefault() + ?.TryGetDateTime(ExifDirectoryBase.TagDateTime, out var dateTime) == true + ? dateTime + : null; + } + + private static DateTime? GetDateTimeFromMp4( + IEnumerable directories + ) + { + return + directories + .OfType() + .FirstOrDefault() + ?.TryGetDateTime(QuickTimeMovieHeaderDirectory.TagCreated, out var dateTime) == true + ? dateTime + : null; + } +} diff --git a/PhotoRenamer/PhotoRenamer.csproj b/PhotoRenamer/PhotoRenamer.csproj index 3c3e88b..c91a2b7 100644 --- a/PhotoRenamer/PhotoRenamer.csproj +++ b/PhotoRenamer/PhotoRenamer.csproj @@ -2,13 +2,13 @@ Exe - net7.0 + net8.0 enable enable - + diff --git a/PhotoRenamer/Program.cs b/PhotoRenamer/Program.cs index a011ef4..0b7c083 100644 --- a/PhotoRenamer/Program.cs +++ b/PhotoRenamer/Program.cs @@ -28,6 +28,10 @@ internal static class Program public static IConfigurationBuilder CreateHostBuilder(string[] args) => new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) - .AddJsonFile(AppDomain.CurrentDomain.BaseDirectory + "\\appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile( + AppDomain.CurrentDomain.BaseDirectory + "\\appsettings.json", + optional: true, + reloadOnChange: true + ) .AddCommandLine(args); } diff --git a/PhotoRenamer/Renamer.cs b/PhotoRenamer/Renamer.cs index 0b96bfd..d7334c8 100644 --- a/PhotoRenamer/Renamer.cs +++ b/PhotoRenamer/Renamer.cs @@ -1,11 +1,6 @@ -using System.Runtime.InteropServices.JavaScript; -using MetadataExtractor; -using MetadataExtractor.Formats.Exif; -using MetadataExtractor.Formats.QuickTime; -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration; using Serilog; using Spectre.Console; -using Directory = System.IO.Directory; namespace PhotoRenamer; @@ -17,15 +12,21 @@ internal class Renamer public Renamer(IConfiguration configuration) { - _sourcePath = Path.GetFullPath(configuration["Source"]); - _targetPath = Path.GetFullPath(configuration["Target"]); + _sourcePath = Path.GetFullPath( + configuration["Source"] + ?? throw new NotSupportedException("Source directory needs to be set.") + ); + _targetPath = Path.GetFullPath( + configuration["Target"] + ?? throw new NotSupportedException("Target directory needs to be set.") + ); Log.Information($"Source path: {_sourcePath}"); Log.Information($"Target path: {_targetPath}"); } public async Task RunAsync() { - var files = Directory.GetFiles(_sourcePath, "*", SearchOption.AllDirectories); + var files = FolderExtension.GetAllFiles(_sourcePath); _currentCount = 0; var po = new ParallelOptions { MaxDegreeOfParallelism = 3 }; await AnsiConsole @@ -37,24 +38,22 @@ internal class Renamer return 0; } - private async ValueTask Body(string file, ProgressContext progressContext, CancellationToken token) + private async ValueTask Body( + string file, + ProgressContext progressContext, + CancellationToken token + ) { + if (token.IsCancellationRequested) + return; var task = progressContext.AddTask($"[green]{file}[/]"); try { - DateTime dateTime; - try - { - var directories = ImageMetadataReader.ReadMetadata(file); - dateTime = GetDateTimeFromExif(directories) ?? GetDateTimeFromMp4(directories) ?? GetDateTimeFromLastWrite(file); - } - catch (ImageProcessingException e) - { - Log.Error(e, $"Error reading file information from {file}"); - dateTime = GetDateTimeFromLastWrite(file); - } - - var folder = CreateFolder(dateTime); + var dateTime = MediaFileExtension.GetDateTimeFromSourceFile(file); + var folder = FolderExtension.CreateFolderStructureByDate( + _targetPath, + DateOnly.FromDateTime(dateTime) + ); var progress = new Progress(v => task.Value = v * 100); await CopyFileAsync(folder, file, progress); @@ -70,22 +69,6 @@ internal class Renamer } } - private static DateTime GetDateTimeFromLastWrite(string file) - { - var creationTime = File.GetLastWriteTime(file); - return creationTime; - } - - private static DateTime? GetDateTimeFromExif(IEnumerable directories) - { - return directories.OfType().FirstOrDefault()?.TryGetDateTime(ExifDirectoryBase.TagDateTime, out var dateTime) == true ? dateTime : null; - } - - private static DateTime? GetDateTimeFromMp4(IEnumerable directories) - { - return directories.OfType().FirstOrDefault()?.TryGetDateTime(QuickTimeMovieHeaderDirectory.TagCreated, out var dateTime) == true ? dateTime : null; - } - private static void ResetTimes(FileSystemInfo destination, FileSystemInfo source) { destination.LastWriteTime = source.LastWriteTime; @@ -94,7 +77,11 @@ internal class Renamer destination.CreationTimeUtc = source.CreationTimeUtc; } - private static async Task CopyFileAsync(string folder, string file, IProgress? progress = null) + private static async Task CopyFileAsync( + string folder, + string file, + IProgress? progress = null + ) { var destination = new FileInfo(Path.Combine(folder, Path.GetFileName(file))); var source = new FileInfo(file); @@ -104,22 +91,21 @@ internal class Renamer return; } - await using var sourceStream = File.Open(source.FullName, FileMode.Open, FileAccess.Read, FileShare.Read); - await using var targetStream = File.Open(destination.FullName, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite); + await using var sourceStream = File.Open( + source.FullName, + FileMode.Open, + FileAccess.Read, + FileShare.Read + ); + await using var targetStream = File.Open( + destination.FullName, + FileMode.Create, + FileAccess.ReadWrite, + FileShare.ReadWrite + ); await sourceStream.CopyToAsync(targetStream, 81920, progress: progress); ResetTimes(destination, source); } - - private string CreateFolder(DateTime dateTime) - { - var folder = Path.Combine(_targetPath, dateTime.Year.ToString(), dateTime.Month.ToString("D2")); - if (!Directory.Exists(folder)) - { - Directory.CreateDirectory(folder); - } - - return folder; - } } diff --git a/PhotoRenamer/StreamExtensions.cs b/PhotoRenamer/StreamExtensions.cs index 7669bd0..163f688 100644 --- a/PhotoRenamer/StreamExtensions.cs +++ b/PhotoRenamer/StreamExtensions.cs @@ -2,7 +2,13 @@ public static class StreamExtensions { - public static async Task CopyToAsync(this Stream source, Stream destination, int bufferSize, IProgress? progress = null, CancellationToken cancellationToken = default) + public static async Task CopyToAsync( + this Stream source, + Stream destination, + int bufferSize, + IProgress? progress = null, + CancellationToken cancellationToken = default + ) { if (source == null) throw new ArgumentNullException(nameof(source)); @@ -18,9 +24,14 @@ public static class StreamExtensions var buffer = new byte[bufferSize]; long totalBytesRead = 0; int bytesRead; - while ((bytesRead = await source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) != 0) + while ( + (bytesRead = await source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) + != 0 + ) { - await destination.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken).ConfigureAwait(false); + await destination + .WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken) + .ConfigureAwait(false); totalBytesRead += bytesRead; progress?.Report(totalBytesRead / (double)source.Length); } diff --git a/PhotoRenamer/appsettings.json b/PhotoRenamer/appsettings.json index 89d5e66..5c42d57 100644 --- a/PhotoRenamer/appsettings.json +++ b/PhotoRenamer/appsettings.json @@ -1,4 +1,4 @@ { - "Source": "C:\\Users\\Holger\\Nextcloud\\DCIM", - "Target": "C:\\Users\\Holger\\Nextcloud\\DCIM" + "Source": "/Volumes/EOS_DIGITAL/DCIM/100CANON", + "Target": "/Users/holger/Nextcloud/SofortUploadCamera" } \ No newline at end of file diff --git a/PhotoRenamerGui/App.axaml b/PhotoRenamerGui/App.axaml index 23065b6..bea6bc2 100644 --- a/PhotoRenamerGui/App.axaml +++ b/PhotoRenamerGui/App.axaml @@ -1,10 +1,12 @@ - - + + + \ No newline at end of file diff --git a/PhotoRenamerGui/MainWindow.axaml b/PhotoRenamerGui/MainWindow.axaml index ef31f80..e6e16ef 100644 --- a/PhotoRenamerGui/MainWindow.axaml +++ b/PhotoRenamerGui/MainWindow.axaml @@ -57,5 +57,23 @@ VerticalAlignment="Stretch" /> + + + + + + + + + diff --git a/PhotoRenamerGui/MainWindowViewModel.cs b/PhotoRenamerGui/MainWindowViewModel.cs index 868d93e..826744c 100644 --- a/PhotoRenamerGui/MainWindowViewModel.cs +++ b/PhotoRenamerGui/MainWindowViewModel.cs @@ -1,9 +1,13 @@ -using System.Linq; +using System; +using System.Collections.ObjectModel; +using System.IO; using System.Threading.Tasks; +using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Platform.Storage; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using PhotoRenamer; namespace PhotoRenamerGui; @@ -11,24 +15,64 @@ public partial class MainWindowViewModel : ObservableObject { [ObservableProperty] private string? _inputFolder; [ObservableProperty] private string? _outputFolder; + public ObservableCollection MediaFiles { get; } = new(); [RelayCommand] - private async Task SelectInputFolder() + private async Task SelectInputFolderAsync() { - InputFolder = await SearchFolder(); + InputFolder = await SearchFolderAsync(); } - + [RelayCommand] - private async Task SelectOutputFolder() + private async Task SelectOutputFolderAsync() { - OutputFolder = await SearchFolder(); + OutputFolder = await SearchFolderAsync(); } - - private async Task SearchFolder() + + private static async Task SearchFolderAsync() { - var storageProvider = (App.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime).MainWindow + var storageProvider = (Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime) + ?.MainWindow? .StorageProvider; - var result =await storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions()); - return result.FirstOrDefault()?.TryGetLocalPath(); + if (storageProvider is null) return ""; + var result = await storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions()); + return result.Count > 0 ? result[0].TryGetLocalPath() : null; } + + partial void OnInputFolderChanged(string? value) + { + UpdatePreviewList(); + } + + partial void OnOutputFolderChanged(string? value) + { + UpdatePreviewList(); + } + + private void UpdatePreviewList() + { + MediaFiles.Clear(); + if (string.IsNullOrWhiteSpace(InputFolder)) return; + if (string.IsNullOrWhiteSpace(OutputFolder)) return; + var allFiles = FolderExtension.GetAllFiles(InputFolder); + foreach (var file in allFiles) + { + var source = Path.GetRelativePath(InputFolder, file); + var date = MediaFileExtension.GetDateTimeFromSourceFile(file); + var target = FolderExtension.BuildTargetDirectoryByDate(OutputFolder, DateOnly.FromDateTime(date)); + + MediaFiles.Add(new MediaFile(true, source, Path.GetRelativePath(OutputFolder, target))); + } + + //TODO + } +} + +public class MediaFile(bool isSelected, string? sourceDirectory, string? targetDirectory) : ObservableObject +{ + public bool IsSelected { get=> isSelected; + set => SetProperty(ref isSelected, value); + } + public string? SourceDirectory { get; } = sourceDirectory; + public string? TargetDirectory { get; } = targetDirectory; } \ No newline at end of file diff --git a/PhotoRenamerGui/PhotoRenamerGui.csproj b/PhotoRenamerGui/PhotoRenamerGui.csproj index 9570578..914cd4b 100644 --- a/PhotoRenamerGui/PhotoRenamerGui.csproj +++ b/PhotoRenamerGui/PhotoRenamerGui.csproj @@ -1,7 +1,7 @@  WinExe - net7.0 + net8.0 enable true app.manifest @@ -10,12 +10,18 @@ - - - - + + + + + - - + + + + + + +