forked from TheLanc3/MusicTagFixer
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1,8 +1,2 @@
|
||||
src/bin/
|
||||
src/obj/
|
||||
|
||||
.vs/
|
||||
bin/
|
||||
obj/
|
||||
*.user
|
||||
*.suo
|
||||
src/obj/
|
||||
@@ -1,15 +0,0 @@
|
||||
<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>
|
||||
@@ -1,104 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -6,8 +6,6 @@ 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
|
||||
@@ -18,17 +16,12 @@ 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}
|
||||
|
||||
142
src/Program.cs
142
src/Program.cs
@@ -1,59 +1,121 @@
|
||||
using System.Text;
|
||||
using UniversalTagEditor.Core;
|
||||
using UniversalTagEditor.Helpers;
|
||||
using UniversalTagEditor.Types;
|
||||
|
||||
using MetaBrainz.MusicBrainz;
|
||||
|
||||
namespace UniversalTagEditor;
|
||||
|
||||
class Program
|
||||
{
|
||||
static async Task Main(string[] args)
|
||||
{
|
||||
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;
|
||||
string albumInfo = "";
|
||||
string albumCover = "";
|
||||
string format = "";
|
||||
string workingDirectory = "";
|
||||
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": 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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(workingDirectory))
|
||||
throw new ArgumentException("Нужен -w <path>");
|
||||
|
||||
return new UteOptions
|
||||
TrackInfo[] records;
|
||||
AlbumInfo album = new();
|
||||
if (albumInfo != "")
|
||||
{
|
||||
WorkingDirectory = workingDirectory,
|
||||
AlbumInfoCsv = albumInfo,
|
||||
AlbumCover = albumCover,
|
||||
Format = format,
|
||||
FixTags = fixTags,
|
||||
EnhanceStructure = enhanceStructure,
|
||||
MaxDegreeOfParallelism = 4
|
||||
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 (!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) =>
|
||||
{
|
||||
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);
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,15 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\UniversalTagEditor.Core\UniversalTagEditor.Core.csproj" />
|
||||
<PackageReference Include="CsvHelper" Version="33.1.0" />
|
||||
<PackageReference Include="MetaBrainz.MusicBrainz" Version="7.0.0" />
|
||||
<PackageReference Include="TagLibSharp" Version="2.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user