Skip to content

Commit

Permalink
Import album extras for manual imports and downloads
Browse files Browse the repository at this point in the history
Co-authored-by: TTY Teapot <ttdev@protonmail.com>
  • Loading branch information
mynameisbogdan and tty418 committed Jan 28, 2024
1 parent f31ecaf commit 134c3be
Show file tree
Hide file tree
Showing 5 changed files with 489 additions and 0 deletions.
270 changes: 270 additions & 0 deletions src/NzbDrone.Core.Test/Extras/ExtraServiceFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Extras;
using NzbDrone.Core.Extras.Others;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.MediaFiles.TrackImport;
using NzbDrone.Core.Music;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Profiles.Qualities;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common;

namespace NzbDrone.Core.Test.Extras
{
public class ExtraServiceFixture : CoreTest<ExtraService>
{
private List<ImportDecision<LocalTrack>> _approvedDecisions;
private string _albumDir;
private Artist _artist;
private Album _album;
private List<string> _albumDirExtraFiles;
private List<string> _downloadDirExtraFiles;

[SetUp]
public void Setup()
{
var artistDir = @"C:\Test\Music\Foo Fooers".AsOsAgnostic();
_artist = new Artist()
{
QualityProfile = new QualityProfile { Items = Qualities.QualityFixture.GetDefaultQualities() },
Path = artistDir,
};
_album = new Album()
{
Id = 15,
Artist = _artist,
Title = "Twenty Thirties"
};
_albumDir = Path.Join(_artist.Path, $"{_album.Title} (1995) [FLAC]");

var release = new AlbumRelease()
{
AlbumId = _album.Id,
Monitored = true,
};
_album.AlbumReleases = new List<AlbumRelease> { release };

var track = new LocalTrack
{
Artist = _artist,
Album = _album,
Release = release,
Tracks = new List<Track> { new Track() { Album = _album, } },
Path = Path.Join(_albumDir, "01 - hurrdurr.flac"),
};
_approvedDecisions = new ()
{
new ImportDecision<LocalTrack>(track)
};

_albumDirExtraFiles = new List<string>
{
Path.Join(_albumDir, "album.cue"),
Path.Join(_albumDir, "albumfoo_barz.jpg"),
Path.Join(_albumDir, "release.nfo"),
Path.Join(_albumDir, "eac.log"),
};

var downloadDir = @"C:\temp\downloads\TT".AsOsAgnostic();
_downloadDirExtraFiles = new List<string>
{
Path.Join(downloadDir, "album.cue"),
Path.Join(downloadDir, "cover.jpg"),
Path.Join(downloadDir, "eac.log"),
};

Mocker.GetMock<IDiskProvider>()
.Setup(x => x.GetParentFolder(
It.Is<string>(arg => arg.AsOsAgnostic() == _approvedDecisions.First().Item.Path.AsOsAgnostic())))
.Returns(_albumDir);

// Rename on by default
var cfg = NamingConfig.Default;
cfg.RenameTracks = true;
Mocker.GetMock<INamingConfigService>().Setup(x => x.GetConfig()).Returns(cfg);
}

[Test]
public void should_import_extras_during_manual_import_with_naming_config_having_rename_on()
{
Mocker.GetMock<IDiskProvider>()
.Setup(x => x.GetFiles(It.Is<string>(arg => arg == _albumDir), false))
.Returns(_approvedDecisions.Select(d => d.Item.Path).Concat(_albumDirExtraFiles));

// act
Subject.ImportAlbumExtras(_approvedDecisions, _albumDir);

// assert
Mocker.GetMock<IOtherExtraFileService>()
.Verify(x => x.Upsert(It.Is<List<OtherExtraFile>>(arg => arg.Count == _albumDirExtraFiles.Count)));
}

[Test]
public void should_import_extras_from_download_location()
{
Mocker.GetMock<IDiskProvider>()
.Setup(x => x.GetFiles(It.Is<string>(arg => arg == _albumDir), false))
.Returns(_approvedDecisions.Select(d => d.Item.Path).Concat(_downloadDirExtraFiles));

Subject.ImportAlbumExtras(_approvedDecisions, _albumDir);

Mocker.GetMock<IOtherExtraFileService>()
.Verify(x => x.Upsert(It.Is<List<OtherExtraFile>>(arg => arg.Count == _downloadDirExtraFiles.Count)));
foreach (var sourcePath in _downloadDirExtraFiles)
{
Mocker.GetMock<IDiskTransferService>()
.Verify(x => x.TransferFile(
It.Is<string>(arg => arg.AsOsAgnostic() == sourcePath.AsOsAgnostic()),
It.Is<string>(arg => arg.AsOsAgnostic().StartsWith(_albumDir.AsOsAgnostic())),
It.IsAny<TransferMode>(),
It.IsAny<bool>()));
}
}

[Test]
public void should_not_import_extras_with_naming_cfg_having_rename_off()
{
Mocker.GetMock<IDiskProvider>()
.Setup(x => x.GetFiles(It.Is<string>(arg => arg == _albumDir), false))
.Returns(_approvedDecisions.Select(d => d.Item.Path).Concat(_albumDirExtraFiles));

var cfg = NamingConfig.Default;
cfg.RenameTracks = false; // explicitly set for readability
SetupNamingConfig(cfg);

Subject.ImportAlbumExtras(_approvedDecisions, _albumDir);

Mocker.GetMock<IOtherExtraFileService>().VerifyNoOtherCalls();
}

[TestCase(false)]
[TestCase(true)]
public void should_not_import_extras_when_no_separate_album_dir_set(bool testStandardTrackFormat)
{
Mocker.GetMock<IDiskProvider>()
.Setup(x => x.GetFiles(It.Is<string>(arg => arg == _albumDir), false))
.Returns(_approvedDecisions.Select(d => d.Item.Path).Concat(_albumDirExtraFiles));

var cfg = NamingConfig.Default;
cfg.RenameTracks = true;

// modify either standard or multidisc format to test both branches:
if (testStandardTrackFormat)
{
cfg.StandardTrackFormat = "{Artist Name} - {Album Title} - {track:00} - {Track Title}";
}
else
{
cfg.MultiDiscTrackFormat = "{Medium Format} {medium:00}/{Artist Name} - {Album Title} - {track:00} - {Track Title}";
}

SetupNamingConfig(cfg);

Subject.ImportAlbumExtras(_approvedDecisions, _albumDir);

Mocker.GetMock<IOtherExtraFileService>().VerifyNoOtherCalls();
}

[TestCase("{Album Title} ({Release Year})")]
[TestCase("{ALBUM TITLE} ({Release Year})")]
[TestCase("{Album Title}")]
[TestCase("{Album.Title}")]
[TestCase("{Album_Title}")]
public void should_import_extras_rename_pattern_contains_album_title(string albumDirPattern)
{
Mocker.GetMock<IDiskProvider>()
.Setup(x => x.GetFiles(It.Is<string>(arg => arg == _albumDir), false))
.Returns(_approvedDecisions.Select(d => d.Item.Path).Concat(_albumDirExtraFiles));

var cfg = NamingConfig.Default;
cfg.RenameTracks = true;

cfg.StandardTrackFormat = cfg.StandardTrackFormat
.Replace("{Album Title} ({Release Year})", albumDirPattern);
cfg.MultiDiscTrackFormat = cfg.MultiDiscTrackFormat
.Replace("{Album Title} ({Release Year})", albumDirPattern);

SetupNamingConfig(cfg);

// act
Subject.ImportAlbumExtras(_approvedDecisions, _albumDir);

// assert
Mocker.GetMock<IOtherExtraFileService>()
.Verify(x => x.Upsert(It.Is<List<OtherExtraFile>>(arg => arg.Count == _albumDirExtraFiles.Count)));
}

[Test]
public void should_move_album_extra_on_artist_renamed_event()
{
var newDir = Path.Combine(_albumDir, "new_subdir").AsOsAgnostic();
var renamed = new List<RenamedTrackFile>();
foreach (var import in _approvedDecisions)
{
renamed.Add(new RenamedTrackFile()
{
PreviousPath = import.Item.Path,
TrackFile = new TrackFile()
{
Id = 11,
Album = _album,
AlbumId = _album.Id,
Path = import.Item.Path.Replace(_albumDir, newDir),
Tracks = new List<Track>()
{
new Track() { Album = _album, Artist = _artist, TrackFileId = 11 },
}
},
});
}

var relativePathBeforeMove = Path.Combine(new DirectoryInfo(_albumDir).Name, "album.cue");
var albumExtra = new OtherExtraFile
{
Id = 251,
AlbumId = _album.Id,
ArtistId = _album.ArtistId,
RelativePath = relativePathBeforeMove,
Extension = ".cue",
Added = DateTime.UtcNow,
TrackFileId = null,
};

Mocker.GetMock<IMediaFileService>().Setup(x => x.GetFilesByArtist(_album.ArtistId))
.Returns(renamed.Select(x => x.TrackFile).ToList());
Mocker.GetMock<ITrackService>().Setup(x => x.GetTracksByArtist(_album.ArtistId))
.Returns(renamed.SelectMany(x => x.TrackFile.Tracks.Value).ToList());
Mocker.GetMock<IOtherExtraFileService>().Setup(x => x.GetFilesByArtist(_album.ArtistId))
.Returns(new List<OtherExtraFile>() { albumExtra });

// act
Subject.Handle(new ArtistRenamedEvent(_artist, renamed));

// assert
Mocker.GetMock<IDiskProvider>()
.Verify(x => x.MoveFile(
It.Is<string>(arg => arg.Contains(relativePathBeforeMove)),
It.Is<string>(arg => arg.Contains(newDir)),
It.IsAny<bool>()), Times.Once);
Mocker.GetMock<IOtherExtraFileService>()
.Verify(x => x.Upsert(It.Is<List<OtherExtraFile>>(arg => arg.Count == 1)));
}

/// <summary>
/// Set <paramref name="cfg"/> as the current naming configuration for the current test.
/// </summary>
/// <param name="cfg">The naming config to return from <see cref="INamingConfigService"/>.</param>
private void SetupNamingConfig(NamingConfig cfg)
{
Mocker.GetMock<INamingConfigService>().Setup(x => x.GetConfig()).Returns(cfg);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;

namespace NzbDrone.Core.Datastore.Migration
{
[Migration(076)]
public class relax_not_null_constraints_extra_files : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Rename.Column("TrackFileId").OnTable("ExtraFiles").To("TrackFileIdLegacy");
Alter.Table("ExtraFiles").AddColumn("TrackFileId").AsInt64().Nullable();
Execute.Sql("UPDATE \"ExtraFiles\" SET \"TrackFileId\" = \"TrackFileIdLegacy\"");
Delete.Column("TrackFileIdLegacy").FromTable("ExtraFiles");
}
}
}
29 changes: 29 additions & 0 deletions src/NzbDrone.Core/Extras/ExtraService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.MediaFiles.TrackImport;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Music;
using NzbDrone.Core.Parser.Model;
Expand All @@ -18,6 +19,7 @@ namespace NzbDrone.Core.Extras
public interface IExtraService
{
void ImportTrack(LocalTrack localTrack, TrackFile trackFile, bool isReadOnly);
void ImportAlbumExtras(List<ImportDecision<LocalTrack>> importedTracks, string destinationDir);
}

public class ExtraService : IExtraService,
Expand All @@ -32,6 +34,7 @@ public class ExtraService : IExtraService,
private readonly IDiskProvider _diskProvider;
private readonly IConfigService _configService;
private readonly List<IManageExtraFiles> _extraFileManagers;
private readonly AlbumExtraFileManager _albumExtraManager;
private readonly Logger _logger;

public ExtraService(IMediaFileService mediaFileService,
Expand All @@ -40,6 +43,7 @@ public class ExtraService : IExtraService,
IDiskProvider diskProvider,
IConfigService configService,
IEnumerable<IManageExtraFiles> extraFileManagers,
AlbumExtraFileManager albumExtraManager,
Logger logger)
{
_mediaFileService = mediaFileService;
Expand All @@ -48,9 +52,32 @@ public class ExtraService : IExtraService,
_diskProvider = diskProvider;
_configService = configService;
_extraFileManagers = extraFileManagers.OrderBy(e => e.Order).ToList();
_albumExtraManager = albumExtraManager;
_logger = logger;
}

/// <summary>
/// Import extra files belonging to an album. Track-specific extras will be ignored.
/// </summary>
/// <param name="importedTracks">The imported tracks in the source dir.</param>
/// <param name="destinationDir">Destination dir for the album extras</param>
public void ImportAlbumExtras(List<ImportDecision<LocalTrack>> importedTracks, string destinationDir)
{
var firstTrack = importedTracks.First();
var artist = firstTrack.Item.Artist;
var albumId = firstTrack.Item.Album.Id;

var sourceFolder = _diskProvider.GetParentFolder(firstTrack.Item.Path);
var albumFiles = _diskProvider.GetFiles(sourceFolder, false);
var filtered = albumFiles.Where(x => !importedTracks.Select(t => t.Item.Path).Contains(x));

// TODO: grab extensions from config?
var extensions = new List<string> { ".cue", ".jpg", ".png", ".nfo", ".log", ".txt" };
var extras = filtered.Where(x => extensions.Any(ext => x.EndsWith(ext)));

_albumExtraManager.ImportAlbumExtras(artist, albumId, extras, destinationDir);
}

public void ImportTrack(LocalTrack localTrack, TrackFile trackFile, bool isReadOnly)
{
ImportExtraFiles(localTrack, trackFile, isReadOnly);
Expand Down Expand Up @@ -176,6 +203,8 @@ public void Handle(ArtistRenamedEvent message)
{
extraFileManager.MoveFilesAfterRename(artist, trackFiles);
}

_albumExtraManager.MoveFilesAfterRename(artist, trackFiles);
}

private List<TrackFile> GetTrackFiles(int artistId)
Expand Down

0 comments on commit 134c3be

Please sign in to comment.