New: Refactor metadata update

pull/865/head
ta264 6 years ago
parent f5c1858d4c
commit 0b7a42ee3b

@ -61,7 +61,8 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Identification
_addArtistService = Mocker.Resolve<AddArtistService>(); _addArtistService = Mocker.Resolve<AddArtistService>();
Mocker.SetConstant<IRefreshTrackService>(Mocker.Resolve<RefreshTrackService>()); Mocker.SetConstant<IRefreshTrackService>(Mocker.Resolve<RefreshTrackService>());
Mocker.SetConstant<IAddAlbumService>(Mocker.Resolve<AddAlbumService>()); Mocker.SetConstant<IRefreshAlbumReleaseService>(Mocker.Resolve<RefreshAlbumReleaseService>());
Mocker.SetConstant<IRefreshAlbumService>(Mocker.Resolve<RefreshAlbumService>());
_refreshArtistService = Mocker.Resolve<RefreshArtistService>(); _refreshArtistService = Mocker.Resolve<RefreshArtistService>();
Mocker.GetMock<IAddArtistValidator>().Setup(x => x.Validate(It.IsAny<Artist>())).Returns(new ValidationResult()); Mocker.GetMock<IAddArtistValidator>().Setup(x => x.Validate(It.IsAny<Artist>())).Returns(new ValidationResult());

@ -13,6 +13,8 @@ using NzbDrone.Core.Music.Commands;
using NzbDrone.Test.Common; using NzbDrone.Test.Common;
using FluentAssertions; using FluentAssertions;
using NzbDrone.Common.Serializer; using NzbDrone.Common.Serializer;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.History;
namespace NzbDrone.Core.Test.MusicTests namespace NzbDrone.Core.Test.MusicTests
{ {
@ -40,6 +42,7 @@ namespace NzbDrone.Core.Test.MusicTests
_releases = new List<AlbumRelease> { release }; _releases = new List<AlbumRelease> { release };
var album1 = Builder<Album>.CreateNew() var album1 = Builder<Album>.CreateNew()
.With(x => x.ArtistMetadata = Builder<ArtistMetadata>.CreateNew().Build())
.With(s => s.Id = 1234) .With(s => s.Id = 1234)
.With(s => s.ForeignAlbumId = "1") .With(s => s.ForeignAlbumId = "1")
.With(s => s.AlbumReleases = _releases) .With(s => s.AlbumReleases = _releases)
@ -63,6 +66,10 @@ namespace NzbDrone.Core.Test.MusicTests
.Setup(s => s.FindById(It.IsAny<List<string>>())) .Setup(s => s.FindById(It.IsAny<List<string>>()))
.Returns(new List<ArtistMetadata>()); .Returns(new List<ArtistMetadata>());
Mocker.GetMock<IArtistMetadataRepository>()
.Setup(s => s.UpsertMany(It.IsAny<List<ArtistMetadata> >()))
.Returns(true);
Mocker.GetMock<IProvideAlbumInfo>() Mocker.GetMock<IProvideAlbumInfo>()
.Setup(s => s.GetAlbumInfo(It.IsAny<string>())) .Setup(s => s.GetAlbumInfo(It.IsAny<string>()))
.Callback(() => { throw new AlbumNotFoundException(album1.ForeignAlbumId); }); .Callback(() => { throw new AlbumNotFoundException(album1.ForeignAlbumId); });
@ -70,6 +77,18 @@ namespace NzbDrone.Core.Test.MusicTests
Mocker.GetMock<ICheckIfAlbumShouldBeRefreshed>() Mocker.GetMock<ICheckIfAlbumShouldBeRefreshed>()
.Setup(s => s.ShouldRefresh(It.IsAny<Album>())) .Setup(s => s.ShouldRefresh(It.IsAny<Album>()))
.Returns(true); .Returns(true);
Mocker.GetMock<IMediaFileService>()
.Setup(x => x.GetFilesByAlbum(It.IsAny<int>()))
.Returns(new List<TrackFile>());
Mocker.GetMock<IMediaFileService>()
.Setup(x => x.GetFilesByRelease(It.IsAny<int>()))
.Returns(new List<TrackFile>());
Mocker.GetMock<IHistoryService>()
.Setup(x => x.GetByAlbum(It.IsAny<int>(), It.IsAny<HistoryEventType?>()))
.Returns(new List<History.History>());
} }
private void GivenNewAlbumInfo(Album album) private void GivenNewAlbumInfo(Album album)
@ -80,27 +99,69 @@ namespace NzbDrone.Core.Test.MusicTests
} }
[Test] [Test]
public void should_log_error_if_musicbrainz_id_not_found() public void should_update_if_musicbrainz_id_changed_and_no_clash()
{ {
Subject.RefreshAlbumInfo(_albums, false, false); var newAlbumInfo = _albums.First().JsonClone();
newAlbumInfo.ArtistMetadata = _albums.First().ArtistMetadata.Value.JsonClone();
newAlbumInfo.ForeignAlbumId = _albums.First().ForeignAlbumId + 1;
newAlbumInfo.AlbumReleases = _releases;
Mocker.GetMock<IAlbumService>() GivenNewAlbumInfo(newAlbumInfo);
.Verify(v => v.UpdateMany(It.IsAny<List<Album>>()), Times.Never());
Subject.RefreshAlbumInfo(_albums, null, false, false);
ExceptionVerification.ExpectedErrors(1); Mocker.GetMock<IAlbumService>()
.Verify(v => v.UpdateMany(It.Is<List<Album>>(s => s.First().ForeignAlbumId == newAlbumInfo.ForeignAlbumId)));
} }
[Test] [Test]
public void should_update_if_musicbrainz_id_changed() public void should_merge_if_musicbrainz_id_changed_and_new_already_exists()
{ {
var newAlbumInfo = _albums.FirstOrDefault().JsonClone(); var existing = _albums.First();
var clash = existing.JsonClone();
clash.Id = 100;
clash.ArtistMetadata = existing.ArtistMetadata.Value.JsonClone();
clash.ForeignAlbumId = clash.ForeignAlbumId + 1;
clash.AlbumReleases = Builder<AlbumRelease>.CreateListOfSize(10)
.All().With(x => x.AlbumId = clash.Id)
.BuildList();
Mocker.GetMock<IAlbumService>()
.Setup(x => x.FindById(clash.ForeignAlbumId))
.Returns(clash);
Mocker.GetMock<IReleaseService>()
.Setup(x => x.GetReleasesByAlbum(_albums.First().Id))
.Returns(_releases);
Mocker.GetMock<IReleaseService>()
.Setup(x => x.GetReleasesByAlbum(clash.Id))
.Returns(new List<AlbumRelease>());
Mocker.GetMock<IReleaseService>()
.Setup(x => x.GetReleasesForRefresh(It.IsAny<int>(), It.IsAny<IEnumerable<string>>()))
.Returns(_releases);
var newAlbumInfo = existing.JsonClone();
newAlbumInfo.ArtistMetadata = existing.ArtistMetadata.Value.JsonClone();
newAlbumInfo.ForeignAlbumId = _albums.First().ForeignAlbumId + 1; newAlbumInfo.ForeignAlbumId = _albums.First().ForeignAlbumId + 1;
newAlbumInfo.AlbumReleases = _releases; newAlbumInfo.AlbumReleases = _releases;
GivenNewAlbumInfo(newAlbumInfo); GivenNewAlbumInfo(newAlbumInfo);
Subject.RefreshAlbumInfo(_albums, false, false); Subject.RefreshAlbumInfo(_albums, null, false, false);
// check releases moved to clashing album
Mocker.GetMock<IReleaseService>()
.Verify(v => v.UpdateMany(It.Is<List<AlbumRelease>>(x => x.All(y => y.AlbumId == clash.Id) && x.Count == _releases.Count)));
// check old album is deleted
Mocker.GetMock<IAlbumService>()
.Verify(v => v.DeleteMany(It.Is<List<Album>>(x => x.First().ForeignAlbumId == existing.ForeignAlbumId)));
// check that clash gets updated
Mocker.GetMock<IAlbumService>() Mocker.GetMock<IAlbumService>()
.Verify(v => v.UpdateMany(It.Is<List<Album>>(s => s.First().ForeignAlbumId == newAlbumInfo.ForeignAlbumId))); .Verify(v => v.UpdateMany(It.Is<List<Album>>(s => s.First().ForeignAlbumId == newAlbumInfo.ForeignAlbumId)));
@ -115,11 +176,23 @@ namespace NzbDrone.Core.Test.MusicTests
GivenNewAlbumInfo(album); GivenNewAlbumInfo(album);
Subject.RefreshAlbumInfo(album, false); Subject.RefreshAlbumInfo(album, null, false);
Mocker.GetMock<IAlbumService>() Mocker.GetMock<IAlbumService>()
.Verify(x => x.DeleteMany(It.Is<List<Album>>(y => y.Count == 1 && y.First().ForeignAlbumId == album.ForeignAlbumId)), .Verify(x => x.DeleteAlbum(album.Id, true),
Times.Once()); Times.Once());
ExceptionVerification.ExpectedWarns(1);
}
[Test]
public void two_equivalent_albums_should_be_equal()
{
var album = Builder<Album>.CreateNew().Build();
var album2 = Builder<Album>.CreateNew().Build();
ReferenceEquals(album, album2).Should().BeFalse();
album.Equals(album2).Should().BeTrue();
} }
[Test] [Test]
@ -160,9 +233,13 @@ namespace NzbDrone.Core.Test.MusicTests
[Test] [Test]
public void should_not_add_duplicate_releases() public void should_not_add_duplicate_releases()
{ {
var newAlbum = Builder<Album>.CreateNew().Build(); var newAlbum = Builder<Album>.CreateNew()
.With(x => x.ArtistMetadata = Builder<ArtistMetadata>.CreateNew().Build())
.Build();
// this is required because RefreshAlbumInfo will edit the album passed in // this is required because RefreshAlbumInfo will edit the album passed in
var albumCopy = Builder<Album>.CreateNew().Build(); var albumCopy = Builder<Album>.CreateNew()
.With(x => x.ArtistMetadata = Builder<ArtistMetadata>.CreateNew().Build())
.Build();
var releases = Builder<AlbumRelease>.CreateListOfSize(10) var releases = Builder<AlbumRelease>.CreateListOfSize(10)
.All() .All()
@ -191,21 +268,13 @@ namespace NzbDrone.Core.Test.MusicTests
.Setup(x => x.GetAlbumInfo(It.IsAny<string>())) .Setup(x => x.GetAlbumInfo(It.IsAny<string>()))
.Returns(Tuple.Create("dummy string", albumCopy, new List<ArtistMetadata>())); .Returns(Tuple.Create("dummy string", albumCopy, new List<ArtistMetadata>()));
Subject.RefreshAlbumInfo(newAlbum, false); Subject.RefreshAlbumInfo(newAlbum, null, false);
newAlbum.AlbumReleases.Value.Should().HaveCount(7);
Mocker.GetMock<IReleaseService>()
.Verify(x => x.DeleteMany(It.Is<List<AlbumRelease>>(l => l.Count == 0)), Times.Once());
Mocker.GetMock<IReleaseService>() Mocker.GetMock<IRefreshAlbumReleaseService>()
.Verify(x => x.UpdateMany(It.Is<List<AlbumRelease>>(l => l.Count == 1 && l.Select(r => r.ForeignReleaseId).Distinct().Count() == 1)), Times.Once()); .Verify(x => x.RefreshEntityInfo(It.Is<List<AlbumRelease>>(l => l.Count == 7 && l.Count(y => y.Monitored) == 1),
It.IsAny<List<AlbumRelease>>(),
Mocker.GetMock<IReleaseService>() It.IsAny<bool>(),
.Verify(x => x.InsertMany(It.Is<List<AlbumRelease>>(l => l.Count == 6 && It.IsAny<bool>()));
l.Select(r => r.ForeignReleaseId).Distinct().Count() == l.Count &&
!l.Select(r => r.ForeignReleaseId).Contains("DuplicateId2"))),
Times.Once());
} }
[TestCase(true, true, 1)] [TestCase(true, true, 1)]
@ -214,9 +283,13 @@ namespace NzbDrone.Core.Test.MusicTests
[TestCase(false, false, 0)] [TestCase(false, false, 0)]
public void should_only_leave_one_release_monitored(bool skyhookMonitored, bool existingMonitored, int expectedUpdates) public void should_only_leave_one_release_monitored(bool skyhookMonitored, bool existingMonitored, int expectedUpdates)
{ {
var newAlbum = Builder<Album>.CreateNew().Build(); var newAlbum = Builder<Album>.CreateNew()
.With(x => x.ArtistMetadata = Builder<ArtistMetadata>.CreateNew().Build())
.Build();
// this is required because RefreshAlbumInfo will edit the album passed in // this is required because RefreshAlbumInfo will edit the album passed in
var albumCopy = Builder<Album>.CreateNew().Build(); var albumCopy = Builder<Album>.CreateNew()
.With(x => x.ArtistMetadata = Builder<ArtistMetadata>.CreateNew().Build())
.Build();
var releases = Builder<AlbumRelease>.CreateListOfSize(10) var releases = Builder<AlbumRelease>.CreateListOfSize(10)
.All() .All()
@ -248,31 +321,26 @@ namespace NzbDrone.Core.Test.MusicTests
.Setup(x => x.GetAlbumInfo(It.IsAny<string>())) .Setup(x => x.GetAlbumInfo(It.IsAny<string>()))
.Returns(Tuple.Create("dummy string", albumCopy, new List<ArtistMetadata>())); .Returns(Tuple.Create("dummy string", albumCopy, new List<ArtistMetadata>()));
Subject.RefreshAlbumInfo(newAlbum, false); Subject.RefreshAlbumInfo(newAlbum, null, false);
newAlbum.AlbumReleases.Value.Should().HaveCount(10); Mocker.GetMock<IRefreshAlbumReleaseService>()
newAlbum.AlbumReleases.Value.Where(x => x.Monitored).Should().HaveCount(1); .Verify(x => x.RefreshEntityInfo(It.Is<List<AlbumRelease>>(l => l.Count == 10 && l.Count(y => y.Monitored) == 1),
It.IsAny<List<AlbumRelease>>(),
It.IsAny<bool>(),
It.IsAny<bool>()));
Mocker.GetMock<IReleaseService>()
.Verify(x => x.DeleteMany(It.Is<List<AlbumRelease>>(l => l.Count == 0)), Times.Once());
Mocker.GetMock<IReleaseService>()
.Verify(x => x.UpdateMany(It.Is<List<AlbumRelease>>(l => l.Count == expectedUpdates && l.Select(r => r.ForeignReleaseId).Distinct().Count() == expectedUpdates)), Times.Once());
Mocker.GetMock<IReleaseService>()
.Verify(x => x.InsertMany(It.Is<List<AlbumRelease>>(l => l.Count == 8 &&
l.Select(r => r.ForeignReleaseId).Distinct().Count() == l.Count &&
!l.Select(r => r.ForeignReleaseId).Contains("ExistingId1") &&
!l.Select(r => r.ForeignReleaseId).Contains("ExistingId2"))),
Times.Once());
} }
[Test] [Test]
public void refreshing_album_should_not_change_monitored_release_if_monitored_release_not_deleted() public void refreshing_album_should_not_change_monitored_release_if_monitored_release_not_deleted()
{ {
var newAlbum = Builder<Album>.CreateNew().Build(); var newAlbum = Builder<Album>.CreateNew()
.With(x => x.ArtistMetadata = Builder<ArtistMetadata>.CreateNew().Build())
.Build();
// this is required because RefreshAlbumInfo will edit the album passed in // this is required because RefreshAlbumInfo will edit the album passed in
var albumCopy = Builder<Album>.CreateNew().Build(); var albumCopy = Builder<Album>.CreateNew()
.With(x => x.ArtistMetadata = Builder<ArtistMetadata>.CreateNew().Build())
.Build();
// only ExistingId1 is monitored from dummy skyhook // only ExistingId1 is monitored from dummy skyhook
var releases = Builder<AlbumRelease>.CreateListOfSize(10) var releases = Builder<AlbumRelease>.CreateListOfSize(10)
@ -308,32 +376,28 @@ namespace NzbDrone.Core.Test.MusicTests
.Setup(x => x.GetAlbumInfo(It.IsAny<string>())) .Setup(x => x.GetAlbumInfo(It.IsAny<string>()))
.Returns(Tuple.Create("dummy string", albumCopy, new List<ArtistMetadata>())); .Returns(Tuple.Create("dummy string", albumCopy, new List<ArtistMetadata>()));
Subject.RefreshAlbumInfo(newAlbum, false); Subject.RefreshAlbumInfo(newAlbum, null, false);
newAlbum.AlbumReleases.Value.Should().HaveCount(10);
newAlbum.AlbumReleases.Value.Where(x => x.Monitored).Should().HaveCount(1);
newAlbum.AlbumReleases.Value.Single(x => x.Monitored).ForeignReleaseId.Should().Be("ExistingId2");
Mocker.GetMock<IReleaseService>() Mocker.GetMock<IRefreshAlbumReleaseService>()
.Verify(x => x.DeleteMany(It.Is<List<AlbumRelease>>(l => l.Count == 0)), Times.Once()); .Verify(x => x.RefreshEntityInfo(It.Is<List<AlbumRelease>>(
l => l.Count == 10 &&
Mocker.GetMock<IReleaseService>() l.Count(y => y.Monitored) == 1 &&
.Verify(x => x.UpdateMany(It.Is<List<AlbumRelease>>(l => l.Count == 0)), Times.Once()); l.Single(y => y.Monitored).ForeignReleaseId == "ExistingId2"),
It.IsAny<List<AlbumRelease>>(),
Mocker.GetMock<IReleaseService>() It.IsAny<bool>(),
.Verify(x => x.InsertMany(It.Is<List<AlbumRelease>>(l => l.Count == 8 && It.IsAny<bool>()));
l.Select(r => r.ForeignReleaseId).Distinct().Count() == l.Count &&
!l.Select(r => r.ForeignReleaseId).Contains("ExistingId1") &&
!l.Select(r => r.ForeignReleaseId).Contains("ExistingId2"))),
Times.Once());
} }
[Test] [Test]
public void refreshing_album_should_change_monitored_release_if_monitored_release_deleted() public void refreshing_album_should_change_monitored_release_if_monitored_release_deleted()
{ {
var newAlbum = Builder<Album>.CreateNew().Build(); var newAlbum = Builder<Album>.CreateNew()
.With(x => x.ArtistMetadata = Builder<ArtistMetadata>.CreateNew().Build())
.Build();
// this is required because RefreshAlbumInfo will edit the album passed in // this is required because RefreshAlbumInfo will edit the album passed in
var albumCopy = Builder<Album>.CreateNew().Build(); var albumCopy = Builder<Album>.CreateNew()
.With(x => x.ArtistMetadata = Builder<ArtistMetadata>.CreateNew().Build())
.Build();
// Only existingId1 monitored in skyhook. ExistingId2 is missing // Only existingId1 monitored in skyhook. ExistingId2 is missing
var releases = Builder<AlbumRelease>.CreateListOfSize(10) var releases = Builder<AlbumRelease>.CreateListOfSize(10)
@ -369,24 +433,16 @@ namespace NzbDrone.Core.Test.MusicTests
.Setup(x => x.GetAlbumInfo(It.IsAny<string>())) .Setup(x => x.GetAlbumInfo(It.IsAny<string>()))
.Returns(Tuple.Create("dummy string", albumCopy, new List<ArtistMetadata>())); .Returns(Tuple.Create("dummy string", albumCopy, new List<ArtistMetadata>()));
Subject.RefreshAlbumInfo(newAlbum, false); Subject.RefreshAlbumInfo(newAlbum, null, false);
newAlbum.AlbumReleases.Value.Should().HaveCount(10);
newAlbum.AlbumReleases.Value.Where(x => x.Monitored).Should().HaveCount(1);
newAlbum.AlbumReleases.Value.Single(x => x.Monitored).ForeignReleaseId.Should().NotBe("ExistingId2");
Mocker.GetMock<IReleaseService>()
.Verify(x => x.DeleteMany(It.Is<List<AlbumRelease>>(l => l.Single().ForeignReleaseId == "ExistingId2")), Times.Once());
Mocker.GetMock<IReleaseService>()
.Verify(x => x.UpdateMany(It.Is<List<AlbumRelease>>(l => l.Count == 0)), Times.Once());
Mocker.GetMock<IReleaseService>() Mocker.GetMock<IRefreshAlbumReleaseService>()
.Verify(x => x.InsertMany(It.Is<List<AlbumRelease>>(l => l.Count == 9 && .Verify(x => x.RefreshEntityInfo(It.Is<List<AlbumRelease>>(
l.Select(r => r.ForeignReleaseId).Distinct().Count() == l.Count && l => l.Count == 11 &&
!l.Select(r => r.ForeignReleaseId).Contains("ExistingId1") && l.Count(y => y.Monitored) == 1 &&
!l.Select(r => r.ForeignReleaseId).Contains("ExistingId2"))), l.Single(y => y.Monitored).ForeignReleaseId != "ExistingId2"),
Times.Once()); It.IsAny<List<AlbumRelease>>(),
It.IsAny<bool>(),
It.IsAny<bool>()));
} }
} }
} }

@ -11,6 +11,9 @@ using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Music; using NzbDrone.Core.Music;
using NzbDrone.Core.Music.Commands; using NzbDrone.Core.Music.Commands;
using NzbDrone.Test.Common; using NzbDrone.Test.Common;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.History;
using NzbDrone.Core.Music.Events;
namespace NzbDrone.Core.Test.MusicTests namespace NzbDrone.Core.Test.MusicTests
{ {
@ -41,17 +44,24 @@ namespace NzbDrone.Core.Test.MusicTests
.With(a => a.Metadata = metadata) .With(a => a.Metadata = metadata)
.Build(); .Build();
Mocker.GetMock<IArtistService>() Mocker.GetMock<IArtistService>(MockBehavior.Strict)
.Setup(s => s.GetArtist(_artist.Id)) .Setup(s => s.GetArtist(_artist.Id))
.Returns(_artist); .Returns(_artist);
Mocker.GetMock<IAlbumService>() Mocker.GetMock<IAlbumService>(MockBehavior.Strict)
.Setup(s => s.GetAlbumsForRefresh(It.IsAny<int>(), It.IsAny<IEnumerable<string>>())) .Setup(s => s.InsertMany(It.IsAny<List<Album>>()));
.Returns(new List<Album>());
Mocker.GetMock<IProvideArtistInfo>() Mocker.GetMock<IProvideArtistInfo>()
.Setup(s => s.GetArtistInfo(It.IsAny<string>(), It.IsAny<int>())) .Setup(s => s.GetArtistInfo(It.IsAny<string>(), It.IsAny<int>()))
.Callback(() => { throw new ArtistNotFoundException(_artist.ForeignArtistId); }); .Callback(() => { throw new ArtistNotFoundException(_artist.ForeignArtistId); });
Mocker.GetMock<IMediaFileService>()
.Setup(x => x.GetFilesByArtist(It.IsAny<int>()))
.Returns(new List<TrackFile>());
Mocker.GetMock<IHistoryService>()
.Setup(x => x.GetByArtist(It.IsAny<int>(), It.IsAny<HistoryEventType?>()))
.Returns(new List<History.History>());
} }
private void GivenNewArtistInfo(Artist artist) private void GivenNewArtistInfo(Artist artist)
@ -61,80 +71,206 @@ namespace NzbDrone.Core.Test.MusicTests
.Returns(artist); .Returns(artist);
} }
private void GivenArtistFiles()
{
Mocker.GetMock<IMediaFileService>()
.Setup(x => x.GetFilesByArtist(It.IsAny<int>()))
.Returns(Builder<TrackFile>.CreateListOfSize(1).BuildList());
}
private void GivenAlbumsForRefresh()
{
Mocker.GetMock<IAlbumService>(MockBehavior.Strict)
.Setup(s => s.GetAlbumsForRefresh(It.IsAny<int>(), It.IsAny<IEnumerable<string>>()))
.Returns(new List<Album>());
}
private void AllowArtistUpdate()
{
Mocker.GetMock<IArtistService>(MockBehavior.Strict)
.Setup(x => x.UpdateArtist(It.IsAny<Artist>()))
.Returns((Artist a) => a);
}
[Test] [Test]
public void should_log_error_if_musicbrainz_id_not_found() public void should_not_publish_artist_updated_event_if_metadata_not_updated()
{ {
Subject.Execute(new RefreshArtistCommand(_artist.Id)); var newArtistInfo = _artist.JsonClone();
newArtistInfo.Metadata = _artist.Metadata.Value.JsonClone();
newArtistInfo.Albums = _albums;
Mocker.GetMock<IArtistService>() GivenNewArtistInfo(newArtistInfo);
.Verify(v => v.UpdateArtist(It.IsAny<Artist>()), Times.Never()); GivenAlbumsForRefresh();
AllowArtistUpdate();
ExceptionVerification.ExpectedErrors(1); Subject.Execute(new RefreshArtistCommand(_artist.Id));
VerifyEventNotPublished<ArtistUpdatedEvent>();
} }
[Test] [Test]
public void should_update_if_musicbrainz_id_changed() public void should_publish_artist_updated_event_if_metadata_updated()
{ {
var newArtistInfo = _artist.JsonClone(); var newArtistInfo = _artist.JsonClone();
newArtistInfo.Metadata = _artist.Metadata.Value.JsonClone(); newArtistInfo.Metadata = _artist.Metadata.Value.JsonClone();
newArtistInfo.Metadata.Value.Images = new List<MediaCover.MediaCover> {
new MediaCover.MediaCover(MediaCover.MediaCoverTypes.Logo, "dummy")
};
newArtistInfo.Albums = _albums; newArtistInfo.Albums = _albums;
newArtistInfo.ForeignArtistId = _artist.ForeignArtistId + 1;
GivenNewArtistInfo(newArtistInfo); GivenNewArtistInfo(newArtistInfo);
GivenAlbumsForRefresh();
AllowArtistUpdate();
Subject.Execute(new RefreshArtistCommand(_artist.Id));
VerifyEventPublished<ArtistUpdatedEvent>();
}
[Test]
public void should_log_error_and_delete_if_musicbrainz_id_not_found_and_artist_has_no_files()
{
Mocker.GetMock<IArtistService>()
.Setup(x => x.DeleteArtist(It.IsAny<int>(), It.IsAny<bool>(), It.IsAny<bool>()));
Subject.Execute(new RefreshArtistCommand(_artist.Id)); Subject.Execute(new RefreshArtistCommand(_artist.Id));
Mocker.GetMock<IArtistService>() Mocker.GetMock<IArtistService>()
.Verify(v => v.UpdateArtist(It.Is<Artist>(s => s.ForeignArtistId == newArtistInfo.ForeignArtistId))); .Verify(v => v.UpdateArtist(It.IsAny<Artist>()), Times.Never());
Mocker.GetMock<IArtistService>()
.Verify(v => v.DeleteArtist(It.IsAny<int>(), It.IsAny<bool>(), It.IsAny<bool>()), Times.Once());
ExceptionVerification.ExpectedErrors(1);
ExceptionVerification.ExpectedWarns(1); ExceptionVerification.ExpectedWarns(1);
} }
[Test] [Test]
[Ignore("This test needs to be re-written as we no longer store albums in artist table or object")] public void should_log_error_but_not_delete_if_musicbrainz_id_not_found_and_artist_has_files()
public void should_not_throw_if_duplicate_album_is_in_existing_info()
{ {
var newArtistInfo = _artist.JsonClone(); GivenArtistFiles();
newArtistInfo.Albums.Value.Add(Builder<Album>.CreateNew() GivenAlbumsForRefresh();
.With(s => s.ForeignAlbumId = "2")
.Build());
_artist.Albums.Value.Add(Builder<Album>.CreateNew() Subject.Execute(new RefreshArtistCommand(_artist.Id));
.With(s => s.ForeignAlbumId = "2")
.Build());
_artist.Albums.Value.Add(Builder<Album>.CreateNew() Mocker.GetMock<IArtistService>()
.With(s => s.ForeignAlbumId = "2") .Verify(v => v.UpdateArtist(It.IsAny<Artist>()), Times.Never());
.Build());
Mocker.GetMock<IArtistService>()
.Verify(v => v.DeleteArtist(It.IsAny<int>(), It.IsAny<bool>(), It.IsAny<bool>()), Times.Never());
ExceptionVerification.ExpectedErrors(2);
}
[Test]
public void should_update_if_musicbrainz_id_changed_and_no_clash()
{
var newArtistInfo = _artist.JsonClone();
newArtistInfo.Metadata = _artist.Metadata.Value.JsonClone();
newArtistInfo.Albums = _albums;
newArtistInfo.ForeignArtistId = _artist.ForeignArtistId + 1;
newArtistInfo.Metadata.Value.Id = 100;
GivenNewArtistInfo(newArtistInfo); GivenNewArtistInfo(newArtistInfo);
var seq = new MockSequence();
Mocker.GetMock<IArtistService>(MockBehavior.Strict)
.Setup(x => x.FindById(newArtistInfo.ForeignArtistId))
.Returns(default(Artist));
// Make sure that the artist is updated before we refresh the albums
Mocker.GetMock<IArtistService>(MockBehavior.Strict)
.InSequence(seq)
.Setup(x => x.UpdateArtist(It.IsAny<Artist>()))
.Returns((Artist a) => a);
Mocker.GetMock<IAlbumService>(MockBehavior.Strict)
.InSequence(seq)
.Setup(x => x.GetAlbumsForRefresh(It.IsAny<int>(), It.IsAny<IEnumerable<string>>()))
.Returns(new List<Album>());
// Update called twice for a move/merge
Mocker.GetMock<IArtistService>(MockBehavior.Strict)
.InSequence(seq)
.Setup(x => x.UpdateArtist(It.IsAny<Artist>()))
.Returns((Artist a) => a);
Subject.Execute(new RefreshArtistCommand(_artist.Id)); Subject.Execute(new RefreshArtistCommand(_artist.Id));
Mocker.GetMock<IArtistService>() Mocker.GetMock<IArtistService>()
.Verify(v => v.UpdateArtist(It.Is<Artist>(s => s.Albums.Value.Count == 2))); .Verify(v => v.UpdateArtist(It.Is<Artist>(s => s.ArtistMetadataId == 100 && s.ForeignArtistId == newArtistInfo.ForeignArtistId)),
Times.Exactly(2));
} }
[Test] [Test]
[Ignore("This test needs to be re-written as we no longer store albums in artist table or object")] public void should_merge_if_musicbrainz_id_changed_and_new_id_already_exists()
public void should_filter_duplicate_albums()
{ {
var newArtistInfo = _artist.JsonClone(); var existing = _artist;
newArtistInfo.Albums.Value.Add(Builder<Album>.CreateNew()
.With(s => s.ForeignAlbumId = "2")
.Build());
newArtistInfo.Albums.Value.Add(Builder<Album>.CreateNew() var clash = _artist.JsonClone();
.With(s => s.ForeignAlbumId = "2") clash.Id = 100;
.Build()); clash.Metadata = existing.Metadata.Value.JsonClone();
clash.Metadata.Value.Id = 101;
clash.Metadata.Value.ForeignArtistId = clash.Metadata.Value.ForeignArtistId + 1;
Mocker.GetMock<IArtistService>(MockBehavior.Strict)
.Setup(x => x.FindById(clash.Metadata.Value.ForeignArtistId))
.Returns(clash);
var newArtistInfo = clash.JsonClone();
newArtistInfo.Metadata = clash.Metadata.Value.JsonClone();
newArtistInfo.Albums = _albums.JsonClone();
newArtistInfo.Albums.Value.ForEach(x => x.Id = 0);
GivenNewArtistInfo(newArtistInfo); GivenNewArtistInfo(newArtistInfo);
var seq = new MockSequence();
// Make sure that the artist is updated before we refresh the albums
Mocker.GetMock<IAlbumService>(MockBehavior.Strict)
.InSequence(seq)
.Setup(x => x.GetAlbumsByArtist(existing.Id))
.Returns(_albums);
Mocker.GetMock<IAlbumService>(MockBehavior.Strict)
.InSequence(seq)
.Setup(x => x.UpdateMany(It.IsAny<List<Album>>()));
Mocker.GetMock<IArtistService>(MockBehavior.Strict)
.InSequence(seq)
.Setup(x => x.DeleteArtist(existing.Id, It.IsAny<bool>(), false));
Mocker.GetMock<IArtistService>(MockBehavior.Strict)
.InSequence(seq)
.Setup(x => x.UpdateArtist(It.Is<Artist>(a => a.Id == clash.Id)))
.Returns((Artist a) => a);
Mocker.GetMock<IAlbumService>(MockBehavior.Strict)
.InSequence(seq)
.Setup(x => x.GetAlbumsForRefresh(clash.ArtistMetadataId, It.IsAny<IEnumerable<string>>()))
.Returns(_albums);
// Update called twice for a move/merge
Mocker.GetMock<IArtistService>(MockBehavior.Strict)
.InSequence(seq)
.Setup(x => x.UpdateArtist(It.IsAny<Artist>()))
.Returns((Artist a) => a);
Subject.Execute(new RefreshArtistCommand(_artist.Id)); Subject.Execute(new RefreshArtistCommand(_artist.Id));
// the retained artist gets updated
Mocker.GetMock<IArtistService>() Mocker.GetMock<IArtistService>()
.Verify(v => v.UpdateArtist(It.Is<Artist>(s => s.Albums.Value.Count == 2))); .Verify(v => v.UpdateArtist(It.Is<Artist>(s => s.Id == clash.Id)), Times.Exactly(2));
// the old one gets removed
Mocker.GetMock<IArtistService>()
.Verify(v => v.DeleteArtist(existing.Id, false, false));
Mocker.GetMock<IAlbumService>()
.Verify(v => v.UpdateMany(It.Is<List<Album>>(x => x.Count == _albums.Count)));
ExceptionVerification.ExpectedWarns(1);
} }
} }
} }

@ -275,7 +275,6 @@
<Compile Include="MetadataSource\SkyHook\SkyHookProxySearchFixture.cs" /> <Compile Include="MetadataSource\SkyHook\SkyHookProxySearchFixture.cs" />
<Compile Include="MetadataSource\SearchArtistComparerFixture.cs" /> <Compile Include="MetadataSource\SearchArtistComparerFixture.cs" />
<Compile Include="MetadataSource\SkyHook\SkyHookProxyFixture.cs" /> <Compile Include="MetadataSource\SkyHook\SkyHookProxyFixture.cs" />
<Compile Include="MusicTests\AddAlbumFixture.cs" />
<Compile Include="MusicTests\AlbumServiceFixture.cs" /> <Compile Include="MusicTests\AlbumServiceFixture.cs" />
<Compile Include="MusicTests\AddArtistFixture.cs" /> <Compile Include="MusicTests\AddArtistFixture.cs" />
<Compile Include="MusicTests\AlbumMonitoredServiceTests\AlbumMonitoredServiceFixture.cs" /> <Compile Include="MusicTests\AlbumMonitoredServiceTests\AlbumMonitoredServiceFixture.cs" />

@ -28,6 +28,7 @@ namespace NzbDrone.Core.History
List<History> Find(string downloadId, HistoryEventType eventType); List<History> Find(string downloadId, HistoryEventType eventType);
List<History> FindByDownloadId(string downloadId); List<History> FindByDownloadId(string downloadId);
List<History> Since(DateTime date, HistoryEventType? eventType); List<History> Since(DateTime date, HistoryEventType? eventType);
void UpdateMany(IList<History> items);
} }
public class HistoryService : IHistoryService, public class HistoryService : IHistoryService,
@ -381,5 +382,10 @@ namespace NzbDrone.Core.History
{ {
return _historyRepository.Since(date, eventType); return _historyRepository.Since(date, eventType);
} }
public void UpdateMany(IList<History> items)
{
_historyRepository.UpdateMany(items);
}
} }
} }

@ -1,14 +1,13 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace NzbDrone.Core.MetadataSource.SkyHook.Resource namespace NzbDrone.Core.MetadataSource.SkyHook.Resource
{ {
public class ArtistResource public class ArtistResource
{ {
public ArtistResource() { public ArtistResource()
{
Albums = new List<AlbumResource>(); Albums = new List<AlbumResource>();
Genres = new List<string>();
} }
public List<string> Genres { get; set; } public List<string> Genres { get; set; }

@ -82,10 +82,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
artist.SortName = Parser.Parser.NormalizeTitle(artist.Metadata.Value.Name); artist.SortName = Parser.Parser.NormalizeTitle(artist.Metadata.Value.Name);
artist.Albums = FilterAlbums(httpResponse.Resource.Albums, metadataProfileId) artist.Albums = FilterAlbums(httpResponse.Resource.Albums, metadataProfileId)
.Select(x => new Album { .Select(x => MapAlbum(x, null)).ToList();
ForeignAlbumId = x.Id
})
.ToList();
return artist; return artist;
} }
@ -136,6 +133,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
var artists = httpResponse.Resource.Artists.Select(MapArtistMetadata).ToList(); var artists = httpResponse.Resource.Artists.Select(MapArtistMetadata).ToList();
var artistDict = artists.ToDictionary(x => x.ForeignArtistId, x => x); var artistDict = artists.ToDictionary(x => x.ForeignArtistId, x => x);
var album = MapAlbum(httpResponse.Resource, artistDict); var album = MapAlbum(httpResponse.Resource, artistDict);
album.ArtistMetadata = artistDict[httpResponse.Resource.ArtistId];
return new Tuple<string, Album, List<ArtistMetadata>>(httpResponse.Resource.ArtistId, album, artists); return new Tuple<string, Album, List<ArtistMetadata>>(httpResponse.Resource.ArtistId, album, artists);
} }
@ -181,8 +179,6 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
.SetSegment("route", "search") .SetSegment("route", "search")
.AddQueryParam("type", "artist") .AddQueryParam("type", "artist")
.AddQueryParam("query", title.ToLower().Trim()) .AddQueryParam("query", title.ToLower().Trim())
//.AddQueryParam("images","false") // Should pass these on import search to avoid looking to fanart and wiki
//.AddQueryParam("overview","false")
.Build(); .Build();

@ -1,160 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FluentValidation;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Core.Exceptions;
using NzbDrone.Core.MetadataSource;
namespace NzbDrone.Core.Music
{
public interface IAddAlbumService
{
Album AddAlbum(Album newAlbum);
List<Album> AddAlbums(List<Album> newAlbums);
}
public class AddAlbumService : IAddAlbumService
{
private readonly IAlbumService _albumService;
private readonly IReleaseService _releaseService;
private readonly IProvideAlbumInfo _albumInfo;
private readonly IArtistMetadataRepository _artistMetadataRepository;
private readonly IRefreshTrackService _refreshTrackService;
private readonly Logger _logger;
public AddAlbumService(IAlbumService albumService,
IReleaseService releaseService,
IProvideAlbumInfo albumInfo,
IArtistMetadataRepository artistMetadataRepository,
IRefreshTrackService refreshTrackService,
Logger logger)
{
_albumService = albumService;
_releaseService = releaseService;
_albumInfo = albumInfo;
_artistMetadataRepository = artistMetadataRepository;
_refreshTrackService = refreshTrackService;
_logger = logger;
}
public List<AlbumRelease> AddAlbumReleases(Album album)
{
var remoteReleases = album.AlbumReleases.Value.DistinctBy(m => m.ForeignReleaseId).ToList();
var existingReleases = _releaseService.GetReleasesForRefresh(album.Id, remoteReleases.Select(x => x.ForeignReleaseId));
var newReleaseList = new List<AlbumRelease>();
var updateReleaseList = new List<AlbumRelease>();
foreach (var release in remoteReleases)
{
release.AlbumId = album.Id;
release.Album = album;
var releaseToRefresh = existingReleases.SingleOrDefault(r => r.ForeignReleaseId == release.ForeignReleaseId);
if (releaseToRefresh != null)
{
existingReleases.Remove(releaseToRefresh);
// copy across the db keys and check for equality
release.Id = releaseToRefresh.Id;
release.AlbumId = releaseToRefresh.AlbumId;
updateReleaseList.Add(release);
}
else
{
newReleaseList.Add(release);
}
}
// Ensure only one release is monitored
remoteReleases.ForEach(x => x.Monitored = false);
remoteReleases.OrderByDescending(x => x.TrackCount).First().Monitored = true;
Ensure.That(remoteReleases.Count(x => x.Monitored) == 1).IsTrue();
// Since this is a new album, we can't be deleting any existing releases
_releaseService.UpdateMany(updateReleaseList);
_releaseService.InsertMany(newReleaseList);
return remoteReleases;
}
private Album AddAlbum(Tuple<string, Album, List<ArtistMetadata>> skyHookData)
{
var newAlbum = skyHookData.Item2;
if (newAlbum.AlbumReleases.Value.Count == 0)
{
_logger.Debug($"Skipping album with no valid releases {newAlbum}");
return null;
}
_logger.ProgressInfo("Adding Album {0}", newAlbum.Title);
_artistMetadataRepository.UpsertMany(skyHookData.Item3);
newAlbum.ArtistMetadata = _artistMetadataRepository.FindById(skyHookData.Item1);
newAlbum.ArtistMetadataId = newAlbum.ArtistMetadata.Value.Id;
_albumService.AddAlbum(newAlbum);
AddAlbumReleases(newAlbum);
_refreshTrackService.RefreshTrackInfo(newAlbum, false);
return newAlbum;
}
public Album AddAlbum(Album newAlbum)
{
Ensure.That(newAlbum, () => newAlbum).IsNotNull();
var tuple = AddSkyhookData(newAlbum);
return AddAlbum(tuple);
}
public List<Album> AddAlbums(List<Album> newAlbums)
{
var added = DateTime.UtcNow;
var albumsToAdd = new List<Album>();
foreach (var newAlbum in newAlbums)
{
var tuple = AddSkyhookData(newAlbum);
tuple.Item2.Added = added;
tuple.Item2.LastInfoSync = added;
albumsToAdd.Add(AddAlbum(tuple));
}
return albumsToAdd;
}
private Tuple<string, Album, List<ArtistMetadata>> AddSkyhookData(Album newAlbum)
{
Tuple<string, Album, List<ArtistMetadata>> tuple;
try
{
tuple = _albumInfo.GetAlbumInfo(newAlbum.ForeignAlbumId);
}
catch (AlbumNotFoundException)
{
_logger.Error("LidarrId {1} was not found, it may have been removed from Lidarr.", newAlbum.ForeignAlbumId);
throw new ValidationException(new List<ValidationFailure>
{
new ValidationFailure("MusicBrainzId", "An album with this ID was not found", newAlbum.ForeignAlbumId)
});
}
tuple.Item2.Monitored = newAlbum.Monitored;
tuple.Item2.ProfileId = newAlbum.ProfileId;
return tuple;
}
}
}

@ -23,18 +23,21 @@ namespace NzbDrone.Core.Music
public class AddArtistService : IAddArtistService public class AddArtistService : IAddArtistService
{ {
private readonly IArtistService _artistService; private readonly IArtistService _artistService;
private readonly IArtistMetadataRepository _artistMetadataRepository;
private readonly IProvideArtistInfo _artistInfo; private readonly IProvideArtistInfo _artistInfo;
private readonly IBuildFileNames _fileNameBuilder; private readonly IBuildFileNames _fileNameBuilder;
private readonly IAddArtistValidator _addArtistValidator; private readonly IAddArtistValidator _addArtistValidator;
private readonly Logger _logger; private readonly Logger _logger;
public AddArtistService(IArtistService artistService, public AddArtistService(IArtistService artistService,
IArtistMetadataRepository artistMetadataRepository,
IProvideArtistInfo artistInfo, IProvideArtistInfo artistInfo,
IBuildFileNames fileNameBuilder, IBuildFileNames fileNameBuilder,
IAddArtistValidator addArtistValidator, IAddArtistValidator addArtistValidator,
Logger logger) Logger logger)
{ {
_artistService = artistService; _artistService = artistService;
_artistMetadataRepository = artistMetadataRepository;
_artistInfo = artistInfo; _artistInfo = artistInfo;
_fileNameBuilder = fileNameBuilder; _fileNameBuilder = fileNameBuilder;
_addArtistValidator = addArtistValidator; _addArtistValidator = addArtistValidator;
@ -49,6 +52,12 @@ namespace NzbDrone.Core.Music
newArtist = SetPropertiesAndValidate(newArtist); newArtist = SetPropertiesAndValidate(newArtist);
_logger.Info("Adding Artist {0} Path: [{1}]", newArtist, newArtist.Path); _logger.Info("Adding Artist {0} Path: [{1}]", newArtist, newArtist.Path);
// add metadata
_artistMetadataRepository.Upsert(newArtist.Metadata.Value);
newArtist.ArtistMetadataId = newArtist.Metadata.Value.Id;
// add the artist itself
_artistService.AddArtist(newArtist); _artistService.AddArtist(newArtist);
return newArtist; return newArtist;
@ -77,6 +86,12 @@ namespace NzbDrone.Core.Music
} }
// add metadata
_artistMetadataRepository.UpsertMany(artistsToAdd);
_logger.Debug("metadata id 1 {0}", string.Join(", ", artistsToAdd.Select(x => x.Metadata.Value.Id)));
_logger.Debug("metadata id 2 {0}", string.Join(", ", artistsToAdd.Select(x => x.ArtistMetadataId)));
return _artistService.AddArtists(artistsToAdd); return _artistService.AddArtists(artistsToAdd);
} }

@ -3,10 +3,12 @@ using NzbDrone.Core.Datastore;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Marr.Data; using Marr.Data;
using System.Linq;
using NzbDrone.Common.Serializer;
namespace NzbDrone.Core.Music namespace NzbDrone.Core.Music
{ {
public class Album : ModelBase public class Album : ModelBase, IEquatable<Album>
{ {
public Album() public Album()
{ {
@ -66,5 +68,72 @@ namespace NzbDrone.Core.Music
Monitored = otherAlbum.Monitored; Monitored = otherAlbum.Monitored;
AnyReleaseOk = otherAlbum.AnyReleaseOk; AnyReleaseOk = otherAlbum.AnyReleaseOk;
} }
public bool Equals(Album other)
{
if (other == null)
{
return false;
}
if (Id == other.Id &&
ForeignAlbumId == other.ForeignAlbumId &&
(OldForeignAlbumIds?.SequenceEqual(other.OldForeignAlbumIds) ?? true) &&
Title == other.Title &&
Overview == other.Overview &&
Disambiguation == other.Disambiguation &&
ReleaseDate == other.ReleaseDate &&
Images?.ToJson() == other.Images?.ToJson() &&
Links?.ToJson() == other.Links?.ToJson() &&
(Genres?.SequenceEqual(other.Genres) ?? true) &&
AlbumType == other.AlbumType &&
(SecondaryTypes?.SequenceEqual(other.SecondaryTypes) ?? true) &&
Ratings?.ToJson() == other.Ratings?.ToJson())
{
return true;
}
return false;
}
public override bool Equals(object obj)
{
if (obj == null)
{
return false;
}
var other = obj as Album;
if (other == null)
{
return false;
}
else
{
return Equals(other);
}
}
public override int GetHashCode()
{
unchecked
{
int hash = 17;
hash = hash * 23 + Id;
hash = hash * 23 + ForeignAlbumId.GetHashCode();
hash = hash * 23 + OldForeignAlbumIds?.GetHashCode() ?? 0;
hash = hash * 23 + Title?.GetHashCode() ?? 0;
hash = hash * 23 + Overview?.GetHashCode() ?? 0;
hash = hash * 23 + Disambiguation?.GetHashCode() ?? 0;
hash = hash * 23 + ReleaseDate?.GetHashCode() ?? 0;
hash = hash * 23 + Images?.GetHashCode() ?? 0;
hash = hash * 23 + Links?.GetHashCode() ?? 0;
hash = hash * 23 + Genres?.GetHashCode() ?? 0;
hash = hash * 23 + AlbumType?.GetHashCode() ?? 0;
hash = hash * 23 + SecondaryTypes?.GetHashCode() ?? 0;
hash = hash * 23 + Ratings?.GetHashCode() ?? 0;
return hash;
}
}
} }
} }

@ -2,7 +2,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
using Marr.Data;
using NLog; using NLog;
namespace NzbDrone.Core.Music namespace NzbDrone.Core.Music
@ -16,8 +15,8 @@ namespace NzbDrone.Core.Music
void UpdateMany(List<Artist> artists); void UpdateMany(List<Artist> artists);
ArtistMetadata FindById(string foreignArtistId); ArtistMetadata FindById(string foreignArtistId);
List<ArtistMetadata> FindById(List<string> foreignIds); List<ArtistMetadata> FindById(List<string> foreignIds);
void UpsertMany(List<ArtistMetadata> artists); bool UpsertMany(List<ArtistMetadata> artists);
void UpsertMany(List<Artist> artists); bool UpsertMany(List<Artist> artists);
} }
public class ArtistMetadataRepository : BasicRepository<ArtistMetadata>, IArtistMetadataRepository public class ArtistMetadataRepository : BasicRepository<ArtistMetadata>, IArtistMetadataRepository
@ -40,10 +39,8 @@ namespace NzbDrone.Core.Music
public List<Artist> InsertMany(List<Artist> artists) public List<Artist> InsertMany(List<Artist> artists)
{ {
InsertMany(artists.Select(x => x.Metadata.Value).ToList()); InsertMany(artists.Select(x => x.Metadata.Value).ToList());
foreach (var artist in artists) artists.ForEach(x => x.ArtistMetadataId = x.Metadata.Value.Id);
{
artist.ArtistMetadataId = artist.Metadata.Value.Id;
}
return artists; return artists;
} }
@ -70,12 +67,12 @@ namespace NzbDrone.Core.Music
return artist; return artist;
} }
public void UpsertMany(List<Artist> artists) public bool UpsertMany(List<Artist> artists)
{
foreach (var artist in artists)
{ {
Upsert(artist); var result = UpsertMany(artists.Select(x => x.Metadata.Value).ToList());
} artists.ForEach(x => x.ArtistMetadataId = x.Metadata.Value.Id);
return result;
} }
public void UpdateMany(List<Artist> artists) public void UpdateMany(List<Artist> artists)
@ -93,22 +90,40 @@ namespace NzbDrone.Core.Music
return Query.Where($"[ForeignArtistId] IN ('{string.Join("','", foreignIds)}')").ToList(); return Query.Where($"[ForeignArtistId] IN ('{string.Join("','", foreignIds)}')").ToList();
} }
public void UpsertMany(List<ArtistMetadata> artists) public bool UpsertMany(List<ArtistMetadata> data)
{ {
foreach (var artist in artists) var existingMetadata = FindById(data.Select(x => x.ForeignArtistId).ToList());
var updateMetadataList = new List<ArtistMetadata>();
var addMetadataList = new List<ArtistMetadata>();
int upToDateMetadataCount = 0;
foreach (var meta in data)
{ {
var existing = FindById(artist.ForeignArtistId); var existing = existingMetadata.SingleOrDefault(x => x.ForeignArtistId == meta.ForeignArtistId);
if (existing != null) if (existing != null)
{ {
artist.Id = existing.Id; meta.Id = existing.Id;
Update(artist); if (!meta.Equals(existing))
{
updateMetadataList.Add(meta);
} }
else else
{ {
Insert(artist); upToDateMetadataCount++;
} }
_logger.Debug("Upserted metadata with ID {0}", artist.Id);
} }
else
{
addMetadataList.Add(meta);
}
}
UpdateMany(updateMetadataList);
InsertMany(addMetadataList);
_logger.Debug($"{upToDateMetadataCount} artist metadata up to date; Updating {updateMetadataList.Count}, Adding {addMetadataList.Count} artist metadata entries.");
return updateMetadataList.Count > 0 || addMetadataList.Count > 0;
} }
} }
} }

@ -34,7 +34,6 @@ namespace NzbDrone.Core.Music
public class ArtistService : IArtistService public class ArtistService : IArtistService
{ {
private readonly IArtistRepository _artistRepository; private readonly IArtistRepository _artistRepository;
private readonly IArtistMetadataRepository _artistMetadataRepository;
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
private readonly ITrackService _trackService; private readonly ITrackService _trackService;
private readonly IImportListExclusionService _importListExclusionService; private readonly IImportListExclusionService _importListExclusionService;
@ -43,7 +42,6 @@ namespace NzbDrone.Core.Music
private readonly ICached<List<Artist>> _cache; private readonly ICached<List<Artist>> _cache;
public ArtistService(IArtistRepository artistRepository, public ArtistService(IArtistRepository artistRepository,
IArtistMetadataRepository artistMetadataRepository,
IEventAggregator eventAggregator, IEventAggregator eventAggregator,
ITrackService trackService, ITrackService trackService,
IImportListExclusionService importListExclusionService, IImportListExclusionService importListExclusionService,
@ -52,7 +50,6 @@ namespace NzbDrone.Core.Music
Logger logger) Logger logger)
{ {
_artistRepository = artistRepository; _artistRepository = artistRepository;
_artistMetadataRepository = artistMetadataRepository;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
_trackService = trackService; _trackService = trackService;
_importListExclusionService = importListExclusionService; _importListExclusionService = importListExclusionService;
@ -64,7 +61,6 @@ namespace NzbDrone.Core.Music
public Artist AddArtist(Artist newArtist) public Artist AddArtist(Artist newArtist)
{ {
_cache.Clear(); _cache.Clear();
_artistMetadataRepository.Upsert(newArtist);
_artistRepository.Insert(newArtist); _artistRepository.Insert(newArtist);
_eventAggregator.PublishEvent(new ArtistAddedEvent(GetArtist(newArtist.Id))); _eventAggregator.PublishEvent(new ArtistAddedEvent(GetArtist(newArtist.Id)));
@ -74,7 +70,6 @@ namespace NzbDrone.Core.Music
public List<Artist> AddArtists(List<Artist> newArtists) public List<Artist> AddArtists(List<Artist> newArtists)
{ {
_cache.Clear(); _cache.Clear();
_artistMetadataRepository.UpsertMany(newArtists);
_artistRepository.InsertMany(newArtists); _artistRepository.InsertMany(newArtists);
_eventAggregator.PublishEvent(new ArtistsImportedEvent(newArtists.Select(s => s.Id).ToList())); _eventAggregator.PublishEvent(new ArtistsImportedEvent(newArtists.Select(s => s.Id).ToList()));
@ -211,9 +206,8 @@ namespace NzbDrone.Core.Music
public Artist UpdateArtist(Artist artist) public Artist UpdateArtist(Artist artist)
{ {
_cache.Clear(); _cache.Clear();
var storedArtist = GetArtist(artist.Id); // Is it Id or iTunesId? var storedArtist = GetArtist(artist.Id);
var updatedArtist = _artistMetadataRepository.Update(artist); var updatedArtist = _artistRepository.Update(artist);
updatedArtist = _artistRepository.Update(updatedArtist);
_eventAggregator.PublishEvent(new ArtistEditedEvent(updatedArtist, storedArtist)); _eventAggregator.PublishEvent(new ArtistEditedEvent(updatedArtist, storedArtist));
return updatedArtist; return updatedArtist;
@ -242,7 +236,6 @@ namespace NzbDrone.Core.Music
} }
} }
_artistMetadataRepository.UpdateMany(artist);
_artistRepository.UpdateMany(artist); _artistRepository.UpdateMany(artist);
_logger.Debug("{0} artists updated", artist.Count); _logger.Debug("{0} artists updated", artist.Count);

@ -0,0 +1,138 @@
using NLog;
using NzbDrone.Common.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Core.MediaFiles;
namespace NzbDrone.Core.Music
{
public interface IRefreshAlbumReleaseService
{
bool RefreshEntityInfo(AlbumRelease entity, List<AlbumRelease> remoteEntityList, bool forceChildRefresh, bool forceUpdateFileTags);
bool RefreshEntityInfo(List<AlbumRelease> releases, List<AlbumRelease> remoteEntityList, bool forceChildRefresh, bool forceUpdateFileTags);
}
public class RefreshAlbumReleaseService : RefreshEntityServiceBase<AlbumRelease, Track>, IRefreshAlbumReleaseService
{
private readonly IReleaseService _releaseService;
private readonly IRefreshTrackService _refreshTrackService;
private readonly ITrackService _trackService;
private readonly IMediaFileService _mediaFileService;
private readonly Logger _logger;
public RefreshAlbumReleaseService(IReleaseService releaseService,
IArtistMetadataRepository artistMetadataRepository,
IRefreshTrackService refreshTrackService,
ITrackService trackService,
IMediaFileService mediaFileService,
Logger logger)
: base(logger, artistMetadataRepository)
{
_releaseService = releaseService;
_trackService = trackService;
_refreshTrackService = refreshTrackService;
_mediaFileService = mediaFileService;
_logger = logger;
}
protected override RemoteData GetRemoteData(AlbumRelease local, List<AlbumRelease> remote)
{
var result = new RemoteData();
result.Entity = remote.SingleOrDefault(x => x.ForeignReleaseId == local.ForeignReleaseId || x.OldForeignReleaseIds.Contains(local.ForeignReleaseId));
return result;
}
protected override bool IsMerge(AlbumRelease local, AlbumRelease remote)
{
return local.ForeignReleaseId != remote.ForeignReleaseId;
}
protected override UpdateResult UpdateEntity(AlbumRelease local, AlbumRelease remote)
{
if (local.Equals(remote))
{
return UpdateResult.None;
}
local.OldForeignReleaseIds = remote.OldForeignReleaseIds;
local.Title = remote.Title;
local.Status = remote.Status;
local.Duration = remote.Duration;
local.Label = remote.Label;
local.Disambiguation = remote.Disambiguation;
local.Country = remote.Country;
local.ReleaseDate = remote.ReleaseDate;
local.Media = remote.Media;
local.TrackCount = remote.TrackCount;
return UpdateResult.UpdateTags;
}
protected override AlbumRelease GetEntityByForeignId(AlbumRelease local)
{
return _releaseService.GetReleaseByForeignReleaseId(local.ForeignReleaseId);
}
protected override void SaveEntity(AlbumRelease local)
{
_releaseService.UpdateMany(new List<AlbumRelease> { local });
}
protected override void DeleteEntity(AlbumRelease local, bool deleteFiles)
{
_releaseService.DeleteMany(new List<AlbumRelease> { local });
}
protected override List<Track> GetRemoteChildren(AlbumRelease remote)
{
return remote.Tracks.Value.DistinctBy(m => m.ForeignTrackId).ToList();
}
protected override List<Track> GetLocalChildren(AlbumRelease entity, List<Track> remoteChildren)
{
return _trackService.GetTracksForRefresh(entity.Id,
remoteChildren.Select(x => x.ForeignTrackId)
.Concat(remoteChildren.SelectMany(x => x.OldForeignTrackIds)));
}
protected override Tuple<Track, List<Track> > GetMatchingExistingChildren(List<Track> existingChildren, Track remote)
{
var existingChild = existingChildren.SingleOrDefault(x => x.ForeignTrackId == remote.ForeignTrackId);
var mergeChildren = existingChildren.Where(x => remote.OldForeignTrackIds.Contains(x.ForeignTrackId)).ToList();
return Tuple.Create(existingChild, mergeChildren);
}
protected override void PrepareNewChild(Track child, AlbumRelease entity)
{
child.AlbumReleaseId = entity.Id;
child.AlbumRelease = entity;
child.ArtistMetadataId = child.ArtistMetadata.Value.Id;
// make sure title is not null
child.Title = child.Title ?? "Unknown";
}
protected override void PrepareExistingChild(Track local, Track remote, AlbumRelease entity)
{
local.AlbumRelease = entity;
local.AlbumReleaseId = entity.Id;
local.ArtistMetadataId = remote.ArtistMetadata.Value.Id;
remote.Id = local.Id;
remote.TrackFileId = local.TrackFileId;
remote.AlbumReleaseId = local.AlbumReleaseId;
remote.ArtistMetadataId = local.ArtistMetadataId;
}
protected override void AddChildren(List<Track> children)
{
_trackService.InsertMany(children);
}
protected override bool RefreshChildren(SortedChildren localChildren, List<Track> remoteChildren, bool forceChildRefresh, bool forceUpdateFileTags)
{
return _refreshTrackService.RefreshTrackInfo(localChildren.Added, localChildren.Updated, localChildren.Merged, localChildren.Deleted, localChildren.UpToDate, remoteChildren, forceUpdateFileTags);
}
}
}

@ -11,247 +11,323 @@ using NzbDrone.Core.Exceptions;
using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Music.Commands; using NzbDrone.Core.Music.Commands;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Common.EnsureThat; using NzbDrone.Core.History;
namespace NzbDrone.Core.Music namespace NzbDrone.Core.Music
{ {
public interface IRefreshAlbumService public interface IRefreshAlbumService
{ {
bool RefreshAlbumInfo(Album album, bool forceUpdateFileTags); bool RefreshAlbumInfo(Album album, List<Album> remoteAlbums, bool forceUpdateFileTags);
bool RefreshAlbumInfo(List<Album> albums, bool forceAlbumRefresh, bool forceUpdateFileTags); bool RefreshAlbumInfo(List<Album> albums, List<Album> remoteAlbums, bool forceAlbumRefresh, bool forceUpdateFileTags);
} }
public class RefreshAlbumService : IRefreshAlbumService, IExecute<RefreshAlbumCommand> public class RefreshAlbumService : RefreshEntityServiceBase<Album, AlbumRelease>, IRefreshAlbumService, IExecute<RefreshAlbumCommand>
{ {
private readonly IAlbumService _albumService; private readonly IAlbumService _albumService;
private readonly IArtistService _artistService; private readonly IArtistService _artistService;
private readonly IArtistMetadataRepository _artistMetadataRepository; private readonly IAddArtistService _addArtistService;
private readonly IReleaseService _releaseService; private readonly IReleaseService _releaseService;
private readonly IProvideAlbumInfo _albumInfo; private readonly IProvideAlbumInfo _albumInfo;
private readonly IRefreshTrackService _refreshTrackService; private readonly IRefreshAlbumReleaseService _refreshAlbumReleaseService;
private readonly IAudioTagService _audioTagService; private readonly IMediaFileService _mediaFileService;
private readonly IHistoryService _historyService;
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
private readonly ICheckIfAlbumShouldBeRefreshed _checkIfAlbumShouldBeRefreshed; private readonly ICheckIfAlbumShouldBeRefreshed _checkIfAlbumShouldBeRefreshed;
private readonly Logger _logger; private readonly Logger _logger;
public RefreshAlbumService(IAlbumService albumService, public RefreshAlbumService(IAlbumService albumService,
IArtistService artistService, IArtistService artistService,
IAddArtistService addArtistService,
IArtistMetadataRepository artistMetadataRepository, IArtistMetadataRepository artistMetadataRepository,
IReleaseService releaseService, IReleaseService releaseService,
IProvideAlbumInfo albumInfo, IProvideAlbumInfo albumInfo,
IRefreshTrackService refreshTrackService, IRefreshAlbumReleaseService refreshAlbumReleaseService,
IAudioTagService audioTagService, IMediaFileService mediaFileService,
IHistoryService historyService,
IEventAggregator eventAggregator, IEventAggregator eventAggregator,
ICheckIfAlbumShouldBeRefreshed checkIfAlbumShouldBeRefreshed, ICheckIfAlbumShouldBeRefreshed checkIfAlbumShouldBeRefreshed,
Logger logger) Logger logger)
: base(logger, artistMetadataRepository)
{ {
_albumService = albumService; _albumService = albumService;
_artistService = artistService; _artistService = artistService;
_artistMetadataRepository = artistMetadataRepository; _addArtistService = addArtistService;
_releaseService = releaseService; _releaseService = releaseService;
_albumInfo = albumInfo; _albumInfo = albumInfo;
_refreshTrackService = refreshTrackService; _refreshAlbumReleaseService = refreshAlbumReleaseService;
_audioTagService = audioTagService; _mediaFileService = mediaFileService;
_historyService = historyService;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
_checkIfAlbumShouldBeRefreshed = checkIfAlbumShouldBeRefreshed; _checkIfAlbumShouldBeRefreshed = checkIfAlbumShouldBeRefreshed;
_logger = logger; _logger = logger;
} }
public bool RefreshAlbumInfo(List<Album> albums, bool forceAlbumRefresh, bool forceUpdateFileTags) protected override RemoteData GetRemoteData(Album local, List<Album> remote)
{ {
bool updated = false; var result = new RemoteData();
foreach (var album in albums)
// remove not in remote list and ShouldDelete is true
if (remote != null &&
!remote.Any(x => x.ForeignAlbumId == local.ForeignAlbumId || x.OldForeignAlbumIds.Contains(local.ForeignAlbumId)) &&
ShouldDelete(local))
{ {
if (forceAlbumRefresh || _checkIfAlbumShouldBeRefreshed.ShouldRefresh(album)) return result;
}
Tuple<string, Album, List<ArtistMetadata>> tuple = null;
try
{ {
updated |= RefreshAlbumInfo(album, forceUpdateFileTags); tuple = _albumInfo.GetAlbumInfo(local.ForeignAlbumId);
} }
catch (AlbumNotFoundException)
{
return result;
} }
return updated;
if (tuple.Item2.AlbumReleases.Value.Count == 0)
{
_logger.Debug($"{local} has no valid releases, removing.");
return result;
} }
public bool RefreshAlbumInfo(Album album, bool forceUpdateFileTags) result.Entity = tuple.Item2;
result.Metadata = tuple.Item3;
return result;
}
protected override void EnsureNewParent(Album local, Album remote)
{ {
_logger.ProgressInfo("Updating Info for {0}", album.Title); // Make sure the appropriate artist exists (it could be that an album changes parent)
bool updated = false; // The artistMetadata entry will be in the db but make sure a corresponding artist is too
// so that the album doesn't just disappear.
Tuple<string, Album, List<ArtistMetadata>> tuple; // TODO filter by metadata id before hitting database
_logger.Trace($"Ensuring parent artist exists [{remote.ArtistMetadata.Value.ForeignArtistId}]");
try var newArtist = _artistService.FindById(remote.ArtistMetadata.Value.ForeignArtistId);
if (newArtist == null)
{ {
tuple = _albumInfo.GetAlbumInfo(album.ForeignAlbumId); var oldArtist = local.Artist.Value;
var addArtist = new Artist {
Metadata = remote.ArtistMetadata.Value,
MetadataProfileId = oldArtist.MetadataProfileId,
QualityProfileId = oldArtist.QualityProfileId,
LanguageProfileId = oldArtist.LanguageProfileId,
RootFolderPath = oldArtist.RootFolderPath,
Monitored = oldArtist.Monitored,
AlbumFolder = oldArtist.AlbumFolder
};
_logger.Debug($"Adding missing parent artist {addArtist}");
_addArtistService.AddArtist(addArtist);
} }
catch (AlbumNotFoundException)
{
_logger.Error($"{album} was not found, it may have been removed from Metadata sources.");
return updated;
} }
if (tuple.Item2.AlbumReleases.Value.Count == 0) protected override bool ShouldDelete(Album local)
{ {
_logger.Debug($"{album} has no valid releases, removing."); return !_mediaFileService.GetFilesByAlbum(local.Id).Any();
_albumService.DeleteMany(new List<Album> { album });
return true;
} }
var remoteMetadata = tuple.Item3.DistinctBy(x => x.ForeignArtistId).ToList(); protected override void LogProgress(Album local)
var existingMetadata = _artistMetadataRepository.FindById(remoteMetadata.Select(x => x.ForeignArtistId).ToList()); {
var updateMetadataList = new List<ArtistMetadata>(); _logger.ProgressInfo("Updating Info for {0}", local.Title);
var addMetadataList = new List<ArtistMetadata>(); }
var upToDateMetadataCount = 0;
foreach (var meta in remoteMetadata) protected override bool IsMerge(Album local, Album remote)
{ {
var existing = existingMetadata.SingleOrDefault(x => x.ForeignArtistId == meta.ForeignArtistId); return local.ForeignAlbumId != remote.ForeignAlbumId;
if (existing != null) }
protected override UpdateResult UpdateEntity(Album local, Album remote)
{ {
meta.Id = existing.Id; UpdateResult result;
if (!meta.Equals(existing))
if (local.Title != (remote.Title ?? "Unknown") ||
local.ForeignAlbumId != remote.ForeignAlbumId ||
local.ArtistMetadata.Value.ForeignArtistId != remote.ArtistMetadata.Value.ForeignArtistId)
{ {
updateMetadataList.Add(meta); result = UpdateResult.UpdateTags;
} }
else else if (!local.Equals(remote))
{ {
upToDateMetadataCount++; result = UpdateResult.Standard;
}
} }
else else
{ {
addMetadataList.Add(meta); result = UpdateResult.None;
} }
local.ArtistMetadataId = remote.ArtistMetadata.Value.Id;
local.ForeignAlbumId = remote.ForeignAlbumId;
local.OldForeignAlbumIds = remote.OldForeignAlbumIds;
local.LastInfoSync = DateTime.UtcNow;
local.CleanTitle = remote.CleanTitle;
local.Title = remote.Title ?? "Unknown";
local.Overview = remote.Overview.IsNullOrWhiteSpace() ? local.Overview : remote.Overview;
local.Disambiguation = remote.Disambiguation;
local.AlbumType = remote.AlbumType;
local.SecondaryTypes = remote.SecondaryTypes;
local.Genres = remote.Genres;
local.Images = remote.Images.Any() ? remote.Images : local.Images;
local.Links = remote.Links;
local.ReleaseDate = remote.ReleaseDate;
local.Ratings = remote.Ratings;
local.AlbumReleases = new List<AlbumRelease>();
return result;
} }
_logger.Debug($"{album}: {upToDateMetadataCount} artist metadata up to date; Updating {updateMetadataList.Count}, Adding {addMetadataList.Count} artist metadata entries."); protected override UpdateResult MergeEntity(Album local, Album target, Album remote)
{
_logger.Warn($"Album {local} was merged with {remote} because the original was a duplicate.");
_artistMetadataRepository.UpdateMany(updateMetadataList); // move releases over to the new album and delete
_artistMetadataRepository.InsertMany(addMetadataList); var localReleases = _releaseService.GetReleasesByAlbum(local.Id);
var allReleases = localReleases.Concat(_releaseService.GetReleasesByAlbum(target.Id)).ToList();
_logger.Trace($"Moving {localReleases.Count} releases from {local} to {remote}");
forceUpdateFileTags |= updateMetadataList.Any(); // Update album ID and unmonitor all releases from the old album
updated |= updateMetadataList.Any() || addMetadataList.Any(); allReleases.ForEach(x => x.AlbumId = target.Id);
MonitorSingleRelease(allReleases);
_releaseService.UpdateMany(allReleases);
var albumInfo = tuple.Item2; // Update album ids for trackfiles
var files = _mediaFileService.GetFilesByAlbum(local.Id);
files.ForEach(x => x.AlbumId = target.Id);
_mediaFileService.Update(files);
if (album.ForeignAlbumId != albumInfo.ForeignAlbumId) // Update album ids for history
{ var items = _historyService.GetByAlbum(local.Id, null);
_logger.Warn( items.ForEach(x => x.AlbumId = target.Id);
"Album '{0}' (Album {1}) was replaced with '{2}' (LidarrAPI {3}), because the original was a duplicate.", _historyService.UpdateMany(items);
album.Title, album.ForeignAlbumId, albumInfo.Title, albumInfo.ForeignAlbumId);
album.ForeignAlbumId = albumInfo.ForeignAlbumId;
}
// the only thing written to tags from the album object is the title // Finally delete the old album
forceUpdateFileTags |= album.Title != (albumInfo.Title ?? "Unknown"); _albumService.DeleteMany(new List<Album> { local });
updated |= forceUpdateFileTags;
album.OldForeignAlbumIds = albumInfo.OldForeignAlbumIds; return UpdateResult.UpdateTags;
album.LastInfoSync = DateTime.UtcNow; }
album.CleanTitle = albumInfo.CleanTitle;
album.Title = albumInfo.Title ?? "Unknown";
album.Overview = albumInfo.Overview.IsNullOrWhiteSpace() ? album.Overview : albumInfo.Overview;
album.Disambiguation = albumInfo.Disambiguation;
album.AlbumType = albumInfo.AlbumType;
album.SecondaryTypes = albumInfo.SecondaryTypes;
album.Genres = albumInfo.Genres;
album.Images = albumInfo.Images.Any() ? albumInfo.Images : album.Images;
album.Links = albumInfo.Links;
album.ReleaseDate = albumInfo.ReleaseDate;
album.Ratings = albumInfo.Ratings;
album.AlbumReleases = new List<AlbumRelease>();
var remoteReleases = albumInfo.AlbumReleases.Value.DistinctBy(m => m.ForeignReleaseId).ToList(); protected override Album GetEntityByForeignId(Album local)
var existingReleases = _releaseService.GetReleasesForRefresh(album.Id, remoteReleases.Select(x => x.ForeignReleaseId)); {
// Keep track of which existing release we want to end up monitored return _albumService.FindById(local.ForeignAlbumId);
var existingToMonitor = existingReleases.Where(x => x.Monitored).OrderByDescending(x => x.TrackCount).FirstOrDefault(); }
var newReleaseList = new List<AlbumRelease>(); protected override void SaveEntity(Album local)
var updateReleaseList = new List<AlbumRelease>(); {
var upToDateReleaseList = new List<AlbumRelease>(); // Use UpdateMany to avoid firing the album edited event
_albumService.UpdateMany(new List<Album> { local });
}
foreach (var release in remoteReleases) protected override void DeleteEntity(Album local, bool deleteFiles)
{ {
release.AlbumId = album.Id; _albumService.DeleteAlbum(local.Id, true);
release.Album = album; }
// force to unmonitored, then fix monitored one later protected override List<AlbumRelease> GetRemoteChildren(Album remote)
// once we have made sure that it's unique. This make sure {
// that we unmonitor anything in database that shouldn't be monitored. return remote.AlbumReleases.Value.DistinctBy(m => m.ForeignReleaseId).ToList();
release.Monitored = false; }
var releaseToRefresh = existingReleases.SingleOrDefault(r => r.ForeignReleaseId == release.ForeignReleaseId); protected override List<AlbumRelease> GetLocalChildren(Album entity, List<AlbumRelease> remoteChildren)
{
var children = _releaseService.GetReleasesForRefresh(entity.Id,
remoteChildren.Select(x => x.ForeignReleaseId)
.Concat(remoteChildren.SelectMany(x => x.OldForeignReleaseIds)));
if (releaseToRefresh != null) // Make sure trackfiles point to the new album where we are grabbing a release from another album
var files = new List<TrackFile>();
foreach (var release in children.Where(x => x.AlbumId != entity.Id))
{ {
existingReleases.Remove(releaseToRefresh); files.AddRange(_mediaFileService.GetFilesByRelease(release.Id));
}
files.ForEach(x => x.AlbumId = entity.Id);
_mediaFileService.Update(files);
// copy across the db keys and check for equality return children;
release.Id = releaseToRefresh.Id; }
release.AlbumId = releaseToRefresh.AlbumId;
if (!releaseToRefresh.Equals(release)) protected override Tuple<AlbumRelease, List<AlbumRelease> > GetMatchingExistingChildren(List<AlbumRelease> existingChildren, AlbumRelease remote)
{ {
updateReleaseList.Add(release); var existingChild = existingChildren.SingleOrDefault(x => x.ForeignReleaseId == remote.ForeignReleaseId);
var mergeChildren = existingChildren.Where(x => remote.OldForeignReleaseIds.Contains(x.ForeignReleaseId)).ToList();
return Tuple.Create(existingChild, mergeChildren);
} }
else
protected override void PrepareNewChild(AlbumRelease child, Album entity)
{ {
upToDateReleaseList.Add(release); child.AlbumId = entity.Id;
} child.Album = entity;
} }
else
protected override void PrepareExistingChild(AlbumRelease local, AlbumRelease remote, Album entity)
{ {
newReleaseList.Add(release); local.AlbumId = entity.Id;
local.Album = entity;
remote.Id = local.Id;
remote.Album = entity;
remote.AlbumId = entity.Id;
remote.Monitored = local.Monitored;
} }
album.AlbumReleases.Value.Add(release); protected override void AddChildren(List<AlbumRelease> children)
{
_releaseService.InsertMany(children);
} }
var refreshedToMonitor = remoteReleases.SingleOrDefault(x => x.ForeignReleaseId == existingToMonitor?.ForeignReleaseId) ?? private void MonitorSingleRelease(List<AlbumRelease> releases)
remoteReleases.OrderByDescending(x => x.TrackCount).First();
refreshedToMonitor.Monitored = true;
if (upToDateReleaseList.Contains(refreshedToMonitor))
{ {
// we weren't going to update, but have changed monitored so now need to var monitored = releases.Where(x => x.Monitored).ToList();
upToDateReleaseList.Remove(refreshedToMonitor); if (!monitored.Any())
updateReleaseList.Add(refreshedToMonitor);
}
else if (updateReleaseList.Contains(refreshedToMonitor) && refreshedToMonitor.Equals(existingToMonitor))
{ {
// we were going to update because Monitored was incorrect but now it matches monitored = releases;
// and so no need to update
updateReleaseList.Remove(refreshedToMonitor);
upToDateReleaseList.Add(refreshedToMonitor);
} }
Ensure.That(album.AlbumReleases.Value.Count(x => x.Monitored) == 1).IsTrue(); var toMonitor = monitored.OrderByDescending(x => _mediaFileService.GetFilesByRelease(x.Id).Count)
.ThenByDescending(x => x.TrackCount)
_logger.Debug($"{album} {upToDateReleaseList.Count} releases up to date; Deleting {existingReleases.Count}, Updating {updateReleaseList.Count}, Adding {newReleaseList.Count} releases."); .First();
// before deleting anything, remove musicbrainz ids for things we are deleting releases.ForEach(x => x.Monitored = false);
_audioTagService.RemoveMusicBrainzTags(existingReleases); toMonitor.Monitored = true;
}
_releaseService.DeleteMany(existingReleases); protected override bool RefreshChildren(SortedChildren localChildren, List<AlbumRelease> remoteChildren, bool forceChildRefresh, bool forceUpdateFileTags)
_releaseService.UpdateMany(updateReleaseList); {
_releaseService.InsertMany(newReleaseList); var refreshList = localChildren.All;
// if we have updated a monitored release, refresh all file tags // make sure only one of the releases ends up monitored
forceUpdateFileTags |= updateReleaseList.Any(x => x.Monitored); localChildren.Old.ForEach(x => x.Monitored = false);
updated |= existingReleases.Any() || updateReleaseList.Any() || newReleaseList.Any(); MonitorSingleRelease(localChildren.Future);
updated |= _refreshTrackService.RefreshTrackInfo(album, forceUpdateFileTags); refreshList.ForEach(x => _logger.Trace($"release: {x} monitored: {x.Monitored}"));
_albumService.UpdateMany(new List<Album>{album});
_logger.Debug("Finished album refresh for {0}", album.Title); return _refreshAlbumReleaseService.RefreshEntityInfo(refreshList, remoteChildren, forceChildRefresh, forceUpdateFileTags);
}
public bool RefreshAlbumInfo(List<Album> albums, List<Album> remoteAlbums, bool forceAlbumRefresh, bool forceUpdateFileTags)
{
bool updated = false;
foreach (var album in albums)
{
if (forceAlbumRefresh || _checkIfAlbumShouldBeRefreshed.ShouldRefresh(album))
{
updated |= RefreshAlbumInfo(album, remoteAlbums, forceUpdateFileTags);
}
}
return updated; return updated;
} }
public bool RefreshAlbumInfo(Album album, List<Album> remoteAlbums, bool forceUpdateFileTags)
{
return RefreshEntityInfo(album, remoteAlbums, true, forceUpdateFileTags);
}
public void Execute(RefreshAlbumCommand message) public void Execute(RefreshAlbumCommand message)
{ {
if (message.AlbumId.HasValue) if (message.AlbumId.HasValue)
{ {
var album = _albumService.GetAlbum(message.AlbumId.Value); var album = _albumService.GetAlbum(message.AlbumId.Value);
var artist = _artistService.GetArtistByMetadataId(album.ArtistMetadataId); var artist = _artistService.GetArtistByMetadataId(album.ArtistMetadataId);
var updated = RefreshAlbumInfo(album, false); var updated = RefreshAlbumInfo(album, null, false);
if (updated) if (updated)
{ {
_eventAggregator.PublishEvent(new ArtistUpdatedEvent(artist)); _eventAggregator.PublishEvent(new ArtistUpdatedEvent(artist));

@ -13,21 +13,22 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text;
using NzbDrone.Core.ImportLists.Exclusions; using NzbDrone.Core.ImportLists.Exclusions;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Core.History;
namespace NzbDrone.Core.Music namespace NzbDrone.Core.Music
{ {
public class RefreshArtistService : IExecute<RefreshArtistCommand> public class RefreshArtistService : RefreshEntityServiceBase<Artist, Album>, IExecute<RefreshArtistCommand>
{ {
private readonly IProvideArtistInfo _artistInfo; private readonly IProvideArtistInfo _artistInfo;
private readonly IArtistService _artistService; private readonly IArtistService _artistService;
private readonly IAddAlbumService _addAlbumService; private readonly IArtistMetadataRepository _artistMetadataRepository;
private readonly IAlbumService _albumService; private readonly IAlbumService _albumService;
private readonly IRefreshAlbumService _refreshAlbumService; private readonly IRefreshAlbumService _refreshAlbumService;
private readonly IRefreshTrackService _refreshTrackService;
private readonly IAudioTagService _audioTagService;
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
private readonly IMediaFileService _mediaFileService;
private readonly IHistoryService _historyService;
private readonly IDiskScanService _diskScanService; private readonly IDiskScanService _diskScanService;
private readonly ICheckIfArtistShouldBeRefreshed _checkIfArtistShouldBeRefreshed; private readonly ICheckIfArtistShouldBeRefreshed _checkIfArtistShouldBeRefreshed;
private readonly IConfigService _configService; private readonly IConfigService _configService;
@ -36,26 +37,27 @@ namespace NzbDrone.Core.Music
public RefreshArtistService(IProvideArtistInfo artistInfo, public RefreshArtistService(IProvideArtistInfo artistInfo,
IArtistService artistService, IArtistService artistService,
IAddAlbumService addAlbumService, IArtistMetadataRepository artistMetadataRepository,
IAlbumService albumService, IAlbumService albumService,
IRefreshAlbumService refreshAlbumService, IRefreshAlbumService refreshAlbumService,
IRefreshTrackService refreshTrackService,
IAudioTagService audioTagService,
IEventAggregator eventAggregator, IEventAggregator eventAggregator,
IMediaFileService mediaFileService,
IHistoryService historyService,
IDiskScanService diskScanService, IDiskScanService diskScanService,
ICheckIfArtistShouldBeRefreshed checkIfArtistShouldBeRefreshed, ICheckIfArtistShouldBeRefreshed checkIfArtistShouldBeRefreshed,
IConfigService configService, IConfigService configService,
IImportListExclusionService importListExclusionService, IImportListExclusionService importListExclusionService,
Logger logger) Logger logger)
: base(logger, artistMetadataRepository)
{ {
_artistInfo = artistInfo; _artistInfo = artistInfo;
_artistService = artistService; _artistService = artistService;
_addAlbumService = addAlbumService; _artistMetadataRepository = artistMetadataRepository;
_albumService = albumService; _albumService = albumService;
_refreshAlbumService = refreshAlbumService; _refreshAlbumService = refreshAlbumService;
_refreshTrackService = refreshTrackService;
_audioTagService = audioTagService;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
_mediaFileService = mediaFileService;
_historyService = historyService;
_diskScanService = diskScanService; _diskScanService = diskScanService;
_checkIfArtistShouldBeRefreshed = checkIfArtistShouldBeRefreshed; _checkIfArtistShouldBeRefreshed = checkIfArtistShouldBeRefreshed;
_configService = configService; _configService = configService;
@ -63,127 +65,191 @@ namespace NzbDrone.Core.Music
_logger = logger; _logger = logger;
} }
private bool RefreshArtistInfo(Artist artist, bool forceAlbumRefresh) protected override RemoteData GetRemoteData(Artist local, List<Artist> remote)
{ {
_logger.ProgressInfo("Updating Info for {0}", artist.Name); var result = new RemoteData();
bool updated = false;
Artist artistInfo;
try try
{ {
artistInfo = _artistInfo.GetArtistInfo(artist.Metadata.Value.ForeignArtistId, artist.MetadataProfileId); result.Entity = _artistInfo.GetArtistInfo(local.Metadata.Value.ForeignArtistId, local.MetadataProfileId);
result.Metadata = new List<ArtistMetadata> { result.Entity.Metadata.Value };
} }
catch (ArtistNotFoundException) catch (ArtistNotFoundException)
{ {
_logger.Error($"Artist {artist} was not found, it may have been removed from Metadata sources."); _logger.Error($"Could not find artist with id {local.Metadata.Value.ForeignArtistId}");
return updated;
} }
var forceUpdateFileTags = artist.Name != artistInfo.Name; return result;
updated |= forceUpdateFileTags; }
if (artist.Metadata.Value.ForeignArtistId != artistInfo.Metadata.Value.ForeignArtistId) protected override bool ShouldDelete(Artist local)
{ {
_logger.Warn($"Artist {artist} was replaced with {artistInfo} because the original was a duplicate."); return !_mediaFileService.GetFilesByArtist(local.Id).Any();
}
// Update list exclusion if one exists protected override void LogProgress(Artist local)
var importExclusion = _importListExclusionService.FindByForeignId(artist.Metadata.Value.ForeignArtistId); {
_logger.ProgressInfo("Updating Info for {0}", local.Name);
}
if (importExclusion != null) protected override bool IsMerge(Artist local, Artist remote)
{ {
importExclusion.ForeignId = artistInfo.Metadata.Value.ForeignArtistId; return local.ArtistMetadataId != remote.Metadata.Value.Id;
_importListExclusionService.Update(importExclusion);
} }
artist.Metadata.Value.ForeignArtistId = artistInfo.Metadata.Value.ForeignArtistId; protected override UpdateResult UpdateEntity(Artist local, Artist remote)
forceUpdateFileTags = true; {
updated = true; UpdateResult result = UpdateResult.None;
if(!local.Metadata.Value.Equals(remote.Metadata.Value))
{
result = UpdateResult.UpdateTags;
} }
artist.Metadata.Value.ApplyChanges(artistInfo.Metadata.Value); local.CleanName = remote.CleanName;
artist.CleanName = artistInfo.CleanName; local.SortName = remote.SortName;
artist.SortName = artistInfo.SortName; local.LastInfoSync = DateTime.UtcNow;
artist.LastInfoSync = DateTime.UtcNow;
try try
{ {
artist.Path = new DirectoryInfo(artist.Path).FullName; local.Path = new DirectoryInfo(local.Path).FullName;
artist.Path = artist.Path.GetActualCasing(); local.Path = local.Path.GetActualCasing();
} }
catch (Exception e) catch (Exception e)
{ {
_logger.Warn(e, "Couldn't update artist path for " + artist.Path); _logger.Warn(e, "Couldn't update artist path for " + local.Path);
} }
var remoteAlbums = artistInfo.Albums.Value.DistinctBy(m => m.ForeignAlbumId).ToList(); return result;
}
// Get list of DB current db albums for artist
var existingAlbums = _albumService.GetAlbumsForRefresh(artist.ArtistMetadataId, remoteAlbums.Select(x => x.ForeignAlbumId));
var newAlbumsList = new List<Album>();
var updateAlbumsList = new List<Album>();
// Cycle thru albums protected override UpdateResult MoveEntity(Artist local, Artist remote)
foreach (var album in remoteAlbums)
{ {
// Check for album in existing albums, if not set properties and add to new list _logger.Debug($"Updating MusicBrainz id for {local} to {remote}");
var albumToRefresh = existingAlbums.SingleOrDefault(s => s.ForeignAlbumId == album.ForeignAlbumId);
if (albumToRefresh != null) // We are moving from one metadata to another (will already have been poplated)
local.ArtistMetadataId = remote.Metadata.Value.Id;
local.Metadata = remote.Metadata.Value;
// Update list exclusion if one exists
var importExclusion = _importListExclusionService.FindByForeignId(local.Metadata.Value.ForeignArtistId);
if (importExclusion != null)
{ {
albumToRefresh.Artist = artist; importExclusion.ForeignId = remote.Metadata.Value.ForeignArtistId;
existingAlbums.Remove(albumToRefresh); _importListExclusionService.Update(importExclusion);
updateAlbumsList.Add(albumToRefresh);
} }
else
// Do the standard update
UpdateEntity(local, remote);
// We know we need to update tags as artist id has changed
return UpdateResult.UpdateTags;
}
protected override UpdateResult MergeEntity(Artist local, Artist target, Artist remote)
{ {
album.Artist = artist; _logger.Warn($"Artist {local} was replaced with {remote} because the original was a duplicate.");
newAlbumsList.Add(album);
// Update list exclusion if one exists
var importExclusionLocal = _importListExclusionService.FindByForeignId(local.Metadata.Value.ForeignArtistId);
if (importExclusionLocal != null)
{
var importExclusionTarget = _importListExclusionService.FindByForeignId(target.Metadata.Value.ForeignArtistId);
if (importExclusionTarget == null)
{
importExclusionLocal.ForeignId = remote.Metadata.Value.ForeignArtistId;
_importListExclusionService.Update(importExclusionLocal);
} }
} }
_logger.Debug("{0} Deleting {1}, Updating {2}, Adding {3} albums", // move any albums over to the new artist and remove the local artist
artist, existingAlbums.Count, updateAlbumsList.Count, newAlbumsList.Count); var albums = _albumService.GetAlbumsByArtist(local.Id);
albums.ForEach(x => x.ArtistMetadataId = target.ArtistMetadataId);
_albumService.UpdateMany(albums);
_artistService.DeleteArtist(local.Id, false);
// before deleting anything, remove musicbrainz ids for things we are deleting // Update history entries to new id
_audioTagService.RemoveMusicBrainzTags(existingAlbums); var items = _historyService.GetByArtist(local.Id, null);
items.ForEach(x => x.ArtistId = target.Id);
_historyService.UpdateMany(items);
// Delete old albums first - this avoids errors if albums have been merged and we'll // We know we need to update tags as artist id has changed
// end up trying to duplicate an existing release under a new album return UpdateResult.UpdateTags;
_albumService.DeleteMany(existingAlbums); }
// Update new albums with artist info and correct monitored status protected override Artist GetEntityByForeignId(Artist local)
newAlbumsList = UpdateAlbums(artist, newAlbumsList); {
_addAlbumService.AddAlbums(newAlbumsList); return _artistService.FindById(local.ForeignArtistId);
}
updated |= existingAlbums.Any() || newAlbumsList.Any(); protected override void SaveEntity(Artist local)
{
_artistService.UpdateArtist(local);
}
updated |= _refreshAlbumService.RefreshAlbumInfo(updateAlbumsList, forceAlbumRefresh, forceUpdateFileTags); protected override void DeleteEntity(Artist local, bool deleteFiles)
{
_artistService.DeleteArtist(local.Id, true);
}
// Do this last so artist only marked as refreshed if refresh of tracks / albums completed successfully protected override List<Album> GetRemoteChildren(Artist remote)
_artistService.UpdateArtist(artist); {
return remote.Albums.Value.DistinctBy(m => m.ForeignAlbumId).ToList();
}
_eventAggregator.PublishEvent(new AlbumInfoRefreshedEvent(artist, newAlbumsList, updateAlbumsList)); protected override List<Album> GetLocalChildren(Artist entity, List<Album> remoteChildren)
{
return _albumService.GetAlbumsForRefresh(entity.ArtistMetadataId,
remoteChildren.Select(x => x.ForeignAlbumId)
.Concat(remoteChildren.SelectMany(x => x.OldForeignAlbumIds)));
}
protected override Tuple<Album, List<Album> > GetMatchingExistingChildren(List<Album> existingChildren, Album remote)
{
var existingChild = existingChildren.SingleOrDefault(x => x.ForeignAlbumId == remote.ForeignAlbumId);
var mergeChildren = existingChildren.Where(x => remote.OldForeignAlbumIds.Contains(x.ForeignAlbumId)).ToList();
return Tuple.Create(existingChild, mergeChildren);
}
if (updated) protected override void PrepareNewChild(Album child, Artist entity)
{ {
_eventAggregator.PublishEvent(new ArtistUpdatedEvent(artist)); child.Artist = entity;
child.ArtistMetadata = entity.Metadata.Value;
child.ArtistMetadataId = entity.Metadata.Value.Id;
child.Added = DateTime.UtcNow;
child.LastInfoSync = DateTime.MinValue;
child.ProfileId = entity.QualityProfileId;
child.Monitored = entity.Monitored;
} }
_logger.Debug("Finished artist refresh for {0}", artist.Name); protected override void PrepareExistingChild(Album local, Album remote, Artist entity)
{
local.Artist = entity;
local.ArtistMetadata = entity.Metadata.Value;
local.ArtistMetadataId = entity.Metadata.Value.Id;
}
return updated; protected override void AddChildren(List<Album> children)
{
_albumService.InsertMany(children);
} }
private List<Album> UpdateAlbums(Artist artist, List<Album> albumsToUpdate) protected override bool RefreshChildren(SortedChildren localChildren, List<Album> remoteChildren, bool forceChildRefresh, bool forceUpdateFileTags)
{ {
foreach (var album in albumsToUpdate) // we always want to end up refreshing the albums since we don't get have proper data
Ensure.That(localChildren.UpToDate.Count, () => localChildren.UpToDate.Count).IsLessThanOrEqualTo(0);
return _refreshAlbumService.RefreshAlbumInfo(localChildren.All, remoteChildren, forceChildRefresh, forceUpdateFileTags);
}
protected override void PublishEntityUpdatedEvent(Artist entity)
{ {
album.ProfileId = artist.QualityProfileId; _eventAggregator.PublishEvent(new ArtistUpdatedEvent(entity));
album.Monitored = artist.Monitored;
} }
return albumsToUpdate; protected override void PublishChildrenUpdatedEvent(Artist entity, List<Album> newChildren, List<Album> updateChildren)
{
_eventAggregator.PublishEvent(new AlbumInfoRefreshedEvent(entity, newChildren, updateChildren));
} }
private void RescanArtist(Artist artist, bool isNew, CommandTrigger trigger, bool infoUpdated) private void RescanArtist(Artist artist, bool isNew, CommandTrigger trigger, bool infoUpdated)
@ -239,7 +305,7 @@ namespace NzbDrone.Core.Music
bool updated = false; bool updated = false;
try try
{ {
updated = RefreshArtistInfo(artist, true); updated = RefreshEntityInfo(artist, null, true, false);
RescanArtist(artist, isNew, trigger, updated); RescanArtist(artist, isNew, trigger, updated);
} }
catch (Exception e) catch (Exception e)
@ -262,7 +328,7 @@ namespace NzbDrone.Core.Music
bool updated = false; bool updated = false;
try try
{ {
updated = RefreshArtistInfo(artist, manualTrigger); updated = RefreshEntityInfo(artist, null, manualTrigger, false);
} }
catch (Exception e) catch (Exception e)
{ {
@ -271,7 +337,6 @@ namespace NzbDrone.Core.Music
RescanArtist(artist, false, trigger, updated); RescanArtist(artist, false, trigger, updated);
} }
else else
{ {
_logger.Info("Skipping refresh of artist: {0}", artist.Name); _logger.Info("Skipping refresh of artist: {0}", artist.Name);

@ -0,0 +1,277 @@
using NLog;
using NzbDrone.Common.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
namespace NzbDrone.Core.Music
{
public abstract class RefreshEntityServiceBase<Entity, Child>
{
private readonly Logger _logger;
private readonly IArtistMetadataRepository _artistMetadataRepository;
public RefreshEntityServiceBase(Logger logger,
IArtistMetadataRepository artistMetadataRepository)
{
_logger = logger;
_artistMetadataRepository = artistMetadataRepository;
}
public enum UpdateResult
{
None,
Standard,
UpdateTags
};
public class SortedChildren
{
public SortedChildren()
{
UpToDate = new List<Child>();
Added = new List<Child>();
Updated = new List<Child>();
Merged = new List<Tuple<Child, Child> >();
Deleted = new List<Child>();
}
public List<Child> UpToDate { get; set; }
public List<Child> Added { get; set; }
public List<Child> Updated { get; set; }
public List<Tuple<Child, Child> > Merged { get; set; }
public List<Child> Deleted { get; set; }
public List<Child> All => UpToDate.Concat(Added).Concat(Updated).Concat(Merged.Select(x => x.Item1)).Concat(Deleted).ToList();
public List<Child> Future => UpToDate.Concat(Added).Concat(Updated).ToList();
public List<Child> Old => Merged.Select(x => x.Item1).Concat(Deleted).ToList();
}
public class RemoteData
{
public Entity Entity { get; set; }
public List<ArtistMetadata> Metadata { get; set; }
}
protected virtual void LogProgress(Entity local)
{
}
protected abstract RemoteData GetRemoteData(Entity local, List<Entity> remote);
protected virtual void EnsureNewParent(Entity local, Entity remote)
{
return;
}
protected abstract bool IsMerge(Entity local, Entity remote);
protected virtual bool ShouldDelete(Entity local)
{
return true;
}
protected abstract UpdateResult UpdateEntity(Entity local, Entity remote);
protected virtual UpdateResult MoveEntity(Entity local, Entity remote)
{
return UpdateEntity(local, remote);
}
protected virtual UpdateResult MergeEntity(Entity local, Entity target, Entity remote)
{
DeleteEntity(local, true);
return UpdateResult.UpdateTags;
}
protected abstract Entity GetEntityByForeignId(Entity local);
protected abstract void SaveEntity(Entity local);
protected abstract void DeleteEntity(Entity local, bool deleteFiles);
protected abstract List<Child> GetRemoteChildren(Entity remote);
protected abstract List<Child> GetLocalChildren(Entity entity, List<Child> remoteChildren);
protected abstract Tuple<Child, List<Child> > GetMatchingExistingChildren(List<Child> existingChildren, Child remote);
protected abstract void PrepareNewChild(Child remoteChild, Entity entity);
protected abstract void PrepareExistingChild(Child existingChild, Child remoteChild, Entity entity);
protected abstract void AddChildren(List<Child> children);
protected abstract bool RefreshChildren(SortedChildren localChildren, List<Child> remoteChildren, bool forceChildRefresh, bool forceUpdateFileTags);
protected virtual void PublishEntityUpdatedEvent(Entity entity)
{
}
protected virtual void PublishChildrenUpdatedEvent(Entity entity, List<Child> newChildren, List<Child> updateChildren)
{
}
public bool RefreshEntityInfo(Entity local, List<Entity> remoteList, bool forceChildRefresh, bool forceUpdateFileTags)
{
bool updated = false;
LogProgress(local);
var data = GetRemoteData(local, remoteList);
var remote = data.Entity;
if (remote == null)
{
if (ShouldDelete(local))
{
_logger.Warn($"{typeof(Entity).Name} {local} not found in metadata and is being deleted");
DeleteEntity(local, true);
return false;
}
else
{
_logger.Error($"{typeof(Entity).Name} {local} was not found, it may have been removed from Metadata sources.");
return false;
}
}
if (data.Metadata != null)
{
var metadataResult = UpdateArtistMetadata(data.Metadata);
updated |= metadataResult >= UpdateResult.Standard;
forceUpdateFileTags |= metadataResult == UpdateResult.UpdateTags;
}
// Validate that the parent object exists (remote data might specify a different one)
EnsureNewParent(local, remote);
UpdateResult result;
if (IsMerge(local, remote))
{
// get entity we're merging into
var target = GetEntityByForeignId(remote);
if (target == null)
{
_logger.Trace($"Moving {typeof(Entity).Name} {local} to {remote}");
result = MoveEntity(local, remote);
}
else
{
_logger.Trace($"Merging {typeof(Entity).Name} {local} into {target}");
result = MergeEntity(local, target, remote);
// having merged local into target, do update for target using remote
local = target;
}
// Save the entity early so that children see the updated ids
SaveEntity(local);
}
else
{
_logger.Trace($"Updating {typeof(Entity).Name} {local}");
result = UpdateEntity(local, remote);
}
updated |= result >= UpdateResult.Standard;
forceUpdateFileTags |= result == UpdateResult.UpdateTags;
_logger.Trace($"updated: {updated} forceUpdateFileTags: {forceUpdateFileTags}");
var remoteChildren = GetRemoteChildren(remote);
updated |= SortChildren(local, remoteChildren, forceChildRefresh, forceUpdateFileTags);
// Do this last so entity only marked as refreshed if refresh of children completed successfully
_logger.Trace($"Saving {typeof(Entity).Name} {local}");
SaveEntity(local);
if (updated)
{
PublishEntityUpdatedEvent(local);
}
_logger.Debug($"Finished {typeof(Entity).Name} refresh for {local}");
return updated;
}
public UpdateResult UpdateArtistMetadata(List<ArtistMetadata> data)
{
var remoteMetadata = data.DistinctBy(x => x.ForeignArtistId).ToList();
var updated = _artistMetadataRepository.UpsertMany(data);
return updated ? UpdateResult.UpdateTags : UpdateResult.None;
}
public bool RefreshEntityInfo(List<Entity> localList, List<Entity> remoteList, bool forceChildRefresh, bool forceUpdateFileTags)
{
bool updated = false;
foreach (var entity in localList)
{
updated |= RefreshEntityInfo(entity, remoteList, forceChildRefresh, forceUpdateFileTags);
}
return updated;
}
protected bool SortChildren(Entity entity, List<Child> remoteChildren, bool forceChildRefresh, bool forceUpdateFileTags)
{
// Get existing children (and children to be) from the database
var localChildren = GetLocalChildren(entity, remoteChildren);
var sortedChildren = new SortedChildren();
sortedChildren.Deleted.AddRange(localChildren);
// Cycle through children
foreach (var remoteChild in remoteChildren)
{
// Check for child in existing children, if not set properties and add to new list
var tuple = GetMatchingExistingChildren(localChildren, remoteChild);
var existingChild = tuple.Item1;
var mergedChildren = tuple.Item2;
if (existingChild != null)
{
sortedChildren.Deleted.Remove(existingChild);
PrepareExistingChild(existingChild, remoteChild, entity);
if (existingChild.Equals(remoteChild))
{
sortedChildren.UpToDate.Add(existingChild);
}
else
{
sortedChildren.Updated.Add(existingChild);
}
// note the children that are going to be merged into existingChild
foreach (var child in mergedChildren)
{
sortedChildren.Merged.Add(Tuple.Create(child, existingChild));
sortedChildren.Deleted.Remove(child);
}
}
else
{
PrepareNewChild(remoteChild, entity);
sortedChildren.Added.Add(remoteChild);
// note the children that will be merged into remoteChild (once added)
foreach (var child in mergedChildren)
{
sortedChildren.Merged.Add(Tuple.Create(child, existingChild));
sortedChildren.Deleted.Remove(child);
}
}
}
_logger.Debug("{0} {1} {2}s up to date. Adding {3}, Updating {4}, Merging {5}, Deleting {6}.",
entity, sortedChildren.UpToDate.Count, typeof(Child).Name.ToLower(),
sortedChildren.Added.Count, sortedChildren.Updated.Count, sortedChildren.Merged.Count, sortedChildren.Deleted.Count);
// Add in the new children (we have checked that foreign IDs don't clash)
AddChildren(sortedChildren.Added);
// now trigger updates
var updated = RefreshChildren(sortedChildren, remoteChildren, forceChildRefresh, forceUpdateFileTags);
PublishChildrenUpdatedEvent(entity, sortedChildren.Added, sortedChildren.Updated);
return updated;
}
}
}

@ -1,7 +1,5 @@
using NLog; using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Messaging.Events;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -10,127 +8,70 @@ namespace NzbDrone.Core.Music
{ {
public interface IRefreshTrackService public interface IRefreshTrackService
{ {
bool RefreshTrackInfo(Album rg, bool forceUpdateFileTags); bool RefreshTrackInfo(List<Track> add, List<Track> update, List<Tuple<Track, Track> > merge, List<Track> delete, List<Track> upToDate, List<Track> remoteTracks, bool forceUpdateFileTags);
} }
public class RefreshTrackService : IRefreshTrackService public class RefreshTrackService : IRefreshTrackService
{ {
private readonly ITrackService _trackService; private readonly ITrackService _trackService;
private readonly IAlbumService _albumService;
private readonly IMediaFileService _mediaFileService;
private readonly IAudioTagService _audioTagService; private readonly IAudioTagService _audioTagService;
private readonly IEventAggregator _eventAggregator;
private readonly Logger _logger; private readonly Logger _logger;
public RefreshTrackService(ITrackService trackService, public RefreshTrackService(ITrackService trackService,
IAlbumService albumService,
IMediaFileService mediaFileService,
IAudioTagService audioTagService, IAudioTagService audioTagService,
IEventAggregator eventAggregator,
Logger logger) Logger logger)
{ {
_trackService = trackService; _trackService = trackService;
_albumService = albumService;
_mediaFileService = mediaFileService;
_audioTagService = audioTagService; _audioTagService = audioTagService;
_eventAggregator = eventAggregator;
_logger = logger; _logger = logger;
} }
public bool RefreshTrackInfo(Album album, bool forceUpdateFileTags) public bool RefreshTrackInfo(List<Track> add, List<Track> update, List<Tuple<Track, Track> > merge, List<Track> delete, List<Track> upToDate, List<Track> remoteTracks, bool forceUpdateFileTags)
{ {
_logger.Info("Starting track info refresh for: {0}", album);
var successCount = 0;
var failCount = 0;
bool updated = false;
foreach (var release in album.AlbumReleases.Value)
{
var remoteTracks = release.Tracks.Value.DistinctBy(m => m.ForeignTrackId).ToList();
var existingTracks = _trackService.GetTracksForRefresh(release.Id, remoteTracks.Select(x => x.ForeignTrackId));
var updateList = new List<Track>(); var updateList = new List<Track>();
var newList = new List<Track>();
var upToDateList = new List<Track>();
foreach (var track in remoteTracks) // for tracks that need updating, just grab the remote track and set db ids
{ foreach (var trackToUpdate in update)
track.AlbumRelease = release;
track.AlbumReleaseId = release.Id;
// the artist metadata will have been inserted by RefreshAlbumInfo so the Id will now be populated
track.ArtistMetadataId = track.ArtistMetadata.Value.Id;
try
{
var trackToUpdate = existingTracks.SingleOrDefault(e => e.ForeignTrackId == track.ForeignTrackId);
if (trackToUpdate != null)
{ {
existingTracks.Remove(trackToUpdate); var track = remoteTracks.Single(e => e.ForeignTrackId == trackToUpdate.ForeignTrackId);
// populate albumrelease for later
trackToUpdate.AlbumRelease = release;
// copy across the db keys to the remote track and check if we need to update // copy across the db keys to the remote track and check if we need to update
track.Id = trackToUpdate.Id; track.Id = trackToUpdate.Id;
track.TrackFileId = trackToUpdate.TrackFileId; track.TrackFileId = trackToUpdate.TrackFileId;
// make sure title is not null // make sure title is not null
track.Title = track.Title ?? "Unknown"; track.Title = track.Title ?? "Unknown";
if (!trackToUpdate.Equals(track))
{
updateList.Add(track); updateList.Add(track);
} }
else
// Move trackfiles from merged entities into new one
foreach (var item in merge)
{ {
upToDateList.Add(track); var trackToMerge = item.Item1;
} var mergeTarget = item.Item2;
}
else if (mergeTarget.TrackFileId == 0)
{ {
newList.Add(track); mergeTarget.TrackFileId = trackToMerge.TrackFileId;
} }
successCount++; if (!updateList.Contains(mergeTarget))
}
catch (Exception e)
{ {
_logger.Fatal(e, "An error has occurred while updating track info for album {0}. {1}", album, track); updateList.Add(mergeTarget);
failCount++;
} }
} }
// if any tracks with files are deleted, strip out the MB tags from the metadata
// so that we stand a chance of matching next time
_audioTagService.RemoveMusicBrainzTags(existingTracks);
var tagsToUpdate = updateList; var tagsToUpdate = updateList;
if (forceUpdateFileTags) if (forceUpdateFileTags)
{ {
_logger.Debug("Forcing tag update due to Artist/Album/Release updates"); _logger.Debug("Forcing tag update due to Artist/Album/Release updates");
tagsToUpdate = updateList.Concat(upToDateList).ToList(); tagsToUpdate = updateList.Concat(upToDate).ToList();
} }
_audioTagService.SyncTags(tagsToUpdate); _audioTagService.SyncTags(tagsToUpdate);
_logger.Debug($"{release}: {upToDateList.Count} tracks up to date; Deleting {existingTracks.Count}, Updating {updateList.Count}, Adding {newList.Count} tracks."); _trackService.DeleteMany(delete.Concat(merge.Select(x => x.Item1)).ToList());
_trackService.DeleteMany(existingTracks);
_trackService.UpdateMany(updateList); _trackService.UpdateMany(updateList);
_trackService.InsertMany(newList);
updated |= existingTracks.Any() || updateList.Any() || newList.Any();
}
if (failCount != 0)
{
_logger.Info("Finished track refresh for album: {0}. Successful: {1} - Failed: {2} ",
album.Title, successCount, failCount);
}
else
{
_logger.Info("Finished track refresh for album: {0}.", album);
}
return updated; return delete.Any() || updateList.Any() || merge.Any();
} }
} }
} }

@ -42,7 +42,7 @@ namespace NzbDrone.Core.Music
// These are retained for compatibility // These are retained for compatibility
// TODO: Remove set, bodged in because tests expect this to be writable // TODO: Remove set, bodged in because tests expect this to be writable
public int AlbumId { get { return AlbumRelease.Value?.Album.Value?.Id ?? 0; } set { /* empty */ } } public int AlbumId { get { return AlbumRelease?.Value?.Album?.Value?.Id ?? 0; } set { /* empty */ } }
public Album Album { get; set; } public Album Album { get; set; }
public override string ToString() public override string ToString()

@ -847,7 +847,6 @@
<Compile Include="Extras\Metadata\MetadataRepository.cs" /> <Compile Include="Extras\Metadata\MetadataRepository.cs" />
<Compile Include="Extras\Metadata\MetadataService.cs" /> <Compile Include="Extras\Metadata\MetadataService.cs" />
<Compile Include="Extras\Metadata\MetadataType.cs" /> <Compile Include="Extras\Metadata\MetadataType.cs" />
<Compile Include="Music\AddAlbumService.cs" />
<Compile Include="Music\AlbumAddedService.cs" /> <Compile Include="Music\AlbumAddedService.cs" />
<Compile Include="Music\AlbumEditedService.cs" /> <Compile Include="Music\AlbumEditedService.cs" />
<Compile Include="Music\AlbumRepository.cs" /> <Compile Include="Music\AlbumRepository.cs" />
@ -898,8 +897,10 @@
<Compile Include="Music\Events\ArtistUpdatedEvent.cs" /> <Compile Include="Music\Events\ArtistUpdatedEvent.cs" />
<Compile Include="Music\Events\AlbumInfoRefreshedEvent.cs" /> <Compile Include="Music\Events\AlbumInfoRefreshedEvent.cs" />
<Compile Include="Music\Ratings.cs" /> <Compile Include="Music\Ratings.cs" />
<Compile Include="Music\RefreshEntityServiceBase.cs" />
<Compile Include="Music\RefreshArtistService.cs" /> <Compile Include="Music\RefreshArtistService.cs" />
<Compile Include="Music\RefreshAlbumService.cs" /> <Compile Include="Music\RefreshAlbumService.cs" />
<Compile Include="Music\RefreshAlbumReleaseService.cs" />
<Compile Include="Music\RefreshTrackService.cs" /> <Compile Include="Music\RefreshTrackService.cs" />
<Compile Include="Music\ShouldRefreshArtist.cs" /> <Compile Include="Music\ShouldRefreshArtist.cs" />
<Compile Include="Music\Track.cs" /> <Compile Include="Music\Track.cs" />

Loading…
Cancel
Save