diff --git a/PhotoRenamer.Test/FilesTest.cs b/PhotoRenamer.Test/FilesTest.cs index 03d4686..0c7e069 100644 --- a/PhotoRenamer.Test/FilesTest.cs +++ b/PhotoRenamer.Test/FilesTest.cs @@ -4,32 +4,31 @@ using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; using PhotoRenamer.Types; -namespace PhotoRenamer.Test +namespace PhotoRenamer.Test; + +[TestClass] +public class FilesTest { - [TestClass] - public class FilesTest + private readonly string[] _files; + + public FilesTest() { - private readonly string[] _files; - - public FilesTest() - { - var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets"); - _files = FilesHelper.FindSupportedFilesRecursively(path).ToArray(); - } - - [TestMethod, Priority(0)] - public void FindSupportedFiles() - { - Assert.AreEqual(2, _files.Length); - Assert.AreEqual("IMG_20120528_125912.jpg", Path.GetFileName(_files[0])); - Assert.AreEqual("IMG_20120526_170007.jpg", Path.GetFileName(_files[1])); - } - - [TestMethod, Priority(1)] - public void GetMetaData() - { - var expected = new[] {new MediaFile("r", DateTime.Now), new MediaFile("r", DateTime.Now)}; - // var actual = FilesHelper.GetMediaFiles(_files); - } + var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets"); + _files = FilesHelper.FindSupportedFilesRecursively(path).ToArray(); } -} \ No newline at end of file + + [TestMethod, Priority(0)] + public void FindSupportedFiles() + { + Assert.AreEqual(2, _files.Length); + Assert.AreEqual("IMG_20120528_125912.jpg", Path.GetFileName(_files[0])); + Assert.AreEqual("IMG_20120526_170007.jpg", Path.GetFileName(_files[1])); + } + + [TestMethod, Priority(1)] + public void GetMetaData() + { + var expected = new[] { new MediaFile("r", DateTime.Now), new MediaFile("r", DateTime.Now) }; + // var actual = FilesHelper.GetMediaFiles(_files); + } +} diff --git a/PhotoRenamer/FilesHelper.cs b/PhotoRenamer/FilesHelper.cs index aeae8d0..42c4ef1 100644 --- a/PhotoRenamer/FilesHelper.cs +++ b/PhotoRenamer/FilesHelper.cs @@ -1,21 +1,21 @@ -namespace PhotoRenamer -{ - public static class FilesHelper - { - private static readonly string[] SupportedFileExtensions = {".jpg", ".cr2", ".mp4"}; +namespace PhotoRenamer; - public static IEnumerable FindSupportedFilesRecursively(string path) +public static class FilesHelper +{ + private static readonly string[] SupportedFileExtensions = { ".jpg", ".cr2", ".mp4" }; + + public static IEnumerable FindSupportedFilesRecursively(string path) + { + var files = Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories); + foreach (var file in files) { - var files = Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories); - foreach (var file in files) + var fileExt = Path.GetExtension(file); + if (fileExt is null) + continue; + foreach (var supportedFileExtension in SupportedFileExtensions) { - var fileExt = Path.GetExtension(file); - if (fileExt is null) continue; - foreach (var supportedFileExtension in SupportedFileExtensions) - { - if (fileExt.Equals(supportedFileExtension, StringComparison.InvariantCultureIgnoreCase)) - yield return file; - } + if (fileExt.Equals(supportedFileExtension, StringComparison.InvariantCultureIgnoreCase)) + yield return file; } } } diff --git a/PhotoRenamer/HttpClientExtensions.cs b/PhotoRenamer/HttpClientExtensions.cs deleted file mode 100644 index 9742290..0000000 --- a/PhotoRenamer/HttpClientExtensions.cs +++ /dev/null @@ -1,76 +0,0 @@ -namespace PhotoRenamer -{ - public static class HttpClientExtensions - { - public static async Task DownloadAsync( - this HttpClient client, - string requestUri, - Stream destination, - IProgress? progress = null, - CancellationToken cancellationToken = default - ) - { - // Get the http headers first to examine the content length - using var response = await client.GetAsync( - requestUri, - HttpCompletionOption.ResponseHeadersRead - ); - var contentLength = response.Content.Headers.ContentLength; - - using var download = await response.Content.ReadAsStreamAsync(cancellationToken); - // Ignore progress reporting when no progress reporter was - // passed or when the content length is unknown - if (progress == null || !contentLength.HasValue) - { - await download.CopyToAsync(destination, cancellationToken); - return; - } - - // Convert absolute progress (bytes downloaded) into relative progress (0% - 100%) - var relativeProgress = new Progress( - totalBytes => progress.Report((float)totalBytes / contentLength.Value) - ); - // Use extension method to report progress while downloading - await download.CopyToAsync(destination, 81920, relativeProgress, cancellationToken); - progress.Report(1); - } - - 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)); - if (!source.CanRead) - throw new ArgumentException("Has to be readable", nameof(source)); - if (destination == null) - throw new ArgumentNullException(nameof(destination)); - if (!destination.CanWrite) - throw new ArgumentException("Has to be writable", nameof(destination)); - if (bufferSize < 0) - throw new ArgumentOutOfRangeException(nameof(bufferSize)); - - var buffer = new byte[bufferSize]; - long totalBytesRead = 0; - int bytesRead; - while ( - ( - bytesRead = await source - .ReadAsync(buffer, 0, buffer.Length, cancellationToken) - .ConfigureAwait(false) - ) != 0 - ) - { - await destination - .WriteAsync(buffer, 0, bytesRead, cancellationToken) - .ConfigureAwait(false); - totalBytesRead += bytesRead; - progress?.Report(totalBytesRead); - } - } - } -} diff --git a/PhotoRenamer/PhotoRenamer.csproj b/PhotoRenamer/PhotoRenamer.csproj index acf406a..302592d 100644 --- a/PhotoRenamer/PhotoRenamer.csproj +++ b/PhotoRenamer/PhotoRenamer.csproj @@ -7,7 +7,14 @@ enable + + true + true + true + + + diff --git a/PhotoRenamer/PhotoRenamer.csproj.DotSettings b/PhotoRenamer/PhotoRenamer.csproj.DotSettings deleted file mode 100644 index 6162834..0000000 --- a/PhotoRenamer/PhotoRenamer.csproj.DotSettings +++ /dev/null @@ -1,2 +0,0 @@ - - CSharp90 \ No newline at end of file diff --git a/PhotoRenamer/Program.cs b/PhotoRenamer/Program.cs index 98ae2d0..8bf7e34 100644 --- a/PhotoRenamer/Program.cs +++ b/PhotoRenamer/Program.cs @@ -1,34 +1,33 @@ using Microsoft.Extensions.Configuration; using Serilog; -namespace PhotoRenamer -{ - internal static class Program - { - private static int Main(string[] args) - { - var configuration = CreateHostBuilder(args).Build(); - foreach (var (key, value) in configuration.AsEnumerable()) - { - Console.WriteLine($"{{{key}: {value}}}"); - } - Log.Logger = new LoggerConfiguration().WriteTo.Console().CreateLogger(); - try - { - var renamer = new Renamer(configuration); - return renamer.Run(); - } - catch (Exception e) - { - Log.Error(e, "Error executing program"); - return -1; - } - } +namespace PhotoRenamer; - public static IConfigurationBuilder CreateHostBuilder(string[] args) => new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .AddJsonFile(AppDomain.CurrentDomain.BaseDirectory + "\\appsettings.json", optional: true, - reloadOnChange: true) - .AddCommandLine(args); +internal static class Program +{ + private static int Main(string[] args) + { + var configuration = CreateHostBuilder(args).Build(); + foreach (var (key, value) in configuration.AsEnumerable()) + { + Console.WriteLine($"{{{key}: {value}}}"); + } + Log.Logger = new LoggerConfiguration().WriteTo.Console().CreateLogger(); + try + { + var renamer = new Renamer(configuration); + return renamer.Run(); + } + catch (Exception e) + { + Log.Error(e, "Error executing program"); + return -1; + } } + + public static IConfigurationBuilder CreateHostBuilder(string[] args) => + new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile(AppDomain.CurrentDomain.BaseDirectory + "\\appsettings.json", optional: true, reloadOnChange: true) + .AddCommandLine(args); } diff --git a/PhotoRenamer/Renamer.cs b/PhotoRenamer/Renamer.cs index 44fcd4e..9353c2e 100644 --- a/PhotoRenamer/Renamer.cs +++ b/PhotoRenamer/Renamer.cs @@ -13,6 +13,7 @@ namespace PhotoRenamer private readonly ProgressBarOptions _childOptions; private readonly string _sourcePath; private readonly string _targetPath; + private int _currentCount; public Renamer(IConfiguration configuration) { @@ -39,71 +40,49 @@ namespace PhotoRenamer BackgroundColor = ConsoleColor.DarkGray, BackgroundCharacter = '\u2593' }; - var i = 0; + _currentCount = 0; using var progressBar = new ProgressBar(files.Length, "Copying files", options); - var po = new ParallelOptions { MaxDegreeOfParallelism = 4 }; - Parallel.ForEach( - files, - po, - file => - { - try - { - var directories = ImageMetadataReader.ReadMetadata(file); - var dateTime = - GetDateTimeFromExif(directories) - ?? GetDateTimeFromMp4(directories) - ?? GetDateTimeFromLastWrite(file); - var folder = CreateFolder(dateTime); - CopyFile(folder, file, progressBar); - } - catch (ImageProcessingException) - { - //silently ignore - } - finally - { - Interlocked.Increment(ref i); - progressBar.Tick(i); - } - } - ); + var po = new ParallelOptions { MaxDegreeOfParallelism = 1 }; + + Parallel.ForEach(files, po, file => Body(file, progressBar)); return 0; } + private void Body(string file, ProgressBar progressBar) + { + try + { + var directories = ImageMetadataReader.ReadMetadata(file); + var dateTime = GetDateTimeFromExif(directories) ?? GetDateTimeFromMp4(directories) ?? GetDateTimeFromLastWrite(file); + var folder = CreateFolder(dateTime); + CopyFile(folder, file, progressBar); + } + catch (ImageProcessingException) + { + //silently ignore + } + finally + { + Interlocked.Increment(ref _currentCount); + progressBar.Tick(_currentCount); + } + } + private static DateTime GetDateTimeFromLastWrite(string file) { var creationTime = File.GetLastWriteTime(file); return creationTime; } - private static DateTime? GetDateTimeFromExif( - IEnumerable directories - ) + private static DateTime? GetDateTimeFromExif(IEnumerable directories) { - DateTime dateTime = default; - return - directories - .OfType() - .FirstOrDefault() - ?.TryGetDateTime(ExifDirectoryBase.TagDateTime, out dateTime) == true - ? (DateTime?)dateTime - : null; + return directories.OfType().FirstOrDefault()?.TryGetDateTime(ExifDirectoryBase.TagDateTime, out var dateTime) == true ? dateTime : null; } - private static DateTime? GetDateTimeFromMp4( - IEnumerable directories - ) + private static DateTime? GetDateTimeFromMp4(IEnumerable directories) { - DateTime dateTime = default; - return - directories - .OfType() - .FirstOrDefault() - ?.TryGetDateTime(QuickTimeMovieHeaderDirectory.TagCreated, out dateTime) == true - ? (DateTime?)dateTime - : null; + return directories.OfType().FirstOrDefault()?.TryGetDateTime(QuickTimeMovieHeaderDirectory.TagCreated, out var dateTime) == true ? dateTime : null; } private static void ResetTimes(FileSystemInfo destination, FileSystemInfo source) @@ -122,11 +101,13 @@ namespace PhotoRenamer return; using var child = progressBar.Spawn(100, destination.FullName, _childOptions); - using var client = new HttpClient(); - using var fileStream = File.OpenWrite(destination.FullName); - var progress = new Progress(o => progressBar.Tick((int)o * 100)); - await client.DownloadAsync(source.FullName, fileStream, progress); + 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); + + var progress = new Progress(o => progressBar.Tick((int)o * 100)); + await sourceStream.CopyToAsync(targetStream, 81920, progress); + ResetTimes(destination, source); child.Tick(100); @@ -134,13 +115,12 @@ namespace PhotoRenamer private string CreateFolder(DateTime dateTime) { - var folder = Path.Combine( - _targetPath, - dateTime.Year.ToString(), - dateTime.Month.ToString("D2") - ); + 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 new file mode 100644 index 0000000..7c1a936 --- /dev/null +++ b/PhotoRenamer/StreamExtensions.cs @@ -0,0 +1,28 @@ +namespace PhotoRenamer; + +public static class StreamExtensions +{ + 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)); + if (!source.CanRead) + throw new ArgumentException("Has to be readable", nameof(source)); + if (destination == null) + throw new ArgumentNullException(nameof(destination)); + if (!destination.CanWrite) + throw new ArgumentException("Has to be writable", nameof(destination)); + if (bufferSize < 0) + throw new ArgumentOutOfRangeException(nameof(bufferSize)); + + var buffer = new byte[bufferSize]; + long totalBytesRead = 0; + int bytesRead; + while ((bytesRead = await source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) != 0) + { + await destination.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken).ConfigureAwait(false); + totalBytesRead += bytesRead; + progress?.Report(totalBytesRead); + } + } +} diff --git a/PhotoRenamer/Types/MediaFile.cs b/PhotoRenamer/Types/MediaFile.cs index f3f1a86..b7c087c 100644 --- a/PhotoRenamer/Types/MediaFile.cs +++ b/PhotoRenamer/Types/MediaFile.cs @@ -1,15 +1,4 @@ namespace PhotoRenamer.Types { - public class MediaFile - { - public MediaFile(string path, DateTime creationDate) - { - Path = path; - CreationDate = creationDate; - } - - public string Path { get; } - public DateTime CreationDate { get; } - - } -} \ No newline at end of file + public record MediaFile(string Path, DateTime CreationDate); +} diff --git a/PhotoRenamer/appsettings.json b/PhotoRenamer/appsettings.json index d44ce81..c2e8346 100644 --- a/PhotoRenamer/appsettings.json +++ b/PhotoRenamer/appsettings.json @@ -1,4 +1,4 @@ { - "Source": "C:\\Users\\Holger\\Desktop\\2013-08-01", - "Target": "C:\\Users\\Holger\\Desktop\\Oneplus5" + "Source": "D:\\Nextcloud2\\Upload", + "Target": "D:\\UploadX" } \ No newline at end of file