Compare commits

...

24 Commits

Author SHA1 Message Date
a6129a73c0 updated nuget packages 2023-12-08 21:32:05 +01:00
3e40ab9eec Working on the gui 2023-11-04 12:08:53 +01:00
52f07cf547 Add output folder selector 2023-09-27 22:47:19 +02:00
43a2d756dc Add gui for photo renamer 2023-09-11 22:23:34 +02:00
182813bd21 Works better now 2022-12-01 17:08:03 +01:00
e705d0d286 code cleanup 2022-12-01 14:07:25 +01:00
99ab384ade small changes 2022-12-01 14:04:35 +01:00
4157993abd Updated to dotnet 7 2022-11-30 15:34:29 +01:00
232846f4d1 updated nuget packages 2020-12-09 22:25:09 +01:00
47822a4693 updated to net 5 2020-12-09 22:21:24 +01:00
63d4f49bdd Cleanup 2020-11-01 22:14:05 +01:00
a8e89b841e Get lastAccessTime from file 2020-11-01 20:35:50 +01:00
64a14f39c1 added reset times 2020-10-31 22:39:06 +01:00
01b2c0e5e7 updated nice progress bars 2020-10-31 21:56:27 +01:00
02d757f35e QuickIO is not needed 2020-10-21 22:36:34 +02:00
29c8fe06c6 This version will wok more properly 2020-10-20 00:10:28 +02:00
e62c996c6e refactoring 2020-09-01 17:29:24 +02:00
eb11d7d2db first working version 2020-05-25 21:30:17 +02:00
4455254952 added app settings and added progress bar 2020-05-24 22:02:27 +02:00
1d316fbe27 woring version 2020-05-22 22:06:32 +02:00
aad679b3f1 ... 2020-05-14 21:14:13 +02:00
96e90f8693 Restart process 2020-05-14 19:35:58 +02:00
ad1e92362d added LoggingModule 2020-01-11 21:15:49 +01:00
a17ea99642 Working on new method 2019-12-16 21:18:54 +01:00
35 changed files with 700 additions and 303 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@ -1,26 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace PhotoRenamer.Base
{
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 fileExt = Path.GetExtension(file);
if(fileExt == null) continue;
foreach (var supportedFileExtension in SupportedFileExtensions)
{
if (fileExt.Equals(supportedFileExtension, StringComparison.InvariantCultureIgnoreCase)) yield return file;
}
}
}
}
}

View File

@ -1,16 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ExifLibNet" Version="2.1.1" />
<PackageReference Include="JetBrains.Annotations" Version="2019.1.3" />
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@ -1,29 +0,0 @@
using System;
using PhotoRenamer.File.Views;
using Prism.Ioc;
using Prism.Modularity;
using Prism.Regions;
namespace PhotoRenamer.File
{
public class FileModule : IModule
{
private readonly IRegionManager _regionManager;
public FileModule(IRegionManager regionManager)
{
_regionManager = regionManager;
}
public void OnInitialized(IContainerProvider containerProvider)
{
}
public void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.RegisterForNavigation<ViewA>();
_regionManager.RequestNavigate("ContentRegion", new Uri(nameof(ViewA), UriKind.Relative));
}
}
}

View File

@ -1,15 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
<UseWPF>true</UseWPF>
<AssemblyName>PhotoRenamer.File</AssemblyName>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2019.1.3" />
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Prism.Wpf" Version="7.2.0.1422" />
</ItemGroup>
</Project>

View File

@ -1,14 +0,0 @@
using Prism.Mvvm;
using System.Windows.Input;
using JetBrains.Annotations;
namespace PhotoRenamer.File.ViewModels
{
[UsedImplicitly]
public class ViewAViewModel : BindableBase
{
public ICommand SelectFilesCommand { get; }
public ICommand ClearCommand { get; }
}
}

View File

@ -1,41 +0,0 @@
<UserControl
x:Class="PhotoRenamer.File.Views.ViewA"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:prism="http://prismlibrary.com/"
xmlns:viewModels="clr-namespace:PhotoRenamer.File.ViewModels"
d:DataContext="{d:DesignInstance viewModels:ViewAViewModel}"
d:DesignHeight="300"
d:DesignWidth="300"
prism:ViewModelLocator.AutoWireViewModel="True"
mc:Ignorable="d">
<DockPanel>
<ToolBarTray DockPanel.Dock="Top" Orientation="Horizontal">
<ToolBar ToolBarTray.IsLocked="True">
<Button Command="{Binding SelectFilesCommand}" Content="Select Files" />
<Button Command="{Binding ClearCommand}" Content="Clear" />
</ToolBar>
</ToolBarTray>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<GroupBox
Grid.Row="0"
Margin="5"
Header="Source">
<ListView />
</GroupBox>
<GroupBox
Grid.Row="2"
Margin="5"
Header="Target">
<ListView />
</GroupBox>
</Grid>
</DockPanel>
</UserControl>

View File

@ -1,13 +0,0 @@
namespace PhotoRenamer.File.Views
{
/// <summary>
/// Interaction logic for ViewA.xaml
/// </summary>
public partial class ViewA
{
public ViewA()
{
InitializeComponent();
}
}
}

View File

@ -2,32 +2,33 @@ using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting;
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 = Base.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()
{
}
} }
} }

View File

@ -1,21 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2019.1.3" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8"> <PackageReference Include="MSTest.TestAdapter" Version="3.1.1" />
<PrivateAssets>all</PrivateAssets> <PackageReference Include="MSTest.TestFramework" Version="3.1.1" />
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <PackageReference Include="coverlet.collector" Version="6.0.0">
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="MSTest.TestAdapter" Version="2.0.0" />
<PackageReference Include="MSTest.TestFramework" Version="2.0.0" />
<PackageReference Include="coverlet.collector" Version="1.1.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
@ -40,7 +34,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\PhotoRenamer.Base\PhotoRenamer.Base.csproj" /> <ProjectReference Include="..\PhotoRenamer\PhotoRenamer.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -5,11 +5,9 @@ VisualStudioVersion = 16.0.29609.76
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PhotoRenamer", "PhotoRenamer\PhotoRenamer.csproj", "{C2C8C238-CB3D-4DDA-96FC-297D029F6022}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PhotoRenamer", "PhotoRenamer\PhotoRenamer.csproj", "{C2C8C238-CB3D-4DDA-96FC-297D029F6022}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhotoRenamer.File", "PhotoRenamer.File\PhotoRenamer.File.csproj", "{185069C2-C1EB-4116-8C33-952A2C0BA1F9}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PhotoRenamer.Test", "PhotoRenamer.Test\PhotoRenamer.Test.csproj", "{07F827BC-CF99-4E81-A5C7-54085C97FC10}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhotoRenamer.Test", "PhotoRenamer.Test\PhotoRenamer.Test.csproj", "{07F827BC-CF99-4E81-A5C7-54085C97FC10}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhotoRenamerGui", "PhotoRenamerGui\PhotoRenamerGui.csproj", "{733313FE-599D-49A4-A909-BBDBFB15E27D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhotoRenamer.Base", "PhotoRenamer.Base\PhotoRenamer.Base.csproj", "{DE85C395-3D50-4C12-9B7A-021180E26CE1}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -21,18 +19,14 @@ Global
{C2C8C238-CB3D-4DDA-96FC-297D029F6022}.Debug|Any CPU.Build.0 = Debug|Any CPU {C2C8C238-CB3D-4DDA-96FC-297D029F6022}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C2C8C238-CB3D-4DDA-96FC-297D029F6022}.Release|Any CPU.ActiveCfg = Release|Any CPU {C2C8C238-CB3D-4DDA-96FC-297D029F6022}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C2C8C238-CB3D-4DDA-96FC-297D029F6022}.Release|Any CPU.Build.0 = Release|Any CPU {C2C8C238-CB3D-4DDA-96FC-297D029F6022}.Release|Any CPU.Build.0 = Release|Any CPU
{185069C2-C1EB-4116-8C33-952A2C0BA1F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{185069C2-C1EB-4116-8C33-952A2C0BA1F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{185069C2-C1EB-4116-8C33-952A2C0BA1F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{185069C2-C1EB-4116-8C33-952A2C0BA1F9}.Release|Any CPU.Build.0 = Release|Any CPU
{07F827BC-CF99-4E81-A5C7-54085C97FC10}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {07F827BC-CF99-4E81-A5C7-54085C97FC10}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{07F827BC-CF99-4E81-A5C7-54085C97FC10}.Debug|Any CPU.Build.0 = Debug|Any CPU {07F827BC-CF99-4E81-A5C7-54085C97FC10}.Debug|Any CPU.Build.0 = Debug|Any CPU
{07F827BC-CF99-4E81-A5C7-54085C97FC10}.Release|Any CPU.ActiveCfg = Release|Any CPU {07F827BC-CF99-4E81-A5C7-54085C97FC10}.Release|Any CPU.ActiveCfg = Release|Any CPU
{07F827BC-CF99-4E81-A5C7-54085C97FC10}.Release|Any CPU.Build.0 = Release|Any CPU {07F827BC-CF99-4E81-A5C7-54085C97FC10}.Release|Any CPU.Build.0 = Release|Any CPU
{DE85C395-3D50-4C12-9B7A-021180E26CE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {733313FE-599D-49A4-A909-BBDBFB15E27D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DE85C395-3D50-4C12-9B7A-021180E26CE1}.Debug|Any CPU.Build.0 = Debug|Any CPU {733313FE-599D-49A4-A909-BBDBFB15E27D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DE85C395-3D50-4C12-9B7A-021180E26CE1}.Release|Any CPU.ActiveCfg = Release|Any CPU {733313FE-599D-49A4-A909-BBDBFB15E27D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DE85C395-3D50-4C12-9B7A-021180E26CE1}.Release|Any CPU.Build.0 = Release|Any CPU {733313FE-599D-49A4-A909-BBDBFB15E27D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

27
PhotoRenamer/.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,27 @@
{
// Use IntelliSense to find out which attributes exist for C# debugging
// Use hover for the description of the existing attributes
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
"version": "0.2.0",
"configurations": [
{
"name": ".NET Core Launch (console)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/bin/Debug/netcoreapp3.1/PhotoRenamer.dll",
"args": [],
"cwd": "${workspaceFolder}",
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
"console": "internalConsole",
"stopAtEntry": false
},
{
"name": ".NET Core Attach",
"type": "coreclr",
"request": "attach",
"processId": "${command:pickProcess}"
}
]
}

42
PhotoRenamer/.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,42 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/PhotoRenamer.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "publish",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}/PhotoRenamer.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "watch",
"command": "dotnet",
"type": "process",
"args": [
"watch",
"run",
"${workspaceFolder}/PhotoRenamer.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
}
]
}

View File

@ -1,7 +0,0 @@
<prism:PrismApplication
x:Class="PhotoRenamer.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:prism="http://prismlibrary.com/">
<Application.Resources />
</prism:PrismApplication>

View File

@ -1,30 +0,0 @@
using Prism.Ioc;
using PhotoRenamer.Views;
using System.Windows;
using PhotoRenamer.File;
using Prism.Modularity;
namespace PhotoRenamer
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App
{
protected override Window CreateShell()
{
return Container.Resolve<MainWindow>();
}
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
}
protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
{
moduleCatalog.AddModule<FileModule>();
base.ConfigureModuleCatalog(moduleCatalog);
}
}
}

View File

@ -0,0 +1,25 @@
namespace PhotoRenamer;
public static class FilesHelper
{
private static readonly string[] SupportedFileExtensions = { ".jpg", ".cr2", ".mp4", ".mov" };
public static IEnumerable<string> FindSupportedFilesRecursively(string path)
{
var files = Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories);
foreach (var file in files)
{
var fileExt = Path.GetExtension(file);
foreach (var supportedFileExtension in SupportedFileExtensions)
{
if (
fileExt.Equals(
supportedFileExtension,
StringComparison.InvariantCultureIgnoreCase
)
)
yield return file;
}
}
}
}

View File

@ -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);
}
}

View File

@ -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<MetadataExtractor.Directory> directories
)
{
return
directories
.OfType<ExifIfd0Directory>()
.FirstOrDefault()
?.TryGetDateTime(ExifDirectoryBase.TagDateTime, out var dateTime) == true
? dateTime
: null;
}
private static DateTime? GetDateTimeFromMp4(
IEnumerable<MetadataExtractor.Directory> directories
)
{
return
directories
.OfType<QuickTimeMovieHeaderDirectory>()
.FirstOrDefault()
?.TryGetDateTime(QuickTimeMovieHeaderDirectory.TagCreated, out var dateTime) == true
? dateTime
: null;
}
}

View File

@ -1,25 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>WinExe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<UseWPF>true</UseWPF> <Nullable>enable</Nullable>
<AssemblyName>PhotoRenamer</AssemblyName> <ImplicitUsings>enable</ImplicitUsings>
<StartupObject>PhotoRenamer.App</StartupObject>
<PublishTrimmed>true</PublishTrimmed>
<PublishReadyToRun>true</PublishReadyToRun>
<PublishSingleFile>false</PublishSingleFile>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<RunAnalyzersDuringBuild>true</RunAnalyzersDuringBuild>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2019.1.3" /> <PackageReference Include="PublishAotCompressed" Version="1.0.2" />
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8"> <PackageReference Include="MetadataExtractor" Version="2.8.1" />
<PrivateAssets>all</PrivateAssets> <PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="8.0.0" />
</PackageReference> <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
<PackageReference Include="Prism.DryIoc" Version="7.2.0.1422" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="Serilog" Version="3.1.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
<PackageReference Include="Spectre.Console" Version="0.48.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\PhotoRenamer.File\PhotoRenamer.File.csproj" /> <None Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup> </ItemGroup>
</Project>
</Project>

37
PhotoRenamer/Program.cs Normal file
View File

@ -0,0 +1,37 @@
using Microsoft.Extensions.Configuration;
using Serilog;
namespace PhotoRenamer;
internal static class Program
{
private static int Main(string[] args)
{
Log.Logger = new LoggerConfiguration().WriteTo.Console().CreateLogger();
var configuration = CreateHostBuilder(args).Build();
foreach (var (key, value) in configuration.AsEnumerable())
{
Log.Information($"{{{key}: {value}}}");
}
try
{
var renamer = new Renamer(configuration);
return renamer.RunAsync().GetAwaiter().GetResult();
}
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);
}

111
PhotoRenamer/Renamer.cs Normal file
View File

@ -0,0 +1,111 @@
using Microsoft.Extensions.Configuration;
using Serilog;
using Spectre.Console;
namespace PhotoRenamer;
internal class Renamer
{
private readonly string _sourcePath;
private readonly string _targetPath;
private int _currentCount;
public Renamer(IConfiguration configuration)
{
_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<int> RunAsync()
{
var files = FolderExtension.GetAllFiles(_sourcePath);
_currentCount = 0;
var po = new ParallelOptions { MaxDegreeOfParallelism = 3 };
await AnsiConsole
.Progress()
.StartAsync(async ctx =>
{
await Parallel.ForEachAsync(files, po, (s, token) => Body(s, ctx, token));
});
return 0;
}
private async ValueTask Body(
string file,
ProgressContext progressContext,
CancellationToken token
)
{
if (token.IsCancellationRequested)
return;
var task = progressContext.AddTask($"[green]{file}[/]");
try
{
var dateTime = MediaFileExtension.GetDateTimeFromSourceFile(file);
var folder = FolderExtension.CreateFolderStructureByDate(
_targetPath,
DateOnly.FromDateTime(dateTime)
);
var progress = new Progress<double>(v => task.Value = v * 100);
await CopyFileAsync(folder, file, progress);
}
catch (Exception ex)
{
Log.Error(ex, "Error reading file information");
}
finally
{
Interlocked.Increment(ref _currentCount);
task.StopTask();
}
}
private static void ResetTimes(FileSystemInfo destination, FileSystemInfo source)
{
destination.LastWriteTime = source.LastWriteTime;
destination.LastWriteTimeUtc = source.LastWriteTimeUtc;
destination.CreationTime = source.CreationTime;
destination.CreationTimeUtc = source.CreationTimeUtc;
}
private static async Task CopyFileAsync(
string folder,
string file,
IProgress<double>? progress = null
)
{
var destination = new FileInfo(Path.Combine(folder, Path.GetFileName(file)));
var source = new FileInfo(file);
if (destination.Exists && destination.Length == source.Length)
{
progress?.Report(1);
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 sourceStream.CopyToAsync(targetStream, 81920, progress: progress);
ResetTimes(destination, source);
}
}

View File

@ -0,0 +1,39 @@
namespace PhotoRenamer;
public static class StreamExtensions
{
public static async Task CopyToAsync(
this Stream source,
Stream destination,
int bufferSize,
IProgress<double>? 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 / (double)source.Length);
}
}
}

View File

@ -0,0 +1,3 @@
namespace PhotoRenamer.Types;
public record MediaFile(string Path, DateTime CreationDate);

View File

@ -1,16 +0,0 @@
using JetBrains.Annotations;
using Prism.Mvvm;
namespace PhotoRenamer.ViewModels
{
[UsedImplicitly]
public class MainWindowViewModel : BindableBase
{
private string _title = "Prism Application";
public string Title
{
get => _title;
set => SetProperty(ref _title, value);
}
}
}

View File

@ -1,18 +0,0 @@
<Window
x:Class="PhotoRenamer.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:prism="http://prismlibrary.com/"
xmlns:viewModels="clr-namespace:PhotoRenamer.ViewModels"
Title="{Binding Title}"
Width="525"
Height="350"
d:DataContext="{d:DesignInstance viewModels:MainWindowViewModel}"
prism:ViewModelLocator.AutoWireViewModel="True"
mc:Ignorable="d">
<Grid>
<ContentControl prism:RegionManager.RegionName="ContentRegion" />
</Grid>
</Window>

View File

@ -1,13 +0,0 @@
namespace PhotoRenamer.Views
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow
{
public MainWindow()
{
InitializeComponent();
}
}
}

View File

@ -0,0 +1,4 @@
{
"Source": "/Volumes/EOS_DIGITAL/DCIM/100CANON",
"Target": "/Users/holger/Nextcloud/SofortUploadCamera"
}

12
PhotoRenamerGui/App.axaml Normal file
View File

@ -0,0 +1,12 @@
<Application
RequestedThemeVariant="Default"
x:Class="PhotoRenamerGui.App"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<Application.Styles>
<FluentTheme />
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml" />
</Application.Styles>
</Application>

View File

@ -0,0 +1,23 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
namespace PhotoRenamerGui;
public partial class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow();
}
base.OnFrameworkInitializationCompleted();
}
}

View File

@ -0,0 +1,79 @@
<Window
Title="PhotoRenamerGui"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d"
x:Class="PhotoRenamerGui.MainWindow"
x:DataType="photoRenamerGui:MainWindowViewModel"
xmlns="https://github.com/avaloniaui"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:photoRenamerGui="clr-namespace:PhotoRenamerGui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Window.DataContext>
<photoRenamerGui:MainWindowViewModel />
</Window.DataContext>
<Grid ColumnDefinitions="Auto,*,Auto" RowDefinitions="Auto,Auto,*">
<Grid.Styles>
<Style Selector=":is(TemplatedControl)">
<Setter Property="Margin" Value="0,5" />
</Style>
</Grid.Styles>
<Label
Content="Input Folder"
Grid.Column="0"
Grid.Row="0"
VerticalAlignment="Center"
VerticalContentAlignment="Center" />
<Label
Content="Output Folder"
Grid.Column="0"
Grid.Row="1"
VerticalAlignment="Center"
VerticalContentAlignment="Center" />
<TextBox
Grid.Column="1"
Grid.Row="0"
Text="{Binding InputFolder}"
Watermark="Input">
<TextBox.InnerRightContent>
<Button
Command="{Binding SelectInputFolderCommand}"
Content="..."
Margin="0,5,5,5"
VerticalAlignment="Stretch" />
</TextBox.InnerRightContent>
</TextBox>
<TextBox
Grid.Column="1"
Grid.Row="1"
Text="{Binding OutputFolder}"
Watermark="Output">
<TextBox.InnerRightContent>
<Button
Command="{Binding SelectOutputFolderCommand}"
Content="..."
Margin="0,5,5,5"
VerticalAlignment="Stretch" />
</TextBox.InnerRightContent>
</TextBox>
<DataGrid
AutoGenerateColumns="False"
BorderBrush="Gray"
BorderThickness="1"
Grid.Column="0"
Grid.ColumnSpan="2"
Grid.Row="2"
GridLinesVisibility="All"
IsReadOnly="True"
ItemsSource="{Binding MediaFiles}">
<DataGrid.Columns>
<DataGridCheckBoxColumn Binding="{Binding IsSelected}" />
<DataGridTextColumn Binding="{Binding SourceDirectory}" Header="Source" />
<DataGridTextColumn Binding="{Binding TargetDirectory}" Header="Target" />
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>

View File

@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace PhotoRenamerGui;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}

View File

@ -0,0 +1,78 @@
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;
public partial class MainWindowViewModel : ObservableObject
{
[ObservableProperty] private string? _inputFolder;
[ObservableProperty] private string? _outputFolder;
public ObservableCollection<MediaFile> MediaFiles { get; } = new();
[RelayCommand]
private async Task SelectInputFolderAsync()
{
InputFolder = await SearchFolderAsync();
}
[RelayCommand]
private async Task SelectOutputFolderAsync()
{
OutputFolder = await SearchFolderAsync();
}
private static async Task<string?> SearchFolderAsync()
{
var storageProvider = (Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)
?.MainWindow?
.StorageProvider;
if (storageProvider is null) return "<not accessible>";
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;
}

View File

@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.0.6" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.6" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.6" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.6" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.6" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.6" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PhotoRenamer\PhotoRenamer.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,21 @@
using Avalonia;
using System;
namespace PhotoRenamerGui;
class Program
{
// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
[STAThread]
public static void Main(string[] args) => BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace();
}

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<!-- This manifest is used on Windows only.
Don't remove it as it might cause problems with window transparency and embeded controls.
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
<assemblyIdentity version="1.0.0.0" name="PhotoRenamerGui.Desktop"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of the Windows versions that this application has been tested on
and is designed to work with. Uncomment the appropriate elements
and Windows will automatically select the most compatible environment. -->
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
</assembly>