Skip to content

Commit

Permalink
Small artwork extraction refactor (#86)
Browse files Browse the repository at this point in the history
* Reorganize namespaces and directories

* Add explicit types to `foreach`es

* Minor code formatting

* Initial working version

* Add `None` extension methods

* Update operation description

* Various refactoring

* Add custom types

* Minor tweaks

* Remove failure count

* Initial working version

* Add `None` extension methods

* Update operation description

* Various refactoring

* Add custom types

* Minor tweaks

* Remove failure count
  • Loading branch information
codeconscious authored Jul 16, 2024
1 parent 305063a commit d522aeb
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 117 deletions.
7 changes: 6 additions & 1 deletion src/AudioTagger.Console/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@ namespace AudioTagger.Console;

public static class Extensions
{
public static bool None<T>(this IEnumerable<T> collection) =>
!collection.Any();

public static bool None<T>(this IEnumerable<T> collection, 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>
/// <remarks>I just got tired of `!string.IsNullOrWhiteSpace` everywhere...</remarks>
public static bool HasText(this string? str) => !string.IsNullOrWhiteSpace(str);

public static string? TextOrNull(this string? text) =>
Expand Down
2 changes: 1 addition & 1 deletion src/AudioTagger.Console/OperationLibrary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ internal static class OperationLibrary
),
new(
["-uea", "--extract-artwork"],
"Extracts artwork from directory files if they have the same artist and album.",
"Extracts artwork from directory files if they have the same artist and album, then deletes the artwork from the files containing it.",
new TagArtworkExtractor()
),
new(
Expand Down
192 changes: 104 additions & 88 deletions src/AudioTagger.Console/Operations/TagArtworkExtractor.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
using AudioTagger.Library;
using FilesGroupedByDir = System.Linq.IGrouping<string?, AudioTagger.Library.MediaFiles.MediaFile>;
using FilesGroupedByCount = System.Linq.IGrouping<int, AudioTagger.Library.MediaFiles.MediaFile>;

namespace AudioTagger.Console.Operations;

public sealed class TagArtworkExtractor : IPathOperation
{
private static readonly string _artworkFileName = "cover.jpg";

static bool AllUnique(IEnumerable<string> items) =>
private static bool AreAllSame(IEnumerable<string> items) =>
items.Distinct().Count() == 1;

private static bool IsAllArtUnique(
FilesGroupedByDir fileGroup,
IEnumerable<FilesGroupedByCount> filesGroupedByCount)
{
return
fileGroup.Count() > 1 &&
filesGroupedByCount.All(g => g.Count() == 1);
}

public void Start(
IReadOnlyCollection<MediaFile> mediaFiles,
DirectoryInfo workingDirectory,
Expand All @@ -17,99 +28,104 @@ public void Start(
{
var watch = new Watch();

var filesWithArtwork = mediaFiles.Where(f => f.AlbumArt.Length != 0);
if (!filesWithArtwork.Any())
var filesWithArt = mediaFiles.Where(f => f.HasAlbumArt());
if (filesWithArt.None())
{
printer.Warning($"No artwork found in any files in \"{workingDirectory.FullName}\".");
return;
}

var filesGroupedByDir = filesWithArt.GroupBy(m => m.FileInfo.DirectoryName);
foreach (FilesGroupedByDir fileGroup in filesGroupedByDir)
{
ProcessDirectory(fileGroup, printer);
}

printer.Print($"Done in {watch.ElapsedFriendly}.");
}

private static void ProcessDirectory(FilesGroupedByDir fileGroup, IPrinter printer)
{
printer.Print($"Processing \"{fileGroup.Key}\"...");

if (fileGroup.None(f => f.HasAlbumArt()))
{
printer.Warning($"No artwork found in this directory.");
return;
}

var albumNames = fileGroup.Select(file => file.Album);
var artistNames = fileGroup
.Select(file => {
return file.AlbumArtists.FirstOrDefault()
?? file.Artists.FirstOrDefault()
?? "None";
});

if (!AreAllSame(artistNames) || !AreAllSame(albumNames))
{
printer.Warning("No artwork found in the files.");
printer.Warning($"Skipping directory \"{fileGroup.Key}\" because either the artists or albums are not unique.");
return;
}

var filesGroupedByDirectory = filesWithArtwork.GroupBy(m => m.FileInfo.DirectoryName);
var filesGroupedByArtSize = fileGroup.GroupBy(f => f.AlbumArt.Length, f => f);
int mostCommonArtCount = filesGroupedByArtSize.Max(a => a.Count());
var filesWithMostCommonArt = filesGroupedByArtSize.Where(a => a.Count() == mostCommonArtCount);

if (IsAllArtUnique(fileGroup, filesWithMostCommonArt))
{
printer.Warning("All of the artwork is unique, so will not extract any artwork.");
return;
}

foreach (var fileGroup in filesGroupedByDirectory)
if (filesWithMostCommonArt.Count() != 1)
{
if (fileGroup.All(f => f.AlbumArt.Length == 0))
{
printer.Warning($"No artwork found in directory {fileGroup.Key}.");
continue;
}

var albumNames = fileGroup.Select(file => file.Album);
var artistNames = fileGroup
.Select(file => {
return file.AlbumArtists.FirstOrDefault()
?? file.Artists.FirstOrDefault()
?? "None";
});

if (!AllUnique(artistNames) || !AllUnique(albumNames))
{
printer.Warning($"Skipping directory \"{fileGroup.Key}\" because either the artists or albums are not unique.");
continue;
}

var filesGroupedByArtSize = fileGroup.GroupBy(f => f.AlbumArt.Length, f => f);
var mostCommonArtCount = filesGroupedByArtSize.Max(a => a.Count());
var filesWithMostCommonArt = filesGroupedByArtSize.Where(a => a.Count() == mostCommonArtCount);

if (mediaFiles.Count > 1 && filesWithMostCommonArt.All(g => g.Count() == 1))
{
printer.Warning("All of the artwork is unique, so will not extract any artwork.");
return;
}

if (filesWithMostCommonArt.Count() != 1)
{
printer.Warning("More than one artwork appears multiple times. Will only extract one.");
}

int failures = 0;
var filesWithChosenMostCommonArt = filesWithMostCommonArt.First();

foreach (MediaFile file in filesWithChosenMostCommonArt)
{
var directoryName = file.FileInfo.DirectoryName!;
var extractResult = file.ExtractArtworkToFile(directoryName, _artworkFileName);

if (extractResult.IsSuccess)
{
printer.Print($"Saved artwork to \"{_artworkFileName}\".");
}
else
{
failures++;
printer.FirstError(extractResult, "Artwork extraction error:");
}
}

if (failures != 0)
{
printer.Warning($"There were {failures} extraction error(s), so will not delete any artwork.");
printer.Print($"Done in {watch.ElapsedFriendly}.");
return;
}

foreach (MediaFile file in filesWithChosenMostCommonArt)
{
file.RemoveAlbumArt();
var saveResult = file.SaveUpdates();
if (saveResult.IsFailed)
{
printer.FirstError(saveResult);
continue;
}

var rewriteResult = file.RewriteFileTags();
if (rewriteResult.IsFailed)
{
printer.FirstError(rewriteResult);
continue;
}

printer.Print($"Removed artwork from \"{file.FileNameOnly}\"");
}

printer.Print($"Done in {watch.ElapsedFriendly}.");
printer.Warning("More than one image is most populous, but only one will be extracted.");
}

var filesWithChosenMostCommonArt = filesWithMostCommonArt.First();

ExtractArtwork(filesWithChosenMostCommonArt, printer);

foreach (MediaFile file in filesWithChosenMostCommonArt)
{
RemoveArtworkAndRewriteTags(file, printer);
}
}

private static void ExtractArtwork(FilesGroupedByCount fileGroup, IPrinter printer)
{
MediaFile artSourceFile = fileGroup.First();
string directoryName = artSourceFile.FileInfo.DirectoryName!;

var extractResult = artSourceFile.ExtractArtworkToFile(directoryName, _artworkFileName);
if (extractResult.IsSuccess)
{
printer.Print($"Saved most common artwork to \"{_artworkFileName}\" in the same directory.");
}
else
{
printer.FirstError(extractResult, "Artwork extraction error:");
}
}

private static void RemoveArtworkAndRewriteTags(MediaFile file, IPrinter printer)
{
file.RemoveAlbumArt();
var saveResult = file.SaveUpdates();
if (saveResult.IsFailed)
{
printer.FirstError(saveResult);
return;
}

var rewriteResult = file.RewriteFileTags();
if (rewriteResult.IsFailed)
{
printer.FirstError(rewriteResult);
return;
}

printer.Print($"Removed artwork from \"{file.FileNameOnly}\"");
}
}
56 changes: 29 additions & 27 deletions src/AudioTagger.Library/MediaFiles/MediaFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ public string ReplayGainSummary()
return $"Track: {trackGain} | Album: {albumGain}";
}

public bool HasAlbumArt() => AlbumArt.Length != 0;

/// <summary>
/// The embedded image for the album, represented as an array of bytes or,
/// if none, an empty array.
Expand All @@ -165,8 +167,32 @@ public byte[] AlbumArt
}
}

public Result ExtractArtworkToFile(string saveDirectory, string saveFileName)
{
if (_taggedFile.Tag.Pictures.Length == 0)
{
return Result.Fail("No artwork was found in the file tags.");
}

var artwork = _taggedFile.Tag.Pictures[^1];

var savePath = Path.Combine(saveDirectory, saveFileName);

try
{
using MemoryStream ms = new([.. artwork.Data]);
using FileStream fs = new(savePath, FileMode.Create, FileAccess.Write);
ms.WriteTo(fs);
return Result.Ok();
}
catch (Exception ex)
{
return Result.Fail(ex.Message);
}
}

/// <summary>
/// Removes album art from the file's tag data.
/// Removes album art from the file's tag data. Requires saving the file separately.
/// </summary>
/// <remarks>Padding space remains, so the file size is not reduced.</remarks>
public void RemoveAlbumArt()
Expand All @@ -192,7 +218,7 @@ public Result SaveUpdates()
}
catch (Exception ex)
{
return Result.Fail($"Save error: {ex.Message}");
return Result.Fail($"Tag save error: {ex.Message}");
}
}

Expand Down Expand Up @@ -233,31 +259,7 @@ public Result RewriteFileTags()
}
catch (Exception ex)
{
return Result.Fail($"Save error: {ex.Message}");
}
}

public Result ExtractArtworkToFile(string saveDirectory, string saveFileName)
{
if (_taggedFile.Tag.Pictures.Length == 0)
{
return Result.Fail("No artwork was found in the file tags.");
}

var artwork = _taggedFile.Tag.Pictures[^1];

var savePath = Path.Combine(saveDirectory, saveFileName);

try
{
using MemoryStream ms = new([.. artwork.Data]);
using FileStream fs = new(savePath, FileMode.Create, FileAccess.Write);
ms.WriteTo(fs);
return Result.Ok();
}
catch (Exception ex)
{
return Result.Fail(ex.Message);
return Result.Fail($"Tag rewrite error: {ex.Message}");
}
}

Expand Down

0 comments on commit d522aeb

Please sign in to comment.