New: Don't require artist mapping

This commit is contained in:
ta264 2020-02-09 19:15:43 +00:00 committed by Qstick
commit be4e748977
159 changed files with 2934 additions and 4208 deletions

View file

@ -39,7 +39,7 @@ namespace Lidarr.Api.V1.Albums
IMapCoversToLocal coverMapper,
IUpgradableSpecification upgradableSpecification,
IBroadcastSignalRMessage signalRBroadcaster,
ProfileExistsValidator profileExistsValidator,
QualityProfileExistsValidator qualityProfileExistsValidator,
MetadataProfileExistsValidator metadataProfileExistsValidator)
: base(albumService, artistStatisticsService, coverMapper, upgradableSpecification, signalRBroadcaster)
@ -54,7 +54,7 @@ namespace Lidarr.Api.V1.Albums
Put("/monitor", x => SetAlbumsMonitored());
PostValidator.RuleFor(s => s.ForeignAlbumId).NotEmpty();
PostValidator.RuleFor(s => s.Artist.QualityProfileId).SetValidator(profileExistsValidator);
PostValidator.RuleFor(s => s.Artist.QualityProfileId).SetValidator(qualityProfileExistsValidator);
PostValidator.RuleFor(s => s.Artist.MetadataProfileId).SetValidator(metadataProfileExistsValidator);
PostValidator.RuleFor(s => s.Artist.RootFolderPath).IsValidPath().When(s => s.Artist.Path.IsNullOrWhiteSpace());
PostValidator.RuleFor(s => s.Artist.ForeignArtistId).NotEmpty();

View file

@ -53,7 +53,7 @@ namespace Lidarr.Api.V1.Artist
ArtistExistsValidator artistExistsValidator,
ArtistAncestorValidator artistAncestorValidator,
SystemFolderValidator systemFolderValidator,
ProfileExistsValidator profileExistsValidator,
QualityProfileExistsValidator qualityProfileExistsValidator,
MetadataProfileExistsValidator metadataProfileExistsValidator)
: base(signalRBroadcaster)
{
@ -85,7 +85,7 @@ namespace Lidarr.Api.V1.Artist
.SetValidator(systemFolderValidator)
.When(s => !s.Path.IsNullOrWhiteSpace());
SharedValidator.RuleFor(s => s.QualityProfileId).SetValidator(profileExistsValidator);
SharedValidator.RuleFor(s => s.QualityProfileId).SetValidator(qualityProfileExistsValidator);
SharedValidator.RuleFor(s => s.MetadataProfileId).SetValidator(metadataProfileExistsValidator);
PostValidator.RuleFor(s => s.Path).IsValidPath().When(s => s.RootFolderPath.IsNullOrWhiteSpace());

View file

@ -62,7 +62,6 @@ namespace Lidarr.Api.V1.FileSystem
return _diskScanService.GetAudioFiles(path).Select(f => new
{
Path = f.FullName,
RelativePath = path.GetRelativePath(f.FullName),
Name = f.Name
});
}

View file

@ -9,7 +9,7 @@ namespace Lidarr.Api.V1.ImportLists
public static readonly ImportListResourceMapper ResourceMapper = new ImportListResourceMapper();
public ImportListModule(ImportListFactory importListFactory,
ProfileExistsValidator profileExistsValidator,
QualityProfileExistsValidator qualityProfileExistsValidator,
MetadataProfileExistsValidator metadataProfileExistsValidator)
: base(importListFactory, "importlist", ResourceMapper)
{
@ -17,7 +17,7 @@ namespace Lidarr.Api.V1.ImportLists
Http.Validation.RuleBuilderExtensions.ValidId(SharedValidator.RuleFor(s => s.MetadataProfileId));
SharedValidator.RuleFor(c => c.RootFolderPath).IsValidPath();
SharedValidator.RuleFor(c => c.QualityProfileId).SetValidator(profileExistsValidator);
SharedValidator.RuleFor(c => c.QualityProfileId).SetValidator(qualityProfileExistsValidator);
SharedValidator.RuleFor(c => c.MetadataProfileId).SetValidator(metadataProfileExistsValidator);
}

View file

@ -71,7 +71,6 @@ namespace Lidarr.Api.V1.ManualImport
{
Id = resource.Id,
Path = resource.Path,
RelativePath = resource.RelativePath,
Name = resource.Name,
Size = resource.Size,
Artist = resource.Artist == null ? null : _artistService.GetArtist(resource.Artist.Id),

View file

@ -14,7 +14,6 @@ namespace Lidarr.Api.V1.ManualImport
public class ManualImportResource : RestResource
{
public string Path { get; set; }
public string RelativePath { get; set; }
public string Name { get; set; }
public long Size { get; set; }
public ArtistResource Artist { get; set; }
@ -44,7 +43,6 @@ namespace Lidarr.Api.V1.ManualImport
{
Id = model.Id,
Path = model.Path,
RelativePath = model.RelativePath,
Name = model.Name,
Size = model.Size,
Artist = model.Artist.ToResource(),

View file

@ -1,7 +1,9 @@
using System.Collections.Generic;
using FluentValidation;
using Lidarr.Http;
using Lidarr.Http.REST;
using NzbDrone.Core.RootFolders;
using NzbDrone.Core.Validation;
using NzbDrone.Core.Validation.Paths;
using NzbDrone.SignalR;
@ -18,7 +20,9 @@ namespace Lidarr.Api.V1.RootFolders
MappedNetworkDriveValidator mappedNetworkDriveValidator,
StartupFolderValidator startupFolderValidator,
SystemFolderValidator systemFolderValidator,
FolderWritableValidator folderWritableValidator)
FolderWritableValidator folderWritableValidator,
QualityProfileExistsValidator qualityProfileExistsValidator,
MetadataProfileExistsValidator metadataProfileExistsValidator)
: base(signalRBroadcaster)
{
_rootFolderService = rootFolderService;
@ -26,17 +30,29 @@ namespace Lidarr.Api.V1.RootFolders
GetResourceAll = GetRootFolders;
GetResourceById = GetRootFolder;
CreateResource = CreateRootFolder;
UpdateResource = UpdateRootFolder;
DeleteResource = DeleteFolder;
SharedValidator.RuleFor(c => c.Path)
.Cascade(CascadeMode.StopOnFirstFailure)
.IsValidPath()
.SetValidator(rootFolderValidator)
.SetValidator(mappedNetworkDriveValidator)
.SetValidator(startupFolderValidator)
.SetValidator(pathExistsValidator)
.SetValidator(systemFolderValidator)
.SetValidator(folderWritableValidator);
.Cascade(CascadeMode.StopOnFirstFailure)
.IsValidPath()
.SetValidator(mappedNetworkDriveValidator)
.SetValidator(startupFolderValidator)
.SetValidator(pathExistsValidator)
.SetValidator(systemFolderValidator)
.SetValidator(folderWritableValidator);
PostValidator.RuleFor(c => c.Path)
.SetValidator(rootFolderValidator);
SharedValidator.RuleFor(c => c.Name)
.NotEmpty();
SharedValidator.RuleFor(c => c.DefaultMetadataProfileId)
.SetValidator(metadataProfileExistsValidator);
SharedValidator.RuleFor(c => c.DefaultQualityProfileId)
.SetValidator(qualityProfileExistsValidator);
}
private RootFolderResource GetRootFolder(int id)
@ -51,9 +67,21 @@ namespace Lidarr.Api.V1.RootFolders
return _rootFolderService.Add(model).Id;
}
private void UpdateRootFolder(RootFolderResource rootFolderResource)
{
var model = rootFolderResource.ToModel();
if (model.Path != rootFolderResource.Path)
{
throw new BadRequestException("Cannot edit root folder path");
}
_rootFolderService.Update(model);
}
private List<RootFolderResource> GetRootFolders()
{
return _rootFolderService.AllWithUnmappedFolders().ToResource();
return _rootFolderService.AllWithSpaceStats().ToResource();
}
private void DeleteFolder(int id)

View file

@ -1,18 +1,23 @@
using System.Collections.Generic;
using System.Linq;
using Lidarr.Http.REST;
using NzbDrone.Core.Music;
using NzbDrone.Core.RootFolders;
namespace Lidarr.Api.V1.RootFolders
{
public class RootFolderResource : RestResource
{
public string Name { get; set; }
public string Path { get; set; }
public int DefaultMetadataProfileId { get; set; }
public int DefaultQualityProfileId { get; set; }
public MonitorTypes DefaultMonitorOption { get; set; }
public HashSet<int> DefaultTags { get; set; }
public bool Accessible { get; set; }
public long? FreeSpace { get; set; }
public long? TotalSpace { get; set; }
public List<UnmappedFolder> UnmappedFolders { get; set; }
}
public static class RootFolderResourceMapper
@ -28,11 +33,16 @@ namespace Lidarr.Api.V1.RootFolders
{
Id = model.Id,
Name = model.Name,
Path = model.Path,
DefaultMetadataProfileId = model.DefaultMetadataProfileId,
DefaultQualityProfileId = model.DefaultQualityProfileId,
DefaultMonitorOption = model.DefaultMonitorOption,
DefaultTags = model.DefaultTags,
Accessible = model.Accessible,
FreeSpace = model.FreeSpace,
TotalSpace = model.TotalSpace,
UnmappedFolders = model.UnmappedFolders
};
}
@ -46,12 +56,13 @@ namespace Lidarr.Api.V1.RootFolders
return new RootFolder
{
Id = resource.Id,
Name = resource.Name,
Path = resource.Path,
//Accessible
//FreeSpace
//UnmappedFolders
DefaultMetadataProfileId = resource.DefaultMetadataProfileId,
DefaultQualityProfileId = resource.DefaultQualityProfileId,
DefaultMonitorOption = resource.DefaultMonitorOption,
DefaultTags = resource.DefaultTags
};
}

View file

@ -13,7 +13,6 @@ namespace Lidarr.Api.V1.TrackFiles
{
public int ArtistId { get; set; }
public int AlbumId { get; set; }
public string RelativePath { get; set; }
public string Path { get; set; }
public long Size { get; set; }
public DateTime DateAdded { get; set; }
@ -74,7 +73,6 @@ namespace Lidarr.Api.V1.TrackFiles
ArtistId = artist.Id,
AlbumId = model.AlbumId,
Path = model.Path,
RelativePath = artist.Path.GetRelativePath(model.Path),
Size = model.Size,
DateAdded = model.DateAdded,
Quality = model.Quality,

View file

@ -17,7 +17,7 @@ namespace Lidarr.Api.V1.Tracks
public int AlbumId { get; set; }
public List<int> TrackNumbers { get; set; }
public int TrackFileId { get; set; }
public string RelativePath { get; set; }
public string Path { get; set; }
public List<TagDifference> Changes { get; set; }
}
@ -36,7 +36,7 @@ namespace Lidarr.Api.V1.Tracks
AlbumId = model.AlbumId,
TrackNumbers = model.TrackNumbers.ToList(),
TrackFileId = model.TrackFileId,
RelativePath = model.RelativePath,
Path = model.Path,
Changes = model.Changes.Select(x => new TagDifference
{
Field = x.Key,

View file

@ -153,5 +153,15 @@ namespace NzbDrone.Common.Extensions
{
return string.Join(separator, source.Select(predicate));
}
public static TSource MostCommon<TSource>(this IEnumerable<TSource> items)
{
return items.GroupBy(x => x).OrderByDescending(x => x.Count()).First().Key;
}
public static TResult MostCommon<TSource, TResult>(this IEnumerable<TSource> items, Func<TSource, TResult> predicate)
{
return items.Select(predicate).GroupBy(x => x).OrderByDescending(x => x.Count()).First().Key;
}
}
}

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using FluentAssertions;
using Moq;
@ -6,6 +7,7 @@ using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Core.DiskSpace;
using NzbDrone.Core.Music;
using NzbDrone.Core.RootFolders;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common;
@ -14,14 +16,20 @@ namespace NzbDrone.Core.Test.DiskSpace
[TestFixture]
public class DiskSpaceServiceFixture : CoreTest<DiskSpaceService>
{
private string _artistFolder;
private string _artostFolder2;
private RootFolder _rootDir;
private string _artistFolder1;
private string _artistFolder2;
[SetUp]
public void SetUp()
{
_artistFolder = @"G:\fasdlfsdf\artist".AsOsAgnostic();
_artostFolder2 = @"G:\fasdlfsdf\artist2".AsOsAgnostic();
_rootDir = new RootFolder { Path = @"G:\fasdlfsdf".AsOsAgnostic() };
_artistFolder1 = Path.Combine(_rootDir.Path, "artist1");
_artistFolder2 = Path.Combine(_rootDir.Path, "artist2");
Mocker.GetMock<IRootFolderService>()
.Setup(x => x.All())
.Returns(new List<RootFolder>() { _rootDir });
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.GetMounts())
@ -59,9 +67,9 @@ namespace NzbDrone.Core.Test.DiskSpace
[Test]
public void should_check_diskspace_for_artist_folders()
{
GivenArtist(new Artist { Path = _artistFolder });
GivenArtist(new Artist { Path = _artistFolder1 });
GivenExistingFolder(_artistFolder);
GivenExistingFolder(_artistFolder1);
var freeSpace = Subject.GetFreeSpace();
@ -71,10 +79,10 @@ namespace NzbDrone.Core.Test.DiskSpace
[Test]
public void should_check_diskspace_for_same_root_folder_only_once()
{
GivenArtist(new Artist { Path = _artistFolder }, new Artist { Path = _artostFolder2 });
GivenArtist(new Artist { Path = _artistFolder1 }, new Artist { Path = _artistFolder2 });
GivenExistingFolder(_artistFolder);
GivenExistingFolder(_artostFolder2);
GivenExistingFolder(_artistFolder1);
GivenExistingFolder(_artistFolder2);
var freeSpace = Subject.GetFreeSpace();
@ -84,19 +92,6 @@ namespace NzbDrone.Core.Test.DiskSpace
.Verify(v => v.GetAvailableSpace(It.IsAny<string>()), Times.Once());
}
[Test]
public void should_not_check_diskspace_for_missing_artist_folders()
{
GivenArtist(new Artist { Path = _artistFolder });
var freeSpace = Subject.GetFreeSpace();
freeSpace.Should().BeEmpty();
Mocker.GetMock<IDiskProvider>()
.Verify(v => v.GetAvailableSpace(It.IsAny<string>()), Times.Never());
}
[TestCase("/boot")]
[TestCase("/var/lib/rancher")]
[TestCase("/var/lib/rancher/volumes")]
@ -114,6 +109,10 @@ namespace NzbDrone.Core.Test.DiskSpace
.Setup(v => v.GetMounts())
.Returns(new List<IMount> { mount.Object });
Mocker.GetMock<IRootFolderService>()
.Setup(x => x.All())
.Returns(new List<RootFolder>());
var freeSpace = Subject.GetFreeSpace();
freeSpace.Should().BeEmpty();

View file

@ -9,7 +9,6 @@ using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.TrackImport;
@ -41,11 +40,15 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
.Build();
Mocker.GetMock<IRootFolderService>()
.Setup(s => s.GetBestRootFolderPath(It.IsAny<string>()))
.Returns(_rootFolder);
.Setup(s => s.GetBestRootFolder(It.IsAny<string>()))
.Returns(new RootFolder { Path = _rootFolder });
Mocker.GetMock<IArtistService>()
.Setup(s => s.GetArtists(It.IsAny<List<int>>()))
.Returns(new List<Artist>());
Mocker.GetMock<IMakeImportDecision>()
.Setup(v => v.GetImportDecisions(It.IsAny<List<IFileInfo>>(), It.IsAny<Artist>(), It.IsAny<FilterFilesType>(), It.IsAny<bool>()))
.Setup(v => v.GetImportDecisions(It.IsAny<List<IFileInfo>>(), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerInfo>(), It.IsAny<ImportDecisionMakerConfig>()))
.Returns(new List<ImportDecision<LocalTrack>>());
Mocker.GetMock<IMediaFileService>()
@ -57,8 +60,8 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
.Returns(new List<TrackFile>());
Mocker.GetMock<IMediaFileService>()
.Setup(v => v.FilterUnchangedFiles(It.IsAny<List<IFileInfo>>(), It.IsAny<Artist>(), It.IsAny<FilterFilesType>()))
.Returns((List<IFileInfo> files, Artist artist, FilterFilesType filter) => files);
.Setup(v => v.FilterUnchangedFiles(It.IsAny<List<IFileInfo>>(), It.IsAny<FilterFilesType>()))
.Returns((List<IFileInfo> files, FilterFilesType filter) => files);
}
private void GivenRootFolder(params string[] subfolders)
@ -112,7 +115,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
[Test]
public void should_not_scan_if_root_folder_does_not_exist()
{
Subject.Scan(_artist);
Subject.Scan(new List<string> { _artist.Path });
ExceptionVerification.ExpectedWarns(1);
@ -120,15 +123,18 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
.Verify(v => v.FolderExists(_artist.Path), Times.Never());
Mocker.GetMock<IMediaFileTableCleanupService>()
.Verify(v => v.Clean(It.IsAny<Artist>(), It.IsAny<List<string>>()), Times.Never());
.Verify(v => v.Clean(It.IsAny<string>(), It.IsAny<List<string>>()), Times.Never());
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.IsAny<List<IFileInfo>>(), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerInfo>(), It.IsAny<ImportDecisionMakerConfig>()), Times.Never());
}
[Test]
public void should_not_scan_if_artist_root_folder_is_empty()
public void should_not_scan_if_root_folder_is_empty()
{
GivenRootFolder();
Subject.Scan(_artist);
Subject.Scan(new List<string> { _artist.Path });
ExceptionVerification.ExpectedWarns(1);
@ -136,72 +142,23 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
.Verify(v => v.FolderExists(_artist.Path), Times.Never());
Mocker.GetMock<IMediaFileTableCleanupService>()
.Verify(v => v.Clean(It.IsAny<Artist>(), It.IsAny<List<string>>()), Times.Never());
.Verify(v => v.Clean(It.IsAny<string>(), It.IsAny<List<string>>()), Times.Never());
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.IsAny<List<IFileInfo>>(), _artist, FilterFilesType.Known, true), Times.Never());
.Verify(v => v.GetImportDecisions(It.IsAny<List<IFileInfo>>(), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerInfo>(), It.IsAny<ImportDecisionMakerConfig>()), Times.Never());
}
[Test]
public void should_create_if_artist_folder_does_not_exist_but_create_folder_enabled()
public void should_clean_if_folder_does_not_exist()
{
GivenRootFolder(_otherArtistFolder);
Mocker.GetMock<IConfigService>()
.Setup(s => s.CreateEmptyArtistFolders)
.Returns(true);
Subject.Scan(_artist);
DiskProvider.FolderExists(_artist.Path).Should().BeTrue();
}
[Test]
public void should_not_create_if_artist_folder_does_not_exist_and_create_folder_disabled()
{
GivenRootFolder(_otherArtistFolder);
Mocker.GetMock<IConfigService>()
.Setup(s => s.CreateEmptyArtistFolders)
.Returns(false);
Subject.Scan(_artist);
DiskProvider.FolderExists(_artist.Path).Should().BeFalse();
}
[Test]
public void should_clean_but_not_import_if_artist_folder_does_not_exist()
{
GivenRootFolder(_otherArtistFolder);
Subject.Scan(_artist);
Subject.Scan(new List<string> { _artist.Path });
DiskProvider.FolderExists(_artist.Path).Should().BeFalse();
Mocker.GetMock<IMediaFileTableCleanupService>()
.Verify(v => v.Clean(It.IsAny<Artist>(), It.IsAny<List<string>>()), Times.Once());
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.IsAny<List<IFileInfo>>(), _artist, FilterFilesType.Known, true), Times.Never());
}
[Test]
public void should_clean_but_not_import_if_artist_folder_does_not_exist_and_create_folder_enabled()
{
GivenRootFolder(_otherArtistFolder);
Mocker.GetMock<IConfigService>()
.Setup(s => s.CreateEmptyArtistFolders)
.Returns(true);
Subject.Scan(_artist);
Mocker.GetMock<IMediaFileTableCleanupService>()
.Verify(v => v.Clean(It.IsAny<Artist>(), It.IsAny<List<string>>()), Times.Once());
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.IsAny<List<IFileInfo>>(), _artist, FilterFilesType.Known, true), Times.Never());
.Verify(v => v.Clean(It.IsAny<string>(), It.IsAny<List<string>>()), Times.Once());
}
[Test]
@ -215,10 +172,10 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
Path.Combine(_artist.Path, "s01e01.flac")
});
Subject.Scan(_artist);
Subject.Scan(new List<string> { _artist.Path });
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 2), _artist, FilterFilesType.Known, true), Times.Once());
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 2), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerInfo>(), It.IsAny<ImportDecisionMakerConfig>()), Times.Once());
}
[Test]
@ -235,10 +192,10 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
Path.Combine(_artist.Path, "Season 1", "s01e01.flac")
});
Subject.Scan(_artist);
Subject.Scan(new List<string> { _artist.Path });
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 1), _artist, FilterFilesType.Known, true), Times.Once());
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 1), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerInfo>(), It.IsAny<ImportDecisionMakerConfig>()), Times.Once());
}
[Test]
@ -253,10 +210,10 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
Path.Combine(_artist.Path, "Season 1", "s01e01.flac")
});
Subject.Scan(_artist);
Subject.Scan(new List<string> { _artist.Path });
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 1), _artist, FilterFilesType.Known, true), Times.Once());
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 1), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerInfo>(), It.IsAny<ImportDecisionMakerConfig>()), Times.Once());
}
[Test]
@ -276,10 +233,10 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
Path.Combine(_artist.Path, "Season 2", "s02e02.flac"),
});
Subject.Scan(_artist);
Subject.Scan(new List<string> { _artist.Path });
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 4), _artist, FilterFilesType.Known, true), Times.Once());
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 4), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerInfo>(), It.IsAny<ImportDecisionMakerConfig>()), Times.Once());
}
[Test]
@ -292,10 +249,10 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
Path.Combine(_artist.Path, "Album 1", ".t01.mp3")
});
Subject.Scan(_artist);
Subject.Scan(new List<string> { _artist.Path });
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 1), _artist, FilterFilesType.Known, true), Times.Once());
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 1), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerInfo>(), It.IsAny<ImportDecisionMakerConfig>()), Times.Once());
}
[Test]
@ -311,10 +268,10 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
Path.Combine(_artist.Path, "Season 1", "s01e01.flac")
});
Subject.Scan(_artist);
Subject.Scan(new List<string> { _artist.Path });
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 1), _artist, FilterFilesType.Known, true), Times.Once());
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 1), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerInfo>(), It.IsAny<ImportDecisionMakerConfig>()), Times.Once());
}
[Test]
@ -331,10 +288,10 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
Path.Combine(_artist.Path, "Season 1", "s01e01.flac")
});
Subject.Scan(_artist);
Subject.Scan(new List<string> { _artist.Path });
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 1), _artist, FilterFilesType.Known, true), Times.Once());
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 1), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerInfo>(), It.IsAny<ImportDecisionMakerConfig>()), Times.Once());
}
[Test]
@ -348,10 +305,10 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
Path.Combine(_artist.Path, "Season 1", "s01e01.flac")
});
Subject.Scan(_artist);
Subject.Scan(new List<string> { _artist.Path });
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 1), _artist, FilterFilesType.Known, true), Times.Once());
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 1), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerInfo>(), It.IsAny<ImportDecisionMakerConfig>()), Times.Once());
}
[Test]
@ -365,10 +322,10 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
Path.Combine(_artist.Path, "Season 1", "s01e01.flac")
});
Subject.Scan(_artist);
Subject.Scan(new List<string> { _artist.Path });
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 1), _artist, FilterFilesType.Known, true), Times.Once());
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 1), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerInfo>(), It.IsAny<ImportDecisionMakerConfig>()), Times.Once());
}
[Test]
@ -384,10 +341,10 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
Path.Combine(_artist.Path, "Season 1", "s01e01.flac")
});
Subject.Scan(_artist);
Subject.Scan(new List<string> { _artist.Path });
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 2), _artist, FilterFilesType.Known, true), Times.Once());
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 2), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerInfo>(), It.IsAny<ImportDecisionMakerConfig>()), Times.Once());
}
[Test]
@ -402,20 +359,20 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
Path.Combine(_artist.Path, "24 The Status Quo Combustion.flac")
});
Subject.Scan(_artist);
Subject.Scan(new List<string> { _artist.Path });
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 1), _artist, FilterFilesType.Known, true), Times.Once());
.Verify(v => v.GetImportDecisions(It.Is<List<IFileInfo>>(l => l.Count == 1), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerInfo>(), It.IsAny<ImportDecisionMakerConfig>()), Times.Once());
}
private void GivenRejections()
{
Mocker.GetMock<IMakeImportDecision>()
.Setup(x => x.GetImportDecisions(It.IsAny<List<IFileInfo>>(), It.IsAny<Artist>(), It.IsAny<FilterFilesType>(), It.IsAny<bool>()))
.Returns((List<IFileInfo> fileList, Artist artist, FilterFilesType filter, bool includeExisting) =>
.Setup(x => x.GetImportDecisions(It.IsAny<List<IFileInfo>>(), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerInfo>(), It.IsAny<ImportDecisionMakerConfig>()))
.Returns((List<IFileInfo> fileList, IdentificationOverrides idOverrides, ImportDecisionMakerInfo idInfo, ImportDecisionMakerConfig idConfig) =>
fileList.Select(x => new LocalTrack
{
Artist = artist,
Artist = _artist,
Path = x.FullName,
Modified = x.LastWriteTimeUtc,
FileTrackInfo = new ParsedTrackInfo()
@ -437,7 +394,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
GivenKnownFiles(new List<string>());
GivenRejections();
Subject.Scan(_artist);
Subject.Scan(new List<string> { _artist.Path });
Mocker.GetMock<IMediaFileService>()
.Verify(x => x.AddMany(It.Is<List<TrackFile>>(l => l.Select(t => t.Path).SequenceEqual(files))),
@ -457,7 +414,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
GivenKnownFiles(files.GetRange(1, 1));
GivenRejections();
Subject.Scan(_artist);
Subject.Scan(new List<string> { _artist.Path });
Mocker.GetMock<IMediaFileService>()
.Verify(x => x.AddMany(It.Is<List<TrackFile>>(l => l.Select(t => t.Path).SequenceEqual(files.GetRange(0, 1)))),
@ -477,7 +434,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
GivenKnownFiles(files);
GivenRejections();
Subject.Scan(_artist);
Subject.Scan(new List<string> { _artist.Path });
Mocker.GetMock<IMediaFileService>()
.Verify(x => x.AddMany(It.Is<List<TrackFile>>(l => l.Count == 0)),
@ -501,7 +458,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
GivenKnownFiles(files);
GivenRejections();
Subject.Scan(_artist);
Subject.Scan(new List<string> { _artist.Path });
Mocker.GetMock<IMediaFileService>()
.Verify(x => x.Update(It.Is<List<TrackFile>>(l => l.Count == 0)),
@ -525,7 +482,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
GivenKnownFiles(files);
GivenRejections();
Subject.Scan(_artist);
Subject.Scan(new List<string> { _artist.Path });
Mocker.GetMock<IMediaFileService>()
.Verify(x => x.Update(It.Is<List<TrackFile>>(l => l.Count == 2)),
@ -556,14 +513,14 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
.Build();
Mocker.GetMock<IMakeImportDecision>()
.Setup(x => x.GetImportDecisions(It.IsAny<List<IFileInfo>>(), It.IsAny<Artist>(), It.IsAny<FilterFilesType>(), It.IsAny<bool>()))
.Setup(x => x.GetImportDecisions(It.IsAny<List<IFileInfo>>(), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerInfo>(), It.IsAny<ImportDecisionMakerConfig>()))
.Returns(new List<ImportDecision<LocalTrack>> { new ImportDecision<LocalTrack>(localTrack, new Rejection("Reject")) });
Subject.Scan(_artist);
Subject.Scan(new List<string> { _artist.Path });
Mocker.GetMock<IMediaFileService>()
.Verify(x => x.Update(It.Is<List<TrackFile>>(
l => l.Count == 1 &&
l => l.Count == 1 &&
l[0].Path == localTrack.Path &&
l[0].Modified == localTrack.Modified &&
l[0].Size == localTrack.Size &&

View file

@ -84,7 +84,7 @@ namespace NzbDrone.Core.Test.MediaFiles
imported.Add(new ImportDecision<LocalTrack>(localTrack));
Mocker.GetMock<IMakeImportDecision>()
.Setup(s => s.GetImportDecisions(It.IsAny<List<IFileInfo>>(), It.IsAny<Artist>(), It.IsAny<DownloadClientItem>(), null))
.Setup(v => v.GetImportDecisions(It.IsAny<List<IFileInfo>>(), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerInfo>(), It.IsAny<ImportDecisionMakerConfig>()))
.Returns(imported);
Mocker.GetMock<IImportApprovedTracks>()
@ -130,8 +130,8 @@ namespace NzbDrone.Core.Test.MediaFiles
Subject.ProcessRootFolder(DiskProvider.GetDirectoryInfo(_droneFactory));
Mocker.GetMock<IMakeImportDecision>()
.Verify(c => c.GetImportDecisions(It.IsAny<List<IFileInfo>>(), It.IsAny<Artist>(), It.IsAny<DownloadClientItem>(), It.IsAny<ParsedTrackInfo>()),
Times.Never());
.Verify(c => c.GetImportDecisions(It.IsAny<List<IFileInfo>>(), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerInfo>(), It.IsAny<ImportDecisionMakerConfig>()),
Times.Never());
VerifyNoImport();
}
@ -181,7 +181,7 @@ namespace NzbDrone.Core.Test.MediaFiles
imported.Add(new ImportDecision<LocalTrack>(localTrack));
Mocker.GetMock<IMakeImportDecision>()
.Setup(s => s.GetImportDecisions(It.IsAny<List<IFileInfo>>(), It.IsAny<Artist>(), It.IsAny<DownloadClientItem>(), null))
.Setup(v => v.GetImportDecisions(It.IsAny<List<IFileInfo>>(), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerInfo>(), It.IsAny<ImportDecisionMakerConfig>()))
.Returns(imported);
Mocker.GetMock<IImportApprovedTracks>()
@ -238,7 +238,7 @@ namespace NzbDrone.Core.Test.MediaFiles
imported.Add(new ImportDecision<LocalTrack>(localTrack));
Mocker.GetMock<IMakeImportDecision>()
.Setup(s => s.GetImportDecisions(It.IsAny<List<IFileInfo>>(), It.IsAny<Artist>(), It.IsAny<DownloadClientItem>(), null))
.Setup(v => v.GetImportDecisions(It.IsAny<List<IFileInfo>>(), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerInfo>(), It.IsAny<ImportDecisionMakerConfig>()))
.Returns(imported);
Mocker.GetMock<IImportApprovedTracks>()
@ -278,7 +278,7 @@ namespace NzbDrone.Core.Test.MediaFiles
imported.Add(new ImportDecision<LocalTrack>(localTrack));
Mocker.GetMock<IMakeImportDecision>()
.Setup(s => s.GetImportDecisions(It.IsAny<List<IFileInfo>>(), It.IsAny<Artist>(), It.IsAny<DownloadClientItem>(), null))
.Setup(v => v.GetImportDecisions(It.IsAny<List<IFileInfo>>(), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerInfo>(), It.IsAny<ImportDecisionMakerConfig>()))
.Returns(imported);
Mocker.GetMock<IImportApprovedTracks>()

View file

@ -123,7 +123,6 @@ namespace NzbDrone.Core.Test.MediaFiles
{
VerifyData();
var firstReleaseFiles = Subject.GetFilesWithBasePath(dir.AsOsAgnostic());
VerifyEagerLoaded(firstReleaseFiles);
firstReleaseFiles.Should().HaveCount(2);
}

View file

@ -17,19 +17,8 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaFileServiceTests
[TestFixture]
public class FilterFixture : FileSystemTest<MediaFileService>
{
private Artist _artist;
private DateTime _lastWrite = new DateTime(2019, 1, 1);
[SetUp]
public void Setup()
{
_artist = new Artist
{
Id = 10,
Path = @"C:\".AsOsAgnostic()
};
}
private List<IFileInfo> GivenFiles(string[] files)
{
foreach (var file in files)
@ -52,10 +41,10 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaFileServiceTests
});
Mocker.GetMock<IMediaFileRepository>()
.Setup(c => c.GetFilesWithBasePath(It.IsAny<string>()))
.Setup(c => c.GetFileWithPath(It.IsAny<List<string>>()))
.Returns(new List<TrackFile>());
Subject.FilterUnchangedFiles(files, _artist, filter).Should().BeEquivalentTo(files);
Subject.FilterUnchangedFiles(files, filter).Should().BeEquivalentTo(files);
}
[TestCase(FilterFilesType.Known)]
@ -70,14 +59,14 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaFileServiceTests
});
Mocker.GetMock<IMediaFileRepository>()
.Setup(c => c.GetFilesWithBasePath(It.IsAny<string>()))
.Setup(c => c.GetFileWithPath(It.IsAny<List<string>>()))
.Returns(files.Select(f => new TrackFile
{
Path = f.FullName,
Modified = _lastWrite
}).ToList());
Subject.FilterUnchangedFiles(files, _artist, filter).Should().BeEmpty();
Subject.FilterUnchangedFiles(files, filter).Should().BeEmpty();
}
[TestCase(FilterFilesType.Known)]
@ -92,7 +81,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaFileServiceTests
});
Mocker.GetMock<IMediaFileRepository>()
.Setup(c => c.GetFilesWithBasePath(It.IsAny<string>()))
.Setup(c => c.GetFileWithPath(It.IsAny<List<string>>()))
.Returns(new List<TrackFile>
{
new TrackFile
@ -102,8 +91,8 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaFileServiceTests
}
});
Subject.FilterUnchangedFiles(files, _artist, filter).Should().HaveCount(2);
Subject.FilterUnchangedFiles(files, _artist, filter).Select(x => x.FullName).Should().NotContain("C:\\file2.avi".AsOsAgnostic());
Subject.FilterUnchangedFiles(files, filter).Should().HaveCount(2);
Subject.FilterUnchangedFiles(files, filter).Select(x => x.FullName).Should().NotContain("C:\\file2.avi".AsOsAgnostic());
}
[TestCase(FilterFilesType.Known)]
@ -120,7 +109,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaFileServiceTests
});
Mocker.GetMock<IMediaFileRepository>()
.Setup(c => c.GetFilesWithBasePath(It.IsAny<string>()))
.Setup(c => c.GetFileWithPath(It.IsAny<List<string>>()))
.Returns(new List<TrackFile>
{
new TrackFile
@ -130,8 +119,8 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaFileServiceTests
}
});
Subject.FilterUnchangedFiles(files, _artist, filter).Should().HaveCount(2);
Subject.FilterUnchangedFiles(files, _artist, filter).Select(x => x.FullName).Should().NotContain("C:\\file2.avi".AsOsAgnostic());
Subject.FilterUnchangedFiles(files, filter).Should().HaveCount(2);
Subject.FilterUnchangedFiles(files, filter).Select(x => x.FullName).Should().NotContain("C:\\file2.avi".AsOsAgnostic());
}
[TestCase(FilterFilesType.Known)]
@ -148,7 +137,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaFileServiceTests
});
Mocker.GetMock<IMediaFileRepository>()
.Setup(c => c.GetFilesWithBasePath(It.IsAny<string>()))
.Setup(c => c.GetFileWithPath(It.IsAny<List<string>>()))
.Returns(new List<TrackFile>
{
new TrackFile
@ -158,7 +147,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaFileServiceTests
}
});
Subject.FilterUnchangedFiles(files, _artist, filter).Should().HaveCount(3);
Subject.FilterUnchangedFiles(files, filter).Should().HaveCount(3);
}
[TestCase(FilterFilesType.Known)]
@ -171,12 +160,12 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaFileServiceTests
});
Mocker.GetMock<IMediaFileRepository>()
.Setup(c => c.GetFilesWithBasePath(It.IsAny<string>()))
.Setup(c => c.GetFileWithPath(It.IsAny<List<string>>()))
.Returns(new List<TrackFile>());
Subject.FilterUnchangedFiles(files, _artist, filter).Should().HaveCount(1);
Subject.FilterUnchangedFiles(files, _artist, filter).Select(x => x.FullName).Should().NotContain(files.First().FullName.ToLower());
Subject.FilterUnchangedFiles(files, _artist, filter).Should().Contain(files.First());
Subject.FilterUnchangedFiles(files, filter).Should().HaveCount(1);
Subject.FilterUnchangedFiles(files, filter).Select(x => x.FullName).Should().NotContain(files.First().FullName.ToLower());
Subject.FilterUnchangedFiles(files, filter).Should().Contain(files.First());
}
[TestCase(FilterFilesType.Known)]
@ -190,7 +179,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaFileServiceTests
var files = FileSystem.AllFiles.Select(x => DiskProvider.GetFileInfo(x)).ToList();
Mocker.GetMock<IMediaFileRepository>()
.Setup(c => c.GetFilesWithBasePath(It.IsAny<string>()))
.Setup(c => c.GetFileWithPath(It.IsAny<List<string>>()))
.Returns(new List<TrackFile>
{
new TrackFile
@ -201,8 +190,8 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaFileServiceTests
}
});
Subject.FilterUnchangedFiles(files, _artist, filter).Should().HaveCount(2);
Subject.FilterUnchangedFiles(files, _artist, filter).Select(x => x.FullName).Should().NotContain("C:\\file2.avi".AsOsAgnostic());
Subject.FilterUnchangedFiles(files, filter).Should().HaveCount(2);
Subject.FilterUnchangedFiles(files, filter).Select(x => x.FullName).Should().NotContain("C:\\file2.avi".AsOsAgnostic());
}
[TestCase(FilterFilesType.Matched)]
@ -215,7 +204,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaFileServiceTests
var files = FileSystem.AllFiles.Select(x => DiskProvider.GetFileInfo(x)).ToList();
Mocker.GetMock<IMediaFileRepository>()
.Setup(c => c.GetFilesWithBasePath(It.IsAny<string>()))
.Setup(c => c.GetFileWithPath(It.IsAny<List<string>>()))
.Returns(new List<TrackFile>
{
new TrackFile
@ -227,8 +216,8 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaFileServiceTests
}
});
Subject.FilterUnchangedFiles(files, _artist, filter).Should().HaveCount(3);
Subject.FilterUnchangedFiles(files, _artist, filter).Select(x => x.FullName).Should().Contain("C:\\file2.avi".AsOsAgnostic());
Subject.FilterUnchangedFiles(files, filter).Should().HaveCount(3);
Subject.FilterUnchangedFiles(files, filter).Select(x => x.FullName).Should().Contain("C:\\file2.avi".AsOsAgnostic());
}
[TestCase(FilterFilesType.Matched)]
@ -241,7 +230,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaFileServiceTests
var files = FileSystem.AllFiles.Select(x => DiskProvider.GetFileInfo(x)).ToList();
Mocker.GetMock<IMediaFileRepository>()
.Setup(c => c.GetFilesWithBasePath(It.IsAny<string>()))
.Setup(c => c.GetFileWithPath(It.IsAny<List<string>>()))
.Returns(new List<TrackFile>
{
new TrackFile
@ -253,8 +242,8 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaFileServiceTests
}
});
Subject.FilterUnchangedFiles(files, _artist, filter).Should().HaveCount(2);
Subject.FilterUnchangedFiles(files, _artist, filter).Select(x => x.FullName).Should().NotContain("C:\\file2.avi".AsOsAgnostic());
Subject.FilterUnchangedFiles(files, filter).Should().HaveCount(2);
Subject.FilterUnchangedFiles(files, filter).Select(x => x.FullName).Should().NotContain("C:\\file2.avi".AsOsAgnostic());
}
[TestCase(FilterFilesType.Known)]
@ -268,7 +257,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaFileServiceTests
var files = FileSystem.AllFiles.Select(x => DiskProvider.GetFileInfo(x)).ToList();
Mocker.GetMock<IMediaFileRepository>()
.Setup(c => c.GetFilesWithBasePath(It.IsAny<string>()))
.Setup(c => c.GetFileWithPath(It.IsAny<List<string>>()))
.Returns(new List<TrackFile>
{
new TrackFile
@ -279,8 +268,8 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaFileServiceTests
}
});
Subject.FilterUnchangedFiles(files, _artist, filter).Should().HaveCount(3);
Subject.FilterUnchangedFiles(files, _artist, filter).Select(x => x.FullName).Should().Contain("C:\\file2.avi".AsOsAgnostic());
Subject.FilterUnchangedFiles(files, filter).Should().HaveCount(3);
Subject.FilterUnchangedFiles(files, filter).Select(x => x.FullName).Should().Contain("C:\\file2.avi".AsOsAgnostic());
}
}
}

View file

@ -13,7 +13,7 @@ namespace NzbDrone.Core.Test.MediaFiles
{
public class MediaFileTableCleanupServiceFixture : CoreTest<MediaFileTableCleanupService>
{
private readonly string _deletedPath = @"c:\ANY FILE STARTING WITH THIS PATH IS CONSIDERED DELETED!".AsOsAgnostic();
private readonly string _DELETED_PATH = @"c:\ANY FILE STARTING WITH THIS PATH IS CONSIDERED DELETED!".AsOsAgnostic();
private List<Track> _tracks;
private Artist _artist;
@ -62,7 +62,7 @@ namespace NzbDrone.Core.Test.MediaFiles
GivenTrackFiles(trackFiles);
Subject.Clean(_artist, FilesOnDisk(trackFiles));
Subject.Clean(_artist.Path, FilesOnDisk(trackFiles));
Mocker.GetMock<IMediaFileService>()
.Verify(c => c.DeleteMany(It.Is<List<TrackFile>>(x => x.Count == 0), DeleteMediaFileReason.MissingFromDisk), Times.Once());
@ -75,15 +75,15 @@ namespace NzbDrone.Core.Test.MediaFiles
.All()
.With(x => x.Path = Path.Combine(@"c:\test".AsOsAgnostic(), Path.GetRandomFileName()))
.Random(2)
.With(c => c.Path = Path.Combine(_deletedPath, Path.GetRandomFileName()))
.With(c => c.Path = Path.Combine(_DELETED_PATH, Path.GetRandomFileName()))
.Build();
GivenTrackFiles(trackFiles);
Subject.Clean(_artist, FilesOnDisk(trackFiles.Where(e => !e.Path.StartsWith(_deletedPath))));
Subject.Clean(_artist.Path, FilesOnDisk(trackFiles.Where(e => !e.Path.StartsWith(_DELETED_PATH))));
Mocker.GetMock<IMediaFileService>()
.Verify(c => c.DeleteMany(It.Is<List<TrackFile>>(e => e.Count == 2 && e.All(y => y.Path.StartsWith(_deletedPath))), DeleteMediaFileReason.MissingFromDisk), Times.Once());
.Verify(c => c.DeleteMany(It.Is<List<TrackFile>>(e => e.Count == 2 && e.All(y => y.Path.StartsWith(_DELETED_PATH))), DeleteMediaFileReason.MissingFromDisk), Times.Once());
}
[Test]
@ -96,7 +96,7 @@ namespace NzbDrone.Core.Test.MediaFiles
GivenTrackFiles(trackFiles);
Subject.Clean(_artist, new List<string>());
Subject.Clean(_artist.Path, new List<string>());
Mocker.GetMock<ITrackService>()
.Verify(c => c.SetFileIds(It.Is<List<Track>>(e => e.Count == 10 && e.All(y => y.TrackFileId == 0))), Times.Once());
@ -112,7 +112,7 @@ namespace NzbDrone.Core.Test.MediaFiles
GivenTrackFiles(trackFiles);
Subject.Clean(_artist, FilesOnDisk(trackFiles));
Subject.Clean(_artist.Path, FilesOnDisk(trackFiles));
Mocker.GetMock<ITrackService>().Verify(c => c.SetFileIds(It.Is<List<Track>>(x => x.Count == 0)), Times.Once());
}

View file

@ -13,7 +13,7 @@ using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
{
[TestFixture]
public class AlbumDistanceFixture : CoreTest<IdentificationService>
public class AlbumDistanceFixture : CoreTest
{
private ArtistMetadata _artist;
@ -28,13 +28,13 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
private List<Track> GivenTracks(int count)
{
return Builder<Track>
.CreateListOfSize(count)
.All()
.With(x => x.ArtistMetadata = _artist)
.With(x => x.MediumNumber = 1)
.Build()
.ToList();
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)
@ -102,7 +102,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
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))));
var distances = local.Zip(remote, (l, r) => Tuple.Create(r, DistanceCalculator.TrackDistance(l, r, DistanceCalculator.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();
@ -118,7 +118,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
var localTracks = GivenLocalTracks(tracks, release);
var mapping = GivenMapping(localTracks, tracks);
Subject.AlbumReleaseDistance(localTracks, release, mapping).NormalizedDistance().Should().Be(0.0);
DistanceCalculator.AlbumReleaseDistance(localTracks, release, mapping).NormalizedDistance().Should().Be(0.0);
}
[Test]
@ -130,7 +130,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
localTracks.RemoveAt(1);
var mapping = GivenMapping(localTracks, tracks);
var dist = Subject.AlbumReleaseDistance(localTracks, release, mapping);
var dist = DistanceCalculator.AlbumReleaseDistance(localTracks, release, mapping);
dist.NormalizedDistance().Should().NotBe(0.0);
dist.NormalizedDistance().Should().BeLessThan(0.2);
}
@ -148,7 +148,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
.With(x => x.Name = "different artist")
.Build();
Subject.AlbumReleaseDistance(localTracks, release, mapping).NormalizedDistance().Should().NotBe(0.0);
DistanceCalculator.AlbumReleaseDistance(localTracks, release, mapping).NormalizedDistance().Should().NotBe(0.0);
}
[Test]
@ -165,7 +165,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
.With(x => x.ForeignArtistId = "89ad4ac3-39f7-470e-963a-56509c546377")
.Build();
Subject.AlbumReleaseDistance(localTracks, release, mapping).NormalizedDistance().Should().Be(0.0);
DistanceCalculator.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
@ -178,7 +178,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
localTracks = new[] { 1, 3, 2 }.Select(x => localTracks[x - 1]).ToList();
var mapping = GivenMapping(localTracks, tracks);
var dist = Subject.AlbumReleaseDistance(localTracks, release, mapping);
var dist = DistanceCalculator.AlbumReleaseDistance(localTracks, release, mapping);
dist.NormalizedDistance().Should().NotBe(0.0);
dist.NormalizedDistance().Should().BeLessThan(0.2);
}
@ -193,7 +193,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
var localTracks = GivenLocalTracks(tracks, release);
var mapping = GivenMapping(localTracks, tracks);
Subject.AlbumReleaseDistance(localTracks, release, mapping).NormalizedDistance().Should().Be(0.0);
DistanceCalculator.AlbumReleaseDistance(localTracks, release, mapping).NormalizedDistance().Should().Be(0.0);
}
[Test]
@ -209,7 +209,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
var mapping = GivenMapping(localTracks, tracks);
Subject.AlbumReleaseDistance(localTracks, release, mapping).NormalizedDistance().Should().Be(0.0);
DistanceCalculator.AlbumReleaseDistance(localTracks, release, mapping).NormalizedDistance().Should().Be(0.0);
}
private static DateTime?[] dates = new DateTime?[] { null, new DateTime(2007, 1, 1), DateTime.Now };
@ -225,7 +225,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
release.Album.Value.ReleaseDate = null;
release.ReleaseDate = releaseDate;
var result = Subject.AlbumReleaseDistance(localTracks, release, mapping).NormalizedDistance();
var result = DistanceCalculator.AlbumReleaseDistance(localTracks, release, mapping).NormalizedDistance();
if (!releaseDate.HasValue || (localTracks[0].FileTrackInfo.Year == (releaseDate?.Year ?? 0)))
{
@ -248,7 +248,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
release.Album.Value.ReleaseDate = albumDate;
release.ReleaseDate = null;
var result = Subject.AlbumReleaseDistance(localTracks, release, mapping).NormalizedDistance();
var result = DistanceCalculator.AlbumReleaseDistance(localTracks, release, mapping).NormalizedDistance();
if (!albumDate.HasValue || (localTracks[0].FileTrackInfo.Year == (albumDate?.Year ?? 0)))
{

View file

@ -4,6 +4,7 @@ using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.MediaFiles.TrackImport;
using NzbDrone.Core.MediaFiles.TrackImport.Identification;
using NzbDrone.Core.Music;
using NzbDrone.Core.Parser;
@ -13,7 +14,7 @@ using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
{
[TestFixture]
public class GetCandidatesFixture : CoreTest<IdentificationService>
public class GetCandidatesFixture : CoreTest<CandidateService>
{
private ArtistMetadata _artist;
@ -28,12 +29,12 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
private List<Track> GivenTracks(int count)
{
return Builder<Track>
.CreateListOfSize(count)
.All()
.With(x => x.ArtistMetadata = _artist)
.Build()
.ToList();
return Builder<Track>
.CreateListOfSize(count)
.All()
.With(x => x.ArtistMetadata = _artist)
.Build()
.ToList();
}
private ParsedTrackInfo GivenParsedTrackInfo(Track track, AlbumRelease release)
@ -108,12 +109,12 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
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;
}
});
foreach (var track in x)
{
track.AcoustIdResults = null;
}
});
Mocker.GetMock<IReleaseService>()
.Setup(x => x.GetReleasesByRecordingIds(It.IsAny<List<string>>()))
@ -121,7 +122,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
var local = GivenLocalAlbumRelease();
Subject.GetCandidatesFromFingerprint(local, null, null, null, false).Should().BeEquivalentTo(new List<CandidateAlbumRelease>());
Subject.GetDbCandidatesFromFingerprint(local, null, false).Should().BeEquivalentTo(new List<CandidateAlbumRelease>());
}
[Test]
@ -131,8 +132,12 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
var release = GivenAlbumRelease("album", tracks);
var localTracks = GivenLocalTracks(tracks, release);
var localAlbumRelease = new LocalAlbumRelease(localTracks);
var idOverrides = new IdentificationOverrides
{
AlbumRelease = release
};
Subject.GetCandidatesFromTags(localAlbumRelease, null, null, release, false).Should().BeEquivalentTo(
Subject.GetDbCandidatesFromTags(localAlbumRelease, idOverrides, false).Should().BeEquivalentTo(
new List<CandidateAlbumRelease> { new CandidateAlbumRelease(release) });
}
@ -149,7 +154,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
.Setup(x => x.GetReleaseByForeignReleaseId("xxx", true))
.Returns(release);
Subject.GetCandidatesFromTags(localAlbumRelease, null, null, null, false).Should().BeEquivalentTo(
Subject.GetDbCandidatesFromTags(localAlbumRelease, null, false).Should().BeEquivalentTo(
new List<CandidateAlbumRelease> { new CandidateAlbumRelease(release) });
}
}

View file

@ -9,6 +9,9 @@ using Newtonsoft.Json;
using NUnit.Framework;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.ImportLists.Exclusions;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.TrackImport;
using NzbDrone.Core.MediaFiles.TrackImport.Aggregation;
using NzbDrone.Core.MediaFiles.TrackImport.Aggregation.Aggregators;
using NzbDrone.Core.MediaFiles.TrackImport.Identification;
@ -32,7 +35,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
private AddArtistService _addArtistService;
private RefreshArtistService _refreshArtistService;
private IdentificationService _subject;
private IdentificationService _Subject;
[SetUp]
public void SetUp()
@ -45,6 +48,8 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
Mocker.SetConstant<IAlbumRepository>(Mocker.Resolve<AlbumRepository>());
Mocker.SetConstant<IReleaseRepository>(Mocker.Resolve<ReleaseRepository>());
Mocker.SetConstant<ITrackRepository>(Mocker.Resolve<TrackRepository>());
Mocker.SetConstant<IImportListExclusionRepository>(Mocker.Resolve<ImportListExclusionRepository>());
Mocker.SetConstant<IMediaFileRepository>(Mocker.Resolve<MediaFileRepository>());
Mocker.GetMock<IMetadataProfileService>().Setup(x => x.Exists(It.IsAny<int>())).Returns(true);
@ -54,6 +59,8 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
Mocker.SetConstant<IAlbumService>(Mocker.Resolve<AlbumService>());
Mocker.SetConstant<IReleaseService>(Mocker.Resolve<ReleaseService>());
Mocker.SetConstant<ITrackService>(Mocker.Resolve<TrackService>());
Mocker.SetConstant<IImportListExclusionService>(Mocker.Resolve<ImportListExclusionService>());
Mocker.SetConstant<IMediaFileService>(Mocker.Resolve<MediaFileService>());
Mocker.SetConstant<IConfigService>(Mocker.Resolve<IConfigService>());
Mocker.SetConstant<IProvideArtistInfo>(Mocker.Resolve<SkyHookProxy>());
@ -69,6 +76,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
Mocker.GetMock<IAddArtistValidator>().Setup(x => x.Validate(It.IsAny<Artist>())).Returns(new ValidationResult());
Mocker.SetConstant<ITrackGroupingService>(Mocker.Resolve<TrackGroupingService>());
Mocker.SetConstant<ICandidateService>(Mocker.Resolve<CandidateService>());
// set up the augmenters
List<IAggregate<LocalAlbumRelease>> aggregators = new List<IAggregate<LocalAlbumRelease>>
@ -78,7 +86,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
Mocker.SetConstant<IEnumerable<IAggregate<LocalAlbumRelease>>>(aggregators);
Mocker.SetConstant<IAugmentingService>(Mocker.Resolve<AugmentingService>());
_subject = Mocker.Resolve<IdentificationService>();
_Subject = Mocker.Resolve<IdentificationService>();
}
private void GivenMetadataProfile(MetadataProfile profile)
@ -131,9 +139,9 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
Mocker.GetMock<IFingerprintingService>()
.Setup(x => x.Lookup(It.IsAny<List<LocalTrack>>(), It.IsAny<double>()))
.Callback((List<LocalTrack> track, double thres) =>
{
track.ForEach(x => x.AcoustIdResults = fingerprints.SingleOrDefault(f => f.Path == x.Path).AcoustIdResults);
});
{
track.ForEach(x => x.AcoustIdResults = fingerprints.SingleOrDefault(f => f.Path == x.Path).AcoustIdResults);
});
}
public static class IdTestCaseFactory
@ -164,7 +172,6 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
// these are slow to run so only do so manually
[Explicit]
[Test]
[TestCaseSource(typeof(IdTestCaseFactory), "TestCases")]
public void should_match_tracks(string file)
{
@ -173,6 +180,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
var artists = GivenArtists(testcase.LibraryArtists);
var specifiedArtist = artists.SingleOrDefault(x => x.Metadata.Value.ForeignArtistId == testcase.Artist);
var idOverrides = new IdentificationOverrides { Artist = specifiedArtist };
var tracks = testcase.Tracks.Select(x => new LocalTrack
{
@ -185,7 +193,14 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
GivenFingerprints(testcase.Fingerprints);
}
var result = _subject.Identify(tracks, specifiedArtist, null, null, testcase.NewDownload, testcase.SingleRelease, false);
var config = new ImportDecisionMakerConfig
{
NewDownload = testcase.NewDownload,
SingleRelease = testcase.SingleRelease,
IncludeExisting = false
};
var result = _Subject.Identify(tracks, idOverrides, config);
TestLogger.Debug($"Found releases:\n{result.Where(x => x.AlbumRelease != null).Select(x => x.AlbumRelease?.ForeignReleaseId).ToJson()}");

View file

@ -10,7 +10,7 @@ using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
{
[TestFixture]
public class TrackDistanceFixture : CoreTest<IdentificationService>
public class TrackDistanceFixture : CoreTest
{
private Track GivenTrack(string title)
{
@ -53,7 +53,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
var track = GivenTrack("one");
var localTrack = GivenLocalTrack(track);
Subject.TrackDistance(localTrack, track, 1, true).NormalizedDistance().Should().Be(0.0);
DistanceCalculator.TrackDistance(localTrack, track, 1, true).NormalizedDistance().Should().Be(0.0);
}
[Test]
@ -63,7 +63,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
var localTrack = GivenLocalTrack(track);
localTrack.FileTrackInfo.Title = "one (feat. two)";
Subject.TrackDistance(localTrack, track, 1, true).NormalizedDistance().Should().Be(0.0);
DistanceCalculator.TrackDistance(localTrack, track, 1, true).NormalizedDistance().Should().Be(0.0);
}
[Test]
@ -73,7 +73,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
var localTrack = GivenLocalTrack(track);
localTrack.FileTrackInfo.CleanTitle = "foo";
Subject.TrackDistance(localTrack, track, 1, true).NormalizedDistance().Should().NotBe(0.0);
DistanceCalculator.TrackDistance(localTrack, track, 1, true).NormalizedDistance().Should().NotBe(0.0);
}
[Test]
@ -83,7 +83,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
var localTrack = GivenLocalTrack(track);
localTrack.FileTrackInfo.ArtistTitle = "foo";
Subject.TrackDistance(localTrack, track, 1, true).NormalizedDistance().Should().NotBe(0.0);
DistanceCalculator.TrackDistance(localTrack, track, 1, true).NormalizedDistance().Should().NotBe(0.0);
}
[Test]
@ -93,7 +93,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
var localTrack = GivenLocalTrack(track);
localTrack.FileTrackInfo.ArtistTitle = "Various Artists";
Subject.TrackDistance(localTrack, track, 1, true).NormalizedDistance().Should().Be(0.0);
DistanceCalculator.TrackDistance(localTrack, track, 1, true).NormalizedDistance().Should().Be(0.0);
}
}
}

View file

@ -27,9 +27,13 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
private List<IFileInfo> _fileInfos;
private LocalTrack _localTrack;
private Artist _artist;
private Album _album;
private AlbumRelease _albumRelease;
private QualityModel _quality;
private IdentificationOverrides _idOverrides;
private ImportDecisionMakerConfig _idConfig;
private Mock<IImportDecisionEngineSpecification<LocalAlbumRelease>> _albumpass1;
private Mock<IImportDecisionEngineSpecification<LocalAlbumRelease>> _albumpass2;
private Mock<IImportDecisionEngineSpecification<LocalAlbumRelease>> _albumpass3;
@ -82,10 +86,16 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
_fail3.Setup(c => c.IsSatisfiedBy(It.IsAny<LocalTrack>(), It.IsAny<DownloadClientItem>())).Returns(Decision.Reject("_fail3"));
_artist = Builder<Artist>.CreateNew()
.With(e => e.QualityProfile = new QualityProfile { Items = Qualities.QualityFixture.GetDefaultQualities() })
.Build();
.With(e => e.QualityProfileId = 1)
.With(e => e.QualityProfile = new QualityProfile { Items = Qualities.QualityFixture.GetDefaultQualities() })
.Build();
_album = Builder<Album>.CreateNew()
.With(x => x.Artist = _artist)
.Build();
_albumRelease = Builder<AlbumRelease>.CreateNew()
.With(x => x.Album = _album)
.Build();
_quality = new QualityModel(Quality.MP3_256);
@ -98,11 +108,18 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
Path = @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV.avi".AsOsAgnostic()
};
_idOverrides = new IdentificationOverrides
{
Artist = _artist
};
_idConfig = new ImportDecisionMakerConfig();
GivenAudioFiles(new List<string> { @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.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>(), It.IsAny<bool>()))
.Returns((List<LocalTrack> tracks, Artist artist, Album album, AlbumRelease release, bool newDownload, bool singleRelease, bool includeExisting) =>
.Setup(s => s.Identify(It.IsAny<List<LocalTrack>>(), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerConfig>()))
.Returns((List<LocalTrack> tracks, IdentificationOverrides idOverrides, ImportDecisionMakerConfig config) =>
{
var ret = new LocalAlbumRelease(tracks);
ret.AlbumRelease = _albumRelease;
@ -110,8 +127,8 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
});
Mocker.GetMock<IMediaFileService>()
.Setup(c => c.FilterUnchangedFiles(It.IsAny<List<IFileInfo>>(), It.IsAny<Artist>(), It.IsAny<FilterFilesType>()))
.Returns((List<IFileInfo> files, Artist artist, FilterFilesType filter) => files);
.Setup(c => c.FilterUnchangedFiles(It.IsAny<List<IFileInfo>>(), It.IsAny<FilterFilesType>()))
.Returns((List<IFileInfo> files, FilterFilesType filter) => files);
GivenSpecifications(_albumpass1);
}
@ -145,10 +162,12 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
public void should_call_all_album_specifications()
{
var downloadClientItem = Builder<DownloadClientItem>.CreateNew().Build();
var itemInfo = new ImportDecisionMakerInfo { DownloadClientItem = downloadClientItem };
GivenAugmentationSuccess();
GivenSpecifications(_albumpass1, _albumpass2, _albumpass3, _albumfail1, _albumfail2, _albumfail3);
Subject.GetImportDecisions(_fileInfos, new Artist(), null, null, downloadClientItem, null, FilterFilesType.None, false, false, false);
Subject.GetImportDecisions(_fileInfos, null, itemInfo, _idConfig);
_albumfail1.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalAlbumRelease>(), It.IsAny<DownloadClientItem>()), Times.Once());
_albumfail2.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalAlbumRelease>(), It.IsAny<DownloadClientItem>()), Times.Once());
@ -162,10 +181,12 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
public void should_call_all_track_specifications_if_album_accepted()
{
var downloadClientItem = Builder<DownloadClientItem>.CreateNew().Build();
var itemInfo = new ImportDecisionMakerInfo { DownloadClientItem = downloadClientItem };
GivenAugmentationSuccess();
GivenSpecifications(_pass1, _pass2, _pass3, _fail1, _fail2, _fail3);
Subject.GetImportDecisions(_fileInfos, new Artist(), null, null, downloadClientItem, null, FilterFilesType.None, false, false, false);
Subject.GetImportDecisions(_fileInfos, null, itemInfo, _idConfig);
_fail1.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalTrack>(), It.IsAny<DownloadClientItem>()), Times.Once());
_fail2.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalTrack>(), It.IsAny<DownloadClientItem>()), Times.Once());
@ -179,11 +200,13 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
public void should_call_no_track_specifications_if_album_rejected()
{
var downloadClientItem = Builder<DownloadClientItem>.CreateNew().Build();
var itemInfo = new ImportDecisionMakerInfo { DownloadClientItem = downloadClientItem };
GivenAugmentationSuccess();
GivenSpecifications(_albumpass1, _albumpass2, _albumpass3, _albumfail1, _albumfail2, _albumfail3);
GivenSpecifications(_pass1, _pass2, _pass3, _fail1, _fail2, _fail3);
Subject.GetImportDecisions(_fileInfos, new Artist(), null, null, downloadClientItem, null, FilterFilesType.None, false, false, false);
Subject.GetImportDecisions(_fileInfos, null, itemInfo, _idConfig);
_fail1.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalTrack>(), It.IsAny<DownloadClientItem>()), Times.Never());
_fail2.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalTrack>(), It.IsAny<DownloadClientItem>()), Times.Never());
@ -199,7 +222,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
GivenSpecifications(_albumfail1);
GivenSpecifications(_pass1);
var result = Subject.GetImportDecisions(_fileInfos, new Artist(), FilterFilesType.None, false);
var result = Subject.GetImportDecisions(_fileInfos, null, null, _idConfig);
result.Single().Approved.Should().BeFalse();
}
@ -210,7 +233,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
GivenSpecifications(_albumpass1);
GivenSpecifications(_fail1);
var result = Subject.GetImportDecisions(_fileInfos, new Artist(), FilterFilesType.None, false);
var result = Subject.GetImportDecisions(_fileInfos, null, null, _idConfig);
result.Single().Approved.Should().BeFalse();
}
@ -221,7 +244,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
GivenSpecifications(_albumpass1, _albumfail1, _albumpass2, _albumpass3);
GivenSpecifications(_pass1, _pass2, _pass3);
var result = Subject.GetImportDecisions(_fileInfos, new Artist(), FilterFilesType.None, false);
var result = Subject.GetImportDecisions(_fileInfos, null, null, _idConfig);
result.Single().Approved.Should().BeFalse();
}
@ -232,7 +255,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
GivenSpecifications(_albumpass1, _albumpass2, _albumpass3);
GivenSpecifications(_pass1, _fail1, _pass2, _pass3);
var result = Subject.GetImportDecisions(_fileInfos, new Artist(), FilterFilesType.None, false);
var result = Subject.GetImportDecisions(_fileInfos, null, null, _idConfig);
result.Single().Approved.Should().BeFalse();
}
@ -244,7 +267,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
GivenSpecifications(_albumpass1, _albumpass2, _albumpass3);
GivenSpecifications(_pass1, _pass2, _pass3);
var result = Subject.GetImportDecisions(_fileInfos, new Artist(), FilterFilesType.None, false);
var result = Subject.GetImportDecisions(_fileInfos, null, null, _idConfig);
result.Single().Approved.Should().BeTrue();
}
@ -255,7 +278,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
GivenAugmentationSuccess();
GivenSpecifications(_pass1, _pass2, _pass3, _fail1, _fail2, _fail3);
var result = Subject.GetImportDecisions(_fileInfos, new Artist(), FilterFilesType.None, false);
var result = Subject.GetImportDecisions(_fileInfos, null, null, _idConfig);
result.Single().Rejections.Should().HaveCount(3);
}
@ -275,7 +298,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
@"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV".AsOsAgnostic()
});
Subject.GetImportDecisions(_fileInfos, _artist, FilterFilesType.None, false);
var decisions = Subject.GetImportDecisions(_fileInfos, _idOverrides, null, _idConfig);
Mocker.GetMock<IAugmentingService>()
.Verify(c => c.Augment(It.IsAny<LocalTrack>(), It.IsAny<bool>()), Times.Exactly(_fileInfos.Count));
@ -296,13 +319,13 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
});
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>(), It.IsAny<bool>()))
.Returns((List<LocalTrack> tracks, Artist artist, Album album, AlbumRelease release, bool newDownload, bool singleRelease, bool includeExisting) =>
{
return new List<LocalAlbumRelease> { new LocalAlbumRelease(tracks) };
});
.Setup(s => s.Identify(It.IsAny<List<LocalTrack>>(), It.IsAny<IdentificationOverrides>(), It.IsAny<ImportDecisionMakerConfig>()))
.Returns((List<LocalTrack> tracks, IdentificationOverrides idOverrides, ImportDecisionMakerConfig config) =>
{
return new List<LocalAlbumRelease> { new LocalAlbumRelease(tracks) };
});
var decisions = Subject.GetImportDecisions(_fileInfos, _artist, FilterFilesType.None, false);
var decisions = Subject.GetImportDecisions(_fileInfos, _idOverrides, null, _idConfig);
Mocker.GetMock<IAugmentingService>()
.Verify(c => c.Augment(It.IsAny<LocalTrack>(), It.IsAny<bool>()), Times.Exactly(_fileInfos.Count));
@ -323,7 +346,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
@"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV".AsOsAgnostic()
});
var decisions = Subject.GetImportDecisions(_fileInfos, _artist, FilterFilesType.None, false);
var decisions = Subject.GetImportDecisions(_fileInfos, _idOverrides, null, _idConfig);
Mocker.GetMock<IAugmentingService>()
.Verify(c => c.Augment(It.IsAny<LocalTrack>(), It.IsAny<bool>()), Times.Exactly(_fileInfos.Count));
@ -344,7 +367,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
@"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV".AsOsAgnostic()
});
Subject.GetImportDecisions(_fileInfos, _artist, FilterFilesType.None, false).Should().HaveCount(1);
Subject.GetImportDecisions(_fileInfos, _idOverrides, null, _idConfig).Should().HaveCount(1);
ExceptionVerification.ExpectedErrors(1);
}

View file

@ -127,5 +127,91 @@ namespace NzbDrone.Core.Test.MusicTests
ExceptionVerification.ExpectedErrors(1);
}
[Test]
public void should_disambiguate_if_artist_folder_exists()
{
var newArtist = new Artist
{
ForeignArtistId = "ce09ea31-3d4a-4487-a797-e315175457a0",
Path = @"C:\Test\Music\Name1",
};
_fakeArtist.Metadata = Builder<ArtistMetadata>.CreateNew().With(x => x.Disambiguation = "Disambiguation").Build();
GivenValidArtist(newArtist.ForeignArtistId);
GivenValidPath();
Mocker.GetMock<IArtistService>()
.Setup(x => x.ArtistPathExists(newArtist.Path))
.Returns(true);
var artist = Subject.AddArtist(newArtist);
artist.Path.Should().Be(newArtist.Path + " (Disambiguation)");
}
[Test]
public void should_disambiguate_with_numbers_if_artist_folder_still_exists()
{
var newArtist = new Artist
{
ForeignArtistId = "ce09ea31-3d4a-4487-a797-e315175457a0",
Path = @"C:\Test\Music\Name1",
};
_fakeArtist.Metadata = Builder<ArtistMetadata>.CreateNew().With(x => x.Disambiguation = "Disambiguation").Build();
GivenValidArtist(newArtist.ForeignArtistId);
GivenValidPath();
Mocker.GetMock<IArtistService>()
.Setup(x => x.ArtistPathExists(newArtist.Path))
.Returns(true);
Mocker.GetMock<IArtistService>()
.Setup(x => x.ArtistPathExists(newArtist.Path + " (Disambiguation)"))
.Returns(true);
Mocker.GetMock<IArtistService>()
.Setup(x => x.ArtistPathExists(newArtist.Path + " (Disambiguation) (1)"))
.Returns(true);
Mocker.GetMock<IArtistService>()
.Setup(x => x.ArtistPathExists(newArtist.Path + " (Disambiguation) (2)"))
.Returns(true);
var artist = Subject.AddArtist(newArtist);
artist.Path.Should().Be(newArtist.Path + " (Disambiguation) (3)");
}
[Test]
public void should_disambiguate_with_numbers_if_artist_folder_exists_and_no_disambiguation()
{
var newArtist = new Artist
{
ForeignArtistId = "ce09ea31-3d4a-4487-a797-e315175457a0",
Path = @"C:\Test\Music\Name1",
};
_fakeArtist.Metadata = Builder<ArtistMetadata>.CreateNew().With(x => x.Disambiguation = string.Empty).Build();
GivenValidArtist(newArtist.ForeignArtistId);
GivenValidPath();
Mocker.GetMock<IArtistService>()
.Setup(x => x.ArtistPathExists(newArtist.Path))
.Returns(true);
Mocker.GetMock<IArtistService>()
.Setup(x => x.ArtistPathExists(newArtist.Path + " (1)"))
.Returns(true);
Mocker.GetMock<IArtistService>()
.Setup(x => x.ArtistPathExists(newArtist.Path + " (2)"))
.Returns(true);
var artist = Subject.AddArtist(newArtist);
artist.Path.Should().Be(newArtist.Path + " (3)");
}
}
}

View file

@ -11,6 +11,7 @@ using NzbDrone.Core.MetadataSource;
using NzbDrone.Core.Music;
using NzbDrone.Core.Music.Commands;
using NzbDrone.Core.Music.Events;
using NzbDrone.Core.RootFolders;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common;
@ -48,8 +49,8 @@ namespace NzbDrone.Core.Test.MusicTests
.Build();
Mocker.GetMock<IArtistService>(MockBehavior.Strict)
.Setup(s => s.GetArtist(_artist.Id))
.Returns(_artist);
.Setup(s => s.GetArtists(new List<int> { _artist.Id }))
.Returns(new List<Artist> { _artist });
Mocker.GetMock<IAlbumService>(MockBehavior.Strict)
.Setup(s => s.InsertMany(It.IsAny<List<Album>>()));
@ -69,6 +70,10 @@ namespace NzbDrone.Core.Test.MusicTests
Mocker.GetMock<IImportListExclusionService>()
.Setup(x => x.FindByForeignId(It.IsAny<List<string>>()))
.Returns(new List<ImportListExclusion>());
Mocker.GetMock<IRootFolderService>()
.Setup(x => x.All())
.Returns(new List<RootFolder>());
}
private void GivenNewArtistInfo(Artist artist)

View file

@ -8,6 +8,7 @@ using NzbDrone.Core.ImportLists;
using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Music;
using NzbDrone.Core.Profiles.Metadata;
using NzbDrone.Core.RootFolders;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Profiles.Metadata
@ -122,8 +123,14 @@ namespace NzbDrone.Core.Test.Profiles.Metadata
.With(c => c.MetadataProfileId = 1)
.Build().ToList();
var rootFolders = Builder<RootFolder>.CreateListOfSize(2)
.All()
.With(f => f.DefaultMetadataProfileId = 1)
.BuildList();
Mocker.GetMock<IArtistService>().Setup(c => c.GetAllArtists()).Returns(artistList);
Mocker.GetMock<IImportListFactory>().Setup(c => c.All()).Returns(importLists);
Mocker.GetMock<IRootFolderService>().Setup(c => c.All()).Returns(rootFolders);
Mocker.GetMock<IMetadataProfileRepository>().Setup(c => c.Get(profile.Id)).Returns(profile);
Assert.Throws<MetadataProfileInUseException>(() => Subject.Delete(profile.Id));
@ -148,8 +155,14 @@ namespace NzbDrone.Core.Test.Profiles.Metadata
.With(c => c.MetadataProfileId = profile.Id)
.Build().ToList();
var rootFolders = Builder<RootFolder>.CreateListOfSize(2)
.All()
.With(f => f.DefaultMetadataProfileId = 1)
.BuildList();
Mocker.GetMock<IArtistService>().Setup(c => c.GetAllArtists()).Returns(artistList);
Mocker.GetMock<IImportListFactory>().Setup(c => c.All()).Returns(importLists);
Mocker.GetMock<IRootFolderService>().Setup(c => c.All()).Returns(rootFolders);
Mocker.GetMock<IMetadataProfileRepository>().Setup(c => c.Get(profile.Id)).Returns(profile);
Assert.Throws<MetadataProfileInUseException>(() => Subject.Delete(profile.Id));
@ -158,7 +171,39 @@ namespace NzbDrone.Core.Test.Profiles.Metadata
}
[Test]
public void should_delete_profile_if_not_assigned_to_artist_or_import_list()
public void should_not_be_able_to_delete_profile_if_assigned_to_root_folder()
{
var profile = Builder<MetadataProfile>.CreateNew()
.With(p => p.Id = 2)
.Build();
var artistList = Builder<Artist>.CreateListOfSize(3)
.All()
.With(c => c.MetadataProfileId = 1)
.Build().ToList();
var importLists = Builder<ImportListDefinition>.CreateListOfSize(2)
.All()
.With(c => c.MetadataProfileId = 1)
.Build().ToList();
var rootFolders = Builder<RootFolder>.CreateListOfSize(2)
.Random(1)
.With(f => f.DefaultMetadataProfileId = profile.Id)
.BuildList();
Mocker.GetMock<IArtistService>().Setup(c => c.GetAllArtists()).Returns(artistList);
Mocker.GetMock<IImportListFactory>().Setup(c => c.All()).Returns(importLists);
Mocker.GetMock<IRootFolderService>().Setup(c => c.All()).Returns(rootFolders);
Mocker.GetMock<IMetadataProfileRepository>().Setup(c => c.Get(profile.Id)).Returns(profile);
Assert.Throws<MetadataProfileInUseException>(() => Subject.Delete(profile.Id));
Mocker.GetMock<IMetadataProfileRepository>().Verify(c => c.Delete(It.IsAny<int>()), Times.Never());
}
[Test]
public void should_delete_profile_if_not_assigned_to_artist_import_list_or_root_folder()
{
var profile = Builder<MetadataProfile>.CreateNew()
.With(p => p.Id = 1)
@ -174,8 +219,14 @@ namespace NzbDrone.Core.Test.Profiles.Metadata
.With(c => c.MetadataProfileId = 2)
.Build().ToList();
var rootFolders = Builder<RootFolder>.CreateListOfSize(2)
.All()
.With(f => f.DefaultMetadataProfileId = 2)
.BuildList();
Mocker.GetMock<IArtistService>().Setup(c => c.GetAllArtists()).Returns(artistList);
Mocker.GetMock<IImportListFactory>().Setup(c => c.All()).Returns(importLists);
Mocker.GetMock<IRootFolderService>().Setup(c => c.All()).Returns(rootFolders);
Mocker.GetMock<IMetadataProfileRepository>().Setup(c => c.Get(profile.Id)).Returns(profile);
Subject.Delete(1);

View file

@ -6,6 +6,7 @@ using NzbDrone.Core.ImportLists;
using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Music;
using NzbDrone.Core.Profiles.Qualities;
using NzbDrone.Core.RootFolders;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Profiles
@ -56,8 +57,14 @@ namespace NzbDrone.Core.Test.Profiles
.With(c => c.ProfileId = 1)
.Build().ToList();
var rootFolders = Builder<RootFolder>.CreateListOfSize(2)
.All()
.With(f => f.DefaultQualityProfileId = 1)
.BuildList();
Mocker.GetMock<IArtistService>().Setup(c => c.GetAllArtists()).Returns(artistList);
Mocker.GetMock<IImportListFactory>().Setup(c => c.All()).Returns(importLists);
Mocker.GetMock<IRootFolderService>().Setup(c => c.All()).Returns(rootFolders);
Mocker.GetMock<IProfileRepository>().Setup(c => c.Get(profile.Id)).Returns(profile);
Assert.Throws<QualityProfileInUseException>(() => Subject.Delete(profile.Id));
@ -82,8 +89,14 @@ namespace NzbDrone.Core.Test.Profiles
.With(c => c.ProfileId = profile.Id)
.Build().ToList();
var rootFolders = Builder<RootFolder>.CreateListOfSize(2)
.All()
.With(f => f.DefaultQualityProfileId = 1)
.BuildList();
Mocker.GetMock<IArtistService>().Setup(c => c.GetAllArtists()).Returns(artistList);
Mocker.GetMock<IImportListFactory>().Setup(c => c.All()).Returns(importLists);
Mocker.GetMock<IRootFolderService>().Setup(c => c.All()).Returns(rootFolders);
Mocker.GetMock<IProfileRepository>().Setup(c => c.Get(profile.Id)).Returns(profile);
Assert.Throws<QualityProfileInUseException>(() => Subject.Delete(profile.Id));
@ -92,7 +105,39 @@ namespace NzbDrone.Core.Test.Profiles
}
[Test]
public void should_delete_profile_if_not_assigned_to_artist_or_import_list()
public void should_not_be_able_to_delete_profile_if_assigned_to_root_folder()
{
var profile = Builder<QualityProfile>.CreateNew()
.With(p => p.Id = 2)
.Build();
var artistList = Builder<Artist>.CreateListOfSize(3)
.All()
.With(c => c.QualityProfileId = 1)
.Build().ToList();
var importLists = Builder<ImportListDefinition>.CreateListOfSize(2)
.All()
.With(c => c.ProfileId = 1)
.Build().ToList();
var rootFolders = Builder<RootFolder>.CreateListOfSize(2)
.Random(1)
.With(f => f.DefaultQualityProfileId = profile.Id)
.BuildList();
Mocker.GetMock<IArtistService>().Setup(c => c.GetAllArtists()).Returns(artistList);
Mocker.GetMock<IImportListFactory>().Setup(c => c.All()).Returns(importLists);
Mocker.GetMock<IRootFolderService>().Setup(c => c.All()).Returns(rootFolders);
Mocker.GetMock<IProfileRepository>().Setup(c => c.Get(profile.Id)).Returns(profile);
Assert.Throws<QualityProfileInUseException>(() => Subject.Delete(profile.Id));
Mocker.GetMock<IProfileRepository>().Verify(c => c.Delete(It.IsAny<int>()), Times.Never());
}
[Test]
public void should_delete_profile_if_not_assigned_to_artist_import_list_or_root_folder()
{
var artistList = Builder<Artist>.CreateListOfSize(3)
.All()
@ -104,8 +149,14 @@ namespace NzbDrone.Core.Test.Profiles
.With(c => c.ProfileId = 2)
.Build().ToList();
var rootFolders = Builder<RootFolder>.CreateListOfSize(2)
.All()
.With(f => f.DefaultQualityProfileId = 2)
.BuildList();
Mocker.GetMock<IArtistService>().Setup(c => c.GetAllArtists()).Returns(artistList);
Mocker.GetMock<IImportListFactory>().Setup(c => c.All()).Returns(importLists);
Mocker.GetMock<IRootFolderService>().Setup(c => c.All()).Returns(rootFolders);
Subject.Delete(1);

View file

@ -93,49 +93,5 @@ namespace NzbDrone.Core.Test.RootFolderTests
Assert.Throws<UnauthorizedAccessException>(() => Subject.Add(new RootFolder { Path = @"C:\Music".AsOsAgnostic() }));
}
[TestCase("$recycle.bin")]
[TestCase("system volume information")]
[TestCase("recycler")]
[TestCase("lost+found")]
[TestCase(".appledb")]
[TestCase(".appledesktop")]
[TestCase(".appledouble")]
[TestCase("@eadir")]
[TestCase(".grab")]
public void should_get_root_folder_with_subfolders_excluding_special_sub_folders(string subFolder)
{
var rootFolderPath = @"C:\Test\Music".AsOsAgnostic();
var rootFolder = Builder<RootFolder>.CreateNew()
.With(r => r.Path = rootFolderPath)
.Build();
var subFolders = new[]
{
"Artist1",
"Artist2",
"Artist3",
subFolder
};
var folders = subFolders.Select(f => Path.Combine(rootFolderPath, f)).ToArray();
Mocker.GetMock<IRootFolderRepository>()
.Setup(s => s.Get(It.IsAny<int>()))
.Returns(rootFolder);
Mocker.GetMock<IArtistService>()
.Setup(s => s.GetAllArtists())
.Returns(new List<Artist>());
Mocker.GetMock<IDiskProvider>()
.Setup(s => s.GetDirectories(rootFolder.Path))
.Returns(folders);
var unmappedFolders = Subject.Get(rootFolder.Id).UnmappedFolders;
unmappedFolders.Count.Should().BeGreaterThan(0);
unmappedFolders.Should().NotContain(u => u.Name == subFolder);
}
}
}

View file

@ -0,0 +1,81 @@
using System.Data;
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(39)]
public class add_root_folder_add_defaults : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("RootFolders").AddColumn("Name").AsString().Nullable();
Alter.Table("RootFolders").AddColumn("DefaultMetadataProfileId").AsInt32().WithDefaultValue(0);
Alter.Table("RootFolders").AddColumn("DefaultQualityProfileId").AsInt32().WithDefaultValue(0);
Alter.Table("RootFolders").AddColumn("DefaultMonitorOption").AsInt32().WithDefaultValue(0);
Alter.Table("RootFolders").AddColumn("DefaultTags").AsString().Nullable();
Execute.WithConnection(SetDefaultOptions);
}
private void SetDefaultOptions(IDbConnection conn, IDbTransaction tran)
{
int metadataId = GetMinProfileId(conn, tran, "MetadataProfiles");
int qualityId = GetMinProfileId(conn, tran, "QualityProfiles");
if (metadataId == 0 || qualityId == 0)
{
return;
}
using (var cmd = conn.CreateCommand())
{
cmd.Transaction = tran;
cmd.CommandText = $"SELECT Id, Path FROM RootFolders";
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
var rootFolderId = reader.GetInt32(0);
var path = reader.GetString(1);
using (var updateCmd = conn.CreateCommand())
{
updateCmd.Transaction = tran;
updateCmd.CommandText = "UPDATE RootFolders SET Name = ?, DefaultMetadataProfileId = ?, DefaultQualityProfileId = ?, DefaultTags = ? WHERE Id = ?";
updateCmd.AddParameter(path);
updateCmd.AddParameter(metadataId);
updateCmd.AddParameter(qualityId);
updateCmd.AddParameter("[]");
updateCmd.AddParameter(rootFolderId);
updateCmd.ExecuteNonQuery();
}
}
}
}
}
private int GetMinProfileId(IDbConnection conn, IDbTransaction tran, string table)
{
using (var cmd = conn.CreateCommand())
{
cmd.Transaction = tran;
// A plain min(id) will return an empty row if table is empty which is a pain to deal with
cmd.CommandText = $"SELECT COALESCE(MIN(Id), 0) FROM {table}";
using (var reader = cmd.ExecuteReader())
{
if (reader.Read())
{
return reader.GetInt32(0);
}
return 0;
}
}
}
}
}

View file

@ -5,7 +5,7 @@ using System.Linq;
using System.Text.RegularExpressions;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Music;
using NzbDrone.Core.RootFolders;
namespace NzbDrone.Core.DiskSpace
{
@ -16,22 +16,24 @@ namespace NzbDrone.Core.DiskSpace
public class DiskSpaceService : IDiskSpaceService
{
private readonly IArtistService _artistService;
private readonly IDiskProvider _diskProvider;
private readonly IRootFolderService _rootFolderService;
private readonly Logger _logger;
private static readonly Regex _regexSpecialDrive = new Regex("^/var/lib/(docker|rancher|kubelet)(/|$)|^/(boot|etc)(/|$)|/docker(/var)?/aufs(/|$)", RegexOptions.Compiled);
public DiskSpaceService(IArtistService artistService, IDiskProvider diskProvider, Logger logger)
public DiskSpaceService(IDiskProvider diskProvider,
IRootFolderService rootFolderService,
Logger logger)
{
_artistService = artistService;
_diskProvider = diskProvider;
_rootFolderService = rootFolderService;
_logger = logger;
}
public List<DiskSpace> GetFreeSpace()
{
var importantRootFolders = GetArtistRootPaths().Distinct().ToList();
var importantRootFolders = _rootFolderService.All().Select(x => x.Path).ToList();
var optionalRootFolders = GetFixedDisksRootPaths().Except(importantRootFolders).Distinct().ToList();
@ -40,14 +42,6 @@ namespace NzbDrone.Core.DiskSpace
return diskSpace;
}
private IEnumerable<string> GetArtistRootPaths()
{
return _artistService.GetAllArtists()
.Where(s => _diskProvider.FolderExists(s.Path))
.Select(s => _diskProvider.GetPathRoot(s.Path))
.Distinct();
}
private IEnumerable<string> GetFixedDisksRootPaths()
{
return _diskProvider.GetMounts()

View file

@ -313,7 +313,6 @@ namespace NzbDrone.Core.History
public void Handle(TrackFileRenamedEvent message)
{
var sourcePath = message.OriginalPath;
var sourceRelativePath = message.Artist.Path.GetRelativePath(message.OriginalPath);
var path = message.TrackFile.Path;
foreach (var track in message.TrackFile.Tracks.Value)
@ -330,7 +329,6 @@ namespace NzbDrone.Core.History
};
history.Data.Add("SourcePath", sourcePath);
history.Data.Add("SourceRelativePath", sourceRelativePath);
history.Data.Add("Path", path);
_historyRepository.Insert(history);

View file

@ -26,6 +26,12 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
{
Ensure.That(title, () => title).IsNotNullOrWhiteSpace();
// Most VA albums are listed as VA, not Various Artists
if (title == "Various Artists")
{
title = "VA";
}
var cleanTitle = BeginningThe.Replace(title, string.Empty);
cleanTitle = cleanTitle.Replace(" & ", " ");

View file

@ -355,7 +355,7 @@ namespace NzbDrone.Core.MediaFiles
AlbumId = file.Album.Value.Id,
TrackNumbers = file.Tracks.Value.Select(e => e.AbsoluteTrackNumber).ToList(),
TrackFileId = file.Id,
RelativePath = file.Artist.Value.Path.GetRelativePath(file.Path),
Path = file.Path,
Changes = diff
};
}

View file

@ -1,23 +0,0 @@
using NzbDrone.Core.Messaging.Commands;
namespace NzbDrone.Core.MediaFiles.Commands
{
public class RescanArtistCommand : Command
{
public int? ArtistId { get; set; }
public FilterFilesType Filter { get; set; }
public override bool SendUpdatesToClient => true;
public RescanArtistCommand(FilterFilesType filter = FilterFilesType.Known)
{
Filter = filter;
}
public RescanArtistCommand(int artistId, FilterFilesType filter = FilterFilesType.Known)
{
ArtistId = artistId;
Filter = filter;
}
}
}

View file

@ -0,0 +1,26 @@
using System.Collections.Generic;
using NzbDrone.Core.Messaging.Commands;
namespace NzbDrone.Core.MediaFiles.Commands
{
public class RescanFoldersCommand : Command
{
public RescanFoldersCommand()
{
}
public RescanFoldersCommand(List<string> folders, FilterFilesType filter, List<int> artistIds)
{
Folders = folders;
Filter = filter;
ArtistIds = artistIds;
}
public List<string> Folders { get; set; }
public FilterFilesType Filter { get; set; }
public List<int> ArtistIds { get; set; }
public override bool SendUpdatesToClient => true;
public override bool RequiresDiskAccess => true;
}
}

View file

@ -10,20 +10,20 @@ using NzbDrone.Common;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles.Commands;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.MediaFiles.TrackImport;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Music;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RootFolders;
namespace NzbDrone.Core.MediaFiles
{
public interface IDiskScanService
{
void Scan(Artist artist, FilterFilesType filter = FilterFilesType.Known);
void Scan(List<string> folders = null, FilterFilesType filter = FilterFilesType.Known, List<int> artistIds = null);
IFileInfo[] GetAudioFiles(string path, bool allDirectories = true);
string[] GetNonAudioFiles(string path, bool allDirectories = true);
List<IFileInfo> FilterFiles(string basePath, IEnumerable<IFileInfo> files);
@ -32,13 +32,12 @@ namespace NzbDrone.Core.MediaFiles
public class DiskScanService :
IDiskScanService,
IExecute<RescanArtistCommand>
IExecute<RescanFoldersCommand>
{
private readonly IDiskProvider _diskProvider;
private readonly IMediaFileService _mediaFileService;
private readonly IMakeImportDecision _importDecisionMaker;
private readonly IImportApprovedTracks _importApprovedTracks;
private readonly IConfigService _configService;
private readonly IArtistService _artistService;
private readonly IMediaFileTableCleanupService _mediaFileTableCleanupService;
private readonly IRootFolderService _rootFolderService;
@ -49,7 +48,6 @@ namespace NzbDrone.Core.MediaFiles
IMediaFileService mediaFileService,
IMakeImportDecision importDecisionMaker,
IImportApprovedTracks importApprovedTracks,
IConfigService configService,
IArtistService artistService,
IRootFolderService rootFolderService,
IMediaFileTableCleanupService mediaFileTableCleanupService,
@ -60,7 +58,6 @@ namespace NzbDrone.Core.MediaFiles
_mediaFileService = mediaFileService;
_importDecisionMaker = importDecisionMaker;
_importApprovedTracks = importApprovedTracks;
_configService = configService;
_artistService = artistService;
_mediaFileTableCleanupService = mediaFileTableCleanupService;
_rootFolderService = rootFolderService;
@ -71,56 +68,90 @@ namespace NzbDrone.Core.MediaFiles
private static readonly Regex ExcludedSubFoldersRegex = new Regex(@"(?:\\|\/|^)(?:extras|@eadir|extrafanart|plex versions|\.[^\\/]+)(?:\\|\/)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex ExcludedFilesRegex = new Regex(@"^\._|^Thumbs\.db$|^\.DS_store$|\.partial~$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
public void Scan(Artist artist, FilterFilesType filter = FilterFilesType.Known)
public void Scan(List<string> folders = null, FilterFilesType filter = FilterFilesType.Known, List<int> artistIds = null)
{
var rootFolder = _rootFolderService.GetBestRootFolderPath(artist.Path);
if (!_diskProvider.FolderExists(rootFolder))
if (folders == null)
{
_logger.Warn("Artist' root folder ({0}) doesn't exist.", rootFolder);
_eventAggregator.PublishEvent(new ArtistScanSkippedEvent(artist, ArtistScanSkippedReason.RootFolderDoesNotExist));
return;
folders = _rootFolderService.All().Select(x => x.Path).ToList();
}
if (_diskProvider.GetDirectories(rootFolder).Empty())
if (artistIds == null)
{
_logger.Warn("Artist' root folder ({0}) is empty.", rootFolder);
_eventAggregator.PublishEvent(new ArtistScanSkippedEvent(artist, ArtistScanSkippedReason.RootFolderIsEmpty));
return;
artistIds = new List<int>();
}
_logger.ProgressInfo("Scanning {0}", artist.Name);
if (!_diskProvider.FolderExists(artist.Path))
{
if (_configService.CreateEmptyArtistFolders)
{
_logger.Debug("Creating missing artist folder: {0}", artist.Path);
_diskProvider.CreateFolder(artist.Path);
SetPermissions(artist.Path);
}
else
{
_logger.Debug("Artist folder doesn't exist: {0}", artist.Path);
}
CleanMediaFiles(artist, new List<string>());
CompletedScanning(artist);
return;
}
var mediaFileList = new List<IFileInfo>();
var musicFilesStopwatch = Stopwatch.StartNew();
var mediaFileList = FilterFiles(artist.Path, GetAudioFiles(artist.Path)).ToList();
musicFilesStopwatch.Stop();
_logger.Trace("Finished getting track files for: {0} [{1}]", artist, musicFilesStopwatch.Elapsed);
CleanMediaFiles(artist, mediaFileList.Select(x => x.FullName).ToList());
foreach (var folder in folders)
{
// We could be scanning a root folder or a subset of a root folder. If it's a subset,
// check if the root folder exists before cleaning.
var rootFolder = _rootFolderService.GetBestRootFolder(folder);
if (rootFolder == null)
{
_logger.Error("Not scanning {0}, it's not a subdirectory of a defined root folder", folder);
return;
}
if (!_diskProvider.FolderExists(rootFolder.Path))
{
_logger.Warn("Root folder {0} doesn't exist.", rootFolder.Path);
var skippedArtists = _artistService.GetArtists(artistIds);
skippedArtists.ForEach(x => _eventAggregator.PublishEvent(new ArtistScanSkippedEvent(x, ArtistScanSkippedReason.RootFolderDoesNotExist)));
return;
}
if (_diskProvider.GetDirectories(rootFolder.Path).Empty())
{
_logger.Warn("Root folder {0} is empty.", rootFolder.Path);
var skippedArtists = _artistService.GetArtists(artistIds);
skippedArtists.ForEach(x => _eventAggregator.PublishEvent(new ArtistScanSkippedEvent(x, ArtistScanSkippedReason.RootFolderIsEmpty)));
return;
}
if (!_diskProvider.FolderExists(folder))
{
_logger.Debug("Specified scan folder ({0}) doesn't exist.", folder);
CleanMediaFiles(folder, new List<string>());
continue;
}
_logger.ProgressInfo("Scanning {0}", folder);
var files = FilterFiles(folder, GetAudioFiles(folder));
if (!files.Any())
{
_logger.Warn("Scan folder {0} is empty.", folder);
continue;
}
CleanMediaFiles(folder, files.Select(x => x.FullName).ToList());
mediaFileList.AddRange(files);
}
musicFilesStopwatch.Stop();
_logger.Trace("Finished getting track files for:\n{0} [{1}]", folders.ConcatToString("\n"), musicFilesStopwatch.Elapsed);
var decisionsStopwatch = Stopwatch.StartNew();
var decisions = _importDecisionMaker.GetImportDecisions(mediaFileList, artist, filter, true);
var config = new ImportDecisionMakerConfig
{
Filter = filter,
IncludeExisting = true,
AddNewArtists = true
};
var decisions = _importDecisionMaker.GetImportDecisions(mediaFileList, null, null, config);
decisionsStopwatch.Stop();
_logger.Debug("Import decisions complete for: {0} [{1}]", artist, decisionsStopwatch.Elapsed);
_logger.Debug("Import decisions complete [{0}]", decisionsStopwatch.Elapsed);
var importStopwatch = Stopwatch.StartNew();
_importApprovedTracks.Import(decisions, false);
@ -128,7 +159,8 @@ namespace NzbDrone.Core.MediaFiles
// decisions may have been filtered to just new files. Anything new and approved will have been inserted.
// Now we need to make sure anything new but not approved gets inserted
// Note that knownFiles will include anything imported just now
var knownFiles = _mediaFileService.GetFilesWithBasePath(artist.Path);
var knownFiles = new List<TrackFile>();
folders.ForEach(x => knownFiles.AddRange(_mediaFileService.GetFilesWithBasePath(x)));
var newFiles = decisions
.ExceptBy(x => x.Item.Path, knownFiles, x => x.Path, PathEqualityComparer.Instance)
@ -173,17 +205,20 @@ namespace NzbDrone.Core.MediaFiles
_logger.Debug($"Updated info for {updatedFiles.Count} known files");
RemoveEmptyArtistFolder(artist.Path);
var artists = _artistService.GetArtists(artistIds);
foreach (var artist in artists)
{
CompletedScanning(artist);
}
CompletedScanning(artist);
importStopwatch.Stop();
_logger.Debug("Track import complete for: {0} [{1}]", artist, importStopwatch.Elapsed);
_logger.Debug("Track import complete for:\n{0} [{1}]", folders.ConcatToString("\n"), importStopwatch.Elapsed);
}
private void CleanMediaFiles(Artist artist, List<string> mediaFileList)
private void CleanMediaFiles(string folder, List<string> mediaFileList)
{
_logger.Debug("{0} Cleaning up media files in DB", artist);
_mediaFileTableCleanupService.Clean(artist, mediaFileList);
_logger.Debug($"Cleaning up media files in DB [{folder}]");
_mediaFileTableCleanupService.Clean(folder, mediaFileList);
}
private void CompletedScanning(Artist artist)
@ -238,56 +273,9 @@ namespace NzbDrone.Core.MediaFiles
.ToList();
}
private void SetPermissions(string path)
public void Execute(RescanFoldersCommand message)
{
if (!_configService.SetPermissionsLinux)
{
return;
}
try
{
var permissions = _configService.FolderChmod;
_diskProvider.SetPermissions(path, permissions, _configService.ChownUser, _configService.ChownGroup);
}
catch (Exception ex)
{
_logger.Warn(ex, "Unable to apply permissions to: " + path);
_logger.Debug(ex, ex.Message);
}
}
private void RemoveEmptyArtistFolder(string path)
{
if (_configService.DeleteEmptyFolders)
{
if (_diskProvider.GetFiles(path, SearchOption.AllDirectories).Empty())
{
_diskProvider.DeleteFolder(path, true);
}
else
{
_diskProvider.RemoveEmptySubfolders(path);
}
}
}
public void Execute(RescanArtistCommand message)
{
if (message.ArtistId.HasValue)
{
var artist = _artistService.GetArtist(message.ArtistId.Value);
Scan(artist, message.Filter);
}
else
{
var allArtists = _artistService.GetAllArtists();
foreach (var artist in allArtists)
{
Scan(artist, message.Filter);
}
}
Scan(message.Folders, message.Filter, message.ArtistIds);
}
}
}

View file

@ -200,7 +200,25 @@ namespace NzbDrone.Core.MediaFiles
}
}
var decisions = _importDecisionMaker.GetImportDecisions(audioFiles, artist, downloadClientItem, trackInfo);
var idOverrides = new IdentificationOverrides
{
Artist = artist
};
var idInfo = new ImportDecisionMakerInfo
{
DownloadClientItem = downloadClientItem,
ParsedTrackInfo = trackInfo
};
var idConfig = new ImportDecisionMakerConfig
{
Filter = FilterFilesType.None,
NewDownload = true,
SingleRelease = false,
IncludeExisting = false,
AddNewArtists = false
};
var decisions = _importDecisionMaker.GetImportDecisions(audioFiles, idOverrides, idInfo, idConfig);
var importResults = _importApprovedTracks.Import(decisions, true, downloadClientItem, importMode);
if (importMode == ImportMode.Auto)
@ -259,7 +277,24 @@ namespace NzbDrone.Core.MediaFiles
}
}
var decisions = _importDecisionMaker.GetImportDecisions(new List<IFileInfo>() { fileInfo }, artist, downloadClientItem, null);
var idOverrides = new IdentificationOverrides
{
Artist = artist
};
var idInfo = new ImportDecisionMakerInfo
{
DownloadClientItem = downloadClientItem
};
var idConfig = new ImportDecisionMakerConfig
{
Filter = FilterFilesType.None,
NewDownload = true,
SingleRelease = false,
IncludeExisting = false,
AddNewArtists = false
};
var decisions = _importDecisionMaker.GetImportDecisions(new List<IFileInfo>() { fileInfo }, idOverrides, idInfo, idConfig);
return _importApprovedTracks.Import(decisions, true, downloadClientItem, importMode);
}

View file

@ -2,6 +2,8 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using Marr.Data.QGen;
using NzbDrone.Common;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Music;
@ -15,6 +17,7 @@ namespace NzbDrone.Core.MediaFiles
List<TrackFile> GetFilesByRelease(int releaseId);
List<TrackFile> GetUnmappedFiles();
List<TrackFile> GetFilesWithBasePath(string path);
List<TrackFile> GetFileWithPath(List<string> paths);
TrackFile GetFileWithPath(string path);
void DeleteFilesByAlbum(int albumId);
void UnlinkFilesByAlbum(int albumId);
@ -88,7 +91,7 @@ namespace NzbDrone.Core.MediaFiles
{
// ensure path ends with a single trailing path separator to avoid matching partial paths
var safePath = path.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
return Query
return DataMapper.Query<TrackFile>()
.Where(x => x.Path.StartsWith(safePath))
.ToList();
}
@ -97,5 +100,15 @@ namespace NzbDrone.Core.MediaFiles
{
return Query.Where(x => x.Path == path).SingleOrDefault();
}
public List<TrackFile> GetFileWithPath(List<string> paths)
{
// use more limited join for speed
var all = DataMapper.Query<TrackFile>()
.Join<TrackFile, Track>(JoinType.Left, t => t.Tracks, (t, x) => t.Id == x.TrackFileId)
.ToList();
var joined = all.Join(paths, x => x.Path, x => x, (file, path) => file, PathEqualityComparer.Instance).ToList();
return joined;
}
}
}

View file

@ -5,10 +5,11 @@ using System.Linq;
using NLog;
using NzbDrone.Common;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Music;
using NzbDrone.Core.Music.Events;
using NzbDrone.Core.RootFolders;
namespace NzbDrone.Core.MediaFiles
{
@ -24,15 +25,18 @@ namespace NzbDrone.Core.MediaFiles
List<TrackFile> GetFilesByAlbum(int albumId);
List<TrackFile> GetFilesByRelease(int releaseId);
List<TrackFile> GetUnmappedFiles();
List<IFileInfo> FilterUnchangedFiles(List<IFileInfo> files, Artist artist, FilterFilesType filter);
List<IFileInfo> FilterUnchangedFiles(List<IFileInfo> files, FilterFilesType filter);
TrackFile Get(int id);
List<TrackFile> Get(IEnumerable<int> ids);
List<TrackFile> GetFilesWithBasePath(string path);
List<TrackFile> GetFileWithPath(List<string> path);
TrackFile GetFileWithPath(string path);
void UpdateMediaInfo(List<TrackFile> trackFiles);
}
public class MediaFileService : IMediaFileService, IHandleAsync<AlbumDeletedEvent>
public class MediaFileService : IMediaFileService,
IHandleAsync<AlbumDeletedEvent>,
IHandleAsync<ModelEvent<RootFolder>>
{
private readonly IEventAggregator _eventAggregator;
private readonly IMediaFileRepository _mediaFileRepository;
@ -93,11 +97,16 @@ namespace NzbDrone.Core.MediaFiles
}
}
public List<IFileInfo> FilterUnchangedFiles(List<IFileInfo> files, Artist artist, FilterFilesType filter)
public List<IFileInfo> FilterUnchangedFiles(List<IFileInfo> files, FilterFilesType filter)
{
if (filter == FilterFilesType.None)
{
return files;
}
_logger.Debug($"Filtering {files.Count} files for unchanged files");
var knownFiles = GetFilesWithBasePath(artist.Path);
var knownFiles = GetFileWithPath(files.Select(x => x.FullName).ToList());
_logger.Trace($"Got {knownFiles.Count} existing files");
if (!knownFiles.Any())
@ -156,21 +165,14 @@ namespace NzbDrone.Core.MediaFiles
return _mediaFileRepository.GetFilesWithBasePath(path);
}
public TrackFile GetFileWithPath(string path)
public List<TrackFile> GetFileWithPath(List<string> path)
{
return _mediaFileRepository.GetFileWithPath(path);
}
public void HandleAsync(AlbumDeletedEvent message)
public TrackFile GetFileWithPath(string path)
{
if (message.DeleteFiles)
{
_mediaFileRepository.DeleteFilesByAlbum(message.Album.Id);
}
else
{
_mediaFileRepository.UnlinkFilesByAlbum(message.Album.Id);
}
return _mediaFileRepository.GetFileWithPath(path);
}
public List<TrackFile> GetFilesByArtist(int artistId)
@ -197,5 +199,26 @@ namespace NzbDrone.Core.MediaFiles
{
_mediaFileRepository.SetFields(trackFiles, t => t.MediaInfo);
}
public void HandleAsync(AlbumDeletedEvent message)
{
if (message.DeleteFiles)
{
_mediaFileRepository.DeleteFilesByAlbum(message.Album.Id);
}
else
{
_mediaFileRepository.UnlinkFilesByAlbum(message.Album.Id);
}
}
public void HandleAsync(ModelEvent<RootFolder> message)
{
if (message.Action == ModelAction.Deleted)
{
var files = GetFilesWithBasePath(message.Model.Path);
DeleteMany(files, DeleteMediaFileReason.Manual);
}
}
}
}

View file

@ -9,7 +9,7 @@ namespace NzbDrone.Core.MediaFiles
{
public interface IMediaFileTableCleanupService
{
void Clean(Artist artist, List<string> filesOnDisk);
void Clean(string folder, List<string> filesOnDisk);
}
public class MediaFileTableCleanupService : IMediaFileTableCleanupService
@ -27,9 +27,9 @@ namespace NzbDrone.Core.MediaFiles
_logger = logger;
}
public void Clean(Artist artist, List<string> filesOnDisk)
public void Clean(string folder, List<string> filesOnDisk)
{
var dbFiles = _mediaFileService.GetFilesWithBasePath(artist.Path);
var dbFiles = _mediaFileService.GetFilesWithBasePath(folder);
// get files in database that are missing on disk and remove from database
var missingFiles = dbFiles.ExceptBy(x => x.Path, filesOnDisk, x => x, PathEqualityComparer.Instance).ToList();

View file

@ -103,8 +103,8 @@ namespace NzbDrone.Core.MediaFiles
AlbumId = album.Id,
TrackNumbers = tracksInFile.Select(e => e.AbsoluteTrackNumber).ToList(),
TrackFileId = file.Id,
ExistingPath = artist.Path.GetRelativePath(file.Path),
NewPath = artist.Path.GetRelativePath(newPath)
ExistingPath = file.Path,
NewPath = newPath
};
}
}

View file

@ -9,7 +9,7 @@ namespace NzbDrone.Core.MediaFiles
public int AlbumId { get; set; }
public List<int> TrackNumbers { get; set; }
public int TrackFileId { get; set; }
public string RelativePath { get; set; }
public string Path { get; set; }
public Dictionary<string, Tuple<string, string>> Changes { get; set; }
}
}

View file

@ -0,0 +1,297 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.MetadataSource;
using NzbDrone.Core.MetadataSource.SkyHook;
using NzbDrone.Core.Music;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
{
public interface ICandidateService
{
List<CandidateAlbumRelease> GetDbCandidatesFromTags(LocalAlbumRelease localAlbumRelease, IdentificationOverrides idOverrides, bool includeExisting);
List<CandidateAlbumRelease> GetDbCandidatesFromFingerprint(LocalAlbumRelease localAlbumRelease, IdentificationOverrides idOverrides, bool includeExisting);
List<CandidateAlbumRelease> GetRemoteCandidates(LocalAlbumRelease localAlbumRelease);
}
public class CandidateService : ICandidateService
{
private readonly ISearchForNewAlbum _albumSearchService;
private readonly IArtistService _artistService;
private readonly IAlbumService _albumService;
private readonly IReleaseService _releaseService;
private readonly IMediaFileService _mediaFileService;
private readonly Logger _logger;
public CandidateService(ISearchForNewAlbum albumSearchService,
IArtistService artistService,
IAlbumService albumService,
IReleaseService releaseService,
IMediaFileService mediaFileService,
Logger logger)
{
_albumSearchService = albumSearchService;
_artistService = artistService;
_albumService = albumService;
_releaseService = releaseService;
_mediaFileService = mediaFileService;
_logger = logger;
}
public List<CandidateAlbumRelease> GetDbCandidatesFromTags(LocalAlbumRelease localAlbumRelease, IdentificationOverrides idOverrides, bool includeExisting)
{
var watch = System.Diagnostics.Stopwatch.StartNew();
// Generally artist, album and release are null. But if they're not then limit candidates appropriately.
// We've tried to make sure that tracks are all for a single release.
List<CandidateAlbumRelease> candidateReleases;
// if we have a release ID, use that
AlbumRelease tagMbidRelease = null;
List<CandidateAlbumRelease> tagCandidate = null;
var releaseIds = localAlbumRelease.LocalTracks.Select(x => x.FileTrackInfo.ReleaseMBId).Distinct().ToList();
if (releaseIds.Count == 1 && releaseIds[0].IsNotNullOrWhiteSpace())
{
_logger.Debug("Selecting release from consensus ForeignReleaseId [{0}]", releaseIds[0]);
tagMbidRelease = _releaseService.GetReleaseByForeignReleaseId(releaseIds[0], true);
if (tagMbidRelease != null)
{
tagCandidate = GetDbCandidatesByRelease(new List<AlbumRelease> { tagMbidRelease }, includeExisting);
}
}
if (idOverrides?.AlbumRelease != null)
{
// this case overrides the release picked up from the file tags
var release = idOverrides.AlbumRelease;
_logger.Debug("Release {0} [{1} tracks] was forced", release, release.TrackCount);
candidateReleases = GetDbCandidatesByRelease(new List<AlbumRelease> { release }, includeExisting);
}
else if (idOverrides?.Album != null)
{
// use the release from file tags if it exists and agrees with the specified album
if (tagMbidRelease?.AlbumId == idOverrides.Album.Id)
{
candidateReleases = tagCandidate;
}
else
{
candidateReleases = GetDbCandidatesByAlbum(localAlbumRelease, idOverrides.Album, includeExisting);
}
}
else if (idOverrides?.Artist != null)
{
// use the release from file tags if it exists and agrees with the specified album
if (tagMbidRelease?.Album.Value.ArtistMetadataId == idOverrides.Artist.ArtistMetadataId)
{
candidateReleases = tagCandidate;
}
else
{
candidateReleases = GetDbCandidatesByArtist(localAlbumRelease, idOverrides.Artist, includeExisting);
}
}
else
{
if (tagMbidRelease != null)
{
candidateReleases = tagCandidate;
}
else
{
candidateReleases = GetDbCandidates(localAlbumRelease, includeExisting);
}
}
watch.Stop();
_logger.Debug($"Getting candidates from tags for {localAlbumRelease.LocalTracks.Count} tracks took {watch.ElapsedMilliseconds}ms");
// if we haven't got any candidates then try fingerprinting
return candidateReleases;
}
private List<CandidateAlbumRelease> GetDbCandidatesByRelease(List<AlbumRelease> releases, bool includeExisting)
{
// get the local tracks on disk for each album
var albumTracks = releases.Select(x => x.AlbumId)
.Distinct()
.ToDictionary(id => id, id => includeExisting ? _mediaFileService.GetFilesByAlbum(id) : new List<TrackFile>());
return releases.Select(x => new CandidateAlbumRelease
{
AlbumRelease = x,
ExistingTracks = albumTracks[x.AlbumId]
}).ToList();
}
private List<CandidateAlbumRelease> GetDbCandidatesByAlbum(LocalAlbumRelease localAlbumRelease, Album album, bool includeExisting)
{
// sort candidate releases by closest track count so that we stand a chance of
// getting a perfect match early on
return GetDbCandidatesByRelease(_releaseService.GetReleasesByAlbum(album.Id)
.OrderBy(x => Math.Abs(localAlbumRelease.TrackCount - x.TrackCount))
.ToList(), includeExisting);
}
private List<CandidateAlbumRelease> GetDbCandidatesByArtist(LocalAlbumRelease localAlbumRelease, Artist artist, bool includeExisting)
{
_logger.Trace("Getting candidates for {0}", artist);
var candidateReleases = new List<CandidateAlbumRelease>();
var albumTag = localAlbumRelease.LocalTracks.MostCommon(x => x.FileTrackInfo.AlbumTitle) ?? "";
if (albumTag.IsNotNullOrWhiteSpace())
{
var possibleAlbums = _albumService.GetCandidates(artist.ArtistMetadataId, albumTag);
foreach (var album in possibleAlbums)
{
candidateReleases.AddRange(GetDbCandidatesByAlbum(localAlbumRelease, album, includeExisting));
}
}
return candidateReleases;
}
private List<CandidateAlbumRelease> GetDbCandidates(LocalAlbumRelease localAlbumRelease, bool includeExisting)
{
// most general version, nothing has been specified.
// get all plausible artists, then all plausible albums, then get releases for each of these.
var candidateReleases = new List<CandidateAlbumRelease>();
// check if it looks like VA.
if (TrackGroupingService.IsVariousArtists(localAlbumRelease.LocalTracks))
{
var va = _artistService.FindById(DistanceCalculator.VariousArtistIds[0]);
if (va != null)
{
candidateReleases.AddRange(GetDbCandidatesByArtist(localAlbumRelease, va, includeExisting));
}
}
var artistTag = localAlbumRelease.LocalTracks.MostCommon(x => x.FileTrackInfo.ArtistTitle) ?? "";
if (artistTag.IsNotNullOrWhiteSpace())
{
var possibleArtists = _artistService.GetCandidates(artistTag);
foreach (var artist in possibleArtists)
{
candidateReleases.AddRange(GetDbCandidatesByArtist(localAlbumRelease, artist, includeExisting));
}
}
return candidateReleases;
}
public List<CandidateAlbumRelease> GetDbCandidatesFromFingerprint(LocalAlbumRelease localAlbumRelease, IdentificationOverrides idOverrides, bool includeExisting)
{
var recordingIds = localAlbumRelease.LocalTracks.Where(x => x.AcoustIdResults != null).SelectMany(x => x.AcoustIdResults).ToList();
var allReleases = _releaseService.GetReleasesByRecordingIds(recordingIds);
// make sure releases are consistent with those selected by the user
if (idOverrides?.AlbumRelease != null)
{
allReleases = allReleases.Where(x => x.Id == idOverrides.AlbumRelease.Id).ToList();
}
else if (idOverrides?.Album != null)
{
allReleases = allReleases.Where(x => x.AlbumId == idOverrides.Album.Id).ToList();
}
else if (idOverrides?.Artist != null)
{
allReleases = allReleases.Where(x => x.Album.Value.ArtistMetadataId == idOverrides.Artist.ArtistMetadataId).ToList();
}
return GetDbCandidatesByRelease(allReleases.Select(x => new
{
Release = x,
TrackCount = x.TrackCount,
CommonProportion = x.Tracks.Value.Select(y => y.ForeignRecordingId).Intersect(recordingIds).Count() / localAlbumRelease.TrackCount
})
.Where(x => x.CommonProportion > 0.6)
.ToList()
.OrderBy(x => Math.Abs(x.TrackCount - localAlbumRelease.TrackCount))
.ThenByDescending(x => x.CommonProportion)
.Select(x => x.Release)
.Take(10)
.ToList(), includeExisting);
}
public List<CandidateAlbumRelease> GetRemoteCandidates(LocalAlbumRelease localAlbumRelease)
{
// Gets candidate album releases from the metadata server.
// Will eventually need adding locally if we find a match
var watch = System.Diagnostics.Stopwatch.StartNew();
List<Album> remoteAlbums;
var candidates = new List<CandidateAlbumRelease>();
var albumIds = localAlbumRelease.LocalTracks.Select(x => x.FileTrackInfo.AlbumMBId).Distinct().ToList();
var recordingIds = localAlbumRelease.LocalTracks.Where(x => x.AcoustIdResults != null).SelectMany(x => x.AcoustIdResults).Distinct().ToList();
try
{
if (albumIds.Count == 1 && albumIds[0].IsNotNullOrWhiteSpace())
{
// Use mbids in tags if set
remoteAlbums = _albumSearchService.SearchForNewAlbum($"mbid:{albumIds[0]}", null);
}
else if (recordingIds.Any())
{
// If fingerprints present use those
remoteAlbums = _albumSearchService.SearchForNewAlbumByRecordingIds(recordingIds);
}
else
{
// fall back to artist / album name search
string artistTag;
if (TrackGroupingService.IsVariousArtists(localAlbumRelease.LocalTracks))
{
artistTag = "Various Artists";
}
else
{
artistTag = localAlbumRelease.LocalTracks.MostCommon(x => x.FileTrackInfo.ArtistTitle) ?? "";
}
var albumTag = localAlbumRelease.LocalTracks.MostCommon(x => x.FileTrackInfo.AlbumTitle) ?? "";
if (artistTag.IsNullOrWhiteSpace() || albumTag.IsNullOrWhiteSpace())
{
return candidates;
}
remoteAlbums = _albumSearchService.SearchForNewAlbum(albumTag, artistTag);
}
}
catch (SkyHookException e)
{
_logger.Info(e, "Skipping album due to SkyHook error");
remoteAlbums = new List<Album>();
}
foreach (var album in remoteAlbums)
{
// We have to make sure various bits and pieces are populated that are normally handled
// by a database lazy load
foreach (var release in album.AlbumReleases.Value)
{
release.Album = album;
candidates.Add(new CandidateAlbumRelease
{
AlbumRelease = release,
ExistingTracks = new List<TrackFile>()
});
}
}
watch.Stop();
_logger.Debug($"Getting {candidates.Count} remote candidates from tags for {localAlbumRelease.LocalTracks.Count} tracks took {watch.ElapsedMilliseconds}ms");
return candidates;
}
}
}

View file

@ -0,0 +1,205 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation;
using NzbDrone.Core.Music;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
{
public static class DistanceCalculator
{
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(DistanceCalculator));
public static readonly List<string> VariousArtistIds = new List<string> { "89ad4ac3-39f7-470e-963a-56509c546377" };
private static readonly List<string> VariousArtistNames = new List<string> { "various artists", "various", "va", "unknown" };
private static readonly List<IsoCountry> PreferredCountries = new List<string>
{
"United States",
"United Kingdom",
"Europe",
"[Worldwide]"
}.Select(x => IsoCountries.Find(x)).ToList();
private static bool TrackIndexIncorrect(LocalTrack localTrack, Track mbTrack, int totalTrackNumber)
{
return localTrack.FileTrackInfo.TrackNumbers[0] != mbTrack.AbsoluteTrackNumber &&
localTrack.FileTrackInfo.TrackNumbers[0] != totalTrackNumber;
}
public static int GetTotalTrackNumber(Track track, List<Track> allTracks)
{
return track.AbsoluteTrackNumber + allTracks.Count(t => t.MediumNumber < track.MediumNumber);
}
public static Distance TrackDistance(LocalTrack localTrack, Track mbTrack, int totalTrackNumber, bool includeArtist = false)
{
var dist = new Distance();
var localLength = localTrack.FileTrackInfo.Duration.TotalSeconds;
var mbLength = mbTrack.Duration / 1000;
var diff = Math.Abs(localLength - mbLength) - 10;
if (mbLength > 0)
{
dist.AddRatio("track_length", diff, 30);
}
// musicbrainz never has 'featuring' in the track title
// see https://musicbrainz.org/doc/Style/Artist_Credits
dist.AddString("track_title", localTrack.FileTrackInfo.CleanTitle ?? "", mbTrack.Title);
if (includeArtist && localTrack.FileTrackInfo.ArtistTitle.IsNotNullOrWhiteSpace()
&& !VariousArtistNames.Any(x => x.Equals(localTrack.FileTrackInfo.ArtistTitle, StringComparison.InvariantCultureIgnoreCase)))
{
dist.AddString("track_artist", localTrack.FileTrackInfo.ArtistTitle, mbTrack.ArtistMetadata.Value.Name);
}
if (localTrack.FileTrackInfo.TrackNumbers.FirstOrDefault() > 0 && mbTrack.AbsoluteTrackNumber > 0)
{
dist.AddBool("track_index", TrackIndexIncorrect(localTrack, mbTrack, totalTrackNumber));
}
var recordingId = localTrack.FileTrackInfo.RecordingMBId;
if (recordingId.IsNotNullOrWhiteSpace())
{
dist.AddBool("recording_id", localTrack.FileTrackInfo.RecordingMBId != mbTrack.ForeignRecordingId &&
!mbTrack.OldForeignRecordingIds.Contains(localTrack.FileTrackInfo.RecordingMBId));
}
// for fingerprinted files
if (localTrack.AcoustIdResults != null)
{
dist.AddBool("recording_id", !localTrack.AcoustIdResults.Contains(mbTrack.ForeignRecordingId));
}
return dist;
}
public static Distance AlbumReleaseDistance(List<LocalTrack> localTracks, AlbumRelease release, TrackMapping mapping)
{
var dist = new Distance();
if (!VariousArtistIds.Contains(release.Album.Value.ArtistMetadata.Value.ForeignArtistId))
{
var artist = localTracks.MostCommon(x => x.FileTrackInfo.ArtistTitle) ?? "";
dist.AddString("artist", artist, release.Album.Value.ArtistMetadata.Value.Name);
Logger.Trace("artist: {0} vs {1}; {2}", artist, release.Album.Value.ArtistMetadata.Value.Name, dist.NormalizedDistance());
}
var title = localTracks.MostCommon(x => x.FileTrackInfo.AlbumTitle) ?? "";
// Use the album title since the differences in release titles can cause confusion and
// aren't always correct in the tags
dist.AddString("album", title, release.Album.Value.Title);
Logger.Trace("album: {0} vs {1}; {2}", title, release.Title, dist.NormalizedDistance());
// Number of discs, either as tagged or the max disc number seen
var discCount = localTracks.MostCommon(x => x.FileTrackInfo.DiscCount);
discCount = discCount != 0 ? discCount : localTracks.Max(x => x.FileTrackInfo.DiscNumber);
if (discCount > 0)
{
dist.AddNumber("media_count", discCount, release.Media.Count);
Logger.Trace("media_count: {0} vs {1}; {2}", discCount, release.Media.Count, dist.NormalizedDistance());
}
// Media format
if (release.Media.Select(x => x.Format).Contains("Unknown"))
{
dist.Add("media_format", 1.0);
}
// Year
var localYear = localTracks.MostCommon(x => x.FileTrackInfo.Year);
if (localYear > 0 && (release.Album.Value.ReleaseDate.HasValue || release.ReleaseDate.HasValue))
{
var albumYear = release.Album.Value.ReleaseDate?.Year ?? 0;
var releaseYear = release.ReleaseDate?.Year ?? 0;
if (localYear == albumYear || localYear == releaseYear)
{
dist.Add("year", 0.0);
}
else
{
var remoteYear = albumYear > 0 ? albumYear : releaseYear;
var diff = Math.Abs(localYear - remoteYear);
var diff_max = Math.Abs(DateTime.Now.Year - remoteYear);
dist.AddRatio("year", diff, diff_max);
}
Logger.Trace($"year: {localYear} vs {release.Album.Value.ReleaseDate?.Year} or {release.ReleaseDate?.Year}; {dist.NormalizedDistance()}");
}
// If we parsed a country from the files use that, otherwise use our preference
var country = localTracks.MostCommon(x => x.FileTrackInfo.Country);
if (release.Country.Count > 0)
{
if (country != null)
{
dist.AddEquality("country", country.Name, release.Country);
Logger.Trace("country: {0} vs {1}; {2}", country.Name, string.Join(", ", release.Country), dist.NormalizedDistance());
}
else if (PreferredCountries.Count > 0)
{
dist.AddPriority("country", release.Country, PreferredCountries.Select(x => x.Name).ToList());
Logger.Trace("country priority: {0} vs {1}; {2}", string.Join(", ", PreferredCountries.Select(x => x.Name)), string.Join(", ", release.Country), dist.NormalizedDistance());
}
}
else
{
// full penalty if MusicBrainz release is missing a country
dist.Add("country", 1.0);
}
var label = localTracks.MostCommon(x => x.FileTrackInfo.Label);
if (label.IsNotNullOrWhiteSpace())
{
dist.AddEquality("label", label, release.Label);
Logger.Trace("label: {0} vs {1}; {2}", label, string.Join(", ", release.Label), dist.NormalizedDistance());
}
var disambig = localTracks.MostCommon(x => x.FileTrackInfo.Disambiguation);
if (disambig.IsNotNullOrWhiteSpace())
{
dist.AddString("album_disambiguation", disambig, release.Disambiguation);
Logger.Trace("album_disambiguation: {0} vs {1}; {2}", disambig, release.Disambiguation, dist.NormalizedDistance());
}
var mbAlbumId = localTracks.MostCommon(x => x.FileTrackInfo.ReleaseMBId);
if (mbAlbumId.IsNotNullOrWhiteSpace())
{
dist.AddBool("album_id", mbAlbumId != release.ForeignReleaseId && !release.OldForeignReleaseIds.Contains(mbAlbumId));
Logger.Trace("album_id: {0} vs {1} or {2}; {3}", mbAlbumId, release.ForeignReleaseId, string.Join(", ", release.OldForeignReleaseIds), dist.NormalizedDistance());
}
// tracks
foreach (var pair in mapping.Mapping)
{
dist.Add("tracks", pair.Value.Item2.NormalizedDistance());
}
Logger.Trace("after trackMapping: {0}", dist.NormalizedDistance());
// missing tracks
foreach (var track in mapping.MBExtra.Take(localTracks.Count))
{
dist.Add("missing_tracks", 1.0);
}
Logger.Trace("after missing tracks: {0}", dist.NormalizedDistance());
// unmatched tracks
foreach (var track in mapping.LocalExtra.Take(localTracks.Count))
{
dist.Add("unmatched_tracks", 1.0);
}
Logger.Trace("after unmatched tracks: {0}", dist.NormalizedDistance());
return dist;
}
}
}

View file

@ -4,8 +4,8 @@ using System.Linq;
using Newtonsoft.Json;
using NLog;
using NzbDrone.Common;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.MediaFiles.TrackImport.Aggregation;
@ -17,59 +17,39 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
{
public interface IIdentificationService
{
List<LocalAlbumRelease> Identify(List<LocalTrack> localTracks, Artist artist, Album album, AlbumRelease release, bool newDownload, bool singleRelease, bool includeExisting);
List<LocalAlbumRelease> Identify(List<LocalTrack> localTracks, IdentificationOverrides idOverrides, ImportDecisionMakerConfig config);
}
public class IdentificationService : IIdentificationService
{
private readonly IArtistService _artistService;
private readonly IAlbumService _albumService;
private readonly IReleaseService _releaseService;
private readonly ITrackService _trackService;
private readonly ITrackGroupingService _trackGroupingService;
private readonly IFingerprintingService _fingerprintingService;
private readonly IAudioTagService _audioTagService;
private readonly IAugmentingService _augmentingService;
private readonly IMediaFileService _mediaFileService;
private readonly ICandidateService _candidateService;
private readonly IConfigService _configService;
private readonly Logger _logger;
public IdentificationService(IArtistService artistService,
IAlbumService albumService,
IReleaseService releaseService,
ITrackService trackService,
public IdentificationService(ITrackService trackService,
ITrackGroupingService trackGroupingService,
IFingerprintingService fingerprintingService,
IAudioTagService audioTagService,
IAugmentingService augmentingService,
IMediaFileService mediaFileService,
ICandidateService candidateService,
IConfigService configService,
Logger logger)
{
_artistService = artistService;
_albumService = albumService;
_releaseService = releaseService;
_trackService = trackService;
_trackGroupingService = trackGroupingService;
_fingerprintingService = fingerprintingService;
_audioTagService = audioTagService;
_augmentingService = augmentingService;
_mediaFileService = mediaFileService;
_candidateService = candidateService;
_configService = configService;
_logger = logger;
}
private readonly List<IsoCountry> _preferredCountries = new List<string>
{
"United States",
"United Kingdom",
"Europe",
"[Worldwide]"
}.Select(x => IsoCountries.Find(x)).ToList();
private readonly List<string> _variousArtistNames = new List<string> { "various artists", "various", "va", "unknown" };
private readonly List<string> _variousArtistIds = new List<string> { "89ad4ac3-39f7-470e-963a-56509c546377" };
private void LogTestCaseOutput(List<LocalTrack> localTracks, Artist artist, Album album, AlbumRelease release, bool newDownload, bool singleRelease)
{
var trackData = localTracks.Select(x => new BasicLocalTrack
@ -104,17 +84,9 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
_logger.Debug($"*** IdentificationService TestCaseGenerator ***\n{output}");
}
public List<LocalAlbumRelease> Identify(List<LocalTrack> localTracks, Artist artist, Album album, AlbumRelease release, bool newDownload, bool singleRelease, bool includeExisting)
public List<LocalAlbumRelease> GetLocalAlbumReleases(List<LocalTrack> localTracks, bool singleRelease)
{
// 1 group localTracks so that we think they represent a single release
// 2 get candidates given specified artist, album and release. Candidates can include extra files already on disk.
// 3 find best candidate
// 4 If best candidate worse than threshold, try fingerprinting
var watch = System.Diagnostics.Stopwatch.StartNew();
_logger.Debug("Starting track identification");
LogTestCaseOutput(localTracks, artist, album, release, newDownload, singleRelease);
List<LocalAlbumRelease> releases = null;
if (singleRelease)
{
@ -137,8 +109,29 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
{
_logger.Warn($"Augmentation failed for {localRelease}");
}
}
IdentifyRelease(localRelease, artist, album, release, newDownload, includeExisting);
return releases;
}
public List<LocalAlbumRelease> Identify(List<LocalTrack> localTracks, IdentificationOverrides idOverrides, ImportDecisionMakerConfig config)
{
// 1 group localTracks so that we think they represent a single release
// 2 get candidates given specified artist, album and release. Candidates can include extra files already on disk.
// 3 find best candidate
// 4 If best candidate worse than threshold, try fingerprinting
var watch = System.Diagnostics.Stopwatch.StartNew();
_logger.Debug("Starting track identification");
var releases = GetLocalAlbumReleases(localTracks, config.SingleRelease);
int i = 0;
foreach (var localRelease in releases)
{
i++;
_logger.ProgressInfo($"Identifying album {i}/{releases.Count}");
IdentifyRelease(localRelease, idOverrides, config);
}
watch.Stop();
@ -191,25 +184,37 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
AdditionalFile = true,
Quality = x.Quality
}))
.ToList();
.ToList();
localTracks.ForEach(x => _augmentingService.Augment(x, true));
return localTracks;
}
private void IdentifyRelease(LocalAlbumRelease localAlbumRelease, Artist artist, Album album, AlbumRelease release, bool newDownload, bool includeExisting)
private void IdentifyRelease(LocalAlbumRelease localAlbumRelease, IdentificationOverrides idOverrides, ImportDecisionMakerConfig config)
{
var watch = System.Diagnostics.Stopwatch.StartNew();
bool fingerprinted = false;
var candidateReleases = GetCandidatesFromTags(localAlbumRelease, artist, album, release, includeExisting);
if (candidateReleases.Count == 0 && FingerprintingAllowed(newDownload))
var candidateReleases = _candidateService.GetDbCandidatesFromTags(localAlbumRelease, idOverrides, config.IncludeExisting);
if (candidateReleases.Count == 0 && config.AddNewArtists)
{
candidateReleases = _candidateService.GetRemoteCandidates(localAlbumRelease);
}
if (candidateReleases.Count == 0 && FingerprintingAllowed(config.NewDownload))
{
_logger.Debug("No candidates found, fingerprinting");
_fingerprintingService.Lookup(localAlbumRelease.LocalTracks, 0.5);
fingerprinted = true;
candidateReleases = GetCandidatesFromFingerprint(localAlbumRelease, artist, album, release, includeExisting);
candidateReleases = _candidateService.GetDbCandidatesFromFingerprint(localAlbumRelease, idOverrides, config.IncludeExisting);
if (candidateReleases.Count == 0 && config.AddNewArtists)
{
// Now fingerprints are populated this will return a different answer
candidateReleases = _candidateService.GetRemoteCandidates(localAlbumRelease);
}
}
if (candidateReleases.Count == 0)
@ -220,32 +225,36 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
_logger.Debug($"Got {candidateReleases.Count} candidates for {localAlbumRelease.LocalTracks.Count} tracks in {watch.ElapsedMilliseconds}ms");
var allTracks = _trackService.GetTracksByReleases(candidateReleases.Select(x => x.AlbumRelease.Id).ToList());
PopulateTracks(candidateReleases);
// convert all the TrackFiles that represent extra files to List<LocalTrack>
var allLocalTracks = ToLocalTrack(candidateReleases
.SelectMany(x => x.ExistingTracks)
.DistinctBy(x => x.Path), localAlbumRelease);
_logger.Debug($"Retrieved {allTracks.Count} possible tracks in {watch.ElapsedMilliseconds}ms");
_logger.Debug($"Retrieved {allLocalTracks.Count} possible tracks in {watch.ElapsedMilliseconds}ms");
GetBestRelease(localAlbumRelease, candidateReleases, allTracks, allLocalTracks);
GetBestRelease(localAlbumRelease, candidateReleases, allLocalTracks);
// If result isn't great and we haven't fingerprinted, try that
// Note that this can improve the match even if we try the same candidates
if (!fingerprinted && FingerprintingAllowed(newDownload) && ShouldFingerprint(localAlbumRelease))
if (!fingerprinted && FingerprintingAllowed(config.NewDownload) && ShouldFingerprint(localAlbumRelease))
{
_logger.Debug($"Match not good enough, fingerprinting");
_fingerprintingService.Lookup(localAlbumRelease.LocalTracks, 0.5);
// Only include extra possible candidates if neither album nor release are specified
// Will generally be specified as part of manual import
if (album == null && release == null)
if (idOverrides?.Album == null && idOverrides?.AlbumRelease == null)
{
var extraCandidates = GetCandidatesFromFingerprint(localAlbumRelease, artist, album, release, includeExisting);
var dbCandidates = _candidateService.GetDbCandidatesFromFingerprint(localAlbumRelease, idOverrides, config.IncludeExisting);
var remoteCandidates = config.AddNewArtists ? _candidateService.GetRemoteCandidates(localAlbumRelease) : new List<CandidateAlbumRelease>();
var extraCandidates = dbCandidates.Concat(remoteCandidates);
var newCandidates = extraCandidates.ExceptBy(x => x.AlbumRelease.Id, candidateReleases, y => y.AlbumRelease.Id, EqualityComparer<int>.Default);
candidateReleases.AddRange(newCandidates);
allTracks.AddRange(_trackService.GetTracksByReleases(newCandidates.Select(x => x.AlbumRelease.Id).ToList()));
PopulateTracks(candidateReleases);
allLocalTracks.AddRange(ToLocalTrack(newCandidates
.SelectMany(x => x.ExistingTracks)
.DistinctBy(x => x.Path)
@ -256,7 +265,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
// fingerprint all the local files in candidates we might be matching against
_fingerprintingService.Lookup(allLocalTracks, 0.5);
GetBestRelease(localAlbumRelease, candidateReleases, allTracks, allLocalTracks);
GetBestRelease(localAlbumRelease, candidateReleases, allLocalTracks);
}
_logger.Debug($"Best release found in {watch.ElapsedMilliseconds}ms");
@ -266,186 +275,22 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
_logger.Debug($"IdentifyRelease done in {watch.ElapsedMilliseconds}ms");
}
public List<CandidateAlbumRelease> GetCandidatesFromTags(LocalAlbumRelease localAlbumRelease, Artist artist, Album album, AlbumRelease release, bool includeExisting)
public void PopulateTracks(List<CandidateAlbumRelease> candidateReleases)
{
var watch = System.Diagnostics.Stopwatch.StartNew();
// Generally artist, album and release are null. But if they're not then limit candidates appropriately.
// We've tried to make sure that tracks are all for a single release.
List<CandidateAlbumRelease> candidateReleases;
var releasesMissingTracks = candidateReleases.Where(x => !x.AlbumRelease.Tracks.IsLoaded);
var allTracks = _trackService.GetTracksByReleases(releasesMissingTracks.Select(x => x.AlbumRelease.Id).ToList());
// if we have a release ID, use that
AlbumRelease tagMbidRelease = null;
List<CandidateAlbumRelease> tagCandidate = null;
_logger.Debug($"Retrieved {allTracks.Count} possible tracks in {watch.ElapsedMilliseconds}ms");
var releaseIds = localAlbumRelease.LocalTracks.Select(x => x.FileTrackInfo.ReleaseMBId).Distinct().ToList();
if (releaseIds.Count == 1 && releaseIds[0].IsNotNullOrWhiteSpace())
foreach (var release in releasesMissingTracks)
{
_logger.Debug("Selecting release from consensus ForeignReleaseId [{0}]", releaseIds[0]);
tagMbidRelease = _releaseService.GetReleaseByForeignReleaseId(releaseIds[0], true);
if (tagMbidRelease != null)
{
tagCandidate = GetCandidatesByRelease(new List<AlbumRelease> { tagMbidRelease }, includeExisting);
}
release.AlbumRelease.Tracks = allTracks.Where(x => x.AlbumReleaseId == release.AlbumRelease.Id).ToList();
}
if (release != null)
{
// this case overrides the release picked up from the file tags
_logger.Debug("Release {0} [{1} tracks] was forced", release, release.TrackCount);
candidateReleases = GetCandidatesByRelease(new List<AlbumRelease> { release }, includeExisting);
}
else if (album != null)
{
// use the release from file tags if it exists and agrees with the specified album
if (tagMbidRelease?.AlbumId == album.Id)
{
candidateReleases = tagCandidate;
}
else
{
candidateReleases = GetCandidatesByAlbum(localAlbumRelease, album, includeExisting);
}
}
else if (artist != null)
{
// use the release from file tags if it exists and agrees with the specified album
if (tagMbidRelease?.Album.Value.ArtistMetadataId == artist.ArtistMetadataId)
{
candidateReleases = tagCandidate;
}
else
{
candidateReleases = GetCandidatesByArtist(localAlbumRelease, artist, includeExisting);
}
}
else
{
if (tagMbidRelease != null)
{
candidateReleases = tagCandidate;
}
else
{
candidateReleases = GetCandidates(localAlbumRelease, includeExisting);
}
}
watch.Stop();
_logger.Debug($"Getting candidates from tags for {localAlbumRelease.LocalTracks.Count} tracks took {watch.ElapsedMilliseconds}ms");
// if we haven't got any candidates then try fingerprinting
return candidateReleases;
}
private List<CandidateAlbumRelease> GetCandidatesByRelease(List<AlbumRelease> releases, bool includeExisting)
{
// get the local tracks on disk for each album
var albumTracks = releases.Select(x => x.AlbumId)
.Distinct()
.ToDictionary(id => id, id => includeExisting ? _mediaFileService.GetFilesByAlbum(id) : new List<TrackFile>());
return releases.Select(x => new CandidateAlbumRelease
{
AlbumRelease = x,
ExistingTracks = albumTracks[x.AlbumId]
}).ToList();
}
private List<CandidateAlbumRelease> GetCandidatesByAlbum(LocalAlbumRelease localAlbumRelease, Album album, bool includeExisting)
{
// sort candidate releases by closest track count so that we stand a chance of
// getting a perfect match early on
return GetCandidatesByRelease(_releaseService.GetReleasesByAlbum(album.Id)
.OrderBy(x => Math.Abs(localAlbumRelease.TrackCount - x.TrackCount))
.ToList(), includeExisting);
}
private List<CandidateAlbumRelease> GetCandidatesByArtist(LocalAlbumRelease localAlbumRelease, Artist artist, bool includeExisting)
{
_logger.Trace("Getting candidates for {0}", artist);
var candidateReleases = new List<CandidateAlbumRelease>();
var albumTag = MostCommon(localAlbumRelease.LocalTracks.Select(x => x.FileTrackInfo.AlbumTitle)) ?? "";
if (albumTag.IsNotNullOrWhiteSpace())
{
var possibleAlbums = _albumService.GetCandidates(artist.ArtistMetadataId, albumTag);
foreach (var album in possibleAlbums)
{
candidateReleases.AddRange(GetCandidatesByAlbum(localAlbumRelease, album, includeExisting));
}
}
return candidateReleases;
}
private List<CandidateAlbumRelease> GetCandidates(LocalAlbumRelease localAlbumRelease, bool includeExisting)
{
// most general version, nothing has been specified.
// get all plausible artists, then all plausible albums, then get releases for each of these.
// check if it looks like VA.
if (TrackGroupingService.IsVariousArtists(localAlbumRelease.LocalTracks))
{
throw new NotImplementedException("Various artists not supported");
}
var candidateReleases = new List<CandidateAlbumRelease>();
var artistTag = MostCommon(localAlbumRelease.LocalTracks.Select(x => x.FileTrackInfo.ArtistTitle)) ?? "";
if (artistTag.IsNotNullOrWhiteSpace())
{
var possibleArtists = _artistService.GetCandidates(artistTag);
foreach (var artist in possibleArtists)
{
candidateReleases.AddRange(GetCandidatesByArtist(localAlbumRelease, artist, includeExisting));
}
}
return candidateReleases;
}
public List<CandidateAlbumRelease> GetCandidatesFromFingerprint(LocalAlbumRelease localAlbumRelease, Artist artist, Album album, AlbumRelease release, bool includeExisting)
{
var recordingIds = localAlbumRelease.LocalTracks.Where(x => x.AcoustIdResults != null).SelectMany(x => x.AcoustIdResults).ToList();
var allReleases = _releaseService.GetReleasesByRecordingIds(recordingIds);
// make sure releases are consistent with those selected by the user
if (release != null)
{
allReleases = allReleases.Where(x => x.Id == release.Id).ToList();
}
else if (album != null)
{
allReleases = allReleases.Where(x => x.AlbumId == album.Id).ToList();
}
else if (artist != null)
{
allReleases = allReleases.Where(x => x.Album.Value.ArtistMetadataId == artist.ArtistMetadataId).ToList();
}
return GetCandidatesByRelease(allReleases.Select(x => new
{
Release = x,
TrackCount = x.TrackCount,
CommonProportion = x.Tracks.Value.Select(y => y.ForeignRecordingId).Intersect(recordingIds).Count() / localAlbumRelease.TrackCount
})
.Where(x => x.CommonProportion > 0.6)
.ToList()
.OrderBy(x => Math.Abs(x.TrackCount - localAlbumRelease.TrackCount))
.ThenByDescending(x => x.CommonProportion)
.Select(x => x.Release)
.Take(10)
.ToList(), includeExisting);
}
private T MostCommon<T>(IEnumerable<T> items)
{
return items.GroupBy(x => x).OrderByDescending(x => x.Count()).First().Key;
}
private void GetBestRelease(LocalAlbumRelease localAlbumRelease, List<CandidateAlbumRelease> candidateReleases, List<Track> dbTracks, List<LocalTrack> extraTracksOnDisk)
private void GetBestRelease(LocalAlbumRelease localAlbumRelease, List<CandidateAlbumRelease> candidateReleases, List<LocalTrack> extraTracksOnDisk)
{
var watch = System.Diagnostics.Stopwatch.StartNew();
@ -464,8 +309,8 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
var extraTracks = extraTracksOnDisk.Where(x => extraTrackPaths.Contains(x.Path)).ToList();
var allLocalTracks = localAlbumRelease.LocalTracks.Concat(extraTracks).DistinctBy(x => x.Path).ToList();
var mapping = MapReleaseTracks(allLocalTracks, dbTracks.Where(x => x.AlbumReleaseId == release.Id).ToList());
var distance = AlbumReleaseDistance(allLocalTracks, release, mapping);
var mapping = MapReleaseTracks(allLocalTracks, release.Tracks.Value);
var distance = DistanceCalculator.AlbumReleaseDistance(allLocalTracks, release, mapping);
var currDistance = distance.NormalizedDistance();
rwatch.Stop();
@ -493,11 +338,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
_logger.Debug($"Best release: {localAlbumRelease.AlbumRelease} Distance {localAlbumRelease.Distance.NormalizedDistance()} found in {watch.ElapsedMilliseconds}ms");
}
public int GetTotalTrackNumber(Track track, List<Track> allTracks)
{
return track.AbsoluteTrackNumber + allTracks.Count(t => t.MediumNumber < track.MediumNumber);
}
public TrackMapping MapReleaseTracks(List<LocalTrack> localTracks, List<Track> mbTracks)
{
var distances = new Distance[localTracks.Count, mbTracks.Count];
@ -505,10 +345,10 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
for (int col = 0; col < mbTracks.Count; col++)
{
var totalTrackNumber = GetTotalTrackNumber(mbTracks[col], mbTracks);
var totalTrackNumber = DistanceCalculator.GetTotalTrackNumber(mbTracks[col], mbTracks);
for (int row = 0; row < localTracks.Count; row++)
{
distances[row, col] = TrackDistance(localTracks[row], mbTracks[col], totalTrackNumber, false);
distances[row, col] = DistanceCalculator.TrackDistance(localTracks[row], mbTracks[col], totalTrackNumber, false);
costs[row, col] = distances[row, col].NormalizedDistance();
}
}
@ -531,178 +371,5 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
return result;
}
private bool TrackIndexIncorrect(LocalTrack localTrack, Track mbTrack, int totalTrackNumber)
{
return localTrack.FileTrackInfo.TrackNumbers[0] != mbTrack.AbsoluteTrackNumber &&
localTrack.FileTrackInfo.TrackNumbers[0] != totalTrackNumber;
}
public Distance TrackDistance(LocalTrack localTrack, Track mbTrack, int totalTrackNumber, bool includeArtist = false)
{
var dist = new Distance();
var localLength = localTrack.FileTrackInfo.Duration.TotalSeconds;
var mbLength = mbTrack.Duration / 1000;
var diff = Math.Abs(localLength - mbLength) - 10;
if (mbLength > 0)
{
dist.AddRatio("track_length", diff, 30);
}
// musicbrainz never has 'featuring' in the track title
// see https://musicbrainz.org/doc/Style/Artist_Credits
dist.AddString("track_title", localTrack.FileTrackInfo.CleanTitle ?? "", mbTrack.Title);
if (includeArtist && localTrack.FileTrackInfo.ArtistTitle.IsNotNullOrWhiteSpace()
&& !_variousArtistNames.Any(x => x.Equals(localTrack.FileTrackInfo.ArtistTitle, StringComparison.InvariantCultureIgnoreCase)))
{
dist.AddString("track_artist", localTrack.FileTrackInfo.ArtistTitle, mbTrack.ArtistMetadata.Value.Name);
}
if (localTrack.FileTrackInfo.TrackNumbers.FirstOrDefault() > 0 && mbTrack.AbsoluteTrackNumber > 0)
{
dist.AddBool("track_index", TrackIndexIncorrect(localTrack, mbTrack, totalTrackNumber));
}
var recordingId = localTrack.FileTrackInfo.RecordingMBId;
if (recordingId.IsNotNullOrWhiteSpace())
{
dist.AddBool("recording_id", localTrack.FileTrackInfo.RecordingMBId != mbTrack.ForeignRecordingId &&
!mbTrack.OldForeignRecordingIds.Contains(localTrack.FileTrackInfo.RecordingMBId));
}
// for fingerprinted files
if (localTrack.AcoustIdResults != null)
{
dist.AddBool("recording_id", !localTrack.AcoustIdResults.Contains(mbTrack.ForeignRecordingId));
}
return dist;
}
public Distance AlbumReleaseDistance(List<LocalTrack> localTracks, AlbumRelease release, TrackMapping mapping)
{
var dist = new Distance();
if (!_variousArtistIds.Contains(release.Album.Value.ArtistMetadata.Value.ForeignArtistId))
{
var artist = MostCommon(localTracks.Select(x => x.FileTrackInfo.ArtistTitle)) ?? "";
dist.AddString("artist", artist, release.Album.Value.ArtistMetadata.Value.Name);
_logger.Trace("artist: {0} vs {1}; {2}", artist, release.Album.Value.ArtistMetadata.Value.Name, dist.NormalizedDistance());
}
var title = MostCommon(localTracks.Select(x => x.FileTrackInfo.AlbumTitle)) ?? "";
// Use the album title since the differences in release titles can cause confusion and
// aren't always correct in the tags
dist.AddString("album", title, release.Album.Value.Title);
_logger.Trace("album: {0} vs {1}; {2}", title, release.Title, dist.NormalizedDistance());
// Number of discs, either as tagged or the max disc number seen
var discCount = MostCommon(localTracks.Select(x => x.FileTrackInfo.DiscCount));
discCount = discCount != 0 ? discCount : localTracks.Max(x => x.FileTrackInfo.DiscNumber);
if (discCount > 0)
{
dist.AddNumber("media_count", discCount, release.Media.Count);
_logger.Trace("media_count: {0} vs {1}; {2}", discCount, release.Media.Count, dist.NormalizedDistance());
}
// Media format
if (release.Media.Select(x => x.Format).Contains("Unknown"))
{
dist.Add("media_format", 1.0);
}
// Year
var localYear = MostCommon(localTracks.Select(x => x.FileTrackInfo.Year));
if (localYear > 0 && (release.Album.Value.ReleaseDate.HasValue || release.ReleaseDate.HasValue))
{
var albumYear = release.Album.Value.ReleaseDate?.Year ?? 0;
var releaseYear = release.ReleaseDate?.Year ?? 0;
if (localYear == albumYear || localYear == releaseYear)
{
dist.Add("year", 0.0);
}
else
{
var remoteYear = albumYear > 0 ? albumYear : releaseYear;
var diff = Math.Abs(localYear - remoteYear);
var diff_max = Math.Abs(DateTime.Now.Year - remoteYear);
dist.AddRatio("year", diff, diff_max);
}
_logger.Trace($"year: {localYear} vs {release.Album.Value.ReleaseDate?.Year} or {release.ReleaseDate?.Year}; {dist.NormalizedDistance()}");
}
// If we parsed a country from the files use that, otherwise use our preference
var country = MostCommon(localTracks.Select(x => x.FileTrackInfo.Country));
if (release.Country.Count > 0)
{
if (country != null)
{
dist.AddEquality("country", country.Name, release.Country);
_logger.Trace("country: {0} vs {1}; {2}", country.Name, string.Join(", ", release.Country), dist.NormalizedDistance());
}
else if (_preferredCountries.Count > 0)
{
dist.AddPriority("country", release.Country, _preferredCountries.Select(x => x.Name).ToList());
_logger.Trace("country priority: {0} vs {1}; {2}", string.Join(", ", _preferredCountries.Select(x => x.Name)), string.Join(", ", release.Country), dist.NormalizedDistance());
}
}
else
{
// full penalty if MusicBrainz release is missing a country
dist.Add("country", 1.0);
}
var label = MostCommon(localTracks.Select(x => x.FileTrackInfo.Label));
if (label.IsNotNullOrWhiteSpace())
{
dist.AddEquality("label", label, release.Label);
_logger.Trace("label: {0} vs {1}; {2}", label, string.Join(", ", release.Label), dist.NormalizedDistance());
}
var disambig = MostCommon(localTracks.Select(x => x.FileTrackInfo.Disambiguation));
if (disambig.IsNotNullOrWhiteSpace())
{
dist.AddString("album_disambiguation", disambig, release.Disambiguation);
_logger.Trace("album_disambiguation: {0} vs {1}; {2}", disambig, release.Disambiguation, dist.NormalizedDistance());
}
var mbAlbumId = MostCommon(localTracks.Select(x => x.FileTrackInfo.ReleaseMBId));
if (mbAlbumId.IsNotNullOrWhiteSpace())
{
dist.AddBool("album_id", mbAlbumId != release.ForeignReleaseId && !release.OldForeignReleaseIds.Contains(mbAlbumId));
_logger.Trace("album_id: {0} vs {1} or {2}; {3}", mbAlbumId, release.ForeignReleaseId, string.Join(", ", release.OldForeignReleaseIds), dist.NormalizedDistance());
}
// tracks
foreach (var pair in mapping.Mapping)
{
dist.Add("tracks", pair.Value.Item2.NormalizedDistance());
}
_logger.Trace("after trackMapping: {0}", dist.NormalizedDistance());
// missing tracks
foreach (var track in mapping.MBExtra.Take(localTracks.Count))
{
dist.Add("missing_tracks", 1.0);
}
_logger.Trace("after missing tracks: {0}", dist.NormalizedDistance());
// unmatched tracks
foreach (var track in mapping.LocalExtra.Take(localTracks.Count))
{
dist.Add("unmatched_tracks", 1.0);
}
_logger.Trace("after unmatched tracks: {0}", dist.NormalizedDistance());
return dist;
}
}
}

View file

@ -7,6 +7,7 @@ using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
@ -26,6 +27,8 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
public List<LocalAlbumRelease> GroupTracks(List<LocalTrack> localTracks)
{
_logger.ProgressInfo($"Grouping {localTracks.Count} tracks");
var releases = new List<LocalAlbumRelease>();
// first attempt, assume grouped by folder

View file

@ -4,14 +4,19 @@ using System.Linq;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download;
using NzbDrone.Core.Extras;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Music;
using NzbDrone.Core.Music.Commands;
using NzbDrone.Core.Music.Events;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.RootFolders;
namespace NzbDrone.Core.MediaFiles.TrackImport
{
@ -26,81 +31,94 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
private readonly IMediaFileService _mediaFileService;
private readonly IAudioTagService _audioTagService;
private readonly ITrackService _trackService;
private readonly IArtistService _artistService;
private readonly IAddArtistService _addArtistService;
private readonly IAlbumService _albumService;
private readonly IRefreshAlbumService _refreshAlbumService;
private readonly IRootFolderService _rootFolderService;
private readonly IRecycleBinProvider _recycleBinProvider;
private readonly IExtraService _extraService;
private readonly IDiskProvider _diskProvider;
private readonly IReleaseService _releaseService;
private readonly IEventAggregator _eventAggregator;
private readonly IManageCommandQueue _commandQueueManager;
private readonly Logger _logger;
public ImportApprovedTracks(IUpgradeMediaFiles trackFileUpgrader,
IMediaFileService mediaFileService,
IAudioTagService audioTagService,
ITrackService trackService,
IArtistService artistService,
IAddArtistService addArtistService,
IAlbumService albumService,
IRefreshAlbumService refreshAlbumService,
IRootFolderService rootFolderService,
IRecycleBinProvider recycleBinProvider,
IExtraService extraService,
IDiskProvider diskProvider,
IReleaseService releaseService,
IEventAggregator eventAggregator,
IManageCommandQueue commandQueueManager,
Logger logger)
{
_trackFileUpgrader = trackFileUpgrader;
_mediaFileService = mediaFileService;
_audioTagService = audioTagService;
_trackService = trackService;
_artistService = artistService;
_addArtistService = addArtistService;
_albumService = albumService;
_refreshAlbumService = refreshAlbumService;
_rootFolderService = rootFolderService;
_recycleBinProvider = recycleBinProvider;
_extraService = extraService;
_diskProvider = diskProvider;
_releaseService = releaseService;
_eventAggregator = eventAggregator;
_commandQueueManager = commandQueueManager;
_logger = logger;
}
public List<ImportResult> Import(List<ImportDecision<LocalTrack>> decisions, bool replaceExisting, DownloadClientItem downloadClientItem = null, ImportMode importMode = ImportMode.Auto)
{
var qualifiedImports = decisions.Where(c => c.Approved)
.GroupBy(c => c.Item.Artist.Id, (i, s) => s
.OrderByDescending(c => c.Item.Quality, new QualityModelComparer(s.First().Item.Artist.QualityProfile))
.ThenByDescending(c => c.Item.Size))
.SelectMany(c => c)
.ToList();
_logger.Debug($"Importing {qualifiedImports.Count} files. replaceExisting: {replaceExisting}");
var importResults = new List<ImportResult>();
var allImportedTrackFiles = new List<TrackFile>();
var allOldTrackFiles = new List<TrackFile>();
var addedArtists = new List<Artist>();
var albumDecisions = decisions.Where(e => e.Item.Album != null && e.Approved)
.GroupBy(e => e.Item.Album.Id).ToList();
.GroupBy(e => e.Item.Album.ForeignAlbumId).ToList();
int iDecision = 1;
foreach (var albumDecision in albumDecisions)
{
var album = albumDecision.First().Item.Album;
var newRelease = albumDecision.First().Item.Release;
_logger.ProgressInfo($"Importing album {iDecision++}/{albumDecisions.Count}");
var decisionList = albumDecision.ToList();
var artist = EnsureArtistAdded(decisionList, addedArtists);
if (artist == null)
{
// failed to add the artist, carry on with next album
continue;
}
var album = EnsureAlbumAdded(decisionList);
if (album == null)
{
// failed to add the album, carry on with next one
continue;
}
if (replaceExisting)
{
var artist = albumDecision.First().Item.Artist;
var rootFolder = _diskProvider.GetParentFolder(artist.Path);
var previousFiles = _mediaFileService.GetFilesByAlbum(album.Id);
_logger.Debug($"Deleting {previousFiles.Count} existing files for {album}");
foreach (var previousFile in previousFiles)
{
var subfolder = rootFolder.GetRelativePath(_diskProvider.GetParentFolder(previousFile.Path));
if (_diskProvider.FileExists(previousFile.Path))
{
_logger.Debug("Removing existing track file: {0}", previousFile);
_recycleBinProvider.DeleteFile(previousFile.Path, subfolder);
}
_mediaFileService.Delete(previousFile, DeleteMediaFileReason.Upgrade);
}
RemoveExistingTrackFiles(artist, album);
}
// set the correct release to be monitored before importing the new files
var newRelease = albumDecision.First().Item.Release;
_logger.Debug("Updating release to {0} [{1} tracks]", newRelease, newRelease.TrackCount);
album.AlbumReleases = _releaseService.SetMonitored(newRelease);
@ -109,6 +127,16 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
_eventAggregator.PublishEvent(new AlbumEditedEvent(album, album));
}
var qualifiedImports = decisions.Where(c => c.Approved)
.GroupBy(c => c.Item.Artist.Id, (i, s) => s
.OrderByDescending(c => c.Item.Quality, new QualityModelComparer(s.First().Item.Artist.QualityProfile))
.ThenByDescending(c => c.Item.Size))
.SelectMany(c => c)
.ToList();
_logger.ProgressInfo($"Importing {qualifiedImports.Count} tracks");
_logger.Debug($"Importing {qualifiedImports.Count} files. replaceExisting: {replaceExisting}");
var filesToAdd = new List<TrackFile>(qualifiedImports.Count);
var albumReleasesDict = new Dictionary<int, List<AlbumRelease>>(albumDecisions.Count);
var trackImportedEvents = new List<TrackImportedEvent>(qualifiedImports.Count);
@ -184,7 +212,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
if (!localTrack.ExistingFile)
{
trackFile.SceneName = GetSceneReleaseName(downloadClientItem, localTrack);
trackFile.SceneName = GetSceneReleaseName(downloadClientItem);
var moveResult = _trackFileUpgrader.UpgradeTrackFile(trackFile, localTrack, copyOnly);
oldFiles = moveResult.OldFiles;
@ -283,10 +311,147 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
importResults.AddRange(decisions.Where(c => !c.Approved)
.Select(d => new ImportResult(d, d.Rejections.Select(r => r.Reason).ToArray())));
// Refresh any artists we added
if (addedArtists.Any())
{
_commandQueueManager.Push(new BulkRefreshArtistCommand(addedArtists.Select(x => x.Id).ToList(), true));
}
return importResults;
}
private string GetSceneReleaseName(DownloadClientItem downloadClientItem, LocalTrack localTrack)
private Artist EnsureArtistAdded(List<ImportDecision<LocalTrack>> decisions, List<Artist> addedArtists)
{
var artist = decisions.First().Item.Artist;
if (artist.Id == 0)
{
var dbArtist = _artistService.FindById(artist.ForeignArtistId);
if (dbArtist == null)
{
_logger.Debug($"Adding remote artist {artist}");
var rootFolder = _rootFolderService.GetBestRootFolder(decisions.First().Item.Path);
artist.RootFolderPath = rootFolder.Path;
artist.MetadataProfileId = rootFolder.DefaultMetadataProfileId;
artist.QualityProfileId = rootFolder.DefaultQualityProfileId;
artist.AlbumFolder = true;
artist.Monitored = rootFolder.DefaultMonitorOption != MonitorTypes.None;
artist.Tags = rootFolder.DefaultTags;
artist.AddOptions = new AddArtistOptions
{
SearchForMissingAlbums = false,
Monitored = artist.Monitored,
Monitor = rootFolder.DefaultMonitorOption
};
try
{
dbArtist = _addArtistService.AddArtist(artist, false);
addedArtists.Add(dbArtist);
}
catch (Exception e)
{
_logger.Error(e, "Failed to add artist {0}", artist);
foreach (var decision in decisions)
{
decision.Reject(new Rejection("Failed to add missing artist", RejectionType.Temporary));
}
return null;
}
}
// Put in the newly loaded artist
foreach (var decision in decisions)
{
decision.Item.Artist = dbArtist;
decision.Item.Album.Artist = dbArtist;
decision.Item.Album.ArtistMetadataId = dbArtist.ArtistMetadataId;
}
artist = dbArtist;
}
return artist;
}
private Album EnsureAlbumAdded(List<ImportDecision<LocalTrack>> decisions)
{
var album = decisions.First().Item.Album;
if (album.Id == 0)
{
var dbAlbum = _albumService.FindById(album.ForeignAlbumId);
if (dbAlbum == null)
{
_logger.Debug($"Adding remote album {album}");
try
{
_albumService.InsertMany(new List<Album> { album });
_refreshAlbumService.RefreshAlbumInfo(album, new List<Album> { album }, false);
dbAlbum = _albumService.FindById(album.ForeignAlbumId);
}
catch (Exception e)
{
_logger.Error(e, "Failed to add album {0}", album);
RejectAlbum(decisions);
return null;
}
}
var release = dbAlbum.AlbumReleases.Value.ExclusiveOrDefault(x => x.ForeignReleaseId == decisions.First().Item.Release.ForeignReleaseId);
if (release == null)
{
RejectAlbum(decisions);
return null;
}
// Populate the new DB album
foreach (var decision in decisions)
{
decision.Item.Album = dbAlbum;
decision.Item.Release = release;
var trackIds = decision.Item.Tracks.Select(x => x.ForeignTrackId).ToList();
decision.Item.Tracks = release.Tracks.Value.Where(x => trackIds.Contains(x.ForeignTrackId)).ToList();
}
}
return album;
}
private void RejectAlbum(List<ImportDecision<LocalTrack>> decisions)
{
foreach (var decision in decisions)
{
decision.Reject(new Rejection("Failed to add missing album", RejectionType.Temporary));
}
}
private void RemoveExistingTrackFiles(Artist artist, Album album)
{
var rootFolder = _diskProvider.GetParentFolder(artist.Path);
var previousFiles = _mediaFileService.GetFilesByAlbum(album.Id);
_logger.Debug($"Deleting {previousFiles.Count} existing files for {album}");
foreach (var previousFile in previousFiles)
{
var subfolder = rootFolder.GetRelativePath(_diskProvider.GetParentFolder(previousFile.Path));
if (_diskProvider.FileExists(previousFile.Path))
{
_logger.Debug("Removing existing track file: {0}", previousFile);
_recycleBinProvider.DeleteFile(previousFile.Path, subfolder);
}
_mediaFileService.Delete(previousFile, DeleteMediaFileReason.Upgrade);
}
}
private string GetSceneReleaseName(DownloadClientItem downloadClientItem)
{
if (downloadClientItem != null)
{

View file

@ -0,0 +1,15 @@
using System.Collections.Generic;
using NzbDrone.Core.Music;
namespace NzbDrone.Core.MediaFiles.TrackImport
{
public class ImportArtistDefaults
{
public int MetadataProfileId { get; set; }
public int LanguageProfileId { get; set; }
public int QualityProfileId { get; set; }
public bool AlbumFolder { get; set; }
public MonitorTypes Monitored { get; set; }
public HashSet<int> Tags { get; set; }
}
}

View file

@ -3,23 +3,44 @@ using System.Collections.Generic;
using System.IO.Abstractions;
using System.Linq;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download;
using NzbDrone.Core.MediaFiles.TrackImport.Aggregation;
using NzbDrone.Core.MediaFiles.TrackImport.Identification;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Music;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Profiles.Qualities;
using NzbDrone.Core.RootFolders;
namespace NzbDrone.Core.MediaFiles.TrackImport
{
public interface IMakeImportDecision
{
List<ImportDecision<LocalTrack>> GetImportDecisions(List<IFileInfo> musicFiles, Artist artist, FilterFilesType filter, bool includeExisting);
List<ImportDecision<LocalTrack>> GetImportDecisions(List<IFileInfo> musicFiles, Artist artist, DownloadClientItem downloadClientItem, ParsedTrackInfo folderInfo);
List<ImportDecision<LocalTrack>> GetImportDecisions(List<IFileInfo> musicFiles, Artist artist, Album album, AlbumRelease albumRelease, DownloadClientItem downloadClientItem, ParsedTrackInfo folderInfo, FilterFilesType filter, bool newDownload, bool singleRelease, bool includeExisting);
List<ImportDecision<LocalTrack>> GetImportDecisions(List<IFileInfo> musicFiles, IdentificationOverrides idOverrides, ImportDecisionMakerInfo itemInfo, ImportDecisionMakerConfig config);
}
public class IdentificationOverrides
{
public Artist Artist { get; set; }
public Album Album { get; set; }
public AlbumRelease AlbumRelease { get; set; }
}
public class ImportDecisionMakerInfo
{
public DownloadClientItem DownloadClientItem { get; set; }
public ParsedTrackInfo ParsedTrackInfo { get; set; }
}
public class ImportDecisionMakerConfig
{
public FilterFilesType Filter { get; set; }
public bool NewDownload { get; set; }
public bool SingleRelease { get; set; }
public bool IncludeExisting { get; set; }
public bool AddNewArtists { get; set; }
}
public class ImportDecisionMaker : IMakeImportDecision
@ -30,10 +51,8 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
private readonly IAudioTagService _audioTagService;
private readonly IAugmentingService _augmentingService;
private readonly IIdentificationService _identificationService;
private readonly IAlbumService _albumService;
private readonly IReleaseService _releaseService;
private readonly IEventAggregator _eventAggregator;
private readonly IDiskProvider _diskProvider;
private readonly IRootFolderService _rootFolderService;
private readonly IProfileService _qualityProfileService;
private readonly Logger _logger;
public ImportDecisionMaker(IEnumerable<IImportDecisionEngineSpecification<LocalTrack>> trackSpecifications,
@ -42,10 +61,8 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
IAudioTagService audioTagService,
IAugmentingService augmentingService,
IIdentificationService identificationService,
IAlbumService albumService,
IReleaseService releaseService,
IEventAggregator eventAggregator,
IDiskProvider diskProvider,
IRootFolderService rootFolderService,
IProfileService qualityProfileService,
Logger logger)
{
_trackSpecifications = trackSpecifications;
@ -54,29 +71,17 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
_audioTagService = audioTagService;
_augmentingService = augmentingService;
_identificationService = identificationService;
_albumService = albumService;
_releaseService = releaseService;
_eventAggregator = eventAggregator;
_diskProvider = diskProvider;
_rootFolderService = rootFolderService;
_qualityProfileService = qualityProfileService;
_logger = logger;
}
public List<ImportDecision<LocalTrack>> GetImportDecisions(List<IFileInfo> musicFiles, Artist artist, FilterFilesType filter, bool includeExisting)
{
return GetImportDecisions(musicFiles, artist, null, null, null, null, filter, false, false, true);
}
public List<ImportDecision<LocalTrack>> GetImportDecisions(List<IFileInfo> musicFiles, Artist artist, DownloadClientItem downloadClientItem, ParsedTrackInfo folderInfo)
{
return GetImportDecisions(musicFiles, artist, null, null, downloadClientItem, folderInfo, FilterFilesType.None, true, false, false);
}
public List<ImportDecision<LocalTrack>> GetImportDecisions(List<IFileInfo> musicFiles, Artist artist, Album album, AlbumRelease albumRelease, DownloadClientItem downloadClientItem, ParsedTrackInfo folderInfo, FilterFilesType filter, bool newDownload, bool singleRelease, bool includeExisting)
public Tuple<List<LocalTrack>, List<ImportDecision<LocalTrack>>> GetLocalTracks(List<IFileInfo> musicFiles, DownloadClientItem downloadClientItem, ParsedTrackInfo folderInfo, FilterFilesType filter)
{
var watch = new System.Diagnostics.Stopwatch();
watch.Start();
var files = filter != FilterFilesType.None && (artist != null) ? _mediaFileService.FilterUnchangedFiles(musicFiles, artist, filter) : musicFiles;
var files = _mediaFileService.FilterUnchangedFiles(musicFiles, filter);
var localTracks = new List<LocalTrack>();
var decisions = new List<ImportDecision<LocalTrack>>();
@ -85,7 +90,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
if (!files.Any())
{
return decisions;
return Tuple.Create(localTracks, decisions);
}
ParsedAlbumInfo downloadClientItemInfo = null;
@ -95,19 +100,19 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
downloadClientItemInfo = Parser.Parser.ParseAlbumTitle(downloadClientItem.Title);
}
int i = 1;
foreach (var file in files)
{
_logger.ProgressInfo($"Reading file {i++}/{files.Count}");
var localTrack = new LocalTrack
{
Artist = artist,
Album = album,
DownloadClientAlbumInfo = downloadClientItemInfo,
FolderTrackInfo = folderInfo,
Path = file.FullName,
Size = file.Length,
Modified = file.LastWriteTimeUtc,
FileTrackInfo = _audioTagService.ReadTags(file.FullName),
ExistingFile = !newDownload,
AdditionalFile = false
};
@ -131,18 +136,36 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
_logger.Debug($"Tags parsed for {files.Count} files in {watch.ElapsedMilliseconds}ms");
var releases = _identificationService.Identify(localTracks, artist, album, albumRelease, newDownload, singleRelease, includeExisting);
return Tuple.Create(localTracks, decisions);
}
public List<ImportDecision<LocalTrack>> GetImportDecisions(List<IFileInfo> musicFiles, IdentificationOverrides idOverrides, ImportDecisionMakerInfo itemInfo, ImportDecisionMakerConfig config)
{
idOverrides = idOverrides ?? new IdentificationOverrides();
itemInfo = itemInfo ?? new ImportDecisionMakerInfo();
var trackData = GetLocalTracks(musicFiles, itemInfo.DownloadClientItem, itemInfo.ParsedTrackInfo, config.Filter);
var localTracks = trackData.Item1;
var decisions = trackData.Item2;
localTracks.ForEach(x => x.ExistingFile = !config.NewDownload);
var releases = _identificationService.Identify(localTracks, idOverrides, config);
foreach (var release in releases)
{
release.NewDownload = newDownload;
var releaseDecision = GetDecision(release, downloadClientItem);
// make sure the appropriate quality profile is set for the release artist
// in case it's a new artist
EnsureData(release);
release.NewDownload = config.NewDownload;
var releaseDecision = GetDecision(release, itemInfo.DownloadClientItem);
foreach (var localTrack in release.LocalTracks)
{
if (releaseDecision.Approved)
{
decisions.AddIfNotNull(GetDecision(localTrack, downloadClientItem));
decisions.AddIfNotNull(GetDecision(localTrack, itemInfo.DownloadClientItem));
}
else
{
@ -154,6 +177,19 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
return decisions;
}
private void EnsureData(LocalAlbumRelease release)
{
if (release.AlbumRelease != null && release.AlbumRelease.Album.Value.Artist.Value.QualityProfileId == 0)
{
var rootFolder = _rootFolderService.GetBestRootFolder(release.LocalTracks.First().Path);
var qualityProfile = _qualityProfileService.Get(rootFolder.DefaultQualityProfileId);
var artist = release.AlbumRelease.Album.Value.Artist.Value;
artist.QualityProfileId = qualityProfile.Id;
artist.QualityProfile = qualityProfile;
}
}
private ImportDecision<LocalAlbumRelease> GetDecision(LocalAlbumRelease localAlbumRelease, DownloadClientItem downloadClientItem)
{
ImportDecision<LocalAlbumRelease> decision = null;

View file

@ -15,7 +15,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
}
public string Path { get; set; }
public string RelativePath { get; set; }
public string Name { get; set; }
public long Size { get; set; }
public Artist Artist { get; set; }

View file

@ -9,6 +9,7 @@ using NzbDrone.Common.Crypto;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download;
using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.Messaging.Commands;
@ -16,6 +17,7 @@ using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Music;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RootFolders;
namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
{
@ -29,6 +31,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
{
private readonly IDiskProvider _diskProvider;
private readonly IParsingService _parsingService;
private readonly IRootFolderService _rootFolderService;
private readonly IDiskScanService _diskScanService;
private readonly IMakeImportDecision _importDecisionMaker;
private readonly IArtistService _artistService;
@ -44,6 +47,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
public ManualImportService(IDiskProvider diskProvider,
IParsingService parsingService,
IRootFolderService rootFolderService,
IDiskScanService diskScanService,
IMakeImportDecision importDecisionMaker,
IArtistService artistService,
@ -59,6 +63,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
{
_diskProvider = diskProvider;
_parsingService = parsingService;
_rootFolderService = rootFolderService;
_diskScanService = diskScanService;
_importDecisionMaker = importDecisionMaker;
_artistService = artistService;
@ -94,8 +99,19 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
return new List<ManualImportItem>();
}
var decision = _importDecisionMaker.GetImportDecisions(new List<IFileInfo> { _diskProvider.GetFileInfo(path) }, null, null, null, null, null, FilterFilesType.None, true, false, !replaceExistingFiles);
var result = MapItem(decision.First(), Path.GetDirectoryName(path), downloadId, replaceExistingFiles, false);
var files = new List<IFileInfo> { _diskProvider.GetFileInfo(path) };
var config = new ImportDecisionMakerConfig
{
Filter = FilterFilesType.None,
NewDownload = true,
SingleRelease = false,
IncludeExisting = !replaceExistingFiles,
AddNewArtists = false
};
var decision = _importDecisionMaker.GetImportDecisions(files, null, null, config);
var result = MapItem(decision.First(), downloadId, replaceExistingFiles, false);
return new List<ManualImportItem> { result };
}
@ -120,9 +136,26 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
}
}
var folderInfo = Parser.Parser.ParseMusicTitle(directoryInfo.Name);
var artistFiles = _diskScanService.GetAudioFiles(folder).ToList();
var decisions = _importDecisionMaker.GetImportDecisions(artistFiles, artist, null, null, downloadClientItem, folderInfo, filter, true, false, !replaceExistingFiles);
var idOverrides = new IdentificationOverrides
{
Artist = artist
};
var itemInfo = new ImportDecisionMakerInfo
{
DownloadClientItem = downloadClientItem,
ParsedTrackInfo = Parser.Parser.ParseMusicTitle(directoryInfo.Name)
};
var config = new ImportDecisionMakerConfig
{
Filter = filter,
NewDownload = true,
SingleRelease = false,
IncludeExisting = !replaceExistingFiles,
AddNewArtists = false
};
var decisions = _importDecisionMaker.GetImportDecisions(artistFiles, idOverrides, itemInfo, config);
// paths will be different for new and old files which is why we need to map separately
var newFiles = artistFiles.Join(decisions,
@ -131,9 +164,9 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
(f, d) => new { File = f, Decision = d },
PathEqualityComparer.Instance);
var newItems = newFiles.Select(x => MapItem(x.Decision, folder, downloadId, replaceExistingFiles, false));
var newItems = newFiles.Select(x => MapItem(x.Decision, downloadId, replaceExistingFiles, false));
var existingDecisions = decisions.Except(newFiles.Select(x => x.Decision));
var existingItems = existingDecisions.Select(x => MapItem(x, x.Item.Artist.Path, null, replaceExistingFiles, false));
var existingItems = existingDecisions.Select(x => MapItem(x, null, replaceExistingFiles, false));
return newItems.Concat(existingItems).ToList();
}
@ -152,7 +185,22 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
var disableReleaseSwitching = group.First().DisableReleaseSwitching;
var decisions = _importDecisionMaker.GetImportDecisions(group.Select(x => _diskProvider.GetFileInfo(x.Path)).ToList(), group.First().Artist, group.First().Album, group.First().Release, null, null, FilterFilesType.None, true, true, !replaceExistingFiles);
var files = group.Select(x => _diskProvider.GetFileInfo(x.Path)).ToList();
var idOverride = new IdentificationOverrides
{
Artist = group.First().Artist,
Album = group.First().Album,
AlbumRelease = group.First().Release
};
var config = new ImportDecisionMakerConfig
{
Filter = FilterFilesType.None,
NewDownload = true,
SingleRelease = true,
IncludeExisting = !replaceExistingFiles,
AddNewArtists = false
};
var decisions = _importDecisionMaker.GetImportDecisions(files, idOverride, null, config);
var existingItems = group.Join(decisions,
i => i.Path,
@ -187,19 +235,18 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
}
var newDecisions = decisions.Except(existingItems.Select(x => x.Decision));
result.AddRange(newDecisions.Select(x => MapItem(x, x.Item.Artist.Path, null, replaceExistingFiles, disableReleaseSwitching)));
result.AddRange(newDecisions.Select(x => MapItem(x, null, replaceExistingFiles, disableReleaseSwitching)));
}
return result;
}
private ManualImportItem MapItem(ImportDecision<LocalTrack> decision, string folder, string downloadId, bool replaceExistingFiles, bool disableReleaseSwitching)
private ManualImportItem MapItem(ImportDecision<LocalTrack> decision, string downloadId, bool replaceExistingFiles, bool disableReleaseSwitching)
{
var item = new ManualImportItem();
item.Id = HashConverter.GetHashInt31(decision.Item.Path);
item.Path = decision.Item.Path;
item.RelativePath = folder.GetRelativePath(decision.Item.Path);
item.Name = Path.GetFileNameWithoutExtension(decision.Item.Path);
item.DownloadId = downloadId;
@ -276,7 +323,14 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
Release = release
};
albumImportDecisions.Add(new ImportDecision<LocalTrack>(localTrack));
var importDecision = new ImportDecision<LocalTrack>(localTrack);
if (_rootFolderService.GetBestRootFolder(artist.Path) == null)
{
_logger.Warn($"Destination artist folder {artist.Path} not in a Root Folder, skipping import");
importDecision.Reject(new Rejection($"Destination artist folder {artist.Path} is not in a Root Folder"));
}
albumImportDecisions.Add(importDecision);
fileCount += 1;
}

View file

@ -18,9 +18,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Specifications
public Decision IsSatisfiedBy(LocalAlbumRelease item, DownloadClientItem downloadClientItem)
{
var artist = item.AlbumRelease.Album.Value.Artist.Value;
var qualityComparer = new QualityModelComparer(artist.QualityProfile);
// check if we are changing release
var currentRelease = item.AlbumRelease.Album.Value.AlbumReleases.Value.Single(x => x.Monitored);
var newRelease = item.AlbumRelease;
@ -28,6 +25,8 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Specifications
// if we are, check we are upgrading
if (newRelease.Id != currentRelease.Id)
{
var qualityComparer = new QualityModelComparer(item.AlbumRelease.Album.Value.Artist.Value.QualityProfile);
// min quality of all new tracks
var newMinQuality = item.LocalTracks.Select(x => x.Quality).OrderBy(x => x, qualityComparer).First();
_logger.Debug("Min quality of new files: {0}", newMinQuality);

View file

@ -0,0 +1,40 @@
using System.Linq;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Download;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.RootFolders;
namespace NzbDrone.Core.MediaFiles.TrackImport.Specifications
{
public class ArtistPathInRootFolderSpecification : IImportDecisionEngineSpecification<LocalAlbumRelease>
{
private readonly IRootFolderService _rootFolderService;
private readonly Logger _logger;
public ArtistPathInRootFolderSpecification(IRootFolderService rootFolderService,
Logger logger)
{
_rootFolderService = rootFolderService;
_logger = logger;
}
public Decision IsSatisfiedBy(LocalAlbumRelease item, DownloadClientItem downloadClientItem)
{
// Prevent imports to artists that are no longer inside a root folder Lidarr manages
var artist = item.AlbumRelease.Album.Value.Artist.Value;
// a new artist will have empty path, and will end up having path assinged based on file location
var pathToCheck = artist.Path.IsNotNullOrWhiteSpace() ? artist.Path : item.LocalTracks.First().Path.GetParentPath();
if (_rootFolderService.GetBestRootFolder(pathToCheck) == null)
{
_logger.Warn($"Destination folder {pathToCheck} not in a Root Folder, skipping import");
return Decision.Reject($"Destination folder {pathToCheck} is not in a Root Folder");
}
return Decision.Accept();
}
}
}

View file

@ -21,6 +21,12 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Specifications
public Decision IsSatisfiedBy(LocalTrack item, DownloadClientItem downloadClientItem)
{
if (!item.Tracks.Any(e => e.TrackFileId > 0))
{
// No existing tracks, skip. This guards against new artists not having a QualityProfile.
return Decision.Accept();
}
var downloadPropersAndRepacks = _configService.DownloadPropersAndRepacks;
var qualityComparer = new QualityModelComparer(item.Artist.QualityProfile);

View file

@ -6,5 +6,6 @@ namespace NzbDrone.Core.MetadataSource
public interface ISearchForNewAlbum
{
List<Album> SearchForNewAlbum(string title, string artist);
List<Album> SearchForNewAlbumByRecordingIds(List<string> recordingIds);
}
}

View file

@ -5,7 +5,7 @@ using System.Net;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Exceptions;
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MetadataSource.SkyHook.Resource;
@ -21,7 +21,6 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
private readonly IArtistService _artistService;
private readonly IAlbumService _albumService;
private readonly IMetadataRequestBuilder _requestBuilder;
private readonly IConfigService _configService;
private readonly IMetadataProfileService _metadataProfileService;
private static readonly List<string> NonAudioMedia = new List<string> { "DVD", "DVD-Video", "Blu-ray", "HD-DVD", "VCD", "SVCD", "UMD", "VHS" };
@ -32,11 +31,9 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
IArtistService artistService,
IAlbumService albumService,
Logger logger,
IConfigService configService,
IMetadataProfileService metadataProfileService)
{
_httpClient = httpClient;
_configService = configService;
_metadataProfileService = metadataProfileService;
_requestBuilder = requestBuilder;
_artistService = artistService;
@ -259,6 +256,21 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
}
}
public List<Album> SearchForNewAlbumByRecordingIds(List<string> recordingIds)
{
var ids = recordingIds.Where(x => x.IsNotNullOrWhiteSpace()).Distinct();
var httpRequest = _requestBuilder.GetRequestBuilder().Create()
.SetSegment("route", "search/fingerprint")
.Build();
httpRequest.SetContent(ids.ToJson());
httpRequest.Headers.ContentType = "application/json";
var httpResponse = _httpClient.Post<List<AlbumResource>>(httpRequest);
return httpResponse.Resource.SelectList(MapSearchResult);
}
public List<object> SearchForNewEntity(string title)
{
try
@ -351,6 +363,17 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
if (resource.Releases != null)
{
album.AlbumReleases = resource.Releases.Select(x => MapRelease(x, artistDict)).Where(x => x.TrackCount > 0).ToList();
// Monitor the release with most tracks
var mostTracks = album.AlbumReleases.Value.OrderByDescending(x => x.TrackCount).FirstOrDefault();
if (mostTracks != null)
{
mostTracks.Monitored = true;
}
}
else
{
album.AlbumReleases = new List<AlbumRelease>();
}
album.AnyReleaseOk = true;

View file

@ -0,0 +1,25 @@
using System.Collections.Generic;
using NzbDrone.Core.Messaging.Commands;
namespace NzbDrone.Core.Music.Commands
{
public class BulkRefreshArtistCommand : Command
{
public BulkRefreshArtistCommand()
{
}
public BulkRefreshArtistCommand(List<int> artistIds, bool areNewArtists = false)
{
ArtistIds = artistIds;
AreNewArtists = areNewArtists;
}
public List<int> ArtistIds { get; set; }
public bool AreNewArtists { get; set; }
public override bool SendUpdatesToClient => true;
public override bool UpdateScheduledTask => false;
}
}

View file

@ -13,7 +13,6 @@ namespace NzbDrone.Core.Music
private readonly IArtistService _artistService;
private readonly IManageCommandQueue _commandQueueManager;
private readonly IAlbumAddedService _albumAddedService;
private readonly Logger _logger;
public ArtistScannedHandler(IAlbumMonitoredService albumMonitoredService,

View file

@ -6,6 +6,7 @@ using FluentValidation;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Exceptions;
using NzbDrone.Core.MetadataSource;
using NzbDrone.Core.Organizer;
@ -115,12 +116,35 @@ namespace NzbDrone.Core.Music
private Artist SetPropertiesAndValidate(Artist newArtist)
{
if (string.IsNullOrWhiteSpace(newArtist.Path))
var path = newArtist.Path;
if (string.IsNullOrWhiteSpace(path))
{
var folderName = _fileNameBuilder.GetArtistFolder(newArtist);
newArtist.Path = Path.Combine(newArtist.RootFolderPath, folderName);
path = Path.Combine(newArtist.RootFolderPath, folderName);
}
// Disambiguate artist path if it exists already
if (_artistService.ArtistPathExists(path))
{
if (newArtist.Metadata.Value.Disambiguation.IsNotNullOrWhiteSpace())
{
path += $" ({newArtist.Metadata.Value.Disambiguation})";
}
if (_artistService.ArtistPathExists(path))
{
var basepath = path;
int i = 0;
do
{
i++;
path = basepath + $" ({i})";
}
while (_artistService.ArtistPathExists(path));
}
}
newArtist.Path = path;
newArtist.CleanName = newArtist.Metadata.Value.Name.CleanArtistName();
newArtist.SortName = Parser.Parser.NormalizeTitle(newArtist.Metadata.Value.Name).ToLower();
newArtist.Added = DateTime.UtcNow;

View file

@ -1,6 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.Commands;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
@ -34,7 +33,7 @@ namespace NzbDrone.Core.Music
tracks.ForEach(x => x.TrackFileId = 0);
_trackService.SetFileIds(tracks);
_commandQueueManager.Push(new RescanArtistCommand(message.Album.ArtistId, FilterFilesType.Matched));
_commandQueueManager.Push(new RescanFoldersCommand());
}
}
}

View file

@ -144,10 +144,6 @@ namespace NzbDrone.Core.Music
.OrderByDescending(s => s.MatchProb)
.ToList();
_logger.Trace("\nFuzzy album match on '{0}':\n{1}",
title,
string.Join("\n", sortedAlbums.Select(x => $"[{x.Album.Title}] {x.Album.CleanTitle}: {x.MatchProb}")));
return sortedAlbums.TakeWhile((x, i) => i == 0 || sortedAlbums[i - 1].MatchProb - x.MatchProb < fuzzGap)
.TakeWhile((x, i) => x.MatchProb > fuzzThreshold || (i > 0 && sortedAlbums[i - 1].MatchProb > fuzzThreshold))
.Select(x => x.Album)

View file

@ -99,6 +99,7 @@ namespace NzbDrone.Core.Music
{
tc((a, t) => a.CleanName.FuzzyMatch(t), cleanTitle),
tc((a, t) => a.Name.FuzzyMatch(t), title),
tc((a, t) => a.Metadata.Value.Aliases.Concat(new List<string> { a.Name }).Max(x => x.CleanArtistName().FuzzyMatch(t)), cleanTitle),
};
if (title.StartsWith("The ", StringComparison.CurrentCultureIgnoreCase))
@ -156,10 +157,6 @@ namespace NzbDrone.Core.Music
.OrderByDescending(s => s.MatchProb)
.ToList();
_logger.Trace("\nFuzzy artist match on '{0}':\n{1}",
title,
string.Join("\n", sortedArtists.Select(x => $"[{x.Artist.Name}] {x.Artist.CleanName}: {x.MatchProb}")));
return sortedArtists.TakeWhile((x, i) => i == 0 || sortedArtists[i - 1].MatchProb - x.MatchProb < fuzzGap)
.TakeWhile((x, i) => x.MatchProb > fuzzThreshold || (i > 0 && sortedArtists[i - 1].MatchProb > fuzzThreshold))
.Select(x => x.Artist)

View file

@ -11,24 +11,29 @@ using NzbDrone.Core.Exceptions;
using NzbDrone.Core.History;
using NzbDrone.Core.ImportLists.Exclusions;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.Commands;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.MetadataSource;
using NzbDrone.Core.Music.Commands;
using NzbDrone.Core.Music.Events;
using NzbDrone.Core.RootFolders;
namespace NzbDrone.Core.Music
{
public class RefreshArtistService : RefreshEntityServiceBase<Artist, Album>, IExecute<RefreshArtistCommand>
public class RefreshArtistService : RefreshEntityServiceBase<Artist, Album>,
IExecute<RefreshArtistCommand>,
IExecute<BulkRefreshArtistCommand>
{
private readonly IProvideArtistInfo _artistInfo;
private readonly IArtistService _artistService;
private readonly IAlbumService _albumService;
private readonly IRefreshAlbumService _refreshAlbumService;
private readonly IEventAggregator _eventAggregator;
private readonly IManageCommandQueue _commandQueueManager;
private readonly IMediaFileService _mediaFileService;
private readonly IHistoryService _historyService;
private readonly IDiskScanService _diskScanService;
private readonly IRootFolderService _rootFolderService;
private readonly ICheckIfArtistShouldBeRefreshed _checkIfArtistShouldBeRefreshed;
private readonly IConfigService _configService;
private readonly IImportListExclusionService _importListExclusionService;
@ -40,9 +45,10 @@ namespace NzbDrone.Core.Music
IAlbumService albumService,
IRefreshAlbumService refreshAlbumService,
IEventAggregator eventAggregator,
IManageCommandQueue commandQueueManager,
IMediaFileService mediaFileService,
IHistoryService historyService,
IDiskScanService diskScanService,
IRootFolderService rootFolderService,
ICheckIfArtistShouldBeRefreshed checkIfArtistShouldBeRefreshed,
IConfigService configService,
IImportListExclusionService importListExclusionService,
@ -54,9 +60,10 @@ namespace NzbDrone.Core.Music
_albumService = albumService;
_refreshAlbumService = refreshAlbumService;
_eventAggregator = eventAggregator;
_commandQueueManager = commandQueueManager;
_mediaFileService = mediaFileService;
_historyService = historyService;
_diskScanService = diskScanService;
_rootFolderService = rootFolderService;
_checkIfArtistShouldBeRefreshed = checkIfArtistShouldBeRefreshed;
_configService = configService;
_importListExclusionService = importListExclusionService;
@ -258,24 +265,24 @@ namespace NzbDrone.Core.Music
_eventAggregator.PublishEvent(new AlbumInfoRefreshedEvent(entity, newChildren, updateChildren));
}
private void RescanArtist(Artist artist, bool isNew, CommandTrigger trigger, bool infoUpdated)
private void Rescan(List<int> artistIds, bool isNew, CommandTrigger trigger, bool infoUpdated)
{
var rescanAfterRefresh = _configService.RescanAfterRefresh;
var shouldRescan = true;
if (isNew)
{
_logger.Trace("Forcing rescan of {0}. Reason: New artist", artist);
_logger.Trace("Forcing rescan. Reason: New artist added");
shouldRescan = true;
}
else if (rescanAfterRefresh == RescanAfterRefreshType.Never)
{
_logger.Trace("Skipping rescan of {0}. Reason: never recan after refresh", artist);
_logger.Trace("Skipping rescan. Reason: never rescan after refresh");
shouldRescan = false;
}
else if (rescanAfterRefresh == RescanAfterRefreshType.AfterManual && trigger != CommandTrigger.Manual)
{
_logger.Trace("Skipping rescan of {0}. Reason: not after automatic scans", artist);
_logger.Trace("Skipping rescan. Reason: not after automatic refreshes");
shouldRescan = false;
}
@ -289,14 +296,43 @@ namespace NzbDrone.Core.Music
// If some metadata has been updated then rescan unmatched files.
// Otherwise only scan files that haven't been seen before.
var filter = infoUpdated ? FilterFilesType.Matched : FilterFilesType.Known;
_diskScanService.Scan(artist, filter);
_logger.Trace($"InfoUpdated: {infoUpdated}, using scan filter {filter}");
var folders = _rootFolderService.All().Select(x => x.Path).ToList();
_commandQueueManager.Push(new RescanFoldersCommand(folders, filter, artistIds));
}
catch (Exception e)
{
_logger.Error(e, "Couldn't rescan artist {0}", artist);
_logger.Error(e, "Couldn't rescan");
}
}
private void RefreshSelectedArtists(List<int> artistIds, bool isNew, CommandTrigger trigger)
{
bool updated = false;
var artists = _artistService.GetArtists(artistIds);
foreach (var artist in artists)
{
try
{
updated |= RefreshEntityInfo(artist, null, true, false);
}
catch (Exception e)
{
_logger.Error(e, "Couldn't refresh info for {0}", artist);
}
}
Rescan(artistIds, isNew, trigger, updated);
}
public void Execute(BulkRefreshArtistCommand message)
{
RefreshSelectedArtists(message.ArtistIds, message.AreNewArtists, message.Trigger);
}
public void Execute(RefreshArtistCommand message)
{
var trigger = message.Trigger;
@ -304,49 +340,36 @@ namespace NzbDrone.Core.Music
if (message.ArtistId.HasValue)
{
var artist = _artistService.GetArtist(message.ArtistId.Value);
bool updated = false;
try
{
updated = RefreshEntityInfo(artist, null, true, false);
_logger.Trace($"Artist {artist} updated: {updated}");
RescanArtist(artist, isNew, trigger, updated);
}
catch (Exception e)
{
_logger.Error(e, "Couldn't refresh info for {0}", artist);
RescanArtist(artist, isNew, trigger, updated);
throw;
}
RefreshSelectedArtists(new List<int> { message.ArtistId.Value }, isNew, trigger);
}
else
{
var allArtists = _artistService.GetAllArtists().OrderBy(c => c.Name).ToList();
var updated = false;
var artists = _artistService.GetAllArtists().OrderBy(c => c.Name).ToList();
var artistIds = artists.Select(x => x.Id).ToList();
foreach (var artist in allArtists)
foreach (var artist in artists)
{
var manualTrigger = message.Trigger == CommandTrigger.Manual;
if (manualTrigger || _checkIfArtistShouldBeRefreshed.ShouldRefresh(artist))
{
bool updated = false;
try
{
updated = RefreshEntityInfo(artist, null, manualTrigger, false);
updated |= RefreshEntityInfo(artist, null, manualTrigger, false);
}
catch (Exception e)
{
_logger.Error(e, "Couldn't refresh info for {0}", artist);
}
RescanArtist(artist, false, trigger, updated);
}
else
{
_logger.Info("Skipping refresh of artist: {0}", artist.Name);
RescanArtist(artist, false, trigger, false);
}
}
Rescan(artistIds, isNew, trigger, updated);
}
}
}

View file

@ -15,7 +15,7 @@ namespace NzbDrone.Core.Music
public AddArtistValidator(RootFolderValidator rootFolderValidator,
ArtistPathValidator artistPathValidator,
ArtistAncestorValidator artistAncestorValidator,
ProfileExistsValidator profileExistsValidator,
QualityProfileExistsValidator qualityProfileExistsValidator,
MetadataProfileExistsValidator metadataProfileExistsValidator)
{
RuleFor(c => c.Path).Cascade(CascadeMode.StopOnFirstFailure)
@ -24,7 +24,7 @@ namespace NzbDrone.Core.Music
.SetValidator(artistPathValidator)
.SetValidator(artistAncestorValidator);
RuleFor(c => c.QualityProfileId).SetValidator(profileExistsValidator);
RuleFor(c => c.QualityProfileId).SetValidator(qualityProfileExistsValidator);
RuleFor(c => c.MetadataProfileId).SetValidator(metadataProfileExistsValidator);
}

View file

@ -326,9 +326,11 @@ namespace NzbDrone.Core.Parser
return null;
}
var artistName = artist.Name == "Various Artists" ? "VA" : artist.Name.RemoveAccent();
Logger.Debug("Parsing string '{0}' using search criteria artist: '{1}' album: '{2}'",
title,
artist.Name.RemoveAccent(),
artistName.RemoveAccent(),
string.Join(", ", album.Select(a => a.Title.RemoveAccent())));
var releaseTitle = RemoveFileExtension(title);
@ -339,7 +341,7 @@ namespace NzbDrone.Core.Parser
simpleTitle = CleanTorrentSuffixRegex.Replace(simpleTitle);
var escapedArtist = Regex.Escape(artist.Name.RemoveAccent()).Replace(@"\ ", @"[\W_]");
var escapedArtist = Regex.Escape(artistName.RemoveAccent()).Replace(@"\ ", @"[\W_]");
var escapedAlbums = string.Join("|", album.Select(s => Regex.Escape(s.Title.RemoveAccent())).ToList()).Replace(@"\ ", @"[\W_]");
var releaseRegex = new Regex(@"^(\W*|\b)(?<artist>" + escapedArtist + @")(\W*|\b).*(\W*|\b)(?<album>" + escapedAlbums + @")(\W*|\b)", RegexOptions.IgnoreCase);
@ -492,10 +494,8 @@ namespace NzbDrone.Core.Parser
public static string CleanArtistName(this string name)
{
long number = 0;
//If Title only contains numbers return it as is.
if (long.TryParse(name, out number))
// If Title only contains numbers return it as is.
if (long.TryParse(name, out _))
{
return name;
}
@ -650,9 +650,6 @@ namespace NzbDrone.Core.Parser
artistName = artistName.Trim(' ');
int trackNumber;
int.TryParse(matchCollection[0].Groups["trackNumber"].Value, out trackNumber);
ParsedTrackInfo result = new ParsedTrackInfo();
result.ArtistTitle = artistName;

View file

@ -6,6 +6,7 @@ using NzbDrone.Core.ImportLists;
using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Music;
using NzbDrone.Core.RootFolders;
namespace NzbDrone.Core.Profiles.Metadata
{
@ -25,16 +26,19 @@ namespace NzbDrone.Core.Profiles.Metadata
private readonly IMetadataProfileRepository _profileRepository;
private readonly IArtistService _artistService;
private readonly IImportListFactory _importListFactory;
private readonly IRootFolderService _rootFolderService;
private readonly Logger _logger;
public MetadataProfileService(IMetadataProfileRepository profileRepository,
IArtistService artistService,
IImportListFactory importListFactory,
IRootFolderService rootFolderService,
Logger logger)
{
_profileRepository = profileRepository;
_artistService = artistService;
_importListFactory = importListFactory;
_rootFolderService = rootFolderService;
_logger = logger;
}
@ -59,7 +63,8 @@ namespace NzbDrone.Core.Profiles.Metadata
if (profile.Name == NONE_PROFILE_NAME ||
_artistService.GetAllArtists().Any(c => c.MetadataProfileId == id) ||
_importListFactory.All().Any(c => c.MetadataProfileId == id))
_importListFactory.All().Any(c => c.MetadataProfileId == id) ||
_rootFolderService.All().Any(c => c.DefaultMetadataProfileId == id))
{
throw new MetadataProfileInUseException(profile.Name);
}

View file

@ -6,6 +6,7 @@ using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Music;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.RootFolders;
namespace NzbDrone.Core.Profiles.Qualities
{
@ -25,13 +26,19 @@ namespace NzbDrone.Core.Profiles.Qualities
private readonly IProfileRepository _profileRepository;
private readonly IArtistService _artistService;
private readonly IImportListFactory _importListFactory;
private readonly IRootFolderService _rootFolderService;
private readonly Logger _logger;
public QualityProfileService(IProfileRepository profileRepository, IArtistService artistService, IImportListFactory importListFactory, Logger logger)
public QualityProfileService(IProfileRepository profileRepository,
IArtistService artistService,
IImportListFactory importListFactory,
IRootFolderService rootFolderService,
Logger logger)
{
_profileRepository = profileRepository;
_artistService = artistService;
_importListFactory = importListFactory;
_rootFolderService = rootFolderService;
_logger = logger;
}
@ -47,7 +54,9 @@ namespace NzbDrone.Core.Profiles.Qualities
public void Delete(int id)
{
if (_artistService.GetAllArtists().Any(c => c.QualityProfileId == id) || _importListFactory.All().Any(c => c.ProfileId == id))
if (_artistService.GetAllArtists().Any(c => c.QualityProfileId == id) ||
_importListFactory.All().Any(c => c.ProfileId == id) ||
_rootFolderService.All().Any(c => c.DefaultQualityProfileId == id))
{
var profile = _profileRepository.Get(id);
throw new QualityProfileInUseException(profile.Name);

View file

@ -1,16 +1,20 @@
using System.Collections.Generic;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Music;
namespace NzbDrone.Core.RootFolders
{
public class RootFolder : ModelBase
{
public string Name { get; set; }
public string Path { get; set; }
public int DefaultMetadataProfileId { get; set; }
public int DefaultQualityProfileId { get; set; }
public MonitorTypes DefaultMonitorOption { get; set; }
public HashSet<int> DefaultTags { get; set; }
public bool Accessible { get; set; }
public long? FreeSpace { get; set; }
public long? TotalSpace { get; set; }
public List<UnmappedFolder> UnmappedFolders { get; set; }
}
}

View file

@ -15,5 +15,12 @@ namespace NzbDrone.Core.RootFolders
}
protected override bool PublishModelEvents => true;
public new void Delete(int id)
{
var model = Get(id);
base.Delete(id);
ModelDeleted(model);
}
}
}

View file

@ -7,17 +7,22 @@ using NLog;
using NzbDrone.Common;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Music;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.MediaFiles.Commands;
using NzbDrone.Core.Messaging.Commands;
namespace NzbDrone.Core.RootFolders
{
public interface IRootFolderService
{
List<RootFolder> All();
List<RootFolder> AllWithUnmappedFolders();
RootFolder Add(RootFolder rootDir);
List<RootFolder> AllWithSpaceStats();
RootFolder Add(RootFolder rootFolder);
RootFolder Update(RootFolder rootFolder);
void Remove(int id);
RootFolder Get(int id);
List<RootFolder> AllForTag(int tagId);
RootFolder GetBestRootFolder(string path);
string GetBestRootFolderPath(string path);
}
@ -25,30 +30,17 @@ namespace NzbDrone.Core.RootFolders
{
private readonly IRootFolderRepository _rootFolderRepository;
private readonly IDiskProvider _diskProvider;
private readonly IArtistRepository _artistRepository;
private readonly IManageCommandQueue _commandQueueManager;
private readonly Logger _logger;
private static readonly HashSet<string> SpecialFolders = new HashSet<string>
{
"$recycle.bin",
"system volume information",
"recycler",
"lost+found",
".appledb",
".appledesktop",
".appledouble",
"@eadir",
".grab"
};
public RootFolderService(IRootFolderRepository rootFolderRepository,
IDiskProvider diskProvider,
IArtistRepository artistRepository,
IManageCommandQueue commandQueueManager,
Logger logger)
{
_rootFolderRepository = rootFolderRepository;
_diskProvider = diskProvider;
_artistRepository = artistRepository;
_commandQueueManager = commandQueueManager;
_logger = logger;
}
@ -59,7 +51,7 @@ namespace NzbDrone.Core.RootFolders
return rootFolders;
}
public List<RootFolder> AllWithUnmappedFolders()
public List<RootFolder> AllWithSpaceStats()
{
var rootFolders = _rootFolderRepository.All().ToList();
@ -77,17 +69,14 @@ namespace NzbDrone.Core.RootFolders
catch (Exception ex)
{
_logger.Error(ex, "Unable to get free space and unmapped folders for root folder {0}", folder.Path);
folder.UnmappedFolders = new List<UnmappedFolder>();
}
});
return rootFolders;
}
public RootFolder Add(RootFolder rootFolder)
private void VerifyRootFolder(RootFolder rootFolder)
{
var all = All();
if (string.IsNullOrWhiteSpace(rootFolder.Path) || !Path.IsPathRooted(rootFolder.Path))
{
throw new ArgumentException("Invalid path");
@ -98,18 +87,36 @@ namespace NzbDrone.Core.RootFolders
throw new DirectoryNotFoundException("Can't add root directory that doesn't exist.");
}
if (all.Exists(r => r.Path.PathEquals(rootFolder.Path)))
{
throw new InvalidOperationException("Recent directory already exists.");
}
if (!_diskProvider.FolderWritable(rootFolder.Path))
{
throw new UnauthorizedAccessException(string.Format("Root folder path '{0}' is not writable by user '{1}'", rootFolder.Path, Environment.UserName));
}
}
public RootFolder Add(RootFolder rootFolder)
{
VerifyRootFolder(rootFolder);
if (All().Exists(r => r.Path.PathEquals(rootFolder.Path)))
{
throw new InvalidOperationException("Root folder already exists.");
}
_rootFolderRepository.Insert(rootFolder);
_commandQueueManager.Push(new RescanFoldersCommand(new List<string> { rootFolder.Path }, FilterFilesType.None, null));
GetDetails(rootFolder);
return rootFolder;
}
public RootFolder Update(RootFolder rootFolder)
{
VerifyRootFolder(rootFolder);
_rootFolderRepository.Update(rootFolder);
GetDetails(rootFolder);
return rootFolder;
@ -120,40 +127,6 @@ namespace NzbDrone.Core.RootFolders
_rootFolderRepository.Delete(id);
}
private List<UnmappedFolder> GetUnmappedFolders(string path)
{
_logger.Debug("Generating list of unmapped folders");
if (string.IsNullOrEmpty(path))
{
throw new ArgumentException("Invalid path provided", nameof(path));
}
var results = new List<UnmappedFolder>();
var artist = _artistRepository.All().ToList();
if (!_diskProvider.FolderExists(path))
{
_logger.Debug("Path supplied does not exist: {0}", path);
return results;
}
var possibleArtistFolders = _diskProvider.GetDirectories(path).ToList();
var unmappedFolders = possibleArtistFolders.Except(artist.Select(s => s.Path), PathEqualityComparer.Instance).ToList();
foreach (string unmappedFolder in unmappedFolders)
{
var di = new DirectoryInfo(unmappedFolder.Normalize());
results.Add(new UnmappedFolder { Name = di.Name, Path = di.FullName });
}
var setToRemove = SpecialFolders;
results.RemoveAll(x => setToRemove.Contains(new DirectoryInfo(x.Path.ToLowerInvariant()).Name));
_logger.Debug("{0} unmapped folders detected.", results.Count);
return results.OrderBy(u => u.Name, StringComparer.InvariantCultureIgnoreCase).ToList();
}
public RootFolder Get(int id)
{
var rootFolder = _rootFolderRepository.Get(id);
@ -162,11 +135,21 @@ namespace NzbDrone.Core.RootFolders
return rootFolder;
}
public string GetBestRootFolderPath(string path)
public List<RootFolder> AllForTag(int tagId)
{
var possibleRootFolder = All().Where(r => r.Path.IsParentPath(path))
return All().Where(r => r.DefaultTags.Contains(tagId)).ToList();
}
public RootFolder GetBestRootFolder(string path)
{
return All().Where(r => PathEqualityComparer.Instance.Equals(r.Path, path) || r.Path.IsParentPath(path))
.OrderByDescending(r => r.Path.Length)
.FirstOrDefault();
}
public string GetBestRootFolderPath(string path)
{
var possibleRootFolder = GetBestRootFolder(path);
if (possibleRootFolder == null)
{
@ -185,7 +168,6 @@ namespace NzbDrone.Core.RootFolders
rootFolder.Accessible = true;
rootFolder.FreeSpace = _diskProvider.GetAvailableSpace(rootFolder.Path);
rootFolder.TotalSpace = _diskProvider.GetTotalSize(rootFolder.Path);
rootFolder.UnmappedFolders = GetUnmappedFolders(rootFolder.Path);
}
}).Wait(5000);
}

View file

@ -12,12 +12,13 @@ namespace NzbDrone.Core.Tags
public List<int> RestrictionIds { get; set; }
public List<int> DelayProfileIds { get; set; }
public List<int> ImportListIds { get; set; }
public List<int> RootFolderIds { get; set; }
public bool InUse
{
get
{
return ArtistIds.Any() || NotificationIds.Any() || RestrictionIds.Any() || DelayProfileIds.Any() || ImportListIds.Any();
return ArtistIds.Any() || NotificationIds.Any() || RestrictionIds.Any() || DelayProfileIds.Any() || ImportListIds.Any() || RootFolderIds.Any();
}
}
}

View file

@ -7,6 +7,7 @@ using NzbDrone.Core.Music;
using NzbDrone.Core.Notifications;
using NzbDrone.Core.Profiles.Delay;
using NzbDrone.Core.Profiles.Releases;
using NzbDrone.Core.RootFolders;
namespace NzbDrone.Core.Tags
{
@ -31,6 +32,7 @@ namespace NzbDrone.Core.Tags
private readonly INotificationFactory _notificationFactory;
private readonly IReleaseProfileService _releaseProfileService;
private readonly IArtistService _artistService;
private readonly IRootFolderService _rootFolderService;
public TagService(ITagRepository repo,
IEventAggregator eventAggregator,
@ -38,7 +40,8 @@ namespace NzbDrone.Core.Tags
ImportListFactory importListFactory,
INotificationFactory notificationFactory,
IReleaseProfileService releaseProfileService,
IArtistService artistService)
IArtistService artistService,
IRootFolderService rootFolderService)
{
_repo = repo;
_eventAggregator = eventAggregator;
@ -47,6 +50,7 @@ namespace NzbDrone.Core.Tags
_notificationFactory = notificationFactory;
_releaseProfileService = releaseProfileService;
_artistService = artistService;
_rootFolderService = rootFolderService;
}
public Tag GetTag(int tagId)
@ -74,6 +78,7 @@ namespace NzbDrone.Core.Tags
var notifications = _notificationFactory.AllForTag(tagId);
var restrictions = _releaseProfileService.AllForTag(tagId);
var artist = _artistService.AllForTag(tagId);
var rootFolders = _rootFolderService.AllForTag(tagId);
return new TagDetails
{
@ -83,7 +88,8 @@ namespace NzbDrone.Core.Tags
ImportListIds = importLists.Select(c => c.Id).ToList(),
NotificationIds = notifications.Select(c => c.Id).ToList(),
RestrictionIds = restrictions.Select(c => c.Id).ToList(),
ArtistIds = artist.Select(c => c.Id).ToList()
ArtistIds = artist.Select(c => c.Id).ToList(),
RootFolderIds = rootFolders.Select(c => c.Id).ToList()
};
}
@ -95,6 +101,7 @@ namespace NzbDrone.Core.Tags
var notifications = _notificationFactory.All();
var restrictions = _releaseProfileService.All();
var artists = _artistService.GetAllArtists();
var rootFolders = _rootFolderService.All();
var details = new List<TagDetails>();
@ -108,7 +115,8 @@ namespace NzbDrone.Core.Tags
ImportListIds = importLists.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
NotificationIds = notifications.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
RestrictionIds = restrictions.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
ArtistIds = artists.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList()
ArtistIds = artists.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
RootFolderIds = rootFolders.Where(c => c.DefaultTags.Contains(tag.Id)).Select(c => c.Id).ToList()
});
}

View file

@ -3,11 +3,11 @@ using NzbDrone.Core.Profiles.Qualities;
namespace NzbDrone.Core.Validation
{
public class ProfileExistsValidator : PropertyValidator
public class QualityProfileExistsValidator : PropertyValidator
{
private readonly IProfileService _profileService;
public ProfileExistsValidator(IProfileService profileService)
public QualityProfileExistsValidator(IProfileService profileService)
: base("Quality Profile does not exist")
{
_profileService = profileService;

View file

@ -115,7 +115,6 @@ namespace NzbDrone.Integration.Test.ApiTests
result.Should().HaveCount(1);
result.First().Should().ContainKey("path");
result.First().Should().ContainKey("relativePath");
result.First().Should().ContainKey("name");
result.First()["name"].Should().Be("somevideo.mp3");

View file

@ -1,6 +1,8 @@
using System.Linq;
using FluentAssertions;
using Lidarr.Api.V1.RootFolders;
using NUnit.Framework;
using NzbDrone.Core.Music;
using NzbDrone.Core.Qualities;
namespace NzbDrone.Integration.Test.ApiTests
@ -8,6 +10,20 @@ namespace NzbDrone.Integration.Test.ApiTests
[TestFixture]
public class WantedFixture : IntegrationTest
{
[SetUp]
public void Setup()
{
// Add a root folder
RootFolders.Post(new RootFolderResource
{
Name = "TestLibrary",
Path = ArtistRootFolder,
DefaultMetadataProfileId = 1,
DefaultQualityProfileId = 1,
DefaultMonitorOption = MonitorTypes.All
});
}
[Test]
[Order(0)]
public void missing_should_be_empty()