small changes
This commit is contained in:
parent
4157993abd
commit
99ab384ade
@ -4,32 +4,31 @@ using System.Linq;
|
|||||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
using PhotoRenamer.Types;
|
using PhotoRenamer.Types;
|
||||||
|
|
||||||
namespace PhotoRenamer.Test
|
namespace PhotoRenamer.Test;
|
||||||
|
|
||||||
|
[TestClass]
|
||||||
|
public class FilesTest
|
||||||
{
|
{
|
||||||
[TestClass]
|
private readonly string[] _files;
|
||||||
public class FilesTest
|
|
||||||
|
public FilesTest()
|
||||||
{
|
{
|
||||||
private readonly string[] _files;
|
var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets");
|
||||||
|
_files = FilesHelper.FindSupportedFilesRecursively(path).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
public FilesTest()
|
[TestMethod, Priority(0)]
|
||||||
{
|
public void FindSupportedFiles()
|
||||||
var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets");
|
{
|
||||||
_files = FilesHelper.FindSupportedFilesRecursively(path).ToArray();
|
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(0)]
|
[TestMethod, Priority(1)]
|
||||||
public void FindSupportedFiles()
|
public void GetMetaData()
|
||||||
{
|
{
|
||||||
Assert.AreEqual(2, _files.Length);
|
var expected = new[] { new MediaFile("r", DateTime.Now), new MediaFile("r", DateTime.Now) };
|
||||||
Assert.AreEqual("IMG_20120528_125912.jpg", Path.GetFileName(_files[0]));
|
// var actual = FilesHelper.GetMediaFiles(_files);
|
||||||
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
|
namespace PhotoRenamer;
|
||||||
{
|
|
||||||
public static class FilesHelper
|
|
||||||
{
|
|
||||||
private static readonly string[] SupportedFileExtensions = {".jpg", ".cr2", ".mp4"};
|
|
||||||
|
|
||||||
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);
|
var fileExt = Path.GetExtension(file);
|
||||||
foreach (var file in files)
|
if (fileExt is null)
|
||||||
|
continue;
|
||||||
|
foreach (var supportedFileExtension in SupportedFileExtensions)
|
||||||
{
|
{
|
||||||
var fileExt = Path.GetExtension(file);
|
if (fileExt.Equals(supportedFileExtension, StringComparison.InvariantCultureIgnoreCase))
|
||||||
if (fileExt is null) continue;
|
yield return file;
|
||||||
foreach (var supportedFileExtension in SupportedFileExtensions)
|
|
||||||
{
|
|
||||||
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>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<PublishAot>true</PublishAot>
|
||||||
|
<UseSystemResourceKeys>true</UseSystemResourceKeys>
|
||||||
|
<InvariantGlobalization>true</InvariantGlobalization>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="PublishAotCompressed" Version="1.0.0" />
|
||||||
<PackageReference Include="MetadataExtractor" Version="2.7.2" />
|
<PackageReference Include="MetadataExtractor" Version="2.7.2" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" 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 Microsoft.Extensions.Configuration;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
|
||||||
namespace PhotoRenamer
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IConfigurationBuilder CreateHostBuilder(string[] args) => new ConfigurationBuilder()
|
internal static class Program
|
||||||
.SetBasePath(Directory.GetCurrentDirectory())
|
{
|
||||||
.AddJsonFile(AppDomain.CurrentDomain.BaseDirectory + "\\appsettings.json", optional: true,
|
private static int Main(string[] args)
|
||||||
reloadOnChange: true)
|
{
|
||||||
.AddCommandLine(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 ProgressBarOptions _childOptions;
|
||||||
private readonly string _sourcePath;
|
private readonly string _sourcePath;
|
||||||
private readonly string _targetPath;
|
private readonly string _targetPath;
|
||||||
|
private int _currentCount;
|
||||||
|
|
||||||
public Renamer(IConfiguration configuration)
|
public Renamer(IConfiguration configuration)
|
||||||
{
|
{
|
||||||
@ -39,71 +40,49 @@ namespace PhotoRenamer
|
|||||||
BackgroundColor = ConsoleColor.DarkGray,
|
BackgroundColor = ConsoleColor.DarkGray,
|
||||||
BackgroundCharacter = '\u2593'
|
BackgroundCharacter = '\u2593'
|
||||||
};
|
};
|
||||||
var i = 0;
|
_currentCount = 0;
|
||||||
using var progressBar = new ProgressBar(files.Length, "Copying files", options);
|
using var progressBar = new ProgressBar(files.Length, "Copying files", options);
|
||||||
var po = new ParallelOptions { MaxDegreeOfParallelism = 4 };
|
var po = new ParallelOptions { MaxDegreeOfParallelism = 1 };
|
||||||
Parallel.ForEach(
|
|
||||||
files,
|
Parallel.ForEach(files, po, file => Body(file, progressBar));
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return 0;
|
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)
|
private static DateTime GetDateTimeFromLastWrite(string file)
|
||||||
{
|
{
|
||||||
var creationTime = File.GetLastWriteTime(file);
|
var creationTime = File.GetLastWriteTime(file);
|
||||||
return creationTime;
|
return creationTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static DateTime? GetDateTimeFromExif(
|
private static DateTime? GetDateTimeFromExif(IEnumerable<MetadataExtractor.Directory> directories)
|
||||||
IEnumerable<MetadataExtractor.Directory> directories
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
DateTime dateTime = default;
|
return directories.OfType<ExifIfd0Directory>().FirstOrDefault()?.TryGetDateTime(ExifDirectoryBase.TagDateTime, out var dateTime) == true ? dateTime : null;
|
||||||
return
|
|
||||||
directories
|
|
||||||
.OfType<ExifIfd0Directory>()
|
|
||||||
.FirstOrDefault()
|
|
||||||
?.TryGetDateTime(ExifDirectoryBase.TagDateTime, out dateTime) == true
|
|
||||||
? (DateTime?)dateTime
|
|
||||||
: null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static DateTime? GetDateTimeFromMp4(
|
private static DateTime? GetDateTimeFromMp4(IEnumerable<MetadataExtractor.Directory> directories)
|
||||||
IEnumerable<MetadataExtractor.Directory> directories
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
DateTime dateTime = default;
|
return directories.OfType<QuickTimeMovieHeaderDirectory>().FirstOrDefault()?.TryGetDateTime(QuickTimeMovieHeaderDirectory.TagCreated, out var dateTime) == true ? dateTime : null;
|
||||||
return
|
|
||||||
directories
|
|
||||||
.OfType<QuickTimeMovieHeaderDirectory>()
|
|
||||||
.FirstOrDefault()
|
|
||||||
?.TryGetDateTime(QuickTimeMovieHeaderDirectory.TagCreated, out dateTime) == true
|
|
||||||
? (DateTime?)dateTime
|
|
||||||
: null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ResetTimes(FileSystemInfo destination, FileSystemInfo source)
|
private static void ResetTimes(FileSystemInfo destination, FileSystemInfo source)
|
||||||
@ -122,11 +101,13 @@ namespace PhotoRenamer
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
using var child = progressBar.Spawn(100, destination.FullName, _childOptions);
|
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 using var sourceStream = File.Open(source.FullName, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
await client.DownloadAsync(source.FullName, fileStream, progress);
|
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);
|
ResetTimes(destination, source);
|
||||||
|
|
||||||
child.Tick(100);
|
child.Tick(100);
|
||||||
@ -134,13 +115,12 @@ namespace PhotoRenamer
|
|||||||
|
|
||||||
private string CreateFolder(DateTime dateTime)
|
private string CreateFolder(DateTime dateTime)
|
||||||
{
|
{
|
||||||
var folder = Path.Combine(
|
var folder = Path.Combine(_targetPath, dateTime.Year.ToString(), dateTime.Month.ToString("D2"));
|
||||||
_targetPath,
|
|
||||||
dateTime.Year.ToString(),
|
|
||||||
dateTime.Month.ToString("D2")
|
|
||||||
);
|
|
||||||
if (!Directory.Exists(folder))
|
if (!Directory.Exists(folder))
|
||||||
|
{
|
||||||
Directory.CreateDirectory(folder);
|
Directory.CreateDirectory(folder);
|
||||||
|
}
|
||||||
|
|
||||||
return 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
|
namespace PhotoRenamer.Types
|
||||||
{
|
{
|
||||||
public class MediaFile
|
public record MediaFile(string Path, DateTime CreationDate);
|
||||||
{
|
|
||||||
public MediaFile(string path, DateTime creationDate)
|
|
||||||
{
|
|
||||||
Path = path;
|
|
||||||
CreationDate = creationDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Path { get; }
|
|
||||||
public DateTime CreationDate { get; }
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"Source": "C:\\Users\\Holger\\Desktop\\2013-08-01",
|
"Source": "D:\\Nextcloud2\\Upload",
|
||||||
"Target": "C:\\Users\\Holger\\Desktop\\Oneplus5"
|
"Target": "D:\\UploadX"
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user