Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 21 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Additionally, your original audio files will be modified during selected operati

## Requirements

- [.NET 9 runtime](https://dotnet.microsoft.com/en-us/download/dotnet/9.0)
- [.NET 10 runtime](https://dotnet.microsoft.com/en-us/download/dotnet/10.0)
- `settings.json` (See below)

## Running
Expand Down Expand Up @@ -36,24 +36,24 @@ dotnet run -- -r ~/Downloads/Audio/

## Flags

| Flags | Description
| Flags | Description |
|---|---|
| -v, --view | View full tag data.
| -vs, --view-summary | View a summary of tag data.
| -u, --update | Update tag data using filename patterns from the settings.
| -u1, --update-single | Update a single tag in multiple files to a single, manually-specified value.
| -ug, --update-genres | Update the genres in all files automatically using the CSV specified in the settings.
| -um, --update-multiple | Update a single tag in multiple files with multiple values.
| -uy, --update-year | Update the year using media files' own dates of creation. (Must do before other updates, lest the creation date be modified by those updates.)
| -urt, --reverse-track-numbers | Reverse the track numbers of the given files.
| -uea, --extract-artwork | Extracts artwork from directory files if they have the same artist and album, then deletes the artwork from the files containing it.
| -ura, --remove-artwork | Removes artwork from files. (File size is not reduced, as padding remains.)
| -rt, --rewrite-tags | Rewrites file tags. (Can be helping in reducing padding, such as from removed artwork.)
| -r, --rename | Rename and reorganize files into folders based on tag data.
| -d, --duplicates | List tracks with identical artists and titles. No files are modified or deleted.
| -s, --stats | Display file statistics based on tag data.
| -g, --genres | Save the primary genre for each artist to a genre file.
| -p, --parse | Get a single tag value by parsing the data of another (generally Comments).
| -v, --view | View full tag data. |
| -vs, --view-summary | View a summary of tag data. |
| -u, --update | Update tag data using filename patterns from the settings. |
| -u1, --update-single | Update a single tag in multiple files to a single, manually-specified value. |
| -ug, --update-genres | Update the genres in all files automatically using the CSV specified in the settings. |
| -um, --update-multiple | Update a single tag in multiple files with multiple values. |
| -uy, --update-year | Update the year using media files' own dates of creation. (Must do before other updates, lest the creation date be modified by those updates.) |
| -urt, --reverse-track-numbers | Reverse the track numbers of the given files. |
| -uea, --extract-artwork | Extracts artwork from directory files if they have the same artist and album, then deletes the artwork from the files containing it. |
| -ura, --remove-artwork | Removes artwork from files. (File size is not reduced, as padding remains.) |
| -rt, --rewrite-tags | Rewrites file tags. (Can be helping in reducing padding, such as from removed artwork.) |
| -r, --rename | Rename and reorganize files into folders based on tag data. |
| -d, --duplicates | List tracks with identical artists and titles. No files are modified or deleted. |
| -s, --stats | Display file statistics based on tag data. |
| -g, --genres | Save the primary genre for each artist to a genre file. |
| -p, --parse | Get a single tag value by parsing the data of another (generally Comments). |

Passing no arguments will also display these instructions.

Expand All @@ -72,7 +72,7 @@ A sample settings file, which can you copy and paste if you wish, follows:
"(?:(?<albumArtists>.+) ≡ )?(?<album>.+?)(?: ?\\[(?<year>\\d{4})\\])? = (?<trackNo>\\d+) [–-] (?<artists>.+?) [–-] (?<title>.+)(?=\\.(?:m4a|opus))",
"(?:(?<albumArtists>.+) ≡ )?(?<album>.+?)(?: ?\\[(?<year>\\d{4})\\])? = (?<trackNo>\\d{1,3}) [–-] (?<title>.+)(?=\\.(?:m4a|opus))",
"(?:(?<albumArtists>.+) ≡ )(?<album>.+?)(?: ?\\[(?<year>\\d{4})\\])? = (?<artists>.+?) [–-] (?<title>.+)(?=\\.(?:m4a|opus))",
"(?:(?<albumArtists>.+) ≡ )?(?<album>.+?)(?: ?\\[(?<year>\\d{4})\\])? = (?<title>.+)(?=\\.(?:m4a|opus))", ]
"(?:(?<albumArtists>.+) ≡ )?(?<album>.+?)(?: ?\\[(?<year>\\d{4})\\])? = (?<title>.+)(?=\\.(?:m4a|opus))" ]
},
"renaming": {
"useAlbumDirectories": true,
Expand All @@ -99,7 +99,7 @@ A sample settings file, which can you copy and paste if you wish, follows:
"exclusions": [
{ "artist": "Artist Name" },
{ "title": "Track Title" },
{ "artist": "Artist Name", "title": "Track Title" },
{ "artist": "Artist Name", "title": "Track Title" }
],
"artistReplacements": [
" ",
Expand Down Expand Up @@ -131,7 +131,7 @@ A sample settings file, which can you copy and paste if you wish, follows:
":",
":"
]
},
}
}
```

Expand Down
4 changes: 2 additions & 2 deletions src/AudioTagger.Console/AudioTagger.Console.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>Audiotagger.Console</AssemblyName>
Expand Down Expand Up @@ -30,4 +30,4 @@
<ItemGroup>
<ProjectReference Include="..\AudioTagger.Library\AudioTagger.Library.csproj"/>
</ItemGroup>
</Project>
</Project>
37 changes: 17 additions & 20 deletions src/AudioTagger.Console/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,24 @@ namespace AudioTagger.Console;

public static class Extensions
{
/// <summary>
/// Determines whether a collection is empty.
/// </summary>
public static bool None<T>(this IEnumerable<T> collection) =>
!collection.Any();
extension<T>(IEnumerable<T> collection)
{
public bool None() =>
!collection.Any();

/// <summary>
/// Determines whether no elements of a sequence satisfy a given condition.
/// </summary>
public static bool None<T>(this IEnumerable<T> collection, Func<T, bool> predicate) =>
!collection.Any(predicate);
public bool None(Func<T, bool> predicate) =>
!collection.Any(predicate);
}

/// <summary>
/// Returns a bool indicating whether a string is not null and has text (true) or not.
/// </summary>
public static bool HasText(this string? str) => !string.IsNullOrWhiteSpace(str);
extension(string? str)
{
public bool HasText() => !string.IsNullOrWhiteSpace(str);

public static string? TextOrNull(this string? text) =>
text switch
{
null or { Length: 0 } => null,
_ => text
};
public string? TextOrNull() =>
str switch
{
null or { Length: 0 } => null,
_ => str
};
}
}
7 changes: 4 additions & 3 deletions src/AudioTagger.Console/GetUserInput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ public static class ResponseHandler
/// <summary>
/// Ask the user a question that they can answer with a single keystroke.
/// </summary>
private static UserResponse AskUserQuestion(IReadOnlyList<LineSubString> question,
IReadOnlyList<KeyResponse> allowedResponses,
IPrinter printer)
private static UserResponse AskUserQuestion(
IReadOnlyList<LineSubString> question,
IReadOnlyList<KeyResponse> allowedResponses,
IPrinter printer)
{
if (question.None())
{
Expand Down
2 changes: 1 addition & 1 deletion src/AudioTagger.Console/MediaFilePathInfo.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
namespace AudioTagger.Console;

/// <summary>
/// Contains all of the path information for a specific MediaFile.
/// Contains all the path information for a specific MediaFile.
/// </summary>
internal sealed class MediaFilePathInfo
{
Expand Down
6 changes: 3 additions & 3 deletions src/AudioTagger.Console/MediaFileViewer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace AudioTagger.Console;

public sealed class MediaFileViewer
{
public void PrintFileDetails(MediaFile file)
public static void PrintFileDetails(MediaFile file)
{
// TODO: Handle colors more gracefully.
string TagNameFormatter(string s) => "[grey]" + s + "[/]";
Expand All @@ -30,7 +30,7 @@ public void PrintFileDetails(MediaFile file)
table.AddRow(TagNameFormatter("Year"), file.Year.ToString());
table.AddRow(TagNameFormatter("Duration"), file.Duration.ToString("m\\:ss"));

int genreCount = file.Genres.Length;
var genreCount = file.Genres.Length;
table.AddRow(TagNameFormatter("Genres"),
file.Genres.Join().EscapeMarkup() +
(genreCount > 1 ? $" ({genreCount})" : string.Empty));
Expand Down Expand Up @@ -82,7 +82,7 @@ public static TableRow PrintFileSummary(MediaFile file)
file.ReplayGainTrack.ToString(CultureInfo.InvariantCulture)
};

IEnumerable<Markup> markups = rows.Select(r => new Markup(r));
var markups = rows.Select(r => new Markup(r));

return new TableRow(markups);
}
Expand Down
15 changes: 4 additions & 11 deletions src/AudioTagger.Console/Operations/MediaFileRenamer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -281,20 +281,15 @@ private static bool RenameSingleFile(

return shouldCancel;

/// <summary>
/// Generates and returns a new filename by replacing placeholders within the rename
/// pattern (e.g., `%ALBUM%`) with actual tag data from the `MediaFile`.
/// </summary>
// Generates and returns a new filename by replacing placeholders within the rename
// pattern (e.g., `%ALBUM%`) with actual tag data from the `MediaFile`.
static string GenerateFileName(
MediaFile file,
ICollection<string> fileTagNames,
string renamePattern)
{
StringBuilder workingFileName =
fileTagNames.Aggregate(
new StringBuilder(renamePattern),
(workingName, tagName) => ReplacePlaceholders(workingName, tagName)
);
fileTagNames.Aggregate(new StringBuilder(renamePattern), ReplacePlaceholders);

var ext = Path.GetExtension(file.FileNameOnly);
var unsanitizedName = workingFileName + ext;
Expand Down Expand Up @@ -333,9 +328,7 @@ StringBuilder ReplacePlaceholders(StringBuilder workingName, string tagName)
}
}

/// <summary>
/// Generates and returns a directory name for a file given its tags. Never returns null.
/// </summary>
// Generates and returns a directory name for a file given its tags. Never returns null.
static string GenerateSafeDirectoryName(MediaFile file)
{
if (MediaFile.HasAnyValues(file.AlbumArtists))
Expand Down
8 changes: 4 additions & 4 deletions src/AudioTagger.Console/Operations/TagDuplicateFinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public void Start(
string artistLabel = PluralizeTerm(artistReplacements.Count);
printer.Print($"Found {artistReplacements.Count} artist replacement {artistLabel}.");

var titleReplacements = settings.Duplicates?.TitleReplacements ?? [];
var titleReplacements = settings.Duplicates.TitleReplacements ?? [];
string titleLabel = PluralizeTerm(titleReplacements.Count);
printer.Print($"Found {titleReplacements.Count} title replacement {titleLabel}.");

Expand All @@ -64,9 +64,9 @@ public void Start(
printer.Print($"Found {groupCount} duplicate {groupLabel} in {watch.ElapsedFriendly}.");
PrintResults(duplicateGroups, printer);

string? searchFor = settings.Duplicates?.PathSearchFor?.TextOrNull();
string? replaceWith = settings.Duplicates?.PathReplaceWith?.TextOrNull();
string? saveDir = settings?.Duplicates?.SavePlaylistDirectory;
string? searchFor = settings.Duplicates.PathSearchFor?.TextOrNull();
string? replaceWith = settings.Duplicates.PathReplaceWith?.TextOrNull();
string? saveDir = settings.Duplicates.SavePlaylistDirectory;
CreatePlaylistFile(duplicateGroups, saveDir, (searchFor, replaceWith), printer);
}

Expand Down
4 changes: 2 additions & 2 deletions src/AudioTagger.Console/Operations/TagUpdater.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,11 @@ private static bool UpdateTags(
return shouldCancel;
}

IEnumerable<Group>? matchedTags = match.Groups
IEnumerable<Group> matchedTags = match.Groups
.OfType<Group>()
.Where(g => g.Success);

if (matchedTags.Any() != true)
if (!matchedTags.Any())
{
printer.Print($"Could not parse data for filename \"{mediaFile.FileNameOnly}.\"",
ResultType.Failure);
Expand Down
16 changes: 4 additions & 12 deletions src/AudioTagger.Console/Operations/TagUpdaterMultiple.cs
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ private static void UpdateTags(MediaFile mediaFile,
string sanitizedTitle = tagValue.Trim().Normalize()
.Replace("___", " ")
.Replace("__", " ");
mediaFile.Title = GetUpdatedValue(mediaFile.Title,
mediaFile.Title = GetUpdatedTagValue(mediaFile.Title,
sanitizedTitle,
updateType,
false);
Expand Down Expand Up @@ -208,7 +208,7 @@ private static void UpdateTags(MediaFile mediaFile,
string sanitizedAlbum = tagValue.Trim().Normalize()
.Replace("___", " ")
.Replace("__", " ");
mediaFile.Album = GetUpdatedValue(mediaFile.Album,
mediaFile.Album = GetUpdatedTagValue(mediaFile.Album,
sanitizedAlbum,
updateType,
false);
Expand All @@ -231,23 +231,15 @@ private static void UpdateTags(MediaFile mediaFile,
mediaFile.TrackNo = ushort.Parse(tagValue);
break;
case "comment":
mediaFile.Comments = GetUpdatedValue(mediaFile.Comments, tagValue, updateType, true);
mediaFile.Comments = GetUpdatedTagValue(mediaFile.Comments, tagValue, updateType, true);
break;
default:
throw new InvalidOperationException($"Unsupported tag \"{tagName}\" could not be processed.");
}

mediaFile.SaveUpdates();

/// <summary>
/// Returns the new, updated value for a tag.
/// </summary>
/// <param name="currentValue">The original value to be modified.</param>
/// <param name="newValue">The text to be added.</param>
/// <param name="updateType"></param>
/// <param name="useNewLine">Whether or not to add line breaks between the new and old text.</param>
/// <returns></returns>
static string GetUpdatedValue(string currentValue, string newValue, TagUpdateType updateType, bool useNewLines)
static string GetUpdatedTagValue(string currentValue, string newValue, TagUpdateType updateType, bool useNewLines)
{
string divider = useNewLines ? Environment.NewLine + Environment.NewLine : string.Empty;
return updateType switch
Expand Down
29 changes: 8 additions & 21 deletions src/AudioTagger.Console/Operations/TagUpdaterSingle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ private static void UpdateTags(MediaFile mediaFile,
string sanitizedTitle = tagValue.Trim().Normalize()
.Replace("___", " ")
.Replace("__", " ");
mediaFile.Title = GetUpdatedValue(mediaFile.Title,
mediaFile.Title = GetUpdatedTagValue(mediaFile.Title,
sanitizedTitle,
updateType,
false);
Expand All @@ -155,7 +155,7 @@ private static void UpdateTags(MediaFile mediaFile,
StringSplitOptions.TrimEntries)
.Select(a => a.Normalize())
.ToArray();
mediaFile.AlbumArtists = GetUpdatedValues(mediaFile.AlbumArtists,
mediaFile.AlbumArtists = GetUpdatedTagValues(mediaFile.AlbumArtists,
sanitizedAlbumArtists,
updateType);
break;
Expand All @@ -171,15 +171,15 @@ private static void UpdateTags(MediaFile mediaFile,
StringSplitOptions.TrimEntries)
.Select(a => a.Normalize())
.ToArray();
mediaFile.Artists = GetUpdatedValues(mediaFile.Artists,
mediaFile.Artists = GetUpdatedTagValues(mediaFile.Artists,
sanitizedArtists,
updateType);
break;
case "album":
string sanitizedAlbum = tagValue.Trim().Normalize()
.Replace("___", " ")
.Replace("__", " ");
mediaFile.Album = GetUpdatedValue(mediaFile.Album,
mediaFile.Album = GetUpdatedTagValue(mediaFile.Album,
sanitizedAlbum,
updateType,
false);
Expand All @@ -196,7 +196,7 @@ private static void UpdateTags(MediaFile mediaFile,
StringSplitOptions.TrimEntries)
.Select(g => g.Normalize())
.ToArray();
mediaFile.Genres = GetUpdatedValues(mediaFile.Genres,
mediaFile.Genres = GetUpdatedTagValues(mediaFile.Genres,
sanitizedGenres,
updateType);
break;
Expand All @@ -207,22 +207,15 @@ private static void UpdateTags(MediaFile mediaFile,
mediaFile.TrackNo = ushort.Parse(tagValue);
break;
case "comment":
mediaFile.Comments = GetUpdatedValue(mediaFile.Comments, tagValue, updateType, true);
mediaFile.Comments = GetUpdatedTagValue(mediaFile.Comments, tagValue, updateType, true);
break;
default:
throw new InvalidOperationException($"Unsupported tag \"{tagName}\" could not be processed.");
}

mediaFile.SaveUpdates();

/// <summary>
/// Returns the new, updated value for a tag.
/// </summary>
/// <param name="currentValue">The original value to be modified.</param>
/// <param name="newValue">The text to be added.</param>
/// <param name="updateType"></param>
/// <param name="useNewLine">Whether or not to add line breaks between the new and old text.</param>
static string GetUpdatedValue(
static string GetUpdatedTagValue(
string currentValue,
string newValue,
TagUpdateType updateType,
Expand All @@ -242,13 +235,7 @@ static string GetUpdatedValue(
};
}

/// <summary>
/// Returns the new, updated values for a tag as a collection.
/// </summary>
/// <param name="currentValues">The original values to be modified.</param>
/// <param name="newValues">The new text to be added.</param>
/// <param name="updateType"></param>
static string[] GetUpdatedValues(
static string[] GetUpdatedTagValues(
string[] currentValues,
string[] newValues,
TagUpdateType updateType)
Expand Down
4 changes: 1 addition & 3 deletions src/AudioTagger.Console/Operations/TagViewer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@ public void Start(IReadOnlyCollection<MediaFile> mediaFiles,
{
try
{
//printer.Print(OutputLine.GetTagPrintedLines(mediaFile));
var viewer = new MediaFileViewer();
viewer.PrintFileDetails(mediaFile);
MediaFileViewer.PrintFileDetails(mediaFile);

#if _WINDOWS
if (mediaFile.AlbumArt.Length > 0)
Expand Down
Loading