Whole album matching and fingerprinting (#592)

* Cache result of GetAllArtists

* Fixed: Manual import not respecting album import notifications

* Fixed: partial album imports stay in queue, prompting manual import

* Fixed: Allow release if tracks are missing

* Fixed: Be tolerant of missing/extra "The" at start of artist name

* Improve manual import UI

* Omit video tracks from DB entirely

* Revert "faster test packaging in build.sh"

This reverts commit 2723e2a7b8.

-u and -T are not supported on macOS

* Fix tests on linux and macOS

* Actually lint on linux

On linux yarn runs scripts with sh not bash so ** doesn't recursively glob

* Match whole albums

* Option to disable fingerprinting

* Rip out MediaInfo

* Don't split up things that have the same album selected in manual import

* Try to speed up IndentificationService

* More speedups

* Some fixes and increase power of recording id

* Fix NRE when no tags

* Fix NRE when some (but not all) files in a directory have missing tags

* Bump taglib, tidy up tag parsing

* Add a health check

* Remove media info setting

* Tags -> audioTags

* Add some tests where tags are null

* Rename history events

* Add missing method to interface

* Reinstate MediaInfo tags and update info with artist scan

Also adds migration to remove old format media info

* This file no longer exists

* Don't penalise year if missing from tags

* Formatting improvements

* Use correct system newline

* Switch to the netstandard2.0 library to support net 461

* TagLib.File is IDisposable so should be in a using

* Improve filename matching and add tests

* Neater logging of parsed tags

* Fix disk scan tests for new media info update

* Fix quality detection source

* Fix Inexact Artist/Album match

* Add button to clear track mapping

* Fix warning

* Pacify eslint

* Use \ not /

* Fix UI updates

* Fix media covers

Prevent localizing URL propaging back to the metadata object

* Reduce database overhead broadcasting UI updates

* Relax timings a bit to make test pass

* Remove irrelevant tests

* Test framework for identification service

* Fix PreferMissingToBadMatch test case

* Make fingerprinting more robust

* More logging

* Penalize unknown media format and country

* Prefer USA to UK

* Allow Data CD

* Fix exception if fingerprinting fails for all files

* Fix tests

* Fix NRE

* Allow apostrophes and remove accents in filename aggregation

* Address codacy issues

* Cope with old versions of fpcalc and suggest upgrade

* fpcalc health check passes if fingerprinting disabled

* Get the Artist meta with the artist

* Fix the mapper so that lazy loaded lists will be populated on Join

And therefore we can join TrackFiles on Tracks by default and avoid an
extra query

* Rename subtitle -> lyric

* Tidy up MediaInfoFormatter
This commit is contained in:
ta264
2019-02-16 14:49:24 +00:00
committed by Qstick
parent 8bf364945f
commit bb02d73c42
174 changed files with 11577 additions and 3490 deletions
@@ -12,6 +12,7 @@ using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Music;
using NzbDrone.Core.RootFolders;
using NzbDrone.Test.Common;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
{
@@ -44,6 +45,14 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
Mocker.GetMock<IRootFolderService>()
.Setup(s => s.GetBestRootFolderPath(It.IsAny<string>()))
.Returns(_rootFolder);
Mocker.GetMock<IMakeImportDecision>()
.Setup(v => v.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Artist>()))
.Returns(new List<ImportDecision<LocalTrack>>());
Mocker.GetMock<IMediaFileService>()
.Setup(v => v.GetFilesByArtist(It.IsAny<int>()))
.Returns(new List<TrackFile>());
}
private void GivenRootFolder(params string[] subfolders)
@@ -44,7 +44,7 @@ namespace NzbDrone.Core.Test.MediaFiles
.Returns(true);
Mocker.GetMock<IImportApprovedTracks>()
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true, null, ImportMode.Auto))
.Setup(s => s.Import(It.IsAny<List<ImportDecision<LocalTrack>>>(), true, null, ImportMode.Auto))
.Returns(new List<ImportResult>());
var downloadItem = Builder<DownloadClientItem>.CreateNew()
@@ -76,15 +76,15 @@ namespace NzbDrone.Core.Test.MediaFiles
{
var localTrack = new LocalTrack();
var imported = new List<ImportDecision>();
imported.Add(new ImportDecision(localTrack));
var imported = new List<ImportDecision<LocalTrack>>();
imported.Add(new ImportDecision<LocalTrack>(localTrack));
Mocker.GetMock<IMakeImportDecision>()
.Setup(s => s.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Artist>(), null))
.Returns(imported);
Mocker.GetMock<IImportApprovedTracks>()
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), It.IsAny<bool>(), It.IsAny<DownloadClientItem>(), It.IsAny<ImportMode>()))
.Setup(s => s.Import(It.IsAny<List<ImportDecision<LocalTrack>>>(), It.IsAny<bool>(), It.IsAny<DownloadClientItem>(), It.IsAny<ImportMode>()))
.Returns(imported.Select(i => new ImportResult(i)).ToList())
.Callback(() => WasImportedResponse());
}
@@ -155,7 +155,7 @@ namespace NzbDrone.Core.Test.MediaFiles
public void should_not_delete_folder_if_no_files_were_imported()
{
Mocker.GetMock<IImportApprovedTracks>()
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), false, null, ImportMode.Auto))
.Setup(s => s.Import(It.IsAny<List<ImportDecision<LocalTrack>>>(), false, null, ImportMode.Auto))
.Returns(new List<ImportResult>());
Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory));
@@ -171,15 +171,15 @@ namespace NzbDrone.Core.Test.MediaFiles
var localTrack = new LocalTrack();
var imported = new List<ImportDecision>();
imported.Add(new ImportDecision(localTrack));
var imported = new List<ImportDecision<LocalTrack>>();
imported.Add(new ImportDecision<LocalTrack>(localTrack));
Mocker.GetMock<IMakeImportDecision>()
.Setup(s => s.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Artist>(), null))
.Returns(imported);
Mocker.GetMock<IImportApprovedTracks>()
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true, null, ImportMode.Auto))
.Setup(s => s.Import(It.IsAny<List<ImportDecision<LocalTrack>>>(), true, null, ImportMode.Auto))
.Returns(imported.Select(i => new ImportResult(i)).ToList());
Subject.ProcessRootFolder(new DirectoryInfo(_droneFactory));
@@ -225,8 +225,8 @@ namespace NzbDrone.Core.Test.MediaFiles
result.Should().HaveCount(1);
result.First().ImportDecision.Should().NotBeNull();
result.First().ImportDecision.LocalTrack.Should().NotBeNull();
result.First().ImportDecision.LocalTrack.Path.Should().Be(fileName);
result.First().ImportDecision.Item.Should().NotBeNull();
result.First().ImportDecision.Item.Path.Should().Be(fileName);
result.First().Result.Should().Be(ImportResultType.Rejected);
}
@@ -237,15 +237,15 @@ namespace NzbDrone.Core.Test.MediaFiles
var localTrack = new LocalTrack();
var imported = new List<ImportDecision>();
imported.Add(new ImportDecision(localTrack));
var imported = new List<ImportDecision<LocalTrack>>();
imported.Add(new ImportDecision<LocalTrack>(localTrack));
Mocker.GetMock<IMakeImportDecision>()
.Setup(s => s.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Artist>(), null))
.Returns(imported);
Mocker.GetMock<IImportApprovedTracks>()
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true, null, ImportMode.Auto))
.Setup(s => s.Import(It.IsAny<List<ImportDecision<LocalTrack>>>(), true, null, ImportMode.Auto))
.Returns(imported.Select(i => new ImportResult(i)).ToList());
//Mocker.GetMock<IDetectSample>()
@@ -272,56 +272,6 @@ namespace NzbDrone.Core.Test.MediaFiles
ExceptionVerification.ExpectedWarns(1);
}
[Test]
public void should_use_folder_if_folder_import()
{
GivenValidArtist();
var folderName = @"C:\media\ba09030e-1234-1234-1234-123456789abc\[HorribleSubs] Maria the Virgin Witch - 09 [720p]".AsOsAgnostic();
var fileName = @"C:\media\ba09030e-1234-1234-1234-123456789abc\[HorribleSubs] Maria the Virgin Witch - 09 [720p]\[HorribleSubs] Maria the Virgin Witch - 09 [720p].mkv".AsOsAgnostic();
Mocker.GetMock<IDiskProvider>().Setup(c => c.FolderExists(folderName))
.Returns(true);
Mocker.GetMock<IDiskProvider>().Setup(c => c.GetFiles(folderName, SearchOption.TopDirectoryOnly))
.Returns(new[] { fileName });
var localTrack = new LocalTrack();
var imported = new List<ImportDecision>();
imported.Add(new ImportDecision(localTrack));
Subject.ProcessPath(fileName);
Mocker.GetMock<IMakeImportDecision>()
.Verify(s => s.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Artist>(), It.Is<ParsedTrackInfo>(v => v.TrackNumbers.First() == 9)), Times.Once());
}
[Test]
public void should_not_use_folder_if_file_import()
{
GivenValidArtist();
var fileName = @"C:\media\ba09030e-1234-1234-1234-123456789abc\Torrents\[HorribleSubs] Maria the Virgin Witch - 09 [720p].mkv".AsOsAgnostic();
Mocker.GetMock<IDiskProvider>().Setup(c => c.FolderExists(fileName))
.Returns(false);
Mocker.GetMock<IDiskProvider>().Setup(c => c.FileExists(fileName))
.Returns(true);
var localTrack = new LocalTrack();
var imported = new List<ImportDecision>();
imported.Add(new ImportDecision(localTrack));
var result = Subject.ProcessPath(fileName);
Mocker.GetMock<IMakeImportDecision>()
.Verify(s => s.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Artist>(), null), Times.Once());
}
[Test]
public void should_not_process_if_file_and_folder_do_not_exist()
{
@@ -348,15 +298,15 @@ namespace NzbDrone.Core.Test.MediaFiles
var localTrack = new LocalTrack();
var imported = new List<ImportDecision>();
imported.Add(new ImportDecision(localTrack));
var imported = new List<ImportDecision<LocalTrack>>();
imported.Add(new ImportDecision<LocalTrack>(localTrack));
Mocker.GetMock<IMakeImportDecision>()
.Setup(s => s.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Artist>(), null))
.Returns(imported);
Mocker.GetMock<IImportApprovedTracks>()
.Setup(s => s.Import(It.IsAny<List<ImportDecision>>(), true, null, ImportMode.Auto))
.Setup(s => s.Import(It.IsAny<List<ImportDecision<LocalTrack>>>(), true, null, ImportMode.Auto))
.Returns(new List<ImportResult>());
//Mocker.GetMock<IDetectSample>()
@@ -424,13 +374,13 @@ namespace NzbDrone.Core.Test.MediaFiles
private void VerifyNoImport()
{
Mocker.GetMock<IImportApprovedTracks>().Verify(c => c.Import(It.IsAny<List<ImportDecision>>(), true, null, ImportMode.Auto),
Mocker.GetMock<IImportApprovedTracks>().Verify(c => c.Import(It.IsAny<List<ImportDecision<LocalTrack>>>(), true, null, ImportMode.Auto),
Times.Never());
}
private void VerifyImport()
{
Mocker.GetMock<IImportApprovedTracks>().Verify(c => c.Import(It.IsAny<List<ImportDecision>>(), true, null, ImportMode.Auto),
Mocker.GetMock<IImportApprovedTracks>().Verify(c => c.Import(It.IsAny<List<ImportDecision<LocalTrack>>>(), true, null, ImportMode.Auto),
Times.Once());
}
}
@@ -25,16 +25,16 @@ namespace NzbDrone.Core.Test.MediaFiles
[TestFixture]
public class ImportApprovedTracksFixture : CoreTest<ImportApprovedTracks>
{
private List<ImportDecision> _rejectedDecisions;
private List<ImportDecision> _approvedDecisions;
private List<ImportDecision<LocalTrack>> _rejectedDecisions;
private List<ImportDecision<LocalTrack>> _approvedDecisions;
private DownloadClientItem _downloadClientItem;
[SetUp]
public void Setup()
{
_rejectedDecisions = new List<ImportDecision>();
_approvedDecisions = new List<ImportDecision>();
_rejectedDecisions = new List<ImportDecision<LocalTrack>>();
_approvedDecisions = new List<ImportDecision<LocalTrack>>();
var artist = Builder<Artist>.CreateNew()
.With(e => e.Profile = new Profile { Items = Qualities.QualityFixture.GetDefaultQualities() })
@@ -52,20 +52,21 @@ namespace NzbDrone.Core.Test.MediaFiles
var release = Builder<AlbumRelease>.CreateNew()
.With(e => e.AlbumId = album.Id)
.With(e => e.Monitored = true)
.Build();
album.AlbumReleases = new List<AlbumRelease> { release };
var tracks = Builder<Track>.CreateListOfSize(5)
.Build();
_rejectedDecisions.Add(new ImportDecision(new LocalTrack(), new Rejection("Rejected!")));
_rejectedDecisions.Add(new ImportDecision(new LocalTrack(), new Rejection("Rejected!")));
_rejectedDecisions.Add(new ImportDecision(new LocalTrack(), new Rejection("Rejected!")));
_rejectedDecisions.Add(new ImportDecision<LocalTrack>(new LocalTrack(), new Rejection("Rejected!")));
_rejectedDecisions.Add(new ImportDecision<LocalTrack>(new LocalTrack(), new Rejection("Rejected!")));
_rejectedDecisions.Add(new ImportDecision<LocalTrack>(new LocalTrack(), new Rejection("Rejected!")));
foreach (var track in tracks)
{
_approvedDecisions.Add(new ImportDecision
_approvedDecisions.Add(new ImportDecision<LocalTrack>
(
new LocalTrack
{
@@ -75,7 +76,7 @@ namespace NzbDrone.Core.Test.MediaFiles
Tracks = new List<Track> { track },
Path = Path.Combine(artist.Path, "Alien Ant Farm - 01 - Pilot.mp3"),
Quality = new QualityModel(Quality.MP3_256),
ParsedTrackInfo = new ParsedTrackInfo
FileTrackInfo = new ParsedTrackInfo
{
ReleaseGroup = "DRONE"
}
@@ -91,6 +92,11 @@ namespace NzbDrone.Core.Test.MediaFiles
Mocker.GetMock<IMediaFileService>()
.Setup(s => s.GetFilesWithRelativePath(It.IsAny<int>(), It.IsAny<string>()))
.Returns(new List<TrackFile>());
Mocker.GetMock<IMediaFileService>()
.Setup(s => s.GetFilesByAlbum(It.IsAny<int>()))
.Returns(new List<TrackFile>());
}
[Test]
@@ -110,7 +116,7 @@ namespace NzbDrone.Core.Test.MediaFiles
[Test]
public void should_only_import_approved()
{
var all = new List<ImportDecision>();
var all = new List<ImportDecision<LocalTrack>>();
all.AddRange(_rejectedDecisions);
all.AddRange(_approvedDecisions);
@@ -123,9 +129,9 @@ namespace NzbDrone.Core.Test.MediaFiles
[Test]
public void should_only_import_each_track_once()
{
var all = new List<ImportDecision>();
var all = new List<ImportDecision<LocalTrack>>();
all.AddRange(_approvedDecisions);
all.Add(new ImportDecision(_approvedDecisions.First().LocalTrack));
all.Add(new ImportDecision<LocalTrack>(_approvedDecisions.First().Item));
var result = Subject.Import(all, false);
@@ -135,17 +141,17 @@ namespace NzbDrone.Core.Test.MediaFiles
[Test]
public void should_move_new_downloads()
{
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true);
Subject.Import(new List<ImportDecision<LocalTrack>> { _approvedDecisions.First() }, true);
Mocker.GetMock<IUpgradeMediaFiles>()
.Verify(v => v.UpgradeTrackFile(It.IsAny<TrackFile>(), _approvedDecisions.First().LocalTrack, false),
.Verify(v => v.UpgradeTrackFile(It.IsAny<TrackFile>(), _approvedDecisions.First().Item, false),
Times.Once());
}
[Test]
public void should_publish_TrackImportedEvent_for_new_downloads()
{
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true);
Subject.Import(new List<ImportDecision<LocalTrack>> { _approvedDecisions.First() }, true);
Mocker.GetMock<IEventAggregator>()
.Verify(v => v.PublishEvent(It.IsAny<TrackImportedEvent>()), Times.Once());
@@ -154,10 +160,10 @@ namespace NzbDrone.Core.Test.MediaFiles
[Test]
public void should_not_move_existing_files()
{
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, false);
Subject.Import(new List<ImportDecision<LocalTrack>> { _approvedDecisions.First() }, false);
Mocker.GetMock<IUpgradeMediaFiles>()
.Verify(v => v.UpgradeTrackFile(It.IsAny<TrackFile>(), _approvedDecisions.First().LocalTrack, false),
.Verify(v => v.UpgradeTrackFile(It.IsAny<TrackFile>(), _approvedDecisions.First().Item, false),
Times.Never());
}
@@ -165,21 +171,21 @@ namespace NzbDrone.Core.Test.MediaFiles
public void should_import_larger_files_first()
{
var fileDecision = _approvedDecisions.First();
fileDecision.LocalTrack.Size = 1.Gigabytes();
fileDecision.Item.Size = 1.Gigabytes();
var sampleDecision = new ImportDecision
var sampleDecision = new ImportDecision<LocalTrack>
(new LocalTrack
{
Artist = fileDecision.LocalTrack.Artist,
Album = fileDecision.LocalTrack.Album,
Tracks = new List<Track> { fileDecision.LocalTrack.Tracks.First() },
Artist = fileDecision.Item.Artist,
Album = fileDecision.Item.Album,
Tracks = new List<Track> { fileDecision.Item.Tracks.First() },
Path = @"C:\Test\Music\Alien Ant Farm\Alien Ant Farm - 01 - Pilot.mp3".AsOsAgnostic(),
Quality = new QualityModel(Quality.MP3_256),
Size = 80.Megabytes()
});
var all = new List<ImportDecision>();
var all = new List<ImportDecision<LocalTrack>>();
all.Add(fileDecision);
all.Add(sampleDecision);
@@ -187,25 +193,25 @@ namespace NzbDrone.Core.Test.MediaFiles
results.Should().HaveCount(all.Count);
results.Should().ContainSingle(d => d.Result == ImportResultType.Imported);
results.Should().ContainSingle(d => d.Result == ImportResultType.Imported && d.ImportDecision.LocalTrack.Size == fileDecision.LocalTrack.Size);
results.Should().ContainSingle(d => d.Result == ImportResultType.Imported && d.ImportDecision.Item.Size == fileDecision.Item.Size);
}
[Test]
public void should_copy_when_cannot_move_files_downloads()
{
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true, new DownloadClientItem { Title = "Alien.Ant.Farm-Truant", CanMoveFiles = false });
Subject.Import(new List<ImportDecision<LocalTrack>> { _approvedDecisions.First() }, true, new DownloadClientItem { Title = "Alien.Ant.Farm-Truant", CanMoveFiles = false });
Mocker.GetMock<IUpgradeMediaFiles>()
.Verify(v => v.UpgradeTrackFile(It.IsAny<TrackFile>(), _approvedDecisions.First().LocalTrack, true), Times.Once());
.Verify(v => v.UpgradeTrackFile(It.IsAny<TrackFile>(), _approvedDecisions.First().Item, true), Times.Once());
}
[Test]
public void should_use_override_importmode()
{
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true, new DownloadClientItem { Title = "Alien.Ant.Farm-Truant", CanMoveFiles = false }, ImportMode.Move);
Subject.Import(new List<ImportDecision<LocalTrack>> { _approvedDecisions.First() }, true, new DownloadClientItem { Title = "Alien.Ant.Farm-Truant", CanMoveFiles = false }, ImportMode.Move);
Mocker.GetMock<IUpgradeMediaFiles>()
.Verify(v => v.UpgradeTrackFile(It.IsAny<TrackFile>(), _approvedDecisions.First().LocalTrack, false), Times.Once());
.Verify(v => v.UpgradeTrackFile(It.IsAny<TrackFile>(), _approvedDecisions.First().Item, false), Times.Once());
}
[Test]
@@ -215,7 +221,7 @@ namespace NzbDrone.Core.Test.MediaFiles
.Setup(s => s.GetFilesWithRelativePath(It.IsAny<int>(), It.IsAny<string>()))
.Returns(Builder<TrackFile>.CreateListOfSize(1).BuildList());
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, false);
Subject.Import(new List<ImportDecision<LocalTrack>> { _approvedDecisions.First() }, false);
Mocker.GetMock<IMediaFileService>()
.Verify(v => v.Delete(It.IsAny<TrackFile>(), DeleteMediaFileReason.ManualOverride), Times.Once());
@@ -5,32 +5,31 @@ using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Music;
using NzbDrone.Core.Test.Framework;
using System.Collections.Generic;
namespace NzbDrone.Core.Test.MediaFiles
{
[TestFixture]
public class MediaFileRepositoryFixture : DbTest<MediaFileRepository, TrackFile>
{
[Test]
public void get_files_by_artist()
private Artist artist;
private Album album;
[SetUp]
public void Setup()
{
var files = Builder<TrackFile>.CreateListOfSize(10)
.All()
.With(c => c.Id = 0)
.With(c => c.Quality =new QualityModel(Quality.MP3_192))
.BuildListOfNew();
Db.InsertMany(files);
Db.All<TrackFile>().Should().HaveCount(10);
var meta = Builder<ArtistMetadata>.CreateNew()
.With(a => a.Id = 0)
.Build();
Db.Insert(meta);
var artist = Builder<Artist>.CreateNew()
.With(a => a.ArtistMetadataId = 11)
artist = Builder<Artist>.CreateNew()
.With(a => a.ArtistMetadataId = meta.Id)
.With(a => a.Id = 0)
.Build();
Db.Insert(artist);
var album = Builder<Album>.CreateNew()
album = Builder<Album>.CreateNew()
.With(a => a.Id = 0)
.With(a => a.ArtistMetadataId = artist.ArtistMetadataId)
.Build();
@@ -43,6 +42,15 @@ namespace NzbDrone.Core.Test.MediaFiles
.Build();
Db.Insert(release);
var files = Builder<TrackFile>.CreateListOfSize(10)
.All()
.With(c => c.Id = 0)
.With(c => c.Quality =new QualityModel(Quality.MP3_192))
.TheFirst(4)
.With(c => c.AlbumId = album.Id)
.BuildListOfNew();
Db.InsertMany(files);
var track = Builder<Track>.CreateListOfSize(10)
.TheFirst(1)
.With(a => a.TrackFileId = files[1].Id)
@@ -59,16 +67,58 @@ namespace NzbDrone.Core.Test.MediaFiles
.With(a => a.AlbumReleaseId = release.Id)
.Build();
Db.InsertMany(track);
}
[Test]
public void get_files_by_artist()
{
VerifyData();
var artistFiles = Subject.GetFilesByArtist(artist.Id);
VerifyEagerLoaded(artistFiles);
artistFiles.Should().HaveCount(4);
artistFiles.Should().OnlyContain(c => c.Artist.Value.Id == artist.Id);
}
[Test]
public void get_files_by_album()
{
VerifyData();
var files = Subject.GetFilesByAlbum(album.Id);
VerifyEagerLoaded(files);
files.Should().HaveCount(4);
files.Should().OnlyContain(c => c.AlbumId == album.Id);
}
[Test]
public void get_files_by_relative_path()
{
VerifyData();
var files = Subject.GetFilesWithRelativePath(artist.Id, "RelativePath2");
VerifyEagerLoaded(files);
files.Should().HaveCount(1);
files.Should().OnlyContain(c => c.AlbumId == album.Id);
files.Should().OnlyContain(c => c.RelativePath == "RelativePath2");
}
private void VerifyData()
{
Db.All<Artist>().Should().HaveCount(1);
Db.All<Album>().Should().HaveCount(1);
Db.All<Track>().Should().HaveCount(10);
Db.All<TrackFile>().Should().HaveCount(10);
}
var artistFiles = Subject.GetFilesByArtist(artist.Id);
artistFiles.Should().HaveCount(4);
artistFiles.Should().OnlyContain(c => c.ArtistId == artist.Id);
private void VerifyEagerLoaded(List<TrackFile> files)
{
foreach (var file in files)
{
file.Album.IsLoaded.Should().BeTrue();
file.Artist.IsLoaded.Should().BeTrue();
file.Artist.Value.Metadata.IsLoaded.Should().BeTrue();
}
}
}
}
@@ -1,149 +0,0 @@
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.MediaFiles.MediaInfo;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.MediaFiles.MediaInfo.MediaInfoFormatterTests
{
[TestFixture]
public class FormatAudioChannelsFixture : TestBase
{
[Test]
public void should_subtract_one_from_AudioChannels_as_total_channels_if_LFE_in_AudioChannelPositionsText()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannels = 6,
AudioChannelPositions = null,
AudioChannelPositionsText = "Front: L C R, Side: L R, LFE"
};
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(5.1m);
}
[Test]
public void should_use_AudioChannels_as_total_channels_if_LFE_not_in_AudioChannelPositionsText()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannels = 2,
AudioChannelPositions = null,
AudioChannelPositionsText = "Front: L R"
};
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(2);
}
[Test]
public void should_return_0_if_schema_revision_is_less_than_3_and_other_properties_are_null()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannels = 2,
AudioChannelPositions = null,
AudioChannelPositionsText = null,
SchemaRevision = 2
};
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(0);
}
[Test]
public void should_use_AudioChannels_if_schema_revision_is_3_and_other_properties_are_null()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannels = 2,
AudioChannelPositions = null,
AudioChannelPositionsText = null,
SchemaRevision = 3
};
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(2);
}
[Test]
public void should_sum_AudioChannelPositions()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannels = 2,
AudioChannelPositions = "2/0/0",
AudioChannelPositionsText = null,
SchemaRevision = 3
};
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(2);
}
[Test]
public void should_sum_AudioChannelPositions_including_decimal()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannels = 2,
AudioChannelPositions = "3/2/0.1",
AudioChannelPositionsText = null,
SchemaRevision = 3
};
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(5.1m);
}
[Test]
public void should_cleanup_extraneous_text_from_AudioChannelPositions()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannels = 2,
AudioChannelPositions = "Object Based / 3/2/2.1",
AudioChannelPositionsText = null,
SchemaRevision = 3
};
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(7.1m);
}
[Test]
public void should_skip_empty_groups_in_AudioChannelPositions()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannels = 2,
AudioChannelPositions = " / 2/0/0.0",
AudioChannelPositionsText = null,
SchemaRevision = 3
};
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(2);
}
[Test]
public void should_sum_first_series_of_numbers_from_AudioChannelPositions()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannels = 2,
AudioChannelPositions = "3/2/2.1 / 3/2/2.1",
AudioChannelPositionsText = null,
SchemaRevision = 3
};
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(7.1m);
}
[Test]
public void should_sum_dual_mono_representation_AudioChannelPositions()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannels = 2,
AudioChannelPositions = "1+1",
AudioChannelPositionsText = null,
SchemaRevision = 3
};
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(2.0m);
}
}
}
@@ -1,70 +0,0 @@
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.MediaFiles.MediaInfo;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.MediaFiles.MediaInfo.MediaInfoFormatterTests
{
[TestFixture]
public class FormatAudioCodecFixture : TestBase
{
[TestCase("AC-3", "AC3")]
[TestCase("E-AC-3", "EAC3")]
[TestCase("MPEG Audio", "MPEG Audio")]
[TestCase("DTS", "DTS")]
public void should_format_audio_format(string audioFormat, string expectedFormat)
{
var mediaInfoModel = new MediaInfoModel
{
AudioFormat = audioFormat
};
MediaInfoFormatter.FormatAudioCodec(mediaInfoModel).Should().Be(expectedFormat);
}
[TestCase("MPEG Audio, A_MPEG/L2, , ", "droned.s01e03.swedish.720p.hdtv.x264-prince", "MP2")]
[TestCase("Vorbis, A_VORBIS, , Xiph.Org libVorbis I 20101101 (Schaufenugget)", "DB Super HDTV", "Vorbis")]
[TestCase("PCM, 1, , ", "DW DVDRip XviD-idTV", "PCM")] // Dubbed most likely
[TestCase("TrueHD, A_TRUEHD, , ", "", "TrueHD")]
[TestCase("WMA, 161, , ", "Droned.wmv", "WMA")]
[TestCase("WMA, 162, Pro, ", "B.N.S04E18.720p.WEB-DL", "WMA")]
public void should_format_audio_format(string audioFormatPack, string sceneName, string expectedFormat)
{
var split = audioFormatPack.Split(new string[] { ", " }, System.StringSplitOptions.None);
var mediaInfoModel = new MediaInfoModel
{
AudioFormat = split[0],
AudioCodecID = split[1],
AudioProfile = split[2],
AudioCodecLibrary = split[3]
};
MediaInfoFormatter.FormatAudioCodec(mediaInfoModel).Should().Be(expectedFormat);
}
[Test]
public void should_return_MP3_for_MPEG_Audio_with_Layer_3_for_the_profile()
{
var mediaInfoModel = new MediaInfoModel
{
AudioFormat = "MPEG Audio",
AudioProfile = "Layer 3"
};
MediaInfoFormatter.FormatAudioCodec(mediaInfoModel).Should().Be("MP3");
}
[Test]
public void should_return_AudioFormat_by_default()
{
var mediaInfoModel = new MediaInfoModel
{
AudioFormat = "Other Audio Format",
AudioCodecID = "Other Audio Codec"
};
MediaInfoFormatter.FormatAudioCodec(mediaInfoModel).Should().Be(mediaInfoModel.AudioFormat);
ExceptionVerification.ExpectedWarns(1);
}
}
}
@@ -1,184 +0,0 @@
using System.IO;
using FizzWare.NBuilder;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.MediaFiles.MediaInfo;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Music;
using NzbDrone.Test.Common;
using NzbDrone.Core.Configuration;
namespace NzbDrone.Core.Test.MediaFiles.MediaInfo
{
[TestFixture]
public class UpdateMediaInfoServiceFixture : CoreTest<UpdateMediaInfoService>
{
private Artist _artist;
[SetUp]
public void Setup()
{
_artist = new Artist
{
Id = 1,
Path = @"C:\artist".AsOsAgnostic()
};
Mocker.GetMock<IConfigService>()
.SetupGet(s => s.EnableMediaInfo)
.Returns(true);
}
private void GivenFileExists()
{
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.FileExists(It.IsAny<string>()))
.Returns(true);
}
private void GivenSuccessfulScan()
{
Mocker.GetMock<IVideoFileInfoReader>()
.Setup(v => v.GetMediaInfo(It.IsAny<string>()))
.Returns(new MediaInfoModel());
}
private void GivenFailedScan(string path)
{
Mocker.GetMock<IVideoFileInfoReader>()
.Setup(v => v.GetMediaInfo(path))
.Returns((MediaInfoModel)null);
}
[Test]
public void should_skip_up_to_date_media_info()
{
var trackFiles = Builder<TrackFile>.CreateListOfSize(3)
.All()
.With(v => v.RelativePath = "media.flac")
.TheFirst(1)
.With(v => v.MediaInfo = new MediaInfoModel { SchemaRevision = VideoFileInfoReader.CURRENT_MEDIA_INFO_SCHEMA_REVISION })
.BuildList();
Mocker.GetMock<IMediaFileService>()
.Setup(v => v.GetFilesByArtist(1))
.Returns(trackFiles);
GivenFileExists();
GivenSuccessfulScan();
Subject.Handle(new ArtistScannedEvent(_artist));
Mocker.GetMock<IVideoFileInfoReader>()
.Verify(v => v.GetMediaInfo(Path.Combine(_artist.Path, "media.flac")), Times.Exactly(2));
Mocker.GetMock<IMediaFileService>()
.Verify(v => v.Update(It.IsAny<TrackFile>()), Times.Exactly(2));
}
[Test]
public void should_skip_not_yet_date_media_info()
{
var trackFiles = Builder<TrackFile>.CreateListOfSize(3)
.All()
.With(v => v.RelativePath = "media.flac")
.TheFirst(1)
.With(v => v.MediaInfo = new MediaInfoModel { SchemaRevision = VideoFileInfoReader.MINIMUM_MEDIA_INFO_SCHEMA_REVISION })
.BuildList();
Mocker.GetMock<IMediaFileService>()
.Setup(v => v.GetFilesByArtist(1))
.Returns(trackFiles);
GivenFileExists();
GivenSuccessfulScan();
Subject.Handle(new ArtistScannedEvent(_artist));
Mocker.GetMock<IVideoFileInfoReader>()
.Verify(v => v.GetMediaInfo(Path.Combine(_artist.Path, "media.flac")), Times.Exactly(2));
Mocker.GetMock<IMediaFileService>()
.Verify(v => v.Update(It.IsAny<TrackFile>()), Times.Exactly(2));
}
[Test]
public void should_update_outdated_media_info()
{
var trackFiles = Builder<TrackFile>.CreateListOfSize(3)
.All()
.With(v => v.RelativePath = "media.flac")
.TheFirst(1)
.With(v => v.MediaInfo = new MediaInfoModel())
.BuildList();
Mocker.GetMock<IMediaFileService>()
.Setup(v => v.GetFilesByArtist(1))
.Returns(trackFiles);
GivenFileExists();
GivenSuccessfulScan();
Subject.Handle(new ArtistScannedEvent(_artist));
Mocker.GetMock<IVideoFileInfoReader>()
.Verify(v => v.GetMediaInfo(Path.Combine(_artist.Path, "media.flac")), Times.Exactly(3));
Mocker.GetMock<IMediaFileService>()
.Verify(v => v.Update(It.IsAny<TrackFile>()), Times.Exactly(3));
}
[Test]
public void should_ignore_missing_files()
{
var trackFiles = Builder<TrackFile>.CreateListOfSize(2)
.All()
.With(v => v.RelativePath = "media.flac")
.BuildList();
Mocker.GetMock<IMediaFileService>()
.Setup(v => v.GetFilesByArtist(1))
.Returns(trackFiles);
GivenSuccessfulScan();
Subject.Handle(new ArtistScannedEvent(_artist));
Mocker.GetMock<IVideoFileInfoReader>()
.Verify(v => v.GetMediaInfo("media.flac"), Times.Never());
Mocker.GetMock<IMediaFileService>()
.Verify(v => v.Update(It.IsAny<TrackFile>()), Times.Never());
}
[Test]
public void should_continue_after_failure()
{
var episodeFiles = Builder<TrackFile>.CreateListOfSize(2)
.All()
.With(v => v.RelativePath = "media.flac")
.TheFirst(1)
.With(v => v.RelativePath = "media2.flac")
.BuildList();
Mocker.GetMock<IMediaFileService>()
.Setup(v => v.GetFilesByArtist(1))
.Returns(episodeFiles);
GivenFileExists();
GivenSuccessfulScan();
GivenFailedScan(Path.Combine(_artist.Path, "media2.flac"));
Subject.Handle(new ArtistScannedEvent(_artist));
Mocker.GetMock<IVideoFileInfoReader>()
.Verify(v => v.GetMediaInfo(Path.Combine(_artist.Path, "media.flac")), Times.Exactly(1));
Mocker.GetMock<IMediaFileService>()
.Verify(v => v.Update(It.IsAny<TrackFile>()), Times.Exactly(1));
}
}
}
@@ -1,115 +0,0 @@
using System.IO;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Core.MediaFiles.MediaInfo;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common.Categories;
namespace NzbDrone.Core.Test.MediaFiles.MediaInfo
{
[TestFixture]
[DiskAccessTest]
public class VideoFileInfoReaderFixture : CoreTest<VideoFileInfoReader>
{
[SetUp]
public void Setup()
{
Mocker.GetMock<IDiskProvider>()
.Setup(s => s.FileExists(It.IsAny<string>()))
.Returns(true);
Mocker.GetMock<IDiskProvider>()
.Setup(s => s.OpenReadStream(It.IsAny<string>()))
.Returns<string>(s => new FileStream(s, FileMode.Open, FileAccess.Read));
}
[Test]
public void get_runtime()
{
var path = Path.Combine(TestContext.CurrentContext.TestDirectory, "Files", "Media", "H264_sample.mp4");
Subject.GetRunTime(path).Seconds.Should().Be(10);
}
[Test]
public void get_info()
{
var path = Path.Combine(TestContext.CurrentContext.TestDirectory, "Files", "Media", "H264_sample.mp4");
var info = Subject.GetMediaInfo(path);
info.VideoCodec.Should().BeNull();
info.VideoFormat.Should().Be("AVC");
info.VideoCodecID.Should().Be("avc1");
info.VideoProfile.Should().Be("Baseline@L2.1");
info.VideoCodecLibrary.Should().Be("");
info.AudioFormat.Should().Be("AAC");
info.AudioCodecID.Should().BeOneOf("40", "mp4a-40-2");
info.AudioProfile.Should().Be("LC");
info.AudioCodecLibrary.Should().Be("");
info.AudioBitrate.Should().Be(128000);
info.AudioChannels.Should().Be(2);
info.AudioLanguages.Should().Be("English");
info.Height.Should().Be(320);
info.RunTime.Seconds.Should().Be(10);
info.ScanType.Should().Be("Progressive");
info.Subtitles.Should().Be("");
info.VideoBitrate.Should().Be(193329);
info.VideoFps.Should().Be(24);
info.Width.Should().Be(480);
}
[Test]
public void get_info_unicode()
{
var srcPath = Path.Combine(TestContext.CurrentContext.TestDirectory, "Files", "Media", "H264_sample.mp4");
var tempPath = GetTempFilePath();
Directory.CreateDirectory(tempPath);
var path = Path.Combine(tempPath, "H264_Pok\u00E9mon.mkv");
File.Copy(srcPath, path);
var info = Subject.GetMediaInfo(path);
info.VideoCodec.Should().BeNull();
info.VideoFormat.Should().Be("AVC");
info.VideoCodecID.Should().Be("avc1");
info.VideoProfile.Should().Be("Baseline@L2.1");
info.VideoCodecLibrary.Should().Be("");
info.AudioFormat.Should().Be("AAC");
info.AudioCodecID.Should().BeOneOf("40", "mp4a-40-2");
info.AudioProfile.Should().Be("LC");
info.AudioCodecLibrary.Should().Be("");
info.AudioBitrate.Should().Be(128000);
info.AudioChannels.Should().Be(2);
info.AudioLanguages.Should().Be("English");
info.Height.Should().Be(320);
info.RunTime.Seconds.Should().Be(10);
info.ScanType.Should().Be("Progressive");
info.Subtitles.Should().Be("");
info.VideoBitrate.Should().Be(193329);
info.VideoFps.Should().Be(24);
info.Width.Should().Be(480);
}
[Test]
public void should_dispose_file_after_scanning_mediainfo()
{
var path = Path.Combine(TestContext.CurrentContext.TestDirectory, "Files", "Media", "H264_sample.mp4");
var info = Subject.GetMediaInfo(path);
var stream = new FileStream(path, FileMode.Open, FileAccess.Write);
stream.Close();
}
}
}
@@ -0,0 +1,202 @@
using NUnit.Framework;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Parser.Model;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using NzbDrone.Test.Common;
using NzbDrone.Core.MediaFiles.TrackImport.Aggregation.Aggregators;
using FluentAssertions;
using System.Text;
using System;
using System.Collections;
namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Aggregation.Aggregators
{
[TestFixture]
public class AggregateFilenameInfoFixture : CoreTest<AggregateFilenameInfo>
{
private LocalAlbumRelease GivenTracks(List<string> files, string root)
{
var tracks = files.Select(x => new LocalTrack {
Path = Path.Combine(root, x),
FileTrackInfo = new ParsedTrackInfo {
TrackNumbers = new [] { 0 },
}
}).ToList();
return new LocalAlbumRelease(tracks);
}
private void VerifyData(LocalTrack track, string artist, string title, int trackNum, int disc)
{
track.FileTrackInfo.ArtistTitle.Should().Be(artist);
track.FileTrackInfo.Title.Should().Be(title);
track.FileTrackInfo.TrackNumbers[0].Should().Be(trackNum);
track.FileTrackInfo.DiscNumber.Should().Be(disc);
}
[Test]
public void should_aggregate_filenames_example()
{
var release = GivenTracks(new List<string> {
"Adele - 19 - 101 - Daydreamer.mp3",
"Adele - 19 - 102 - Best for Last.mp3",
"Adele - 19 - 103 - Chasing Pavements.mp3",
"Adele - 19 - 203 - That's It, I Quit, I'm Moving On.mp3"
}, @"C:\incoming".AsOsAgnostic());
Subject.Aggregate(release, true);
VerifyData(release.LocalTracks[0], "Adele", "Daydreamer", 1, 1);
VerifyData(release.LocalTracks[1], "Adele", "Best for Last", 2, 1);
VerifyData(release.LocalTracks[2], "Adele", "Chasing Pavements", 3, 1);
VerifyData(release.LocalTracks[3], "Adele", "That's It, I Quit, I'm Moving On", 3, 2);
}
public static class TestCaseFactory
{
private static List<string[]> tokenList = new List<string[]> {
new [] {"trackNum2", "artist", "title", "tag"},
new [] {"trackNum3", "artist", "title", "tag"},
new [] {"trackNum2", "artist", "tag", "title"},
new [] {"trackNum3", "artist", "tag", "title"},
new [] {"trackNum2", "artist", "title"},
new [] {"trackNum3", "artist", "title"},
new [] {"artist", "tag", "trackNum2", "title"},
new [] {"artist", "tag", "trackNum3", "title"},
new [] {"artist", "trackNum2", "title", "tag"},
new [] {"artist", "trackNum3", "title", "tag"},
new [] {"artist", "trackNum2", "title"},
new [] {"artist", "trackNum3", "title"},
new [] {"artist", "title", "tag"},
new [] {"artist", "tag", "title"},
new [] {"artist", "title"},
new [] {"trackNum2", "title"},
new [] {"trackNum3", "title"},
new [] {"title"},
};
private static List<Tuple<string, string>> separators = new List<Tuple<string, string>> {
Tuple.Create(" - ", " "),
Tuple.Create("_", " "),
Tuple.Create("-", "_")
};
private static List<Tuple<string[], string, string>> otherCases = new List<Tuple<string[], string, string>> {
Tuple.Create(new [] {"track2", "title"}, " ", " "),
Tuple.Create(new [] {"track3", "title"}, " ", " ")
};
public static IEnumerable TestCases
{
get
{
int i = 0;
foreach (var tokens in tokenList)
{
foreach (var separator in separators)
{
i++;
yield return new TestCaseData(Tuple.Create(tokens, separator.Item1, separator.Item2))
.SetName($"should_aggregate_filenames_auto_{i}")
.SetDescription($"tokens: {string.Join(", ", tokens)}, separator: '{separator.Item1}', whitespace: '{separator.Item2}'");
}
}
// and a few other cases where all the permutations don't make sense
foreach (var item in otherCases)
{
i++;
yield return new TestCaseData(item)
.SetName($"should_aggregate_filenames_auto_{i}")
.SetDescription($"tokens: {string.Join(", ", item.Item1)}, separator: '{item.Item2}', whitespace: '{item.Item3}'");
}
}
}
}
private List<string> GivenFilenames(string[] fields, string fieldSeparator, string whitespace)
{
var outp = new List<string>();
for (int i = 1; i <= 3; i++)
{
var components = new List<string>();
foreach (var field in fields)
{
switch(field)
{
case "artist":
components.Add("artist name".Replace(" ", whitespace));
break;
case "tag":
components.Add("tag string ignore".Replace(" ", whitespace));
break;
case "title":
components.Add($"{(char)(96+i)} track title {i}".Replace(" ", whitespace));
break;
case "trackNum2":
components.Add(i.ToString("00"));
break;
case "trackNum3":
components.Add((100 + i).ToString("000"));
break;
}
}
outp.Add(string.Join(fieldSeparator, components) + ".mp3");
}
return outp;
}
private void VerifyDataAuto(List<LocalTrack> tracks, string[] tokens, string whitespace)
{
for (int i = 1; i <= tracks.Count; i++)
{
var info = tracks[i-1].FileTrackInfo;
if (tokens.Contains("artist"))
{
info.ArtistTitle.Should().Be("artist name".Replace(" ", whitespace));
}
if (tokens.Contains("title"))
{
info.Title.Should().Be($"{(char)(96+i)} track title {i}".Replace(" ", whitespace));
}
if (tokens.Contains("trackNum2") || tokens.Contains("trackNum3"))
{
info.TrackNumbers[0].Should().Be(i);
}
if (tokens.Contains("trackNum3"))
{
info.DiscNumber.Should().Be(1);
}
else
{
info.DiscNumber.Should().Be(0);
}
}
}
[Test, TestCaseSource(typeof(TestCaseFactory), "TestCases")]
public void should_aggregate_filenames_auto(Tuple<string[], string, string> testcase)
{
var files = GivenFilenames(testcase.Item1, testcase.Item2, testcase.Item3);
var release = GivenTracks(files, @"C:\incoming".AsOsAgnostic());
Subject.Aggregate(release, true);
VerifyDataAuto(release.LocalTracks, testcase.Item1, testcase.Item3);
}
}
}
@@ -0,0 +1,216 @@
using NUnit.Framework;
using NzbDrone.Core.MediaFiles.TrackImport.Identification;
using FluentAssertions;
using NzbDrone.Core.Test.Framework;
using FizzWare.NBuilder;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Music;
using System.Collections.Generic;
using System.Linq;
using System;
using NzbDrone.Core.Parser;
namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
{
[TestFixture]
public class AlbumDistanceFixture : CoreTest<IdentificationService>
{
private ArtistMetadata artist;
[SetUp]
public void Setup()
{
artist = Builder<ArtistMetadata>
.CreateNew()
.With(x => x.Name = "artist")
.Build();
}
private List<Track> GivenTracks(int count)
{
return Builder<Track>
.CreateListOfSize(count)
.All()
.With(x => x.ArtistMetadata = artist)
.With(x => x.MediumNumber = 1)
.Build()
.ToList();
}
private LocalTrack GivenLocalTrack(Track track, AlbumRelease release)
{
var fileInfo = Builder<ParsedTrackInfo>
.CreateNew()
.With(x => x.Title = track.Title)
.With(x => x.CleanTitle = track.Title.CleanTrackTitle())
.With(x => x.AlbumTitle = release.Title)
.With(x => x.Disambiguation = release.Disambiguation)
.With(x => x.ReleaseMBId = release.ForeignReleaseId)
.With(x => x.ArtistTitle = track.ArtistMetadata.Value.Name)
.With(x => x.TrackNumbers = new[] { track.AbsoluteTrackNumber })
.With(x => x.DiscCount = release.Media.Count)
.With(x => x.DiscNumber = track.MediumNumber)
.With(x => x.RecordingMBId = track.ForeignRecordingId)
.With(x => x.Country = IsoCountries.Find("US"))
.With(x => x.Label = release.Label.First())
.With(x => x.Year = (uint)release.Album.Value.ReleaseDate.Value.Year)
.Build();
var localTrack = Builder<LocalTrack>
.CreateNew()
.With(x => x.FileTrackInfo = fileInfo)
.Build();
return localTrack;
}
private List<LocalTrack> GivenLocalTracks(List<Track> tracks, AlbumRelease release)
{
var output = new List<LocalTrack>();
foreach (var track in tracks)
{
output.Add(GivenLocalTrack(track, release));
}
return output;
}
private AlbumRelease GivenAlbumRelease(string title, List<Track> tracks)
{
var album = Builder<Album>
.CreateNew()
.With(x => x.Title = title)
.With(x => x.ArtistMetadata = artist)
.Build();
var media = Builder<Medium>
.CreateListOfSize(tracks.Max(x => x.MediumNumber))
.Build()
.ToList();
return Builder<AlbumRelease>
.CreateNew()
.With(x => x.Tracks = tracks)
.With(x => x.Title = title)
.With(x => x.Album = album)
.With(x => x.Media = media)
.With(x => x.Country = new List<string> { "United States" })
.With(x => x.Label = new List<string> { "label" })
.Build();
}
private TrackMapping GivenMapping(List<LocalTrack> local, List<Track> remote)
{
var mapping = new TrackMapping();
var distances = local.Zip(remote, (l, r) => Tuple.Create(r, Subject.TrackDistance(l, r, Subject.GetTotalTrackNumber(r, remote))));
mapping.Mapping = local.Zip(distances, (l, r) => new { l, r }).ToDictionary(x => x.l, x => x.r);
mapping.LocalExtra = local.Except(mapping.Mapping.Keys).ToList();
mapping.MBExtra = remote.Except(mapping.Mapping.Values.Select(x => x.Item1)).ToList();
return mapping;
}
[Test]
public void test_identical_albums()
{
var tracks = GivenTracks(3);
var release = GivenAlbumRelease("album", tracks);
var localTracks = GivenLocalTracks(tracks, release);
var mapping = GivenMapping(localTracks, tracks);
Subject.AlbumReleaseDistance(localTracks, release, mapping).NormalizedDistance().Should().Be(0.0);
}
[Test]
public void test_incomplete_album()
{
var tracks = GivenTracks(3);
var release = GivenAlbumRelease("album", tracks);
var localTracks = GivenLocalTracks(tracks, release);
localTracks.RemoveAt(1);
var mapping = GivenMapping(localTracks, tracks);
var dist = Subject.AlbumReleaseDistance(localTracks, release, mapping);
dist.NormalizedDistance().Should().NotBe(0.0);
dist.NormalizedDistance().Should().BeLessThan(0.2);
}
[Test]
public void test_global_artists_differ()
{
var tracks = GivenTracks(3);
var release = GivenAlbumRelease("album", tracks);
var localTracks = GivenLocalTracks(tracks, release);
var mapping = GivenMapping(localTracks, tracks);
release.Album.Value.ArtistMetadata = Builder<ArtistMetadata>
.CreateNew()
.With(x => x.Name = "different artist")
.Build();
Subject.AlbumReleaseDistance(localTracks, release, mapping).NormalizedDistance().Should().NotBe(0.0);
}
[Test]
public void test_comp_track_artists_match()
{
var tracks = GivenTracks(3);
var release = GivenAlbumRelease("album", tracks);
var localTracks = GivenLocalTracks(tracks, release);
var mapping = GivenMapping(localTracks, tracks);
release.Album.Value.ArtistMetadata = Builder<ArtistMetadata>
.CreateNew()
.With(x => x.Name = "Various Artists")
.With(x => x.ForeignArtistId = "89ad4ac3-39f7-470e-963a-56509c546377")
.Build();
Subject.AlbumReleaseDistance(localTracks, release, mapping).NormalizedDistance().Should().Be(0.0);
}
// TODO: there are a couple more VA tests in beets but we don't support VA yet anyway
[Test]
public void test_tracks_out_of_order()
{
var tracks = GivenTracks(3);
var release = GivenAlbumRelease("album", tracks);
var localTracks = GivenLocalTracks(tracks, release);
localTracks = new [] {1, 3, 2}.Select(x => localTracks[x-1]).ToList();
var mapping = GivenMapping(localTracks, tracks);
var dist = Subject.AlbumReleaseDistance(localTracks, release, mapping);
dist.NormalizedDistance().Should().NotBe(0.0);
dist.NormalizedDistance().Should().BeLessThan(0.2);
}
[Test]
public void test_two_medium_release()
{
var tracks = GivenTracks(3);
tracks[2].AbsoluteTrackNumber = 1;
tracks[2].MediumNumber = 2;
var release = GivenAlbumRelease("album", tracks);
var localTracks = GivenLocalTracks(tracks, release);
var mapping = GivenMapping(localTracks, tracks);
Subject.AlbumReleaseDistance(localTracks, release, mapping).NormalizedDistance().Should().Be(0.0);
}
[Test]
public void test_absolute_track_numbering()
{
var tracks = GivenTracks(3);
tracks[2].AbsoluteTrackNumber = 1;
tracks[2].MediumNumber = 2;
var release = GivenAlbumRelease("album", tracks);
var localTracks = GivenLocalTracks(tracks, release);
localTracks[2].FileTrackInfo.DiscNumber = 2;
localTracks[2].FileTrackInfo.TrackNumbers = new[] { 3 };
var mapping = GivenMapping(localTracks, tracks);
Subject.AlbumReleaseDistance(localTracks, release, mapping).NormalizedDistance().Should().Be(0.0);
}
}
}
@@ -0,0 +1,167 @@
using NUnit.Framework;
using NzbDrone.Core.MediaFiles.TrackImport.Identification;
using NzbDrone.Test.Common;
using FluentAssertions;
using System.Collections.Generic;
namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
{
[TestFixture]
public class DistanceFixture : TestBase
{
[Test]
public void test_add()
{
var dist = new Distance();
dist.Add("add", 1.0);
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"add", new List<double> { 1.0 }}} );
}
[Test]
public void test_equality()
{
var dist = new Distance();
dist.AddEquality("equality", "ghi", new List<string> { "abc", "def", "ghi" });
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"equality", new List<double> { 0.0 }}} );
dist.AddEquality("equality", "xyz", new List<string> { "abc", "def", "ghi" });
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"equality", new List<double> { 0.0, 1.0 }}} );
dist.AddEquality("equality", "abc", new List<string> { "abc", "def", "ghi" });
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"equality", new List<double> { 0.0, 1.0, 0.0 }}} );
}
[Test]
public void test_add_bool()
{
var dist = new Distance();
dist.AddBool("expr", true);
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"expr", new List<double> { 1.0 }}} );
dist.AddBool("expr", false);
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"expr", new List<double> { 1.0, 0.0 }}} );
}
[Test]
public void test_add_number()
{
var dist = new Distance();
dist.AddNumber("number", 1, 1);
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"number", new List<double> { 0.0 }}} );
dist.AddNumber("number", 1, 2);
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"number", new List<double> { 0.0, 1.0 }}} );
dist.AddNumber("number", 2, 1);
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"number", new List<double> { 0.0, 1.0, 1.0 }}} );
dist.AddNumber("number", -1, 2);
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"number", new List<double> { 0.0, 1.0, 1.0, 1.0, 1.0, 1.0 }}} );
}
[Test]
public void test_add_priority_value()
{
var dist = new Distance();
dist.AddPriority("priority", "abc", new List<string> { "abc" });
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"priority", new List<double> { 0.0 }}} );
dist.AddPriority("priority", "def", new List<string> { "abc", "def" });
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"priority", new List<double> { 0.0, 0.5 }}} );
dist.AddPriority("priority", "xyz", new List<string> { "abc", "def" });
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"priority", new List<double> { 0.0, 0.5, 1.0 }}} );
}
[Test]
public void test_add_priority_list()
{
var dist = new Distance();
dist.AddPriority("priority", new List<string> { "abc" }, new List<string> { "abc" });
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"priority", new List<double> { 0.0 }}} );
dist.AddPriority("priority", new List<string> { "def" }, new List<string> { "abc" });
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"priority", new List<double> { 0.0, 1.0 }}} );
dist.AddPriority("priority", new List<string> { "abc", "xyz" }, new List<string> { "abc" });
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"priority", new List<double> { 0.0, 1.0, 0.0 }}} );
dist.AddPriority("priority", new List<string> { "def", "xyz" }, new List<string> { "abc", "def" });
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"priority", new List<double> { 0.0, 1.0, 0.0, 0.5 }}} );
}
[Test]
public void test_add_ratio()
{
var dist = new Distance();
dist.AddRatio("ratio", 25, 100);
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"ratio", new List<double> { 0.25 }}} );
dist.AddRatio("ratio", 10, 5);
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"ratio", new List<double> { 0.25, 1.0 }}} );
dist.AddRatio("ratio", -5, 5);
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"ratio", new List<double> { 0.25, 1.0, 0.0 }}} );
dist.AddRatio("ratio", 5, 0);
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"ratio", new List<double> { 0.25, 1.0, 0.0, 0.0 }}} );
}
[Test]
public void test_add_string()
{
var dist = new Distance();
dist.AddString("string", "abcd", "bcde");
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"string", new List<double> { 0.5 }}} );
}
[Test]
public void test_add_string_none()
{
var dist = new Distance();
dist.AddString("string", string.Empty, "bcd");
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"string", new List<double> { 1.0 }}} );
}
[Test]
public void test_add_string_both_none()
{
var dist = new Distance();
dist.AddString("string", string.Empty, string.Empty);
dist.Penalties.ShouldBeEquivalentTo(new Dictionary<string, List<double>> { {"string", new List<double> { 0.0 }}} );
}
[Test]
public void test_distance()
{
var dist = new Distance();
dist.Add("album", 0.5);
dist.Add("media_count", 0.25);
dist.Add("media_count", 0.75);
dist.NormalizedDistance().Should().Be(0.5);
}
[Test]
public void test_max_distance()
{
var dist = new Distance();
dist.Add("album", 0.5);
dist.Add("media_count", 0.0);
dist.Add("media_count", 0.0);
dist.MaxDistance().Should().Be(5.0);
}
[Test]
public void test_raw_distance()
{
var dist = new Distance();
dist.Add("album", 0.5);
dist.Add("media_count", 0.25);
dist.Add("media_count", 0.5);
dist.RawDistance().Should().Be(2.25);
}
}
}
@@ -0,0 +1,155 @@
using NUnit.Framework;
using NzbDrone.Core.MediaFiles.TrackImport.Identification;
using FluentAssertions;
using NzbDrone.Core.Test.Framework;
using FizzWare.NBuilder;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Music;
using System.Collections.Generic;
using System.Linq;
using System;
using NzbDrone.Core.Parser;
using NzbDrone.Common.Serializer;
using Moq;
namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
{
[TestFixture]
public class GetCandidatesFixture : CoreTest<IdentificationService>
{
private ArtistMetadata artist;
[SetUp]
public void Setup()
{
artist = Builder<ArtistMetadata>
.CreateNew()
.With(x => x.Name = "artist")
.Build();
}
private List<Track> GivenTracks(int count)
{
return Builder<Track>
.CreateListOfSize(count)
.All()
.With(x => x.ArtistMetadata = artist)
.Build()
.ToList();
}
private ParsedTrackInfo GivenParsedTrackInfo(Track track, AlbumRelease release)
{
return Builder<ParsedTrackInfo>
.CreateNew()
.With(x => x.Title = track.Title)
.With(x => x.AlbumTitle = release.Title)
.With(x => x.Disambiguation = release.Disambiguation)
.With(x => x.ReleaseMBId = release.ForeignReleaseId)
.With(x => x.ArtistTitle = track.ArtistMetadata.Value.Name)
.With(x => x.TrackNumbers = new[] { track.AbsoluteTrackNumber })
.With(x => x.RecordingMBId = track.ForeignRecordingId)
.With(x => x.Country = IsoCountries.Find("US"))
.With(x => x.Label = release.Label.First())
.With(x => x.Year = (uint)release.Album.Value.ReleaseDate.Value.Year)
.Build();
}
private List<LocalTrack> GivenLocalTracks(List<Track> tracks, AlbumRelease release)
{
var output = Builder<LocalTrack>
.CreateListOfSize(tracks.Count)
.Build()
.ToList();
for (int i = 0; i < tracks.Count; i++)
{
output[i].FileTrackInfo = GivenParsedTrackInfo(tracks[i], release);
}
return output;
}
private AlbumRelease GivenAlbumRelease(string title, List<Track> tracks)
{
var album = Builder<Album>
.CreateNew()
.With(x => x.Title = title)
.With(x => x.ArtistMetadata = artist)
.Build();
var media = Builder<Medium>
.CreateListOfSize(1)
.Build()
.ToList();
return Builder<AlbumRelease>
.CreateNew()
.With(x => x.Tracks = tracks)
.With(x => x.Title = title)
.With(x => x.Album = album)
.With(x => x.Media = media)
.With(x => x.Country = new List<string>())
.With(x => x.Label = new List<string> { "label" })
.With(x => x.ForeignReleaseId = null)
.Build();
}
private LocalAlbumRelease GivenLocalAlbumRelease()
{
var tracks = GivenTracks(3);
var release = GivenAlbumRelease("album", tracks);
var localTracks = GivenLocalTracks(tracks, release);
return new LocalAlbumRelease(localTracks);
}
[Test]
public void get_candidates_by_fingerprint_should_not_fail_if_fingerprint_lookup_returned_null()
{
Mocker.GetMock<IFingerprintingService>()
.Setup(x => x.Lookup(It.IsAny<List<LocalTrack>>(), It.IsAny<double>()))
.Callback((List<LocalTrack> x, double thres) => {
foreach(var track in x) {
track.AcoustIdResults = null;
}
});
Mocker.GetMock<IReleaseService>()
.Setup(x => x.GetReleasesByRecordingIds(It.IsAny<List<string>>()))
.Returns(new List<AlbumRelease>());
var local = GivenLocalAlbumRelease();
Subject.GetCandidatesFromFingerprint(local).ShouldBeEquivalentTo(new List<AlbumRelease>());
}
[Test]
public void get_candidates_should_only_return_specified_release_if_set()
{
var tracks = GivenTracks(3);
var release = GivenAlbumRelease("album", tracks);
var localTracks = GivenLocalTracks(tracks, release);
var localAlbumRelease = new LocalAlbumRelease(localTracks);
Subject.GetCandidatesFromTags(localAlbumRelease, null, null, release).ShouldBeEquivalentTo(new List<AlbumRelease> { release });
}
[Test]
public void get_candidates_should_use_consensus_release_id()
{
var tracks = GivenTracks(3);
var release = GivenAlbumRelease("album", tracks);
release.ForeignReleaseId = "xxx";
var localTracks = GivenLocalTracks(tracks, release);
var localAlbumRelease = new LocalAlbumRelease(localTracks);
Mocker.GetMock<IReleaseService>()
.Setup(x => x.GetReleasesByForeignReleaseId(new List<string>{ "xxx" }))
.Returns(new List<AlbumRelease> { release });
Subject.GetCandidatesFromTags(localAlbumRelease, null, null, null).ShouldBeEquivalentTo(new List<AlbumRelease> { release });
}
}
}
@@ -0,0 +1,140 @@
using System.IO;
using System.Linq;
using System.Collections;
using FluentAssertions;
using FluentValidation.Results;
using Moq;
using Newtonsoft.Json;
using NUnit.Framework;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles.TrackImport.Identification;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.MetadataSource;
using NzbDrone.Core.MetadataSource.SkyHook;
using NzbDrone.Core.Music;
using NzbDrone.Core.Music.Commands;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Profiles.Metadata;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
{
[TestFixture]
public class IdentificationServiceFixture : DbTest
{
private ArtistService _artistService;
private AddArtistService _addArtistService;
private RefreshArtistService _refreshArtistService;
private IdentificationService Subject;
[SetUp]
public void SetUp()
{
UseRealHttp();
// Resolve all the parts we need
Mocker.SetConstant<IArtistRepository>(Mocker.Resolve<ArtistRepository>());
Mocker.SetConstant<IArtistMetadataRepository>(Mocker.Resolve<ArtistMetadataRepository>());
Mocker.SetConstant<IAlbumRepository>(Mocker.Resolve<AlbumRepository>());
Mocker.SetConstant<IReleaseRepository>(Mocker.Resolve<ReleaseRepository>());
Mocker.SetConstant<ITrackRepository>(Mocker.Resolve<TrackRepository>());
Mocker.GetMock<IMetadataProfileService>().Setup(x => x.Exists(It.IsAny<int>())).Returns(true);
_artistService = Mocker.Resolve<ArtistService>();
Mocker.SetConstant<IArtistService>(_artistService);
Mocker.SetConstant<IAlbumService>(Mocker.Resolve<AlbumService>());
Mocker.SetConstant<IReleaseService>(Mocker.Resolve<ReleaseService>());
Mocker.SetConstant<ITrackService>(Mocker.Resolve<TrackService>());
Mocker.SetConstant<IConfigService>(Mocker.Resolve<IConfigService>());
Mocker.SetConstant<IProvideArtistInfo>(Mocker.Resolve<SkyHookProxy>());
Mocker.SetConstant<IProvideAlbumInfo>(Mocker.Resolve<SkyHookProxy>());
_addArtistService = Mocker.Resolve<AddArtistService>();
Mocker.SetConstant<IRefreshTrackService>(Mocker.Resolve<RefreshTrackService>());
Mocker.SetConstant<IAddAlbumService>(Mocker.Resolve<AddAlbumService>());
_refreshArtistService = Mocker.Resolve<RefreshArtistService>();
Mocker.GetMock<IAddArtistValidator>().Setup(x => x.Validate(It.IsAny<Artist>())).Returns(new ValidationResult());
Mocker.SetConstant<ITrackGroupingService>(Mocker.Resolve<TrackGroupingService>());
Subject = Mocker.Resolve<IdentificationService>();
}
private void GivenMetadataProfile(MetadataProfile profile)
{
Mocker.GetMock<IMetadataProfileService>().Setup(x => x.Get(It.IsAny<int>())).Returns(profile);
}
private Artist GivenArtist(string foreignArtistId)
{
var artist = _addArtistService.AddArtist(new Artist {
Metadata = new ArtistMetadata {
ForeignArtistId = foreignArtistId
},
Path = @"c:\test".AsOsAgnostic(),
MetadataProfileId = 1
});
var command = new RefreshArtistCommand{
ArtistId = artist.Id,
Trigger = CommandTrigger.Unspecified
};
_refreshArtistService.Execute(command);
return _artistService.FindById(foreignArtistId);
}
public static class IdTestCaseFactory
{
// for some reason using Directory.GetFiles causes nUnit to error
private static string[] files = {
"FilesWithMBIds.json",
"PreferMissingToBadMatch.json",
"InconsistentTyposInAlbum.json",
"SucceedWhenManyAlbumsHaveSameTitle.json",
"PenalizeUnknownMedia.json"
};
public static IEnumerable TestCases
{
get
{
foreach (var file in files)
{
yield return new TestCaseData(file).SetName($"should_match_tracks_{file.Replace(".json", "")}");
}
}
}
}
// these are slow to run so only do so manually
[Explicit]
[Test, TestCaseSource(typeof(IdTestCaseFactory), "TestCases")]
public void should_match_tracks(string file)
{
var path = Path.Combine(TestContext.CurrentContext.TestDirectory, "Files", "Identification", file);
var testcase = JsonConvert.DeserializeObject<IdTestCase>(File.ReadAllText(path));
GivenMetadataProfile(testcase.MetadataProfile);
var artist = GivenArtist(testcase.Artist);
var tracks = testcase.Tracks.Select(x => new LocalTrack {
Path = x.Path.AsOsAgnostic(),
FileTrackInfo = x.FileTrackInfo
}).ToList();
var result = Subject.Identify(tracks, artist, null, null, testcase.NewDownload, testcase.SingleRelease);
result.Should().HaveCount(testcase.ExpectedMusicBrainzReleaseIds.Count);
result.Select(x => x.AlbumRelease.ForeignReleaseId).ShouldBeEquivalentTo(testcase.ExpectedMusicBrainzReleaseIds);
}
}
}
@@ -0,0 +1,184 @@
using NUnit.Framework;
using NzbDrone.Core.MediaFiles.TrackImport.Identification;
using NzbDrone.Test.Common;
using FluentAssertions;
namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
{
[TestFixture]
public class MunkresFixture : TestBase
{
// 2d arrays don't play nicely with attributes
public void RunTest(double[,] costMatrix, double expectedCost)
{
var m = new Munkres(costMatrix);
m.Run();
m.Cost.Should().Be(expectedCost);
}
[Test]
public void MunkresSquareTest1()
{
var C = new double[,] {
{ 1, 2, 3 },
{ 2, 4, 6 },
{ 3, 6, 9 }
};
RunTest(C, 10);
}
[Test]
public void MunkresSquareTest2()
{
var C = new double[,] {
{ 400, 150, 400 },
{ 400, 450, 600 },
{ 300, 225, 300 }
};
RunTest(C, 850);
}
[Test]
public void MunkresSquareTest3()
{
var C = new double[,] {
{ 10, 10, 8 },
{ 9, 8, 1 },
{ 9, 7, 4 }
};
RunTest(C, 18);
}
[Test]
public void MunkresSquareTest4()
{
var C = new double[,] {
{ 5, 9, 1 },
{ 10, 3, 2 },
{ 8, 7, 4 }
};
RunTest(C, 12);
}
[Test]
public void MunkresSquareTest5()
{
var C = new double[,] {
{12, 26, 17, 0, 0},
{49, 43, 36, 10, 5},
{97, 9, 66, 34, 0},
{52, 42, 19, 36, 0},
{15, 93, 55, 80, 0}
};
RunTest(C, 48);
}
[Test]
public void Munkres5x5Test()
{
var C = new double[,] {
{12, 9, 27, 10, 23},
{7, 13, 13, 30, 19},
{25, 18, 26, 11, 26},
{9, 28, 26, 23, 13},
{16, 16, 24, 6, 9}
};
RunTest(C, 51);
}
[Test]
public void Munkres10x10Test()
{
var C = new double[,] {
{37, 34, 29, 26, 19, 8, 9, 23, 19, 29},
{9, 28, 20, 8, 18, 20, 14, 33, 23, 14},
{15, 26, 12, 28, 6, 17, 9, 13, 21, 7},
{2, 8, 38, 36, 39, 5, 36, 2, 38, 27},
{30, 3, 33, 16, 21, 39, 7, 23, 28, 36},
{7, 5, 19, 22, 36, 36, 24, 19, 30, 2},
{34, 20, 13, 36, 12, 33, 9, 10, 23, 5},
{7, 37, 22, 39, 33, 39, 10, 3, 13, 26},
{21, 25, 23, 39, 31, 37, 32, 33, 38, 1},
{17, 34, 40, 10, 29, 37, 40, 3, 25, 3}
};
RunTest(C, 66);
}
[Test]
public void Munkres20x20Test()
{
var C = new double[,] {
{5, 4, 3, 9, 8, 9, 3, 5, 6, 9, 4, 10, 3, 5, 6, 6, 1, 8, 10, 2},
{10, 9, 9, 2, 8, 3, 9, 9, 10, 1, 7, 10, 8, 4, 2, 1, 4, 8, 4, 8},
{10, 4, 4, 3, 1, 3, 5, 10, 6, 8, 6, 8, 4, 10, 7, 2, 4, 5, 1, 8},
{2, 1, 4, 2, 3, 9, 3, 4, 7, 3, 4, 1, 3, 2, 9, 8, 6, 5, 7, 8},
{3, 4, 4, 1, 4, 10, 1, 2, 6, 4, 5, 10, 2, 2, 3, 9, 10, 9, 9, 10},
{1, 10, 1, 8, 1, 3, 1, 7, 1, 1, 2, 1, 2, 6, 3, 3, 4, 4, 8, 6},
{1, 8, 7, 10, 10, 3, 4, 6, 1, 6, 6, 4, 9, 6, 9, 6, 4, 5, 4, 7},
{8, 10, 3, 9, 4, 9, 3, 3, 4, 6, 4, 2, 6, 7, 7, 4, 4, 3, 4, 7},
{1, 3, 8, 2, 6, 9, 2, 7, 4, 8, 10, 8, 10, 5, 1, 3, 10, 10, 2, 9},
{2, 4, 1, 9, 2, 9, 7, 8, 2, 1, 4, 10, 5, 2, 7, 6, 5, 7, 2, 6},
{4, 5, 1, 4, 2, 3, 3, 4, 1, 8, 8, 2, 6, 9, 5, 9, 6, 3, 9, 3},
{3, 1, 1, 8, 6, 8, 8, 7, 9, 3, 2, 1, 8, 2, 4, 7, 3, 1, 2, 4},
{5, 9, 8, 6, 10, 4, 10, 3, 4, 10, 10, 10, 1, 7, 8, 8, 7, 7, 8, 8},
{1, 4, 6, 1, 6, 1, 2, 10, 5, 10, 2, 6, 2, 4, 5, 5, 3, 5, 1, 5},
{5, 6, 9, 10, 6, 6, 10, 6, 4, 1, 5, 3, 9, 5, 2, 10, 9, 9, 5, 1},
{10, 9, 4, 6, 9, 5, 3, 7, 10, 1, 6, 8, 1, 1, 10, 9, 5, 7, 7, 5},
{2, 6, 6, 6, 6, 2, 9, 4, 7, 5, 3, 2, 10, 3, 4, 5, 10, 9, 1, 7},
{5, 2, 4, 9, 8, 4, 8, 2, 4, 1, 3, 7, 6, 8, 1, 6, 8, 8, 10, 10},
{9, 6, 3, 1, 8, 5, 7, 8, 7, 2, 1, 8, 2, 8, 3, 7, 4, 8, 7, 7},
{8, 4, 4, 9, 7, 10, 6, 2, 1, 5, 8, 5, 1, 1, 1, 9, 1, 3, 5, 3}
};
RunTest(C, 22);
}
[Test]
public void MunkresRectangularTest1()
{
var C = new double[,] {
{ 400, 150, 400, 1 },
{ 400, 450, 600, 2 },
{ 300, 225, 300, 3 }
};
RunTest(C, 452);
}
[Test]
public void MunkresRectangularTest2()
{
var C = new double[,] {
{ 10, 10, 8, 11 },
{ 9, 8, 1, 1 },
{ 9, 7, 4, 10 }
};
RunTest(C, 15);
}
[Test]
public void MunkresRectangularTest3()
{
var C = new double[,] {
{34, 26, 17, 12},
{43, 43, 36, 10},
{97, 47, 66, 34},
{52, 42, 19, 36},
{15, 93, 55, 80}
};
RunTest(C, 70);
}
}
}
@@ -0,0 +1,99 @@
using NUnit.Framework;
using NzbDrone.Core.MediaFiles.TrackImport.Identification;
using FluentAssertions;
using NzbDrone.Core.Test.Framework;
using FizzWare.NBuilder;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Music;
using NzbDrone.Core.Parser;
namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
{
[TestFixture]
public class TrackDistanceFixture : CoreTest<IdentificationService>
{
private Track GivenTrack(string title)
{
var artist = Builder<ArtistMetadata>
.CreateNew()
.With(x => x.Name = "artist")
.Build();
var mbTrack = Builder<Track>
.CreateNew()
.With(x => x.Title = title)
.With(x => x.ArtistMetadata = artist)
.Build();
return mbTrack;
}
private LocalTrack GivenLocalTrack(Track track)
{
var fileInfo = Builder<ParsedTrackInfo>
.CreateNew()
.With(x => x.Title = track.Title)
.With(x => x.CleanTitle = track.Title.CleanTrackTitle())
.With(x => x.ArtistTitle = track.ArtistMetadata.Value.Name)
.With(x => x.TrackNumbers = new[] { 1 })
.With(x => x.RecordingMBId = track.ForeignRecordingId)
.Build();
var localTrack = Builder<LocalTrack>
.CreateNew()
.With(x => x.FileTrackInfo = fileInfo)
.Build();
return localTrack;
}
[Test]
public void test_identical_tracks()
{
var track = GivenTrack("one");
var localTrack = GivenLocalTrack(track);
Subject.TrackDistance(localTrack, track, 1, true).NormalizedDistance().Should().Be(0.0);
}
[Test]
public void test_feat_removed_from_localtrack()
{
var track = GivenTrack("one");
var localTrack = GivenLocalTrack(track);
localTrack.FileTrackInfo.Title = "one (feat. two)";
Subject.TrackDistance(localTrack, track, 1, true).NormalizedDistance().Should().Be(0.0);
}
[Test]
public void test_different_title()
{
var track = GivenTrack("one");
var localTrack = GivenLocalTrack(track);
localTrack.FileTrackInfo.CleanTitle = "foo";
Subject.TrackDistance(localTrack, track, 1, true).NormalizedDistance().Should().NotBe(0.0);
}
[Test]
public void test_different_artist()
{
var track = GivenTrack("one");
var localTrack = GivenLocalTrack(track);
localTrack.FileTrackInfo.ArtistTitle = "foo";
Subject.TrackDistance(localTrack, track, 1, true).NormalizedDistance().Should().NotBe(0.0);
}
[Test]
public void test_various_artists_tolerated()
{
var track = GivenTrack("one");
var localTrack = GivenLocalTrack(track);
localTrack.FileTrackInfo.ArtistTitle = "Various Artists";
Subject.TrackDistance(localTrack, track, 1, true).NormalizedDistance().Should().Be(0.0);
}
}
}
@@ -0,0 +1,369 @@
using NUnit.Framework;
using NzbDrone.Core.MediaFiles.TrackImport.Identification;
using NzbDrone.Test.Common;
using FluentAssertions;
using NzbDrone.Core.Test.Framework;
using FizzWare.NBuilder;
using NzbDrone.Core.Parser.Model;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using FizzWare.NBuilder.PropertyNaming;
using System.Reflection;
using System.Text;
namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
{
// we need to use random strings to test the va (so we don't just get artist1, artist2 etc which are too similar)
// but the standard random value namer would give paths that are too long on windows
public class RandomValueNamerShortStrings : RandomValuePropertyNamer
{
private readonly IRandomGenerator generator;
private static readonly List<char> allowedChars;
public RandomValueNamerShortStrings(BuilderSettings settings) : base(settings)
{
generator = new RandomGenerator();
}
static RandomValueNamerShortStrings()
{
allowedChars = new List<char>();
for (char c = 'a'; c < 'z'; c++)
{
allowedChars.Add(c);
}
for (char c = 'A'; c < 'Z'; c++)
{
allowedChars.Add(c);
}
for (char c = '0'; c < '9'; c++)
{
allowedChars.Add(c);
}
}
protected override string GetString(MemberInfo memberInfo)
{
int length = generator.Next(0, 100);
char[] chars = new char[length];
for (int i = 0; i < length; i++)
{
int index = generator.Next(0, allowedChars.Count - 1);
chars[i] = allowedChars[index];
}
byte[] bytes = Encoding.UTF8.GetBytes(chars);
return Encoding.UTF8.GetString(bytes, 0, bytes.Length);
}
}
[TestFixture]
public class TrackGroupingServiceFixture : CoreTest<TrackGroupingService>
{
private List<LocalTrack> GivenTracks(string root, string artist, string album, int count)
{
var fileInfos = Builder<ParsedTrackInfo>
.CreateListOfSize(count)
.All()
.With(f => f.ArtistTitle = artist)
.With(f => f.AlbumTitle = album)
.With(f => f.AlbumMBId = null)
.With(f => f.ReleaseMBId = null)
.Build();
var tracks = fileInfos.Select(x => Builder<LocalTrack>
.CreateNew()
.With(y => y.FileTrackInfo = x)
.With(y => y.Path = Path.Combine(root, x.Title))
.Build()).ToList();
return tracks;
}
private List<LocalTrack> GivenTracksWithNoTags(string root, int count)
{
var outp = new List<LocalTrack>();
for (int i = 0; i < count; i++)
{
var track = Builder<LocalTrack>
.CreateNew()
.With(y => y.FileTrackInfo = new ParsedTrackInfo())
.With(y => y.Path = Path.Combine(root, $"{i}.mp3"))
.Build();
outp.Add(track);
}
return outp;
}
private List<LocalTrack> GivenVaTracks(string root, string album, int count)
{
var settings = new BuilderSettings();
settings.SetPropertyNamerFor<ParsedTrackInfo>(new RandomValueNamerShortStrings(settings));
var builder = new Builder(settings);
var fileInfos = builder
.CreateListOfSize<ParsedTrackInfo>(count)
.All()
.With(f => f.AlbumTitle = "album")
.With(f => f.AlbumMBId = null)
.With(f => f.ReleaseMBId = null)
.Build();
var tracks = fileInfos.Select(x => Builder<LocalTrack>
.CreateNew()
.With(y => y.FileTrackInfo = x)
.With(y => y.Path = Path.Combine(@"C:\music\incoming".AsOsAgnostic(), x.Title))
.Build()).ToList();
return tracks;
}
[TestCase(1)]
[TestCase(2)]
[TestCase(10)]
public void single_artist_is_not_various_artists(int count)
{
var tracks = GivenTracks(@"C:\music\incoming".AsOsAgnostic(), "artist", "album", count);
TrackGroupingService.IsVariousArtists(tracks).Should().Be(false);
}
[Test]
public void all_different_artists_is_various_artists()
{
var tracks = GivenVaTracks(@"C:\music\incoming".AsOsAgnostic(), "album", 10);
TrackGroupingService.IsVariousArtists(tracks).Should().Be(true);
}
[Test]
public void two_artists_is_not_various_artists()
{
var dir = @"C:\music\incoming".AsOsAgnostic();
var tracks = GivenTracks(dir, "artist1", "album", 10);
tracks.AddRange(GivenTracks(dir, "artist2", "album", 10));
TrackGroupingService.IsVariousArtists(tracks).Should().Be(false);
}
[Test]
public void mostly_different_artists_is_various_artists()
{
var dir = @"C:\music\incoming".AsOsAgnostic();
var tracks = GivenVaTracks(dir, "album", 10);
tracks.AddRange(GivenTracks(dir, "single_artist", "album", 2));
TrackGroupingService.IsVariousArtists(tracks).Should().Be(true);
}
[TestCase("")]
[TestCase("Various Artists")]
[TestCase("Various")]
[TestCase("VA")]
[TestCase("Unknown")]
public void va_artist_title_is_various_artists(string artist)
{
var tracks = GivenTracks(@"C:\music\incoming".AsOsAgnostic(), artist, "album", 10);
TrackGroupingService.IsVariousArtists(tracks).Should().Be(true);
}
[TestCase(1)]
[TestCase(2)]
[TestCase(10)]
public void should_group_single_artist_album(int count)
{
var tracks = GivenTracks(@"C:\music\incoming".AsOsAgnostic(), "artist", "album", count);
var output = Subject.GroupTracks(tracks);
TrackGroupingService.IsVariousArtists(tracks).Should().Be(false);
TrackGroupingService.LooksLikeSingleRelease(tracks).Should().Be(true);
output.Count.Should().Be(1);
output[0].LocalTracks.Count.Should().Be(count);
}
[TestCase("cd")]
[TestCase("disc")]
[TestCase("disk")]
public void should_group_multi_disc_release(string mediaName)
{
var tracks = GivenTracks($"C:\\music\\incoming\\artist - album\\{mediaName} 1".AsOsAgnostic(),
"artist", "album", 10);
tracks.AddRange(GivenTracks($"C:\\music\\incoming\\artist - album\\{mediaName} 2".AsOsAgnostic(),
"artist", "album", 5));
TrackGroupingService.IsVariousArtists(tracks).Should().Be(false);
TrackGroupingService.LooksLikeSingleRelease(tracks).Should().Be(true);
var output = Subject.GroupTracks(tracks);
output.Count.Should().Be(1);
output[0].LocalTracks.Count.Should().Be(15);
}
[Test]
public void should_not_group_two_different_albums_by_same_artist()
{
var tracks = GivenTracks($"C:\\music\\incoming\\artist - album1".AsOsAgnostic(),
"artist", "album1", 10);
tracks.AddRange(GivenTracks($"C:\\music\\incoming\\artist - album2".AsOsAgnostic(),
"artist", "album2", 5));
TrackGroupingService.IsVariousArtists(tracks).Should().Be(false);
TrackGroupingService.LooksLikeSingleRelease(tracks).Should().Be(false);
var output = Subject.GroupTracks(tracks);
output.Count.Should().Be(2);
output[0].LocalTracks.Count.Should().Be(10);
output[1].LocalTracks.Count.Should().Be(5);
}
[Test]
public void should_group_albums_with_typos()
{
var tracks = GivenTracks($"C:\\music\\incoming\\artist - album".AsOsAgnostic(),
"artist", "Rastaman Vibration (Remastered)", 10);
tracks.AddRange(GivenTracks($"C:\\music\\incoming\\artist - album".AsOsAgnostic(),
"artist", "Rastaman Vibration (Remastered", 5));
TrackGroupingService.IsVariousArtists(tracks).Should().Be(false);
TrackGroupingService.LooksLikeSingleRelease(tracks).Should().Be(true);
var output = Subject.GroupTracks(tracks);
output.Count.Should().Be(1);
output[0].LocalTracks.Count.Should().Be(15);
}
[Test]
public void should_not_group_two_different_tracks_in_same_directory()
{
var tracks = GivenTracks($"C:\\music\\incoming".AsOsAgnostic(),
"artist", "album1", 1);
tracks.AddRange(GivenTracks($"C:\\music\\incoming".AsOsAgnostic(),
"artist", "album2", 1));
TrackGroupingService.IsVariousArtists(tracks).Should().Be(false);
TrackGroupingService.LooksLikeSingleRelease(tracks).Should().Be(false);
var output = Subject.GroupTracks(tracks);
output.Count.Should().Be(2);
output[0].LocalTracks.Count.Should().Be(1);
output[1].LocalTracks.Count.Should().Be(1);
}
[Test]
public void should_separate_two_albums_in_same_directory()
{
var tracks = GivenTracks($"C:\\music\\incoming\\artist discog".AsOsAgnostic(),
"artist", "album1", 10);
tracks.AddRange(GivenTracks($"C:\\music\\incoming\\artist disog".AsOsAgnostic(),
"artist", "album2", 5));
TrackGroupingService.IsVariousArtists(tracks).Should().Be(false);
TrackGroupingService.LooksLikeSingleRelease(tracks).Should().Be(false);
var output = Subject.GroupTracks(tracks);
output.Count.Should().Be(2);
output[0].LocalTracks.Count.Should().Be(10);
output[1].LocalTracks.Count.Should().Be(5);
}
[Test]
public void should_separate_many_albums_in_same_directory()
{
var tracks = new List<LocalTrack>();
for (int i = 0; i < 100; i++)
{
tracks.AddRange(GivenTracks($"C:\\music".AsOsAgnostic(),
"artist" + i, "album" + i, 10));
}
// don't test various artists here because it's designed to only work if there's a common album
TrackGroupingService.LooksLikeSingleRelease(tracks).Should().Be(false);
var output = Subject.GroupTracks(tracks);
output.Count.Should().Be(100);
output.Select(x => x.LocalTracks.Count).Distinct().ShouldBeEquivalentTo(new List<int> { 10 });
}
[Test]
public void should_separate_two_albums_by_different_artists_in_same_directory()
{
var tracks = GivenTracks($"C:\\music\\incoming".AsOsAgnostic(),
"artist1", "album1", 10);
tracks.AddRange(GivenTracks($"C:\\music\\incoming".AsOsAgnostic(),
"artist2", "album2", 5));
TrackGroupingService.IsVariousArtists(tracks).Should().Be(false);
TrackGroupingService.LooksLikeSingleRelease(tracks).Should().Be(false);
var output = Subject.GroupTracks(tracks);
output.Count.Should().Be(2);
output[0].LocalTracks.Count.Should().Be(10);
output[1].LocalTracks.Count.Should().Be(5);
}
[Test]
public void should_group_va_release()
{
var tracks = GivenVaTracks(@"C:\music\incoming".AsOsAgnostic(), "album", 10);
TrackGroupingService.IsVariousArtists(tracks).Should().Be(true);
TrackGroupingService.LooksLikeSingleRelease(tracks).Should().Be(true);
var output = Subject.GroupTracks(tracks);
output.Count.Should().Be(1);
output[0].LocalTracks.Count.Should().Be(10);
}
[Test]
public void should_not_group_two_albums_by_different_artists_with_same_title()
{
var tracks = GivenTracks($"C:\\music\\incoming\\album".AsOsAgnostic(),
"artist1", "album", 10);
tracks.AddRange(GivenTracks($"C:\\music\\incoming\\album".AsOsAgnostic(),
"artist2", "album", 5));
TrackGroupingService.IsVariousArtists(tracks).Should().Be(false);
TrackGroupingService.LooksLikeSingleRelease(tracks).Should().Be(false);
var output = Subject.GroupTracks(tracks);
output.Count.Should().Be(2);
output[0].LocalTracks.Count.Should().Be(10);
output[1].LocalTracks.Count.Should().Be(5);
}
[Test]
public void should_not_fail_if_all_tags_null()
{
var tracks = GivenTracksWithNoTags($"C:\\music\\incoming\\album".AsOsAgnostic(), 10);
TrackGroupingService.IsVariousArtists(tracks).Should().Be(false);
TrackGroupingService.LooksLikeSingleRelease(tracks).Should().Be(true);
var output = Subject.GroupTracks(tracks);
output.Count.Should().Be(1);
output[0].LocalTracks.Count.Should().Be(10);
}
[Test]
public void should_not_fail_if_some_tags_null()
{
var tracks = GivenTracks($"C:\\music\\incoming\\album".AsOsAgnostic(),
"artist1", "album", 10);
tracks.AddRange(GivenTracksWithNoTags($"C:\\music\\incoming\\album".AsOsAgnostic(), 2));
TrackGroupingService.IsVariousArtists(tracks).Should().Be(false);
TrackGroupingService.LooksLikeSingleRelease(tracks).Should().Be(true);
var output = Subject.GroupTracks(tracks);
output.Count.Should().Be(1);
output[0].LocalTracks.Count.Should().Be(12);
}
}
}
@@ -0,0 +1,187 @@
using NUnit.Framework;
using NzbDrone.Core.MediaFiles.TrackImport.Identification;
using FluentAssertions;
using NzbDrone.Core.Test.Framework;
using FizzWare.NBuilder;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Music;
using System.Collections.Generic;
using System.Linq;
using System;
using NzbDrone.Core.Parser;
using NzbDrone.Common.Serializer;
namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
{
[TestFixture]
public class TrackMappingFixture : CoreTest<IdentificationService>
{
private ArtistMetadata artist;
[SetUp]
public void Setup()
{
artist = Builder<ArtistMetadata>
.CreateNew()
.With(x => x.Name = "artist")
.Build();
}
private List<Track> GivenTracks(int count)
{
return Builder<Track>
.CreateListOfSize(count)
.All()
.With(x => x.ArtistMetadata = artist)
.Build()
.ToList();
}
private ParsedTrackInfo GivenParsedTrackInfo(Track track, AlbumRelease release)
{
return Builder<ParsedTrackInfo>
.CreateNew()
.With(x => x.Title = track.Title)
.With(x => x.CleanTitle = track.Title.CleanTrackTitle())
.With(x => x.AlbumTitle = release.Title)
.With(x => x.Disambiguation = release.Disambiguation)
.With(x => x.ReleaseMBId = release.ForeignReleaseId)
.With(x => x.ArtistTitle = track.ArtistMetadata.Value.Name)
.With(x => x.TrackNumbers = new[] { track.AbsoluteTrackNumber })
.With(x => x.RecordingMBId = track.ForeignRecordingId)
.With(x => x.Country = IsoCountries.Find("US"))
.With(x => x.Label = release.Label.First())
.With(x => x.Year = (uint)release.Album.Value.ReleaseDate.Value.Year)
.Build();
}
private List<LocalTrack> GivenLocalTracks(List<Track> tracks, AlbumRelease release)
{
var output = Builder<LocalTrack>
.CreateListOfSize(tracks.Count)
.Build()
.ToList();
for (int i = 0; i < tracks.Count; i++)
{
output[i].FileTrackInfo = GivenParsedTrackInfo(tracks[i], release);
}
return output;
}
private AlbumRelease GivenAlbumRelease(string title, List<Track> tracks)
{
var album = Builder<Album>
.CreateNew()
.With(x => x.Title = title)
.With(x => x.ArtistMetadata = artist)
.Build();
var media = Builder<Medium>
.CreateListOfSize(1)
.Build()
.ToList();
return Builder<AlbumRelease>
.CreateNew()
.With(x => x.Tracks = tracks)
.With(x => x.Title = title)
.With(x => x.Album = album)
.With(x => x.Media = media)
.With(x => x.Country = new List<string>())
.With(x => x.Label = new List<string> { "label" })
.Build();
}
[Test]
public void test_reorder_when_track_numbers_incorrect()
{
var tracks = GivenTracks(3);
var release = GivenAlbumRelease("album", tracks);
var localTracks = GivenLocalTracks(tracks, release);
localTracks[2].FileTrackInfo.TrackNumbers = new [] { 2 };
localTracks[1].FileTrackInfo.TrackNumbers = new [] { 3 };
localTracks = new [] {0, 2, 1}.Select(x => localTracks[x]).ToList();
var result = Subject.MapReleaseTracks(localTracks, tracks);
result.Mapping
.ToDictionary(x => x.Key, y => y.Value.Item1)
.ShouldBeEquivalentTo(new Dictionary<LocalTrack, Track> {
{localTracks[0], tracks[0]},
{localTracks[1], tracks[2]},
{localTracks[2], tracks[1]},
});
result.LocalExtra.Should().BeEmpty();
result.MBExtra.Should().BeEmpty();
}
[Test]
public void test_order_works_with_invalid_track_numbers()
{
var tracks = GivenTracks(3);
var release = GivenAlbumRelease("album", tracks);
var localTracks = GivenLocalTracks(tracks, release);
foreach (var track in localTracks)
{
track.FileTrackInfo.TrackNumbers = new[] { 1 };
}
var result = Subject.MapReleaseTracks(localTracks, tracks);
result.Mapping
.ToDictionary(x => x.Key, y => y.Value.Item1)
.ShouldBeEquivalentTo(new Dictionary<LocalTrack, Track> {
{localTracks[0], tracks[0]},
{localTracks[1], tracks[1]},
{localTracks[2], tracks[2]},
});
result.LocalExtra.Should().BeEmpty();
result.MBExtra.Should().BeEmpty();
}
[Test]
public void test_order_works_with_missing_tracks()
{
var tracks = GivenTracks(3);
var release = GivenAlbumRelease("album", tracks);
var localTracks = GivenLocalTracks(tracks, release);
localTracks.RemoveAt(1);
var result = Subject.MapReleaseTracks(localTracks, tracks);
result.Mapping
.ToDictionary(x => x.Key, y => y.Value.Item1)
.ShouldBeEquivalentTo(new Dictionary<LocalTrack, Track> {
{localTracks[0], tracks[0]},
{localTracks[1], tracks[2]}
});
result.LocalExtra.Should().BeEmpty();
result.MBExtra.ShouldBeEquivalentTo(new List<Track> { tracks[1] });
}
[Test]
public void test_order_works_with_extra_tracks()
{
var tracks = GivenTracks(3);
var release = GivenAlbumRelease("album", tracks);
var localTracks = GivenLocalTracks(tracks, release);
tracks.RemoveAt(1);
var result = Subject.MapReleaseTracks(localTracks, tracks);
result.Mapping
.ToDictionary(x => x.Key, y => y.Value.Item1)
.ShouldBeEquivalentTo(new Dictionary<LocalTrack, Track> {
{localTracks[0], tracks[0]},
{localTracks[2], tracks[1]}
});
result.LocalExtra.ShouldBeEquivalentTo(new List<LocalTrack> { localTracks[1] });
result.MBExtra.Should().BeEmpty();
}
}
}
@@ -6,16 +6,18 @@ using NUnit.Framework;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.TrackImport;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Profiles.Qualities;
using NzbDrone.Core.Profiles;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Music;
using NzbDrone.Test.Common;
using FizzWare.NBuilder;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Download;
using NzbDrone.Core.MediaFiles.TrackImport.Aggregation;
using NzbDrone.Core.Profiles.Qualities;
using NzbDrone.Core.Profiles.Languages;
using NzbDrone.Core.MediaFiles.TrackImport.Identification;
namespace NzbDrone.Core.Test.MediaFiles.TrackImport
{
@@ -25,27 +27,54 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
private List<string> _audioFiles;
private LocalTrack _localTrack;
private Artist _artist;
private AlbumRelease _albumRelease;
private QualityModel _quality;
private Mock<IImportDecisionEngineSpecification> _pass1;
private Mock<IImportDecisionEngineSpecification> _pass2;
private Mock<IImportDecisionEngineSpecification> _pass3;
private Mock<IImportDecisionEngineSpecification<LocalAlbumRelease>> _albumpass1;
private Mock<IImportDecisionEngineSpecification<LocalAlbumRelease>> _albumpass2;
private Mock<IImportDecisionEngineSpecification<LocalAlbumRelease>> _albumpass3;
private Mock<IImportDecisionEngineSpecification> _fail1;
private Mock<IImportDecisionEngineSpecification> _fail2;
private Mock<IImportDecisionEngineSpecification> _fail3;
private Mock<IImportDecisionEngineSpecification<LocalAlbumRelease>> _albumfail1;
private Mock<IImportDecisionEngineSpecification<LocalAlbumRelease>> _albumfail2;
private Mock<IImportDecisionEngineSpecification<LocalAlbumRelease>> _albumfail3;
private Mock<IImportDecisionEngineSpecification<LocalTrack>> _pass1;
private Mock<IImportDecisionEngineSpecification<LocalTrack>> _pass2;
private Mock<IImportDecisionEngineSpecification<LocalTrack>> _pass3;
private Mock<IImportDecisionEngineSpecification<LocalTrack>> _fail1;
private Mock<IImportDecisionEngineSpecification<LocalTrack>> _fail2;
private Mock<IImportDecisionEngineSpecification<LocalTrack>> _fail3;
[SetUp]
public void Setup()
{
_pass1 = new Mock<IImportDecisionEngineSpecification>();
_pass2 = new Mock<IImportDecisionEngineSpecification>();
_pass3 = new Mock<IImportDecisionEngineSpecification>();
_albumpass1 = new Mock<IImportDecisionEngineSpecification<LocalAlbumRelease>>();
_albumpass2 = new Mock<IImportDecisionEngineSpecification<LocalAlbumRelease>>();
_albumpass3 = new Mock<IImportDecisionEngineSpecification<LocalAlbumRelease>>();
_fail1 = new Mock<IImportDecisionEngineSpecification>();
_fail2 = new Mock<IImportDecisionEngineSpecification>();
_fail3 = new Mock<IImportDecisionEngineSpecification>();
_albumfail1 = new Mock<IImportDecisionEngineSpecification<LocalAlbumRelease>>();
_albumfail2 = new Mock<IImportDecisionEngineSpecification<LocalAlbumRelease>>();
_albumfail3 = new Mock<IImportDecisionEngineSpecification<LocalAlbumRelease>>();
_pass1 = new Mock<IImportDecisionEngineSpecification<LocalTrack>>();
_pass2 = new Mock<IImportDecisionEngineSpecification<LocalTrack>>();
_pass3 = new Mock<IImportDecisionEngineSpecification<LocalTrack>>();
_fail1 = new Mock<IImportDecisionEngineSpecification<LocalTrack>>();
_fail2 = new Mock<IImportDecisionEngineSpecification<LocalTrack>>();
_fail3 = new Mock<IImportDecisionEngineSpecification<LocalTrack>>();
_albumpass1.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalAlbumRelease>())).Returns(Decision.Accept());
_albumpass2.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalAlbumRelease>())).Returns(Decision.Accept());
_albumpass3.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalAlbumRelease>())).Returns(Decision.Accept());
_albumfail1.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalAlbumRelease>())).Returns(Decision.Reject("_albumfail1"));
_albumfail2.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalAlbumRelease>())).Returns(Decision.Reject("_albumfail2"));
_albumfail3.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalAlbumRelease>())).Returns(Decision.Reject("_albumfail3"));
_pass1.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalTrack>())).Returns(Decision.Accept());
_pass2.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalTrack>())).Returns(Decision.Accept());
_pass3.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalTrack>())).Returns(Decision.Accept());
@@ -59,25 +88,33 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
.With(e => e.LanguageProfile = new LanguageProfile { Languages = Languages.LanguageFixture.GetDefaultLanguages() })
.Build();
_albumRelease = Builder<AlbumRelease>.CreateNew()
.Build();
_quality = new QualityModel(Quality.MP3_256);
_localTrack = new LocalTrack
{
{
Artist = _artist,
Quality = _quality,
Language = Language.Spanish,
Tracks = new List<Track> { new Track() },
Path = @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.Spanish.XviD-OSiTV.avi"
Path = @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV.avi"
};
Mocker.GetMock<IParsingService>()
.Setup(c => c.GetLocalTrack(It.IsAny<string>(), It.IsAny<Artist>(), It.IsAny<Album>(), It.IsAny<ParsedTrackInfo>()))
.Returns(_localTrack);
GivenVideoFiles(new List<string> { @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV.avi".AsOsAgnostic() });
GivenVideoFiles(new List<string> { @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.Spanish.XviD-OSiTV.avi".AsOsAgnostic() });
Mocker.GetMock<IIdentificationService>()
.Setup(s => s.Identify(It.IsAny<List<LocalTrack>>(), It.IsAny<Artist>(), It.IsAny<Album>(), It.IsAny<AlbumRelease>(), It.IsAny<bool>(), It.IsAny<bool>()))
.Returns((List<LocalTrack> tracks, Artist artist, Album album, AlbumRelease release, bool newDownload, bool singleRelease) => {
var ret = new LocalAlbumRelease(tracks);
ret.AlbumRelease = _albumRelease;
return new List<LocalAlbumRelease> { ret };
});
GivenSpecifications(_albumpass1);
}
private void GivenSpecifications(params Mock<IImportDecisionEngineSpecification>[] mocks)
private void GivenSpecifications<T>(params Mock<IImportDecisionEngineSpecification<T>>[] mocks)
{
Mocker.SetConstant(mocks.Select(c => c.Object));
}
@@ -91,24 +128,83 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
.Returns(_audioFiles);
}
[Test]
public void should_call_all_specifications()
private void GivenAugmentationSuccess()
{
GivenSpecifications(_pass1, _pass2, _pass3, _fail1, _fail2, _fail3);
Subject.GetImportDecisions(_audioFiles, new Artist(), null);
_fail1.Verify(c => c.IsSatisfiedBy(_localTrack), Times.Once());
_fail2.Verify(c => c.IsSatisfiedBy(_localTrack), Times.Once());
_fail3.Verify(c => c.IsSatisfiedBy(_localTrack), Times.Once());
_pass1.Verify(c => c.IsSatisfiedBy(_localTrack), Times.Once());
_pass2.Verify(c => c.IsSatisfiedBy(_localTrack), Times.Once());
_pass3.Verify(c => c.IsSatisfiedBy(_localTrack), Times.Once());
Mocker.GetMock<IAugmentingService>()
.Setup(s => s.Augment(It.IsAny<LocalTrack>(), It.IsAny<bool>()))
.Callback<LocalTrack, bool>((localTrack, otherFiles) =>
{
localTrack.Tracks = _localTrack.Tracks;
});
}
[Test]
public void should_return_rejected_if_single_specs_fail()
public void should_call_all_album_specifications()
{
var downloadClientItem = Builder<DownloadClientItem>.CreateNew().Build();
GivenAugmentationSuccess();
GivenSpecifications(_albumpass1, _albumpass2, _albumpass3, _albumfail1, _albumfail2, _albumfail3);
Subject.GetImportDecisions(_audioFiles, new Artist(), null, downloadClientItem, null, false, false, false);
_albumfail1.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalAlbumRelease>()), Times.Once());
_albumfail2.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalAlbumRelease>()), Times.Once());
_albumfail3.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalAlbumRelease>()), Times.Once());
_albumpass1.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalAlbumRelease>()), Times.Once());
_albumpass2.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalAlbumRelease>()), Times.Once());
_albumpass3.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalAlbumRelease>()), Times.Once());
}
[Test]
public void should_call_all_track_specifications_if_album_accepted()
{
var downloadClientItem = Builder<DownloadClientItem>.CreateNew().Build();
GivenAugmentationSuccess();
GivenSpecifications(_pass1, _pass2, _pass3, _fail1, _fail2, _fail3);
Subject.GetImportDecisions(_audioFiles, new Artist(), null, downloadClientItem, null, false, false, false);
_fail1.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalTrack>()), Times.Once());
_fail2.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalTrack>()), Times.Once());
_fail3.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalTrack>()), Times.Once());
_pass1.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalTrack>()), Times.Once());
_pass2.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalTrack>()), Times.Once());
_pass3.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalTrack>()), Times.Once());
}
[Test]
public void should_call_no_track_specifications_if_album_rejected()
{
var downloadClientItem = Builder<DownloadClientItem>.CreateNew().Build();
GivenAugmentationSuccess();
GivenSpecifications(_albumpass1, _albumpass2, _albumpass3, _albumfail1, _albumfail2, _albumfail3);
GivenSpecifications(_pass1, _pass2, _pass3, _fail1, _fail2, _fail3);
Subject.GetImportDecisions(_audioFiles, new Artist(), null, downloadClientItem, null, false, false, false);
_fail1.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalTrack>()), Times.Never());
_fail2.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalTrack>()), Times.Never());
_fail3.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalTrack>()), Times.Never());
_pass1.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalTrack>()), Times.Never());
_pass2.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalTrack>()), Times.Never());
_pass3.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalTrack>()), Times.Never());
}
[Test]
public void should_return_rejected_if_only_album_spec_fails()
{
GivenSpecifications(_albumfail1);
GivenSpecifications(_pass1);
var result = Subject.GetImportDecisions(_audioFiles, new Artist());
result.Single().Approved.Should().BeFalse();
}
[Test]
public void should_return_rejected_if_only_track_spec_fails()
{
GivenSpecifications(_albumpass1);
GivenSpecifications(_fail1);
var result = Subject.GetImportDecisions(_audioFiles, new Artist());
@@ -117,8 +213,20 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
}
[Test]
public void should_return_rejected_if_one_of_specs_fail()
public void should_return_rejected_if_one_album_spec_fails()
{
GivenSpecifications(_albumpass1, _albumfail1, _albumpass2, _albumpass3);
GivenSpecifications(_pass1, _pass2, _pass3);
var result = Subject.GetImportDecisions(_audioFiles, new Artist());
result.Single().Approved.Should().BeFalse();
}
[Test]
public void should_return_rejected_if_one_track_spec_fails()
{
GivenSpecifications(_albumpass1, _albumpass2, _albumpass3);
GivenSpecifications(_pass1, _fail1, _pass2, _pass3);
var result = Subject.GetImportDecisions(_audioFiles, new Artist());
@@ -127,8 +235,10 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
}
[Test]
public void should_return_pass_if_all_specs_pass()
public void should_return_approved_if_all_specs_pass()
{
GivenAugmentationSuccess();
GivenSpecifications(_albumpass1, _albumpass2, _albumpass3);
GivenSpecifications(_pass1, _pass2, _pass3);
var result = Subject.GetImportDecisions(_audioFiles, new Artist());
@@ -139,6 +249,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
[Test]
public void should_have_same_number_of_rejections_as_specs_that_failed()
{
GivenAugmentationSuccess();
GivenSpecifications(_pass1, _pass2, _pass3, _fail1, _fail2, _fail3);
var result = Subject.GetImportDecisions(_audioFiles, new Artist());
@@ -146,12 +257,12 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
}
[Test]
public void should_not_blowup_the_process_due_to_failed_parse()
public void should_not_blowup_the_process_due_to_failed_augment()
{
GivenSpecifications(_pass1);
Mocker.GetMock<IParsingService>()
.Setup(c => c.GetLocalTrack(It.IsAny<string>(), It.IsAny<Artist>(), It.IsAny<Album>(), It.IsAny<ParsedTrackInfo>()))
Mocker.GetMock<IAugmentingService>()
.Setup(c => c.Augment(It.IsAny<LocalTrack>(), It.IsAny<bool>()))
.Throws<TestException>();
_audioFiles = new List<string>
@@ -165,103 +276,45 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
Subject.GetImportDecisions(_audioFiles, _artist);
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalTrack(It.IsAny<string>(), It.IsAny<Artist>(), It.IsAny<Album>(), It.IsAny<ParsedTrackInfo>()), Times.Exactly(_audioFiles.Count));
Mocker.GetMock<IAugmentingService>()
.Verify(c => c.Augment(It.IsAny<LocalTrack>(), It.IsAny<bool>()), Times.Exactly(_audioFiles.Count));
ExceptionVerification.ExpectedErrors(3);
}
[Test]
public void should_use_file_quality_if_folder_quality_is_null()
{
GivenSpecifications(_pass1, _pass2, _pass3);
var expectedQuality = QualityParser.ParseQuality(_audioFiles.Single(), null, 0);
var result = Subject.GetImportDecisions(_audioFiles, _artist);
result.Single().LocalTrack.Quality.Should().Be(expectedQuality);
}
[Test]
public void should_use_file_language_if_folder_language_is_null()
{
GivenSpecifications(_pass1, _pass2, _pass3);
var expectedLanguage = LanguageParser.ParseLanguage(_audioFiles.Single());
var result = Subject.GetImportDecisions(_audioFiles, _artist);
result.Single().LocalTrack.Language.Should().Be(expectedLanguage);
}
[Test]
public void should_use_file_quality_if_file_quality_was_determined_by_name()
{
GivenSpecifications(_pass1, _pass2, _pass3);
var expectedQuality = QualityParser.ParseQuality(_audioFiles.Single(), null, 0);
var result = Subject.GetImportDecisions(_audioFiles, _artist, new ParsedTrackInfo{Quality = new QualityModel(Quality.MP3_256) });
result.Single().LocalTrack.Quality.Should().Be(expectedQuality);
}
[Test]
public void should_use_folder_quality_when_file_quality_was_determined_by_the_extension()
{
GivenSpecifications(_pass1, _pass2, _pass3);
GivenVideoFiles(new string[] { @"C:\Test\Unsorted\The.Office.S03E115.mkv".AsOsAgnostic() });
_localTrack.Path = _audioFiles.Single();
_localTrack.Quality.QualitySource = QualitySource.Extension;
_localTrack.Quality.Quality = Quality.MP3_256;
var expectedQuality = new QualityModel(Quality.MP3_256);
var result = Subject.GetImportDecisions(_audioFiles, _artist, new ParsedTrackInfo { Quality = expectedQuality });
result.Single().LocalTrack.Quality.Should().Be(expectedQuality);
}
[Test]
public void should_use_folder_quality_when_greater_than_file_quality()
{
GivenSpecifications(_pass1, _pass2, _pass3);
GivenVideoFiles(new string[] { @"C:\Test\Unsorted\The.Office.S03E115.mkv".AsOsAgnostic() });
_localTrack.Path = _audioFiles.Single();
_localTrack.Quality.Quality = Quality.MP3_256;
var expectedQuality = new QualityModel(Quality.MP3_256);
var result = Subject.GetImportDecisions(_audioFiles, _artist, new ParsedTrackInfo { Quality = expectedQuality });
result.Single().LocalTrack.Quality.Should().Be(expectedQuality);
}
[Test]
public void should_use_folder_language_when_greater_than_file_language()
{
GivenSpecifications(_pass1, _pass2, _pass3);
GivenVideoFiles(new string[] { @"C:\Test\Unsorted\The.Office.S03E115.Spanish.mkv".AsOsAgnostic() });
_localTrack.Path = _audioFiles.Single();
_localTrack.Quality.Quality = Quality.MP3_320;
_localTrack.Language = Language.Spanish;
var expectedLanguage = Language.French;
var result = Subject.GetImportDecisions(_audioFiles, _artist, new ParsedTrackInfo { Language = expectedLanguage, Quality = new QualityModel(Quality.MP3_192) });
result.Single().LocalTrack.Language.Should().Be(expectedLanguage);
}
[Test]
public void should_not_throw_if_episodes_are_not_found()
public void should_not_throw_if_release_not_identified()
{
GivenSpecifications(_pass1);
Mocker.GetMock<IParsingService>()
.Setup(c => c.GetLocalTrack(It.IsAny<string>(), It.IsAny<Artist>(), It.IsAny<Album>(), It.IsAny<ParsedTrackInfo>()))
.Returns(new LocalTrack() { Path = "test" });
_audioFiles = new List<string>
{
"The.Office.S03E115.DVDRip.XviD-OSiTV",
"The.Office.S03E115.DVDRip.XviD-OSiTV",
"The.Office.S03E115.DVDRip.XviD-OSiTV"
};
GivenVideoFiles(_audioFiles);
Mocker.GetMock<IIdentificationService>()
.Setup(s => s.Identify(It.IsAny<List<LocalTrack>>(), It.IsAny<Artist>(), It.IsAny<Album>(), It.IsAny<AlbumRelease>(), It.IsAny<bool>(), It.IsAny<bool>()))
.Returns((List<LocalTrack> tracks, Artist artist, Album album, AlbumRelease release, bool newDownload, bool singleRelease) => {
return new List<LocalAlbumRelease> { new LocalAlbumRelease(tracks) };
});
var decisions = Subject.GetImportDecisions(_audioFiles, _artist);
Mocker.GetMock<IAugmentingService>()
.Verify(c => c.Augment(It.IsAny<LocalTrack>(), It.IsAny<bool>()), Times.Exactly(_audioFiles.Count));
decisions.Should().HaveCount(3);
decisions.First().Rejections.Should().NotBeEmpty();
}
[Test]
public void should_not_throw_if_tracks_are_not_found()
{
GivenSpecifications(_pass1);
_audioFiles = new List<string>
{
@@ -274,127 +327,18 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
var decisions = Subject.GetImportDecisions(_audioFiles, _artist);
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalTrack(It.IsAny<string>(), It.IsAny<Artist>(), It.IsAny<Album>(), It.IsAny<ParsedTrackInfo>()), Times.Exactly(_audioFiles.Count));
Mocker.GetMock<IAugmentingService>()
.Verify(c => c.Augment(It.IsAny<LocalTrack>(), It.IsAny<bool>()), Times.Exactly(_audioFiles.Count));
decisions.Should().HaveCount(3);
decisions.First().Rejections.Should().NotBeEmpty();
}
[Test]
public void should_not_use_folder_for_full_season()
{
var videoFiles = new[]
{
@"C:\Test\Unsorted\Artist.Title.S01\S01E01.mkv".AsOsAgnostic(),
@"C:\Test\Unsorted\Artist.Title.S01\S01E02.mkv".AsOsAgnostic(),
@"C:\Test\Unsorted\Artist.Title.S01\S01E03.mkv".AsOsAgnostic()
};
GivenSpecifications(_pass1);
GivenVideoFiles(videoFiles);
var folderInfo = Parser.Parser.ParseMusicTitle("Artist.Title.S01");
Subject.GetImportDecisions(_audioFiles, _artist, folderInfo);
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalTrack(It.IsAny<string>(), It.IsAny<Artist>(), It.IsAny<Album>(), null), Times.Exactly(3));
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalTrack(It.IsAny<string>(), It.IsAny<Artist>(), It.IsAny<Album>(), It.Is<ParsedTrackInfo>(p => p != null)), Times.Never());
}
[Test]
public void should_not_use_folder_when_it_contains_more_than_one_valid_video_file()
{
var videoFiles = new[]
{
@"C:\Test\Unsorted\Artist.Title.S01E01\S01E01.mkv".AsOsAgnostic(),
@"C:\Test\Unsorted\Artist.Title.S01E01\1x01.mkv".AsOsAgnostic()
};
GivenSpecifications(_pass1);
GivenVideoFiles(videoFiles);
var folderInfo = Parser.Parser.ParseMusicTitle("Artist.Title.S01E01");
Subject.GetImportDecisions(_audioFiles, _artist, folderInfo);
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalTrack(It.IsAny<string>(), It.IsAny<Artist>(), It.IsAny<Album>(), null), Times.Exactly(2));
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalTrack(It.IsAny<string>(), It.IsAny<Artist>(), It.IsAny<Album>(), It.Is<ParsedTrackInfo>(p => p != null)), Times.Never());
}
[Test]
public void should_use_folder_when_only_one_video_file()
{
var videoFiles = new[]
{
@"C:\Test\Unsorted\Artist.Title.S01E01\S01E01.mkv".AsOsAgnostic()
};
GivenSpecifications(_pass1);
GivenVideoFiles(videoFiles);
var folderInfo = Parser.Parser.ParseMusicTitle("Artist.Title.S01E01");
Subject.GetImportDecisions(_audioFiles, _artist, folderInfo);
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalTrack(It.IsAny<string>(), It.IsAny<Artist>(), It.IsAny<Album>(), It.IsAny<ParsedTrackInfo>()), Times.Exactly(1));
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalTrack(It.IsAny<string>(), It.IsAny<Artist>(), It.IsAny<Album>(), null), Times.Never());
}
[Test]
public void should_not_use_folder_name_if_file_name_is_scene_name()
{
var videoFiles = new[]
{
@"C:\Test\Unsorted\Artist.Title.S01E01.720p.HDTV-LOL\Artist.Title.S01E01.720p.HDTV-LOL.mkv".AsOsAgnostic()
};
GivenSpecifications(_pass1);
GivenVideoFiles(videoFiles);
var folderInfo = Parser.Parser.ParseMusicTitle("Artist.Title.S01E01.720p.HDTV-LOL");
Subject.GetImportDecisions(_audioFiles, _artist, folderInfo);
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalTrack(It.IsAny<string>(), It.IsAny<Artist>(), It.IsAny<Album>(), null), Times.Exactly(1));
Mocker.GetMock<IParsingService>()
.Verify(c => c.GetLocalTrack(It.IsAny<string>(), It.IsAny<Artist>(), It.IsAny<Album>(), It.Is<ParsedTrackInfo>(p => p != null)), Times.Never());
}
[Test]
public void should_not_use_folder_quality_when_it_is_unknown()
{
GivenSpecifications(_pass1, _pass2, _pass3);
_artist.Profile = new Profile
{
Items = Qualities.QualityFixture.GetDefaultQualities(Quality.MP3_256, Quality.Unknown)
};
var folderQuality = new QualityModel(Quality.Unknown);
var result = Subject.GetImportDecisions(_audioFiles, _artist, new ParsedTrackInfo { Quality = folderQuality});
result.Single().LocalTrack.Quality.Should().Be(_quality);
}
[Test]
public void should_return_a_decision_when_exception_is_caught()
{
Mocker.GetMock<IParsingService>()
.Setup(c => c.GetLocalTrack(It.IsAny<string>(), It.IsAny<Artist>(), It.IsAny<Album>(), It.IsAny<ParsedTrackInfo>()))
Mocker.GetMock<IAugmentingService>()
.Setup(c => c.Augment(It.IsAny<LocalTrack>(), It.IsAny<bool>()))
.Throws<TestException>();
_audioFiles = new List<string>