small changes
This commit is contained in:
parent
4157993abd
commit
99ab384ade
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
}
|
||||
|
@ -1,21 +1,21 @@
|
||||
namespace PhotoRenamer
|
||||
{
|
||||
public static class FilesHelper
|
||||
{
|
||||
private static readonly string[] SupportedFileExtensions = {".jpg", ".cr2", ".mp4"};
|
||||
namespace PhotoRenamer;
|
||||
|
||||
public static IEnumerable<string> FindSupportedFilesRecursively(string path)
|
||||
public static class FilesHelper
|
||||
{
|
||||
private static readonly string[] SupportedFileExtensions = { ".jpg", ".cr2", ".mp4" };
|
||||
|
||||
public static IEnumerable<string> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,76 +0,0 @@
|
||||
namespace PhotoRenamer
|
||||
{
|
||||
public static class HttpClientExtensions
|
||||
{
|
||||
public static async Task DownloadAsync(
|
||||
this HttpClient client,
|
||||
string requestUri,
|
||||
Stream destination,
|
||||
IProgress<float>? 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<long>(
|
||||
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<long>? 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -7,7 +7,14 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<PublishAot>true</PublishAot>
|
||||
<UseSystemResourceKeys>true</UseSystemResourceKeys>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="PublishAotCompressed" Version="1.0.0" />
|
||||
<PackageReference Include="MetadataExtractor" Version="2.7.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="7.0.0" />
|
||||
|
@ -1,2 +0,0 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:String x:Key="/Default/CodeInspection/CSharpLanguageProject/LanguageLevel/@EntryValue">CSharp90</s:String></wpf:ResourceDictionary>
|
@ -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);
|
||||
}
|
||||
|
@ -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<MetadataExtractor.Directory> directories
|
||||
)
|
||||
private static DateTime? GetDateTimeFromExif(IEnumerable<MetadataExtractor.Directory> directories)
|
||||
{
|
||||
DateTime dateTime = default;
|
||||
return
|
||||
directories
|
||||
.OfType<ExifIfd0Directory>()
|
||||
.FirstOrDefault()
|
||||
?.TryGetDateTime(ExifDirectoryBase.TagDateTime, out dateTime) == true
|
||||
? (DateTime?)dateTime
|
||||
: null;
|
||||
return directories.OfType<ExifIfd0Directory>().FirstOrDefault()?.TryGetDateTime(ExifDirectoryBase.TagDateTime, out var dateTime) == true ? dateTime : null;
|
||||
}
|
||||
|
||||
private static DateTime? GetDateTimeFromMp4(
|
||||
IEnumerable<MetadataExtractor.Directory> directories
|
||||
)
|
||||
private static DateTime? GetDateTimeFromMp4(IEnumerable<MetadataExtractor.Directory> directories)
|
||||
{
|
||||
DateTime dateTime = default;
|
||||
return
|
||||
directories
|
||||
.OfType<QuickTimeMovieHeaderDirectory>()
|
||||
.FirstOrDefault()
|
||||
?.TryGetDateTime(QuickTimeMovieHeaderDirectory.TagCreated, out dateTime) == true
|
||||
? (DateTime?)dateTime
|
||||
: null;
|
||||
return directories.OfType<QuickTimeMovieHeaderDirectory>().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<float>(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<long>(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;
|
||||
}
|
||||
}
|
||||
|
28
PhotoRenamer/StreamExtensions.cs
Normal file
28
PhotoRenamer/StreamExtensions.cs
Normal file
@ -0,0 +1,28 @@
|
||||
namespace PhotoRenamer;
|
||||
|
||||
public static class StreamExtensions
|
||||
{
|
||||
public static async Task CopyToAsync(this Stream source, Stream destination, int bufferSize, IProgress<long>? 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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; }
|
||||
|
||||
}
|
||||
}
|
||||
public record MediaFile(string Path, DateTime CreationDate);
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user