Refactor project structure

This commit is contained in:
Александр
2026-03-04 12:19:34 +03:00
parent 399691cb05
commit a9cf2ec6b8
11 changed files with 174 additions and 106 deletions

8
.gitignore vendored
View File

@@ -1,2 +1,8 @@
src/bin/
src/obj/
src/obj/
.vs/
bin/
obj/
*.user
*.suo

View File

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CsvHelper" Version="33.1.0" />
<PackageReference Include="MetaBrainz.MusicBrainz" Version="8.0.1" />
<PackageReference Include="TagLibSharp" Version="2.3.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,104 @@
using MetaBrainz.MusicBrainz;
using UniversalTagEditor.Helpers;
using UniversalTagEditor.Types;
namespace UniversalTagEditor.Core;
public sealed class UteOptions
{
public required string WorkingDirectory { get; init; }
public string? AlbumInfoCsv { get; init; } // имя файла или путь
public string? AlbumCover { get; init; } // имя файла или путь
public string? Format { get; init; } // flac/mp3/...
public bool FixTags { get; init; }
public bool EnhanceStructure { get; init; }
public int MaxDegreeOfParallelism { get; init; } = 4;
}
public sealed record ProgressInfo(int Done, int Total, string? CurrentFile);
public static class Runner
{
public static async Task RunAsync(
UteOptions opt,
IProgress<string>? log = null,
IProgress<ProgressInfo>? progress = null,
CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(opt.WorkingDirectory))
throw new ArgumentException("WorkingDirectory is empty.");
if (!Directory.Exists(opt.WorkingDirectory))
throw new DirectoryNotFoundException(opt.WorkingDirectory);
var format = (opt.Format ?? "").Trim();
if (string.IsNullOrWhiteSpace(format))
throw new ArgumentException("Format (-f) is required.");
AlbumInfo album = new();
if (!string.IsNullOrWhiteSpace(opt.AlbumInfoCsv))
{
var csvPath = MakePath(opt.WorkingDirectory, opt.AlbumInfoCsv!);
var records = CsvParser.ParseCsvTable(csvPath);
album = new AlbumInfo { Tracks = records };
}
if (!string.IsNullOrWhiteSpace(opt.AlbumCover))
album.Cover = MakePath(opt.WorkingDirectory, opt.AlbumCover!);
var files = new List<string>();
files.AddRange(Directory.GetFiles(opt.WorkingDirectory, $"*.{format}"));
if (!opt.EnhanceStructure)
{
foreach (var dir in Directory.GetDirectories(opt.WorkingDirectory))
files.AddRange(Directory.GetFiles(dir, $"*.{format}"));
}
if (files.Count == 0)
{
log?.Report("Файлы не найдены.");
return;
}
var queries = new ThreadLocal<Query>(() =>
new Query("NavidromeMetadataRecovery", "1.0"));
int done = 0;
int total = files.Count;
await Parallel.ForEachAsync(
files.Select((path, index) => (path, index)),
new ParallelOptions
{
MaxDegreeOfParallelism = Math.Max(1, opt.MaxDegreeOfParallelism),
CancellationToken = ct
},
async (item, token) =>
{
var (file, index) = item;
if (opt.FixTags)
{
await TagEditor.Fix(file, queries.Value!, opt.EnhanceStructure, format);
}
else
{
TagEditor.Edit(file, album, index);
}
var nowDone = Interlocked.Increment(ref done);
progress?.Report(new ProgressInfo(nowDone, total, file));
log?.Report($"{nowDone}/{total}: {file} успешно обработан");
});
log?.Report("Готово!");
}
private static string MakePath(string workDir, string maybeFileOrPath)
=> Path.IsPathRooted(maybeFileOrPath)
? maybeFileOrPath
: Path.Combine(workDir, maybeFileOrPath);
}

View File

@@ -6,6 +6,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UniversalTagEditor", "src\UniversalTagEditor.csproj", "{F12A64BA-EF0D-DBA6-7B72-63FC4220F8EC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UniversalTagEditor.Core", "UniversalTagEditor.Core\UniversalTagEditor.Core.csproj", "{A0248697-C889-481E-89C2-FB3C4DF6A3ED}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -16,12 +18,17 @@ Global
{F12A64BA-EF0D-DBA6-7B72-63FC4220F8EC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F12A64BA-EF0D-DBA6-7B72-63FC4220F8EC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F12A64BA-EF0D-DBA6-7B72-63FC4220F8EC}.Release|Any CPU.Build.0 = Release|Any CPU
{A0248697-C889-481E-89C2-FB3C4DF6A3ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A0248697-C889-481E-89C2-FB3C4DF6A3ED}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A0248697-C889-481E-89C2-FB3C4DF6A3ED}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A0248697-C889-481E-89C2-FB3C4DF6A3ED}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{F12A64BA-EF0D-DBA6-7B72-63FC4220F8EC} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{A0248697-C889-481E-89C2-FB3C4DF6A3ED} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D0E6B6A0-0573-41C6-88A8-2678B8799BA7}

View File

@@ -1,121 +1,59 @@
using UniversalTagEditor.Helpers;
using UniversalTagEditor.Types;
using MetaBrainz.MusicBrainz;
namespace UniversalTagEditor;
using System.Text;
using UniversalTagEditor.Core;
class Program
{
static async Task Main(string[] args)
{
string albumInfo = "";
string albumCover = "";
string format = "";
string workingDirectory = "";
var opt = ParseArgs(args);
var log = new Progress<string>(Console.WriteLine);
var prog = new Progress<ProgressInfo>(p =>
Console.Title = $"UniversalTagEditor {p.Done}/{p.Total}");
Console.OutputEncoding = Encoding.UTF8;
Console.InputEncoding = Encoding.UTF8;
Console.WriteLine("WORKDIR RAW : " + opt.WorkingDirectory);
Console.WriteLine("WORKDIR FULL: " + Path.GetFullPath(opt.WorkingDirectory));
Console.WriteLine("EXISTS : " + Directory.Exists(opt.WorkingDirectory));
await Runner.RunAsync(opt, log, prog);
}
static UteOptions ParseArgs(string[] args)
{
string? albumInfo = null;
string? albumCover = null;
string? format = null;
string? workingDirectory = null;
bool fixTags = false;
bool enhanceStructure = false;
List<decimal> threads = new();
var query = new Query("NavidromeMetadataRecovery1", "1.0");
var query2 = new Query("NavidromeMetadataRecovery2", "1.0");
var query3 = new Query("NavidromeMetadataRecovery3", "1.0");
var query4 = new Query("NavidromeMetadataRecovery4", "1.0");
for (int i = 0; i < args.Length; i++)
{
switch (args[i])
{
case "-w":
if (i + 1 < args.Length) workingDirectory = args[i + 1];
i++;
break;
case "-a":
if (i + 1 < args.Length) albumInfo = args[i + 1];
i++;
break;
case "-f":
if (i + 1 < args.Length) format = args[i + 1];
i++;
break;
case "-c":
if (i + 1 < args.Length) albumCover = args[i + 1];
i++;
break;
case "--fix-tags":
fixTags = true;
break;
case "--enhance-structure":
enhanceStructure = true;
break;
case "-w": workingDirectory = args[++i]; break;
case "-a": albumInfo = args[++i]; break;
case "-f": format = args[++i]; break;
case "-c": albumCover = args[++i]; break;
case "--fix-tags": fixTags = true; break;
case "--enhance-structure": enhanceStructure = true; break;
}
}
TrackInfo[] records;
AlbumInfo album = new();
if (albumInfo != "")
{
records = CsvParser.ParseCsvTable(Path.Combine(workingDirectory, albumInfo));
album = new()
{
Tracks = records
};
}
if (albumCover != "")
album.Cover = Path.Combine(workingDirectory, albumCover);
List<string> files = new();
files.AddRange(Directory.GetFiles(workingDirectory, $"*.{format}"));
if (string.IsNullOrWhiteSpace(workingDirectory))
throw new ArgumentException("Нужен -w <path>");
if (!enhanceStructure) {
string[] directories = Directory.GetDirectories(workingDirectory);
foreach(string directory in directories)
files.AddRange(Directory.GetFiles(directory, $"*.{format}"));
}
Func<List<string>, Query, int, Task> cycle = async (List<string> chunk, Query query, int thread) =>
return new UteOptions
{
threads.Add(thread);
for (int i = 0; i < chunk.Count; i++)
{
if (fixTags)
await TagEditor.Fix(chunk[i], query, enhanceStructure, format);
else
TagEditor.Edit(chunk[i], album, i);
Console.WriteLine($"{i+1}. {chunk[i]} successfully edited");
}
threads.Remove(thread);
WorkingDirectory = workingDirectory,
AlbumInfoCsv = albumInfo,
AlbumCover = albumCover,
Format = format,
FixTags = fixTags,
EnhanceStructure = enhanceStructure,
MaxDegreeOfParallelism = 4
};
if ((int)(files.Count / 4) > 1)
{
Thread thread = new Thread(async () => await cycle(files.GetRange(0, (int)(files.Count / 4)), query, 1)) { IsBackground = true };
thread.Start();
Thread thread2 = new Thread(async () => await cycle(files.GetRange((int)(files.Count / 4), (int)(files.Count / 4)), query2, 2)) { IsBackground = true };
thread2.Start();
if ((int)(files.Count / 4) >= 3) {
Thread thread3 = new Thread(async () => await cycle(files.GetRange(((int)(files.Count / 4) * 2), (int)(files.Count / 4)), query3, 3)) { IsBackground = true };
thread3.Start();
}
if ((int)(files.Count / 4) >= 4) {
Thread thread4 = new Thread(async () => await cycle(files.GetRange(((int)(files.Count / 4) * 3), files.Count - ((int)(files.Count / 4) * 3)), query4, 4)) { IsBackground = true };
thread4.Start();
}
Console.WriteLine("Waiting for threads complete");
while(threads.Count > 0) { }
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("\n\nTag fixing completed!");
Console.ResetColor();
}
else
await cycle(files, query, 1);
}
}

View File

@@ -2,15 +2,13 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CsvHelper" Version="33.1.0" />
<PackageReference Include="MetaBrainz.MusicBrainz" Version="7.0.0" />
<PackageReference Include="TagLibSharp" Version="2.3.0" />
<ProjectReference Include="..\UniversalTagEditor.Core\UniversalTagEditor.Core.csproj" />
</ItemGroup>
</Project>