New: Manual import improvements (#683)

* New: Manual import improvements

 - Detect and merge import with files already in library.
 - Allow selection of album release from Manual Import modal.
 - Loading indicator while fetching updated decisions

* Disable release switching if user manually overrode release
This commit is contained in:
ta264 2019-04-04 09:20:47 +01:00 committed by GitHub
commit 188e0e1040
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 1295 additions and 371 deletions

View file

@ -119,7 +119,6 @@
<Compile Include="Parse\ParseModule.cs" />
<Compile Include="Parse\ParseResource.cs" />
<Compile Include="ManualImport\ManualImportModule.cs" />
<Compile Include="ManualImport\ManualImportModuleWithSignalR.cs" />
<Compile Include="ManualImport\ManualImportResource.cs" />
<Compile Include="Profiles\Delay\DelayProfileModule.cs" />
<Compile Include="Profiles\Delay\DelayProfileResource.cs" />
@ -255,4 +254,4 @@
<Target Name="AfterBuild">
</Target>
-->
</Project>
</Project>

View file

@ -3,38 +3,39 @@ using System.Linq;
using NzbDrone.Core.MediaFiles.TrackImport.Manual;
using NzbDrone.Core.Qualities;
using Lidarr.Http.Extensions;
using NzbDrone.SignalR;
using NzbDrone.Core.Music;
using NLog;
using Nancy;
using Lidarr.Http;
namespace Lidarr.Api.V1.ManualImport
{
public class ManualImportModule : ManualImportModuleWithSignalR
public class ManualImportModule : LidarrRestModule<ManualImportResource>
{
private readonly IArtistService _artistService;
private readonly IAlbumService _albumService;
private readonly IReleaseService _releaseService;
private readonly IManualImportService _manualImportService;
private readonly Logger _logger;
public ManualImportModule(IManualImportService manualImportService,
IArtistService artistService,
IAlbumService albumService,
IReleaseService releaseService,
IBroadcastSignalRMessage signalRBroadcaster,
Logger logger)
: base(manualImportService, signalRBroadcaster, logger)
{
_artistService = artistService;
_albumService = albumService;
_releaseService = releaseService;
_manualImportService = manualImportService;
_logger = logger;
GetResourceAll = GetMediaFiles;
Put["/"] = options =>
{
var resource = Request.Body.FromJson<List<ManualImportResource>>();
UpdateImportItems(resource);
return GetManualImportItems(resource.Select(x => x.Id)).AsResponse(HttpStatusCode.Accepted);
return UpdateImportItems(resource).AsResponse(HttpStatusCode.Accepted);
};
}
@ -43,8 +44,9 @@ namespace Lidarr.Api.V1.ManualImport
var folder = (string)Request.Query.folder;
var downloadId = (string)Request.Query.downloadId;
var filterExistingFiles = Request.GetBooleanQueryParameter("filterExistingFiles", true);
var replaceExistingFiles = Request.GetBooleanQueryParameter("replaceExistingFiles", true);
return _manualImportService.GetMediaFiles(folder, downloadId, filterExistingFiles).ToResource().Select(AddQualityWeight).ToList();
return _manualImportService.GetMediaFiles(folder, downloadId, filterExistingFiles, replaceExistingFiles).ToResource().Select(AddQualityWeight).ToList();
}
private ManualImportResource AddQualityWeight(ManualImportResource item)
@ -59,7 +61,7 @@ namespace Lidarr.Api.V1.ManualImport
return item;
}
private void UpdateImportItems(List<ManualImportResource> resources)
private List<ManualImportResource> UpdateImportItems(List<ManualImportResource> resources)
{
var items = new List<ManualImportItem>();
foreach (var resource in resources)
@ -76,12 +78,14 @@ namespace Lidarr.Api.V1.ManualImport
Release = resource.AlbumReleaseId == 0 ? null : _releaseService.GetRelease(resource.AlbumReleaseId),
Quality = resource.Quality,
Language = resource.Language,
DownloadId = resource.DownloadId
DownloadId = resource.DownloadId,
AdditionalFile = resource.AdditionalFile,
ReplaceExistingFiles = resource.ReplaceExistingFiles,
DisableReleaseSwitching = resource.DisableReleaseSwitching
});
}
//recalculate import and broadcast
_manualImportService.UpdateItems(items);
return _manualImportService.UpdateItems(items).Select(x => x.ToResource()).ToList();
}
}
}

View file

@ -1,50 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Core.MediaFiles.TrackImport.Manual;
using NzbDrone.Core.Qualities;
using Lidarr.Http;
using Lidarr.Http.Extensions;
using NzbDrone.SignalR;
using NLog;
namespace Lidarr.Api.V1.ManualImport
{
public abstract class ManualImportModuleWithSignalR : LidarrRestModuleWithSignalR<ManualImportResource, ManualImportItem>
{
protected readonly IManualImportService _manualImportService;
protected readonly Logger _logger;
protected ManualImportModuleWithSignalR(IManualImportService manualImportService,
IBroadcastSignalRMessage signalRBroadcaster,
Logger logger)
: base(signalRBroadcaster)
{
_manualImportService = manualImportService;
_logger = logger;
GetResourceById = GetManualImportItem;
}
protected ManualImportModuleWithSignalR(IManualImportService manualImportService,
IBroadcastSignalRMessage signalRBroadcaster,
Logger logger,
string resource)
: base(signalRBroadcaster, resource)
{
_manualImportService = manualImportService;
_logger = logger;
GetResourceById = GetManualImportItem;
}
protected ManualImportResource GetManualImportItem(int id)
{
return _manualImportService.Find(id).ToResource();
}
protected List<ManualImportResource> GetManualImportItems(IEnumerable<int> ids)
{
return ids.Select(x => _manualImportService.Find(x).ToResource()).ToList();
}
}
}

View file

@ -1,4 +1,3 @@
using NzbDrone.Common.Crypto;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.MediaFiles.TrackImport.Manual;
using NzbDrone.Core.Qualities;
@ -9,7 +8,6 @@ using Lidarr.Api.V1.Tracks;
using Lidarr.Http.REST;
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Core.Music;
using NzbDrone.Core.Parser.Model;
namespace Lidarr.Api.V1.ManualImport
@ -31,6 +29,9 @@ namespace Lidarr.Api.V1.ManualImport
public string DownloadId { get; set; }
public IEnumerable<Rejection> Rejections { get; set; }
public ParsedTrackInfo AudioTags { get; set; }
public bool AdditionalFile { get; set; }
public bool ReplaceExistingFiles { get; set; }
public bool DisableReleaseSwitching { get; set; }
}
public static class ManualImportResourceMapper
@ -56,7 +57,10 @@ namespace Lidarr.Api.V1.ManualImport
//QualityWeight
DownloadId = model.DownloadId,
Rejections = model.Rejections,
AudioTags = model.Tags
AudioTags = model.Tags,
AdditionalFile = model.AdditionalFile,
ReplaceExistingFiles = model.ReplaceExistingFiles,
DisableReleaseSwitching = model.DisableReleaseSwitching
};
}

View file

@ -78,11 +78,21 @@ namespace Lidarr.Api.V1.TrackFiles
if (albumIdQuery.HasValue)
{
int albumId = Convert.ToInt32(albumIdQuery.Value);
var album = _albumService.GetAlbum(albumId);
var albumArtist = _artistService.GetArtist(album.ArtistId);
string albumIdValue = albumIdQuery.Value.ToString();
return _mediaFileService.GetFilesByAlbum(album.Id).ConvertAll(f => f.ToResource(albumArtist, _upgradableSpecification));
var albumIds = albumIdValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(e => Convert.ToInt32(e))
.ToList();
var result = new List<TrackFileResource>();
foreach (var albumId in albumIds)
{
var album = _albumService.GetAlbum(albumId);
var albumArtist = _artistService.GetArtist(album.ArtistId);
result.AddRange(_mediaFileService.GetFilesByAlbum(album.Id).ConvertAll(f => f.ToResource(albumArtist, _upgradableSpecification)));
}
return result;
}
else

View file

@ -47,7 +47,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
.Returns(_rootFolder);
Mocker.GetMock<IMakeImportDecision>()
.Setup(v => v.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Artist>()))
.Setup(v => v.GetImportDecisions(It.IsAny<List<string>>(), It.IsAny<Artist>(), It.IsAny<bool>()))
.Returns(new List<ImportDecision<LocalTrack>>());
Mocker.GetMock<IMediaFileService>()
@ -115,7 +115,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
.Verify(v => v.Clean(It.IsAny<Artist>(), It.IsAny<List<string>>()), Times.Never());
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.IsAny<List<string>>(), _artist), Times.Never());
.Verify(v => v.GetImportDecisions(It.IsAny<List<string>>(), _artist, false), Times.Never());
}
[Test]
@ -162,7 +162,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
.Verify(v => v.Clean(It.IsAny<Artist>(), It.IsAny<List<string>>()), Times.Once());
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.IsAny<List<string>>(), _artist), Times.Never());
.Verify(v => v.GetImportDecisions(It.IsAny<List<string>>(), _artist, false), Times.Never());
}
[Test]
@ -180,7 +180,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
.Verify(v => v.Clean(It.IsAny<Artist>(), It.IsAny<List<string>>()), Times.Once());
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.IsAny<List<string>>(), _artist), Times.Never());
.Verify(v => v.GetImportDecisions(It.IsAny<List<string>>(), _artist, false), Times.Never());
}
[Test]
@ -197,7 +197,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
Subject.Scan(_artist);
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 2), _artist), Times.Once());
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 2), _artist, false), Times.Once());
}
[Test]
@ -220,7 +220,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
.Verify(v => v.GetFiles(It.IsAny<string>(), It.IsAny<SearchOption>()), Times.Once());
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _artist), Times.Once());
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _artist, false), Times.Once());
}
[Test]
@ -238,7 +238,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
Subject.Scan(_artist);
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _artist), Times.Once());
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _artist, false), Times.Once());
}
[Test]
@ -261,7 +261,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
Subject.Scan(_artist);
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 4), _artist), Times.Once());
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 4), _artist, false), Times.Once());
}
[Test]
@ -277,7 +277,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
Subject.Scan(_artist);
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _artist), Times.Once());
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _artist, false), Times.Once());
}
[Test]
@ -296,7 +296,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
Subject.Scan(_artist);
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _artist), Times.Once());
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _artist, false), Times.Once());
}
[Test]
@ -316,7 +316,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
Subject.Scan(_artist);
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _artist), Times.Once());
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _artist, false), Times.Once());
}
[Test]
@ -333,7 +333,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
Subject.Scan(_artist);
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _artist), Times.Once());
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _artist, false), Times.Once());
}
[Test]
@ -350,7 +350,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
Subject.Scan(_artist);
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _artist), Times.Once());
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _artist, false), Times.Once());
}
[Test]
@ -369,7 +369,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
Subject.Scan(_artist);
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 2), _artist), Times.Once());
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 2), _artist, false), Times.Once());
}
[Test]
@ -387,7 +387,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
Subject.Scan(_artist);
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _artist), Times.Once());
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _artist, false), Times.Once());
}
}
}

View file

@ -160,7 +160,9 @@ namespace NzbDrone.Core.Test.MediaFiles
[Test]
public void should_not_move_existing_files()
{
Subject.Import(new List<ImportDecision<LocalTrack>> { _approvedDecisions.First() }, false);
var track = _approvedDecisions.First();
track.Item.ExistingFile = true;
Subject.Import(new List<ImportDecision<LocalTrack>> { track }, false);
Mocker.GetMock<IUpgradeMediaFiles>()
.Verify(v => v.UpgradeTrackFile(It.IsAny<TrackFile>(), _approvedDecisions.First().Item, false),
@ -215,13 +217,15 @@ namespace NzbDrone.Core.Test.MediaFiles
}
[Test]
public void should_delete_existing_metadata_files_with_the_same_path()
public void should_delete_existing_trackfiles_with_the_same_path()
{
Mocker.GetMock<IMediaFileService>()
.Setup(s => s.GetFilesWithRelativePath(It.IsAny<int>(), It.IsAny<string>()))
.Returns(Builder<TrackFile>.CreateListOfSize(1).BuildList());
Subject.Import(new List<ImportDecision<LocalTrack>> { _approvedDecisions.First() }, false);
var track = _approvedDecisions.First();
track.Item.ExistingFile = true;
Subject.Import(new List<ImportDecision<LocalTrack>> { track }, false);
Mocker.GetMock<IMediaFileService>()
.Verify(v => v.Delete(It.IsAny<TrackFile>(), DeleteMediaFileReason.ManualOverride), Times.Once());

View file

@ -122,7 +122,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
var local = GivenLocalAlbumRelease();
Subject.GetCandidatesFromFingerprint(local).ShouldBeEquivalentTo(new List<AlbumRelease>());
Subject.GetCandidatesFromFingerprint(local, null, null, null, false).ShouldBeEquivalentTo(new List<CandidateAlbumRelease>());
}
[Test]
@ -133,7 +133,9 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
var localTracks = GivenLocalTracks(tracks, release);
var localAlbumRelease = new LocalAlbumRelease(localTracks);
Subject.GetCandidatesFromTags(localAlbumRelease, null, null, release).ShouldBeEquivalentTo(new List<AlbumRelease> { release });
Subject.GetCandidatesFromTags(localAlbumRelease, null, null, release, false).ShouldBeEquivalentTo(
new List<CandidateAlbumRelease> { new CandidateAlbumRelease(release) }
);
}
[Test]
@ -149,7 +151,9 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
.Setup(x => x.GetReleaseByForeignReleaseId("xxx"))
.Returns(release);
Subject.GetCandidatesFromTags(localAlbumRelease, null, null, null).ShouldBeEquivalentTo(new List<AlbumRelease> { release });
Subject.GetCandidatesFromTags(localAlbumRelease, null, null, null, false).ShouldBeEquivalentTo(
new List<CandidateAlbumRelease> { new CandidateAlbumRelease(release) }
);
}
}
}

View file

@ -176,7 +176,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
GivenFingerprints(testcase.Fingerprints);
}
var result = Subject.Identify(tracks, specifiedArtist, null, null, testcase.NewDownload, testcase.SingleRelease);
var result = Subject.Identify(tracks, specifiedArtist, null, null, testcase.NewDownload, testcase.SingleRelease, false);
TestLogger.Debug($"Found releases:\n{result.Where(x => x.AlbumRelease != null).Select(x => x.AlbumRelease?.ForeignReleaseId).ToJson()}");

View file

@ -98,19 +98,23 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
Artist = _artist,
Quality = _quality,
Tracks = new List<Track> { new Track() },
Path = @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV.avi"
Path = @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV.avi".AsOsAgnostic()
};
GivenVideoFiles(new List<string> { @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV.avi".AsOsAgnostic() });
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>()))
.Returns((List<LocalTrack> tracks, Artist artist, Album album, AlbumRelease release, bool newDownload, bool singleRelease) => {
.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) => {
var ret = new LocalAlbumRelease(tracks);
ret.AlbumRelease = _albumRelease;
return new List<LocalAlbumRelease> { ret };
});
Mocker.GetMock<IMediaFileService>()
.Setup(c => c.FilterExistingFiles(It.IsAny<List<string>>(), It.IsAny<Artist>()))
.Returns((List<string> files, Artist artist) => files);
GivenSpecifications(_albumpass1);
}
@ -119,7 +123,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
Mocker.SetConstant(mocks.Select(c => c.Object));
}
private void GivenVideoFiles(IEnumerable<string> videoFiles)
private void GivenAudioFiles(IEnumerable<string> videoFiles)
{
_audioFiles = videoFiles.ToList();
@ -145,7 +149,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
GivenAugmentationSuccess();
GivenSpecifications(_albumpass1, _albumpass2, _albumpass3, _albumfail1, _albumfail2, _albumfail3);
Subject.GetImportDecisions(_audioFiles, new Artist(), null, downloadClientItem, null, false, false, false);
Subject.GetImportDecisions(_audioFiles, new Artist(), null, null, downloadClientItem, null, false, false, false, false);
_albumfail1.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalAlbumRelease>()), Times.Once());
_albumfail2.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalAlbumRelease>()), Times.Once());
@ -162,7 +166,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
GivenAugmentationSuccess();
GivenSpecifications(_pass1, _pass2, _pass3, _fail1, _fail2, _fail3);
Subject.GetImportDecisions(_audioFiles, new Artist(), null, downloadClientItem, null, false, false, false);
Subject.GetImportDecisions(_audioFiles, new Artist(), null, null, downloadClientItem, null, false, false, false, false);
_fail1.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalTrack>()), Times.Once());
_fail2.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalTrack>()), Times.Once());
@ -180,7 +184,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
GivenSpecifications(_albumpass1, _albumpass2, _albumpass3, _albumfail1, _albumfail2, _albumfail3);
GivenSpecifications(_pass1, _pass2, _pass3, _fail1, _fail2, _fail3);
Subject.GetImportDecisions(_audioFiles, new Artist(), null, downloadClientItem, null, false, false, false);
Subject.GetImportDecisions(_audioFiles, new Artist(), null, null, downloadClientItem, null, false, false, false, false);
_fail1.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalTrack>()), Times.Never());
_fail2.Verify(c => c.IsSatisfiedBy(It.IsAny<LocalTrack>()), Times.Never());
@ -196,7 +200,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
GivenSpecifications(_albumfail1);
GivenSpecifications(_pass1);
var result = Subject.GetImportDecisions(_audioFiles, new Artist());
var result = Subject.GetImportDecisions(_audioFiles, new Artist(), false);
result.Single().Approved.Should().BeFalse();
}
@ -207,7 +211,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
GivenSpecifications(_albumpass1);
GivenSpecifications(_fail1);
var result = Subject.GetImportDecisions(_audioFiles, new Artist());
var result = Subject.GetImportDecisions(_audioFiles, new Artist(), false);
result.Single().Approved.Should().BeFalse();
}
@ -218,7 +222,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
GivenSpecifications(_albumpass1, _albumfail1, _albumpass2, _albumpass3);
GivenSpecifications(_pass1, _pass2, _pass3);
var result = Subject.GetImportDecisions(_audioFiles, new Artist());
var result = Subject.GetImportDecisions(_audioFiles, new Artist(), false);
result.Single().Approved.Should().BeFalse();
}
@ -229,7 +233,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
GivenSpecifications(_albumpass1, _albumpass2, _albumpass3);
GivenSpecifications(_pass1, _fail1, _pass2, _pass3);
var result = Subject.GetImportDecisions(_audioFiles, new Artist());
var result = Subject.GetImportDecisions(_audioFiles, new Artist(), false);
result.Single().Approved.Should().BeFalse();
}
@ -241,7 +245,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
GivenSpecifications(_albumpass1, _albumpass2, _albumpass3);
GivenSpecifications(_pass1, _pass2, _pass3);
var result = Subject.GetImportDecisions(_audioFiles, new Artist());
var result = Subject.GetImportDecisions(_audioFiles, new Artist(), false);
result.Single().Approved.Should().BeTrue();
}
@ -252,7 +256,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
GivenAugmentationSuccess();
GivenSpecifications(_pass1, _pass2, _pass3, _fail1, _fail2, _fail3);
var result = Subject.GetImportDecisions(_audioFiles, new Artist());
var result = Subject.GetImportDecisions(_audioFiles, new Artist(), false);
result.Single().Rejections.Should().HaveCount(3);
}
@ -265,16 +269,14 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
.Setup(c => c.Augment(It.IsAny<LocalTrack>(), It.IsAny<bool>()))
.Throws<TestException>();
_audioFiles = new List<string>
GivenAudioFiles(new []
{
"The.Office.S03E115.DVDRip.XviD-OSiTV",
"The.Office.S03E115.DVDRip.XviD-OSiTV",
"The.Office.S03E115.DVDRip.XviD-OSiTV"
};
@"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV".AsOsAgnostic(),
@"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV".AsOsAgnostic(),
@"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV".AsOsAgnostic()
});
GivenVideoFiles(_audioFiles);
Subject.GetImportDecisions(_audioFiles, _artist);
Subject.GetImportDecisions(_audioFiles, _artist, false);
Mocker.GetMock<IAugmentingService>()
.Verify(c => c.Augment(It.IsAny<LocalTrack>(), It.IsAny<bool>()), Times.Exactly(_audioFiles.Count));
@ -287,22 +289,20 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
{
GivenSpecifications(_pass1);
_audioFiles = new List<string>
{
"The.Office.S03E115.DVDRip.XviD-OSiTV",
"The.Office.S03E115.DVDRip.XviD-OSiTV",
"The.Office.S03E115.DVDRip.XviD-OSiTV"
};
GivenVideoFiles(_audioFiles);
GivenAudioFiles(new []
{
@"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV".AsOsAgnostic(),
@"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV".AsOsAgnostic(),
@"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV".AsOsAgnostic()
});
Mocker.GetMock<IIdentificationService>()
.Setup(s => s.Identify(It.IsAny<List<LocalTrack>>(), It.IsAny<Artist>(), It.IsAny<Album>(), It.IsAny<AlbumRelease>(), It.IsAny<bool>(), It.IsAny<bool>()))
.Returns((List<LocalTrack> tracks, Artist artist, Album album, AlbumRelease release, bool newDownload, bool singleRelease) => {
.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) };
});
var decisions = Subject.GetImportDecisions(_audioFiles, _artist);
var decisions = Subject.GetImportDecisions(_audioFiles, _artist, false);
Mocker.GetMock<IAugmentingService>()
.Verify(c => c.Augment(It.IsAny<LocalTrack>(), It.IsAny<bool>()), Times.Exactly(_audioFiles.Count));
@ -316,16 +316,14 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
{
GivenSpecifications(_pass1);
_audioFiles = new List<string>
GivenAudioFiles(new []
{
"The.Office.S03E115.DVDRip.XviD-OSiTV",
"The.Office.S03E115.DVDRip.XviD-OSiTV",
"The.Office.S03E115.DVDRip.XviD-OSiTV"
};
@"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV".AsOsAgnostic(),
@"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV".AsOsAgnostic(),
@"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV".AsOsAgnostic()
});
GivenVideoFiles(_audioFiles);
var decisions = Subject.GetImportDecisions(_audioFiles, _artist);
var decisions = Subject.GetImportDecisions(_audioFiles, _artist, false);
Mocker.GetMock<IAugmentingService>()
.Verify(c => c.Augment(It.IsAny<LocalTrack>(), It.IsAny<bool>()), Times.Exactly(_audioFiles.Count));
@ -341,14 +339,12 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport
.Setup(c => c.Augment(It.IsAny<LocalTrack>(), It.IsAny<bool>()))
.Throws<TestException>();
_audioFiles = new List<string>
GivenAudioFiles(new []
{
"The.Office.S03E115.DVDRip.XviD-OSiTV"
};
@"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV".AsOsAgnostic()
});
GivenVideoFiles(_audioFiles);
Subject.GetImportDecisions(_audioFiles, _artist).Should().HaveCount(1);
Subject.GetImportDecisions(_audioFiles, _artist, false).Should().HaveCount(1);
ExceptionVerification.ExpectedErrors(1);
}

View file

@ -117,7 +117,7 @@ namespace NzbDrone.Core.MediaFiles
CleanMediaFiles(artist, mediaFileList);
var decisionsStopwatch = Stopwatch.StartNew();
var decisions = _importDecisionMaker.GetImportDecisions(mediaFileList, artist);
var decisions = _importDecisionMaker.GetImportDecisions(mediaFileList, artist, false);
decisionsStopwatch.Stop();
_logger.Debug("Import decisions complete for: {0} [{1}]", artist, decisionsStopwatch.Elapsed);

View file

@ -0,0 +1,22 @@
using System.Collections.Generic;
using NzbDrone.Core.Music;
namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
{
public class CandidateAlbumRelease
{
public CandidateAlbumRelease()
{
}
public CandidateAlbumRelease(AlbumRelease release)
{
AlbumRelease = release;
ExistingTracks = new List<TrackFile>();
}
public AlbumRelease AlbumRelease { get; set; }
public List<TrackFile> ExistingTracks { get; set; }
}
}

View file

@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Newtonsoft.Json;
using NLog;
using NzbDrone.Common;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Serializer;
@ -16,7 +18,7 @@ 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);
List<LocalAlbumRelease> Identify(List<LocalTrack> localTracks, Artist artist, Album album, AlbumRelease release, bool newDownload, bool singleRelease, bool includeExisting);
}
public class IdentificationService : IIdentificationService
@ -27,7 +29,9 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
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 IConfigService _configService;
private readonly Logger _logger;
@ -37,7 +41,9 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
ITrackService trackService,
ITrackGroupingService trackGroupingService,
IFingerprintingService fingerprintingService,
IAudioTagService audioTagService,
IAugmentingService augmentingService,
IMediaFileService mediaFileService,
IConfigService configService,
Logger logger)
{
@ -47,7 +53,9 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
_trackService = trackService;
_trackGroupingService = trackGroupingService;
_fingerprintingService = fingerprintingService;
_audioTagService = audioTagService;
_augmentingService = augmentingService;
_mediaFileService = mediaFileService;
_configService = configService;
_logger = logger;
}
@ -92,10 +100,10 @@ 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)
public List<LocalAlbumRelease> Identify(List<LocalTrack> localTracks, Artist artist, Album album, AlbumRelease release, bool newDownload, bool singleRelease, bool includeExisting)
{
// 1 group localTracks so that we think they represent a single release
// 2 get candidates given specified artist, album and 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
@ -126,7 +134,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
{
_logger.Warn($"Augmentation failed for {localRelease}");
}
IdentifyRelease(localRelease, artist, album, release, newDownload);
IdentifyRelease(localRelease, artist, album, release, newDownload, includeExisting);
}
watch.Stop();
@ -165,18 +173,33 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
return false;
}
private void IdentifyRelease(LocalAlbumRelease localAlbumRelease, Artist artist, Album album, AlbumRelease release, bool newDownload)
private List<LocalTrack> ToLocalTrack(IEnumerable<TrackFile> trackfiles)
{
var localTracks = trackfiles.Select(x => new LocalTrack {
Path = x.Path,
FileTrackInfo = _audioTagService.ReadTags(x.Path),
ExistingFile = true,
AdditionalFile = true
})
.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)
{
var watch = System.Diagnostics.Stopwatch.StartNew();
bool fingerprinted = false;
var candidateReleases = GetCandidatesFromTags(localAlbumRelease, artist, album, release);
var candidateReleases = GetCandidatesFromTags(localAlbumRelease, artist, album, release, includeExisting);
if (candidateReleases.Count == 0 && FingerprintingAllowed(newDownload))
{
_logger.Debug("No candidates found, fingerprinting");
_fingerprintingService.Lookup(localAlbumRelease.LocalTracks, 0.5);
fingerprinted = true;
candidateReleases = GetCandidatesFromFingerprint(localAlbumRelease);
candidateReleases = GetCandidatesFromFingerprint(localAlbumRelease, artist, album, release, includeExisting);
}
if (candidateReleases.Count == 0)
@ -187,11 +210,16 @@ 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.Id).ToList());
var allTracks = _trackService.GetTracksByReleases(candidateReleases.Select(x => x.AlbumRelease.Id).ToList());
// convert all the TrackFiles that represent extra files to List<LocalTrack>
var allLocalTracks = ToLocalTrack(candidateReleases
.SelectMany(x => x.ExistingTracks)
.DistinctBy(x => x.Path));
_logger.Debug($"Retrieved {allTracks.Count} possible tracks in {watch.ElapsedMilliseconds}ms");
GetBestRelease(localAlbumRelease, candidateReleases, allTracks);
GetBestRelease(localAlbumRelease, candidateReleases, allTracks, 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
@ -204,12 +232,20 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
// Will generally be specified as part of manual import
if (album == null && release == null)
{
var extraCandidates = GetCandidatesFromFingerprint(localAlbumRelease).DistinctBy(x => x.Id);
candidateReleases.AddRange(extraCandidates);
allTracks.AddRange(_trackService.GetTracksByReleases(extraCandidates.Select(x => x.Id).ToList()));
var extraCandidates = GetCandidatesFromFingerprint(localAlbumRelease, artist, album, release, includeExisting);
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()));
allLocalTracks.AddRange(ToLocalTrack(newCandidates
.SelectMany(x => x.ExistingTracks)
.DistinctBy(x => x.Path)
.ExceptBy(x => x.Path, allLocalTracks, x => x.Path, PathEqualityComparer.Instance)));
}
GetBestRelease(localAlbumRelease, candidateReleases, allTracks);
// fingerprint all the local files in candidates we might be matching against
_fingerprintingService.Lookup(allLocalTracks, 0.5);
GetBestRelease(localAlbumRelease, candidateReleases, allTracks, allLocalTracks);
}
_logger.Debug($"Best release found in {watch.ElapsedMilliseconds}ms");
@ -219,43 +255,70 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
_logger.Debug($"IdentifyRelease done in {watch.ElapsedMilliseconds}ms");
}
public List<AlbumRelease> GetCandidatesFromTags(LocalAlbumRelease localAlbumRelease, Artist artist, Album album, AlbumRelease release)
public List<CandidateAlbumRelease> GetCandidatesFromTags(LocalAlbumRelease localAlbumRelease, Artist artist, Album album, AlbumRelease release, 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<AlbumRelease> candidateReleases;
List<CandidateAlbumRelease> candidateReleases;
// if we have a release ID that makes sense, use that
// 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())
{
var tagRelease = _releaseService.GetReleaseByForeignReleaseId(releaseIds[0]);
if (tagRelease != null)
_logger.Debug("Selecting release from consensus ForeignReleaseId [{0}]", releaseIds[0]);
tagMbidRelease = _releaseService.GetReleaseByForeignReleaseId(releaseIds[0]);
if (tagMbidRelease != null)
{
_logger.Debug("Selecting release from consensus ForeignReleaseId [{0}]", releaseIds[0]);
return new List<AlbumRelease> { tagRelease };
tagCandidate = GetCandidatesByRelease(new List<AlbumRelease> { tagMbidRelease }, includeExisting);
}
}
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 = new List<AlbumRelease> { release };
candidateReleases = GetCandidatesByRelease(new List<AlbumRelease> { release }, includeExisting);
}
else if (album != null)
{
candidateReleases = GetCandidatesByAlbum(localAlbumRelease, album);
// 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)
{
candidateReleases = GetCandidatesByArtist(localAlbumRelease, artist);
// 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
{
candidateReleases = GetCandidates(localAlbumRelease);
if (tagMbidRelease != null)
{
candidateReleases = tagCandidate;
}
else
{
candidateReleases = GetCandidates(localAlbumRelease, includeExisting);
}
}
watch.Stop();
@ -265,19 +328,41 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
return candidateReleases;
}
private List<AlbumRelease> GetCandidatesByAlbum(LocalAlbumRelease localAlbumRelease, Album album)
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>());
// populate the path. Artist will have been returned by mediaFileService
foreach (var trackfiles in albumTracks.Values)
{
foreach (var trackfile in trackfiles)
{
trackfile.Path = Path.Combine(trackfile.Artist.Value.Path, trackfile.RelativePath);
}
}
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 _releaseService.GetReleasesByAlbum(album.Id)
.OrderBy(x => Math.Abs(localAlbumRelease.TrackCount - x.TrackCount))
.ToList();
return GetCandidatesByRelease(_releaseService.GetReleasesByAlbum(album.Id)
.OrderBy(x => Math.Abs(localAlbumRelease.TrackCount - x.TrackCount))
.ToList(), includeExisting);
}
private List<AlbumRelease> GetCandidatesByArtist(LocalAlbumRelease localAlbumRelease, Artist artist)
private List<CandidateAlbumRelease> GetCandidatesByArtist(LocalAlbumRelease localAlbumRelease, Artist artist, bool includeExisting)
{
_logger.Trace("Getting candidates for {0}", artist);
var candidateReleases = new List<AlbumRelease>();
var candidateReleases = new List<CandidateAlbumRelease>();
var albumTag = MostCommon(localAlbumRelease.LocalTracks.Select(x => x.FileTrackInfo.AlbumTitle)) ?? "";
if (albumTag.IsNotNullOrWhiteSpace())
@ -285,14 +370,14 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
var possibleAlbums = _albumService.GetCandidates(artist.ArtistMetadataId, albumTag);
foreach (var album in possibleAlbums)
{
candidateReleases.AddRange(GetCandidatesByAlbum(localAlbumRelease, album));
candidateReleases.AddRange(GetCandidatesByAlbum(localAlbumRelease, album, includeExisting));
}
}
return candidateReleases;
}
private List<AlbumRelease> GetCandidates(LocalAlbumRelease localAlbumRelease)
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.
@ -303,7 +388,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
throw new NotImplementedException("Various artists not supported");
}
var candidateReleases = new List<AlbumRelease>();
var candidateReleases = new List<CandidateAlbumRelease>();
var artistTag = MostCommon(localAlbumRelease.LocalTracks.Select(x => x.FileTrackInfo.ArtistTitle)) ?? "";
if (artistTag.IsNotNullOrWhiteSpace())
@ -311,30 +396,44 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
var possibleArtists = _artistService.GetCandidates(artistTag);
foreach (var artist in possibleArtists)
{
candidateReleases.AddRange(GetCandidatesByArtist(localAlbumRelease, artist));
candidateReleases.AddRange(GetCandidatesByArtist(localAlbumRelease, artist, includeExisting));
}
}
return candidateReleases;
}
public List<AlbumRelease> GetCandidatesFromFingerprint(LocalAlbumRelease localAlbumRelease)
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);
return allReleases.Select(x => new {
Release = x,
TrackCount = x.TrackCount,
CommonProportion = x.Tracks.Value.Select(y => y.ForeignRecordingId).Intersect(recordingIds).Count() / localAlbumRelease.TrackCount
})
// 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();
.ToList(), includeExisting);
}
private T MostCommon<T>(IEnumerable<T> items)
@ -342,7 +441,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
return items.GroupBy(x => x).OrderByDescending(x => x.Count()).First().Key;
}
private void GetBestRelease(LocalAlbumRelease localAlbumRelease, List<AlbumRelease> candidateReleases, List<Track> tracks)
private void GetBestRelease(LocalAlbumRelease localAlbumRelease, List<CandidateAlbumRelease> candidateReleases, List<Track> dbTracks, List<LocalTrack> extraTracksOnDisk)
{
var watch = System.Diagnostics.Stopwatch.StartNew();
@ -351,13 +450,18 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
double bestDistance = 1.0;
foreach (var release in candidateReleases)
foreach (var candidateRelease in candidateReleases)
{
_logger.Debug("Trying Release {0} [{1}, {2} tracks]", release, release.Title, release.TrackCount);
var release = candidateRelease.AlbumRelease;
_logger.Debug("Trying Release {0} [{1}, {2} tracks, {3} existing]", release, release.Title, release.TrackCount, candidateRelease.ExistingTracks.Count);
var rwatch = System.Diagnostics.Stopwatch.StartNew();
var extraTrackPaths = candidateRelease.ExistingTracks.Select(x => x.Path).ToList();
var extraTracks = extraTracksOnDisk.Where(x => extraTrackPaths.Contains(x.Path)).ToList();
var allLocalTracks = localAlbumRelease.LocalTracks.Concat(extraTracks).DistinctBy(x => x.Path).ToList();
var mapping = MapReleaseTracks(localAlbumRelease.LocalTracks, tracks.Where(x => x.AlbumReleaseId == release.Id).ToList());
var distance = AlbumReleaseDistance(localAlbumRelease.LocalTracks, release, mapping);
var mapping = MapReleaseTracks(allLocalTracks, dbTracks.Where(x => x.AlbumReleaseId == release.Id).ToList());
var distance = AlbumReleaseDistance(allLocalTracks, release, mapping);
var currDistance = distance.NormalizedDistance();
rwatch.Stop();
@ -368,6 +472,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Identification
bestDistance = currDistance;
localAlbumRelease.Distance = distance;
localAlbumRelease.AlbumRelease = release;
localAlbumRelease.ExistingTracks = extraTracks;
localAlbumRelease.TrackMapping = mapping;
if (currDistance == 0.0)
{

View file

@ -19,7 +19,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
{
public interface IImportApprovedTracks
{
List<ImportResult> Import(List<ImportDecision<LocalTrack>> decisions, bool newDownload, DownloadClientItem downloadClientItem = null, ImportMode importMode = ImportMode.Auto);
List<ImportResult> Import(List<ImportDecision<LocalTrack>> decisions, bool replaceExisting, DownloadClientItem downloadClientItem = null, ImportMode importMode = ImportMode.Auto);
}
public class ImportApprovedTracks : IImportApprovedTracks
@ -58,7 +58,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
_logger = logger;
}
public List<ImportResult> Import(List<ImportDecision<LocalTrack>> decisions, bool newDownload, DownloadClientItem downloadClientItem = null, ImportMode importMode = ImportMode.Auto)
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
@ -67,54 +67,49 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
.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 albumDecisions = decisions.Where(e => e.Item.Album != null)
var albumDecisions = decisions.Where(e => e.Item.Album != null && e.Approved)
.GroupBy(e => e.Item.Album.Id).ToList();
foreach (var albumDecision in albumDecisions)
{
var album = albumDecision.First().Item.Album;
var currentRelease = album.AlbumReleases.Value.Single(x => x.Monitored);
var newRelease = albumDecision.First().Item.Release;
if (albumDecision.Any(x => x.Approved))
if (replaceExisting)
{
var newRelease = albumDecision.First(x => x.Approved).Item.Release;
var artist = albumDecision.First().Item.Artist;
var rootFolder = _diskProvider.GetParentFolder(artist.Path);
var previousFiles = _mediaFileService.GetFilesByAlbum(album.Id);
if (currentRelease.Id != newRelease.Id)
_logger.Debug($"Deleting {previousFiles.Count} existing files for {album}");
foreach (var previousFile in previousFiles)
{
// if we are importing a new release, delete all old files and don't attempt to upgrade
if (newDownload)
var trackFilePath = Path.Combine(artist.Path, previousFile.RelativePath);
var subfolder = rootFolder.GetRelativePath(_diskProvider.GetParentFolder(trackFilePath));
if (_diskProvider.FileExists(trackFilePath))
{
var artist = albumDecision.First().Item.Artist;
var rootFolder = _diskProvider.GetParentFolder(artist.Path);
var previousFiles = _mediaFileService.GetFilesByAlbum(album.Id);
foreach (var previousFile in previousFiles)
{
var trackFilePath = Path.Combine(artist.Path, previousFile.RelativePath);
var subfolder = rootFolder.GetRelativePath(_diskProvider.GetParentFolder(trackFilePath));
if (_diskProvider.FileExists(trackFilePath))
{
_logger.Debug("Removing existing track file: {0}", previousFile);
_recycleBinProvider.DeleteFile(trackFilePath, subfolder);
}
_mediaFileService.Delete(previousFile, DeleteMediaFileReason.Upgrade);
}
_logger.Debug("Removing existing track file: {0}", previousFile);
_recycleBinProvider.DeleteFile(trackFilePath, subfolder);
}
// set the correct release to be monitored before importing the new files
_logger.Debug("Updating release to {0} [{1} tracks]", newRelease, newRelease.TrackCount);
_releaseService.SetMonitored(newRelease);
// Publish album edited event.
// Deliberatly don't put in the old album since we don't want to trigger an ArtistScan.
_eventAggregator.PublishEvent(new AlbumEditedEvent(album, album));
_mediaFileService.Delete(previousFile, DeleteMediaFileReason.Upgrade);
}
}
// set the correct release to be monitored before importing the new files
_logger.Debug("Updating release to {0} [{1} tracks]", newRelease, newRelease.TrackCount);
_releaseService.SetMonitored(newRelease);
// Publish album edited event.
// Deliberatly don't put in the old album since we don't want to trigger an ArtistScan.
_eventAggregator.PublishEvent(new AlbumEditedEvent(album, album));
}
var filesToAdd = new List<TrackFile>(qualifiedImports.Count);
@ -186,7 +181,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
break;
}
if (newDownload)
if (!localTrack.ExistingFile)
{
trackFile.SceneName = GetSceneReleaseName(downloadClientItem, localTrack);
@ -205,13 +200,13 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
_mediaFileService.Delete(previousFile, DeleteMediaFileReason.ManualOverride);
}
_audioTagService.WriteTags(trackFile, newDownload);
_audioTagService.WriteTags(trackFile, false);
}
filesToAdd.Add(trackFile);
importResults.Add(new ImportResult(importDecision));
if (newDownload)
if (!localTrack.ExistingFile)
{
_extraService.ImportTrack(localTrack, trackFile, copyOnly);
}
@ -219,12 +214,12 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
allImportedTrackFiles.Add(trackFile);
allOldTrackFiles.AddRange(oldFiles);
_eventAggregator.PublishEvent(new TrackImportedEvent(localTrack, trackFile, oldFiles, newDownload, downloadClientItem));
_eventAggregator.PublishEvent(new TrackImportedEvent(localTrack, trackFile, oldFiles, !localTrack.ExistingFile, downloadClientItem));
}
catch (RootFolderNotFoundException e)
{
_logger.Warn(e, "Couldn't import track " + localTrack);
_eventAggregator.PublishEvent(new TrackImportFailedEvent(e, localTrack, newDownload, downloadClientItem));
_eventAggregator.PublishEvent(new TrackImportFailedEvent(e, localTrack, !localTrack.ExistingFile, downloadClientItem));
importResults.Add(new ImportResult(importDecision, "Failed to import track, Root folder missing."));
}
@ -269,7 +264,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
album,
release,
allImportedTrackFiles.Where(s => s.AlbumId == album.Id).ToList(),
allOldTrackFiles.Where(s => s.AlbumId == album.Id).ToList(), newDownload,
allOldTrackFiles.Where(s => s.AlbumId == album.Id).ToList(), replaceExisting,
downloadClientItem));
}

View file

@ -16,9 +16,9 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
{
public interface IMakeImportDecision
{
List<ImportDecision<LocalTrack>> GetImportDecisions(List<string> musicFiles, Artist artist);
List<ImportDecision<LocalTrack>> GetImportDecisions(List<string> musicFiles, Artist artist, bool includeExisting);
List<ImportDecision<LocalTrack>> GetImportDecisions(List<string> musicFiles, Artist artist, ParsedTrackInfo folderInfo);
List<ImportDecision<LocalTrack>> GetImportDecisions(List<string> musicFiles, Artist artist, Album album, DownloadClientItem downloadClientItem, ParsedTrackInfo folderInfo, bool filterExistingFiles, bool newDownload, bool singleRelease);
List<ImportDecision<LocalTrack>> GetImportDecisions(List<string> musicFiles, Artist artist, Album album, AlbumRelease albumRelease, DownloadClientItem downloadClientItem, ParsedTrackInfo folderInfo, bool filterExistingFiles, bool newDownload, bool singleRelease, bool includeExisting);
}
public class ImportDecisionMaker : IMakeImportDecision
@ -60,22 +60,22 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
_logger = logger;
}
public List<ImportDecision<LocalTrack>> GetImportDecisions(List<string> musicFiles, Artist artist)
public List<ImportDecision<LocalTrack>> GetImportDecisions(List<string> musicFiles, Artist artist, bool includeExisting)
{
return GetImportDecisions(musicFiles, artist, null, null, null, false, false, false);
return GetImportDecisions(musicFiles, artist, null, null, null, null, false, false, false, true);
}
public List<ImportDecision<LocalTrack>> GetImportDecisions(List<string> musicFiles, Artist artist, ParsedTrackInfo folderInfo)
{
return GetImportDecisions(musicFiles, artist, null, null, folderInfo, false, true, false);
return GetImportDecisions(musicFiles, artist, null, null, null, folderInfo, false, true, false, false);
}
public List<ImportDecision<LocalTrack>> GetImportDecisions(List<string> musicFiles, Artist artist, Album album, DownloadClientItem downloadClientItem, ParsedTrackInfo folderInfo, bool filterExistingFiles, bool newDownload, bool singleRelease)
public List<ImportDecision<LocalTrack>> GetImportDecisions(List<string> musicFiles, Artist artist, Album album, AlbumRelease albumRelease, DownloadClientItem downloadClientItem, ParsedTrackInfo folderInfo, bool filterExistingFiles, bool newDownload, bool singleRelease, bool includeExisting)
{
var watch = new System.Diagnostics.Stopwatch();
watch.Start();
var files = filterExistingFiles && (artist != null) ? _mediaFileService.FilterExistingFiles(musicFiles.ToList(), artist) : musicFiles.ToList();
var files = filterExistingFiles && (artist != null) ? _mediaFileService.FilterExistingFiles(musicFiles, artist) : musicFiles;
_logger.Debug("Analyzing {0}/{1} files.", files.Count, musicFiles.Count);
@ -98,7 +98,9 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
DownloadClientAlbumInfo = downloadClientItemInfo,
FolderTrackInfo = folderInfo,
Path = file,
FileTrackInfo = _audioTagService.ReadTags(file)
FileTrackInfo = _audioTagService.ReadTags(file),
ExistingFile = !newDownload,
AdditionalFile = false
};
try
@ -121,7 +123,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
_logger.Debug($"Tags parsed for {files.Count} files in {watch.ElapsedMilliseconds}ms");
var releases = _identificationService.Identify(localTracks, artist, album, null, newDownload, singleRelease);
var releases = _identificationService.Identify(localTracks, artist, album, albumRelease, newDownload, singleRelease, includeExisting);
foreach (var release in releases)
{
@ -133,7 +135,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport
if (releaseDecision.Approved)
{
decisions.AddIfNotNull(GetDecision(localTrack));
}
else
{

View file

@ -11,5 +11,6 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
public override bool RequiresDiskAccess => true;
public ImportMode ImportMode { get; set; }
public bool ReplaceExistingFiles { get; set; }
}
}

View file

@ -17,6 +17,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
public QualityModel Quality { get; set; }
public Language Language { get; set; }
public string DownloadId { get; set; }
public bool DisableReleaseSwitching { get; set; }
public bool Equals(ManualImportFile other)
{

View file

@ -24,5 +24,8 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
public string DownloadId { get; set; }
public IEnumerable<Rejection> Rejections { get; set; }
public ParsedTrackInfo Tags { get; set; }
public bool AdditionalFile { get; set; }
public bool ReplaceExistingFiles { get; set; }
public bool DisableReleaseSwitching { get; set; }
}
}

View file

@ -14,15 +14,14 @@ using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Music;
using NzbDrone.Common.Crypto;
using NzbDrone.Common.Cache;
using NzbDrone.Common;
namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
{
public interface IManualImportService
{
List<ManualImportItem> GetMediaFiles(string path, string downloadId, bool filterExistingFiles);
void UpdateItems(List<ManualImportItem> item);
ManualImportItem Find(int id);
List<ManualImportItem> GetMediaFiles(string path, string downloadId, bool filterExistingFiles, bool replaceExistingFiles);
List<ManualImportItem> UpdateItems(List<ManualImportItem> item);
}
public class ManualImportService : IExecute<ManualImportCommand>, IManualImportService
@ -35,10 +34,10 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
private readonly IAlbumService _albumService;
private readonly IReleaseService _releaseService;
private readonly ITrackService _trackService;
private readonly IAudioTagService _audioTagService;
private readonly IImportApprovedTracks _importApprovedTracks;
private readonly ITrackedDownloadService _trackedDownloadService;
private readonly IDownloadedTracksImportService _downloadedTracksImportService;
private readonly ICached<ManualImportItem> _cache;
private readonly IEventAggregator _eventAggregator;
private readonly Logger _logger;
@ -50,10 +49,10 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
IAlbumService albumService,
IReleaseService releaseService,
ITrackService trackService,
IAudioTagService audioTagService,
IImportApprovedTracks importApprovedTracks,
ITrackedDownloadService trackedDownloadService,
IDownloadedTracksImportService downloadedTracksImportService,
ICacheManager cacheManager,
IEventAggregator eventAggregator,
Logger logger)
{
@ -65,23 +64,16 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
_albumService = albumService;
_releaseService = releaseService;
_trackService = trackService;
_audioTagService = audioTagService;
_importApprovedTracks = importApprovedTracks;
_trackedDownloadService = trackedDownloadService;
_downloadedTracksImportService = downloadedTracksImportService;
_cache = cacheManager.GetCache<ManualImportItem>(GetType());
_eventAggregator = eventAggregator;
_logger = logger;
}
public ManualImportItem Find(int id)
public List<ManualImportItem> GetMediaFiles(string path, string downloadId, bool filterExistingFiles, bool replaceExistingFiles)
{
return _cache.Find(id.ToString());
}
public List<ManualImportItem> GetMediaFiles(string path, string downloadId, bool filterExistingFiles)
{
_cache.Clear();
if (downloadId.IsNotNullOrWhiteSpace())
{
var trackedDownload = _trackedDownloadService.Find(downloadId);
@ -101,23 +93,16 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
return new List<ManualImportItem>();
}
var decision = _importDecisionMaker.GetImportDecisions(new List<string> { path }, null, null, null, null, false, true, false);
var result = MapItem(decision.First(), Path.GetDirectoryName(path), downloadId);
_cache.Set(result.Id.ToString(), result);
var decision = _importDecisionMaker.GetImportDecisions(new List<string> { path }, null, null, null, null, null, false, true, false, !replaceExistingFiles);
var result = MapItem(decision.First(), Path.GetDirectoryName(path), downloadId, replaceExistingFiles, false);
return new List<ManualImportItem> { result };
}
var items = ProcessFolder(path, downloadId, filterExistingFiles);
foreach (var item in items)
{
_cache.Set(item.Id.ToString(), item);
}
return items;
return ProcessFolder(path, downloadId, filterExistingFiles, replaceExistingFiles);
}
private List<ManualImportItem> ProcessFolder(string folder, string downloadId, bool filterExistingFiles)
private List<ManualImportItem> ProcessFolder(string folder, string downloadId, bool filterExistingFiles, bool replaceExistingFiles)
{
var directoryInfo = new DirectoryInfo(folder);
var artist = _parsingService.GetArtist(directoryInfo.Name);
@ -130,24 +115,48 @@ 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, folderInfo, filterExistingFiles, true, false);
var decisions = _importDecisionMaker.GetImportDecisions(artistFiles, artist, null, null, null, folderInfo, filterExistingFiles, true, false, !replaceExistingFiles);
return decisions.Select(decision => MapItem(decision, folder, downloadId)).ToList();
// paths will be different for new and old files which is why we need to map separately
var newFiles = artistFiles.Join(decisions,
f => f,
d => d.Item.Path,
(f, d) => new { File = f, Decision = d },
PathEqualityComparer.Instance);
var newItems = newFiles.Select(x => MapItem(x.Decision, folder, 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));
return newItems.Concat(existingItems).ToList();
}
public void UpdateItems(List<ManualImportItem> items)
public List<ManualImportItem> UpdateItems(List<ManualImportItem> items)
{
var groupedItems = items.GroupBy(x => x.Album?.Id);
_logger.Debug("UpdateItems, {0} groups", groupedItems.Count());
var replaceExistingFiles = items.All(x => x.ReplaceExistingFiles);
var groupedItems = items.Where(x => !x.AdditionalFile).GroupBy(x => x.Album?.Id);
_logger.Debug($"UpdateItems, {groupedItems.Count()} groups, replaceExisting {replaceExistingFiles}");
var result = new List<ManualImportItem>();
foreach(var group in groupedItems)
{
// generate dummy decisions that don't match the release
_logger.Debug("UpdateItems, group key: {0}", group.Key);
var decisions = _importDecisionMaker.GetImportDecisions(group.Select(x => x.Path).ToList(), group.First().Artist, group.First().Album, null, null, false, true, true);
foreach (var decision in decisions)
var disableReleaseSwitching = group.First().DisableReleaseSwitching;
var decisions = _importDecisionMaker.GetImportDecisions(group.Select(x => x.Path).ToList(), group.First().Artist, group.First().Album, group.First().Release, null, null, false, true, true, !replaceExistingFiles);
var existingItems = group.Join(decisions,
i => i.Path,
d => d.Item.Path,
(i, d) => new { Item = i, Decision = d },
PathEqualityComparer.Instance);
foreach (var pair in existingItems)
{
var item = items.Where(x => x.Path == decision.Item.Path).Single();
var item = pair.Item;
var decision = pair.Decision;
if (decision.Item.Artist != null)
{
@ -167,12 +176,17 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
item.Rejections = decision.Rejections;
_cache.Set(item.Id.ToString(), item);
result.Add(item);
}
var newDecisions = decisions.Except(existingItems.Select(x => x.Decision));
result.AddRange(newDecisions.Select(x => MapItem(x, x.Item.Artist.Path, null, replaceExistingFiles, disableReleaseSwitching)));
}
return result;
}
private ManualImportItem MapItem(ImportDecision<LocalTrack> decision, string folder, string downloadId)
private ManualImportItem MapItem(ImportDecision<LocalTrack> decision, string folder, string downloadId, bool replaceExistingFiles, bool disableReleaseSwitching)
{
var item = new ManualImportItem();
@ -203,6 +217,9 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
item.Size = _diskProvider.GetFileSize(decision.Item.Path);
item.Rejections = decision.Rejections;
item.Tags = decision.Item.FileTrackInfo;
item.AdditionalFile = decision.Item.AdditionalFile;
item.ReplaceExistingFiles = replaceExistingFiles;
item.DisableReleaseSwitching = disableReleaseSwitching;
return item;
}
@ -220,6 +237,14 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
{
var albumImportDecisions = new List<ImportDecision<LocalTrack>>();
// turn off anyReleaseOk if specified
if (importAlbumId.First().DisableReleaseSwitching)
{
var album = _albumService.GetAlbum(importAlbumId.First().AlbumId);
album.AnyReleaseOk = false;
_albumService.UpdateAlbum(album);
}
foreach (var file in importAlbumId)
{
_logger.ProgressTrace("Processing file {0} of {1}", fileCount + 1, message.Files.Count);
@ -228,37 +253,35 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
var album = _albumService.GetAlbum(file.AlbumId);
var release = _releaseService.GetRelease(file.AlbumReleaseId);
var tracks = _trackService.GetTracks(file.TrackIds);
var fileTrackInfo = Parser.Parser.ParseMusicPath(file.Path) ?? new ParsedTrackInfo();
var fileTrackInfo = _audioTagService.ReadTags(file.Path) ?? new ParsedTrackInfo();
var localTrack = new LocalTrack
{
ExistingFile = false,
ExistingFile = artist.Path.IsParentPath(file.Path),
Tracks = tracks,
MediaInfo = null,
FileTrackInfo = fileTrackInfo,
MediaInfo = fileTrackInfo.MediaInfo,
Path = file.Path,
Quality = file.Quality,
Language = file.Language,
Artist = artist,
Album = album,
Release = release,
Size = 0
Release = release
};
albumImportDecisions.Add(new ImportDecision<LocalTrack>(localTrack));
fileCount += 1;
}
var existingFile = albumImportDecisions.First().Item.Artist.Path.IsParentPath(importAlbumId.First().Path);
if (importAlbumId.First().DownloadId.IsNullOrWhiteSpace())
var downloadId = importAlbumId.Select(x => x.DownloadId).FirstOrDefault(x => x.IsNotNullOrWhiteSpace());
if (downloadId.IsNullOrWhiteSpace())
{
imported.AddRange(_importApprovedTracks.Import(albumImportDecisions, !existingFile, null, message.ImportMode));
imported.AddRange(_importApprovedTracks.Import(albumImportDecisions, message.ReplaceExistingFiles, null, message.ImportMode));
}
else
{
var trackedDownload = _trackedDownloadService.Find(importAlbumId.First().DownloadId);
var importResults = _importApprovedTracks.Import(albumImportDecisions, true, trackedDownload.DownloadItem, message.ImportMode);
var trackedDownload = _trackedDownloadService.Find(downloadId);
var importResults = _importApprovedTracks.Import(albumImportDecisions, message.ReplaceExistingFiles, trackedDownload.DownloadItem, message.ImportMode);
imported.AddRange(importResults);

View file

@ -17,10 +17,11 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Specifications
public Decision IsSatisfiedBy(LocalAlbumRelease localAlbumRelease)
{
var existingRelease = localAlbumRelease.AlbumRelease.Album.Value.AlbumReleases.Value.Single(x => x.Monitored);
var existingTrackCount = existingRelease.Tracks.Value.Count(x => x.HasFile);
if (localAlbumRelease.AlbumRelease.Id != existingRelease.Id &&
localAlbumRelease.TrackCount < existingRelease.Tracks.Value.Count(x => x.HasFile))
localAlbumRelease.TrackCount < existingTrackCount)
{
_logger.Debug("This release has fewer tracks than the existing one. Skipping {0}", localAlbumRelease);
_logger.Debug($"This release has fewer tracks ({localAlbumRelease.TrackCount}) than existing {existingRelease} ({existingTrackCount}). Skipping {localAlbumRelease}");
return Decision.Reject("Has fewer tracks than existing release");
}

View file

@ -789,6 +789,7 @@
<Compile Include="MediaFiles\TrackImport\Specifications\NoMissingOrUnmatchedTracksSpecification.cs" />
<Compile Include="MediaFiles\TrackImport\Specifications\ReleaseWantedSpecification.cs" />
<Compile Include="MediaFiles\TrackImport\Identification\TrackGroupingService.cs" />
<Compile Include="MediaFiles\TrackImport\Identification\CandidateAlbumRelease.cs" />
<Compile Include="MediaFiles\TrackImport\Identification\IdentificationService.cs" />
<Compile Include="MediaFiles\TrackImport\Identification\IdentificationTestCase.cs" />
<Compile Include="MediaFiles\TrackImport\Identification\Distance.cs" />

View file

@ -4,6 +4,8 @@ using System.Linq;
using NzbDrone.Core.MediaFiles.TrackImport.Identification;
using System.IO;
using System;
using NzbDrone.Common.Extensions;
using NzbDrone.Common;
namespace NzbDrone.Core.Parser.Model
{
@ -33,23 +35,25 @@ namespace NzbDrone.Core.Parser.Model
public TrackMapping TrackMapping { get; set; }
public Distance Distance { get; set; }
public AlbumRelease AlbumRelease { get; set; }
public List<LocalTrack> ExistingTracks { get; set; }
public bool NewDownload { get; set; }
public void PopulateMatch()
{
if (AlbumRelease != null)
{
LocalTracks = LocalTracks.Concat(ExistingTracks).DistinctBy(x => x.Path).ToList();
foreach (var localTrack in LocalTracks)
{
localTrack.Release = AlbumRelease;
localTrack.Album = AlbumRelease.Album.Value;
localTrack.Artist = localTrack.Album.Artist.Value;
if (TrackMapping.Mapping.ContainsKey(localTrack))
{
var track = TrackMapping.Mapping[localTrack].Item1;
localTrack.Tracks = new List<Track> { track };
localTrack.Distance = TrackMapping.Mapping[localTrack].Item2;
localTrack.Artist = localTrack.Album.Artist.Value;
}
}
}

View file

@ -28,6 +28,7 @@ namespace NzbDrone.Core.Parser.Model
public Language Language { get; set; }
public MediaInfoModel MediaInfo { get; set; }
public bool ExistingFile { get; set; }
public bool AdditionalFile { get; set; }
public bool SceneSource { get; set; }
public string ReleaseGroup { get; set; }