From 2097bfff94e3f7272948bd82fff7f74bc5049cfc Mon Sep 17 00:00:00 2001 From: ta264 Date: Tue, 27 Aug 2019 21:23:35 +0100 Subject: [PATCH] Fixed: Null reference exceptions on update Simplify entity equality code and enfore db/metadata split Use a nuget package to remove boilerplate code that needs careful update when adding/removing fields. Add tests to enforce that all fields are allocated to 'UseMetadataFrom' or 'UseDbFieldsFrom' to make metadata refresh more foolproof. Fix NRE when tracks are merged because artist wasn't set. Fix NRE when tracks are merged and the merge target wasn't yet in the database. --- .../Lidarr.Core.Test.csproj | 1 + .../MusicTests/EntityFixture.cs | 298 ++++++++++++++++++ .../RefreshAlbumReleaseServiceFixture.cs | 147 +++++++++ .../MusicTests/RefreshAlbumServiceFixture.cs | 48 --- .../MusicTests/RefreshTrackServiceFixture.cs | 55 ++++ src/NzbDrone.Core/Lidarr.Core.csproj | 1 + src/NzbDrone.Core/MediaCover/MediaCover.cs | 3 +- src/NzbDrone.Core/Music/Album.cs | 117 +++---- src/NzbDrone.Core/Music/Artist.cs | 62 +++- src/NzbDrone.Core/Music/ArtistMetadata.cs | 102 +----- .../Music/ArtistMetadataRepository.cs | 2 +- src/NzbDrone.Core/Music/Entity.cs | 37 +++ src/NzbDrone.Core/Music/Links.cs | 3 +- src/NzbDrone.Core/Music/Medium.cs | 4 +- src/NzbDrone.Core/Music/Member.cs | 3 +- src/NzbDrone.Core/Music/Ratings.cs | 5 +- .../Music/RefreshAlbumReleaseService.cs | 15 +- .../Music/RefreshAlbumService.cs | 28 +- .../Music/RefreshArtistService.cs | 4 +- .../Music/RefreshEntityServiceBase.cs | 2 +- .../Music/RefreshTrackService.cs | 14 +- src/NzbDrone.Core/Music/Release.cs | 91 ++---- src/NzbDrone.Core/Music/Track.cs | 98 ++---- 23 files changed, 726 insertions(+), 414 deletions(-) create mode 100644 src/NzbDrone.Core.Test/MusicTests/EntityFixture.cs create mode 100644 src/NzbDrone.Core.Test/MusicTests/RefreshAlbumReleaseServiceFixture.cs create mode 100644 src/NzbDrone.Core.Test/MusicTests/RefreshTrackServiceFixture.cs create mode 100644 src/NzbDrone.Core/Music/Entity.cs diff --git a/src/NzbDrone.Core.Test/Lidarr.Core.Test.csproj b/src/NzbDrone.Core.Test/Lidarr.Core.Test.csproj index 16efb130f..e314a2c58 100644 --- a/src/NzbDrone.Core.Test/Lidarr.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/Lidarr.Core.Test.csproj @@ -6,6 +6,7 @@ + diff --git a/src/NzbDrone.Core.Test/MusicTests/EntityFixture.cs b/src/NzbDrone.Core.Test/MusicTests/EntityFixture.cs new file mode 100644 index 000000000..ba5fb83b5 --- /dev/null +++ b/src/NzbDrone.Core.Test/MusicTests/EntityFixture.cs @@ -0,0 +1,298 @@ +using NUnit.Framework; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Music; +using NzbDrone.Test.Common; +using FluentAssertions; +using System.Collections; +using System.Reflection; +using AutoFixture; +using System.Linq; +using Equ; +using Marr.Data; + +namespace NzbDrone.Core.Test.MusicTests +{ + [TestFixture] + public class EntityFixture : LoggingTest + { + + Fixture fixture = new Fixture(); + + private static bool IsNotMarkedAsIgnore(PropertyInfo propertyInfo) + { + return !propertyInfo.GetCustomAttributes(typeof(MemberwiseEqualityIgnoreAttribute), true).Any(); + } + + public class EqualityPropertySource + { + public static IEnumerable TestCases + { + get + { + foreach (var property in typeof(T).GetProperties().Where(x => x.CanRead && x.CanWrite && IsNotMarkedAsIgnore(x))) + { + yield return new TestCaseData(property).SetName($"{{m}}_{property.Name}"); + } + } + } + } + + public class IgnoredPropertySource + { + public static IEnumerable TestCases + { + get + { + foreach (var property in typeof(T).GetProperties().Where(x => x.CanRead && x.CanWrite && !IsNotMarkedAsIgnore(x))) + { + yield return new TestCaseData(property).SetName($"{{m}}_{property.Name}"); + } + } + } + } + + [Test] + public void two_equivalent_artist_metadata_should_be_equal() + { + var item1 = fixture.Create(); + var item2 = item1.JsonClone(); + + item1.Should().NotBeSameAs(item2); + item1.Should().Be(item2); + } + + [Test, TestCaseSource(typeof(EqualityPropertySource), "TestCases")] + public void two_different_artist_metadata_should_not_be_equal(PropertyInfo prop) + { + var item1 = fixture.Create(); + var item2 = item1.JsonClone(); + var different = fixture.Create(); + + // make item2 different in the property under consideration + var differentEntry = prop.GetValue(different); + prop.SetValue(item2, differentEntry); + + item1.Should().NotBeSameAs(item2); + item1.Should().NotBe(item2); + } + + [Test] + public void metadata_and_db_fields_should_replicate_artist_metadata() + { + var item1 = fixture.Create(); + var item2 = fixture.Create(); + + item1.Should().NotBe(item2); + + item1.UseMetadataFrom(item2); + item1.UseDbFieldsFrom(item2); + item1.Should().Be(item2); + } + + private Track GivenTrack() + { + return fixture.Build() + .Without(x => x.AlbumRelease) + .Without(x => x.ArtistMetadata) + .Without(x => x.TrackFile) + .Without(x => x.Artist) + .Without(x => x.AlbumId) + .Without(x => x.Album) + .Create(); + } + + [Test] + public void two_equivalent_track_should_be_equal() + { + var item1 = GivenTrack(); + var item2 = item1.JsonClone(); + + item1.Should().NotBeSameAs(item2); + item1.Should().Be(item2); + } + + [Test, TestCaseSource(typeof(EqualityPropertySource), "TestCases")] + public void two_different_tracks_should_not_be_equal(PropertyInfo prop) + { + var item1 = GivenTrack(); + var item2 = item1.JsonClone(); + var different = GivenTrack(); + + // make item2 different in the property under consideration + var differentEntry = prop.GetValue(different); + prop.SetValue(item2, differentEntry); + + item1.Should().NotBeSameAs(item2); + item1.Should().NotBe(item2); + } + + [Test] + public void metadata_and_db_fields_should_replicate_track() + { + var item1 = GivenTrack(); + var item2 = GivenTrack(); + + item1.Should().NotBe(item2); + + item1.UseMetadataFrom(item2); + item1.UseDbFieldsFrom(item2); + item1.Should().Be(item2); + } + + private AlbumRelease GivenAlbumRelease() + { + return fixture.Build() + .Without(x => x.Album) + .Without(x => x.Tracks) + .Create(); + } + + [Test] + public void two_equivalent_album_releases_should_be_equal() + { + var item1 = GivenAlbumRelease(); + var item2 = item1.JsonClone(); + + item1.Should().NotBeSameAs(item2); + item1.Should().Be(item2); + } + + [Test, TestCaseSource(typeof(EqualityPropertySource), "TestCases")] + public void two_different_album_releases_should_not_be_equal(PropertyInfo prop) + { + var item1 = GivenAlbumRelease(); + var item2 = item1.JsonClone(); + var different = GivenAlbumRelease(); + + // make item2 different in the property under consideration + var differentEntry = prop.GetValue(different); + prop.SetValue(item2, differentEntry); + + item1.Should().NotBeSameAs(item2); + item1.Should().NotBe(item2); + } + + [Test] + public void metadata_and_db_fields_should_replicate_release() + { + var item1 = GivenAlbumRelease(); + var item2 = GivenAlbumRelease(); + + item1.Should().NotBe(item2); + + item1.UseMetadataFrom(item2); + item1.UseDbFieldsFrom(item2); + item1.Should().Be(item2); + } + + private Album GivenAlbum() + { + return fixture.Build() + .Without(x => x.ArtistMetadata) + .Without(x => x.AlbumReleases) + .Without(x => x.Artist) + .Without(x => x.ArtistId) + .Create(); + } + + [Test] + public void two_equivalent_albums_should_be_equal() + { + var item1 = GivenAlbum(); + var item2 = item1.JsonClone(); + + item1.Should().NotBeSameAs(item2); + item1.Should().Be(item2); + } + + [Test, TestCaseSource(typeof(EqualityPropertySource), "TestCases")] + public void two_different_albums_should_not_be_equal(PropertyInfo prop) + { + var item1 = GivenAlbum(); + var item2 = item1.JsonClone(); + var different = GivenAlbum(); + + // make item2 different in the property under consideration + if (prop.PropertyType == typeof(bool)) + { + prop.SetValue(item2, !(bool)prop.GetValue(item1)); + } + else + { + prop.SetValue(item2, prop.GetValue(different)); + } + + item1.Should().NotBeSameAs(item2); + item1.Should().NotBe(item2); + } + + [Test] + public void metadata_and_db_fields_should_replicate_album() + { + var item1 = GivenAlbum(); + var item2 = GivenAlbum(); + + item1.Should().NotBe(item2); + + item1.UseMetadataFrom(item2); + item1.UseDbFieldsFrom(item2); + item1.Should().Be(item2); + } + + private Artist GivenArtist() + { + return fixture.Build() + .With(x => x.Metadata, new LazyLoaded(fixture.Create())) + .Without(x => x.QualityProfile) + .Without(x => x.MetadataProfile) + .Without(x => x.Albums) + .Without(x => x.Name) + .Without(x => x.ForeignArtistId) + .Create(); + } + + [Test] + public void two_equivalent_artists_should_be_equal() + { + var item1 = GivenArtist(); + var item2 = item1.JsonClone(); + + item1.Should().NotBeSameAs(item2); + item1.Should().Be(item2); + } + + [Test, TestCaseSource(typeof(EqualityPropertySource), "TestCases")] + public void two_different_artists_should_not_be_equal(PropertyInfo prop) + { + var item1 = GivenArtist(); + var item2 = item1.JsonClone(); + var different = GivenArtist(); + + // make item2 different in the property under consideration + if (prop.PropertyType == typeof(bool)) + { + prop.SetValue(item2, !(bool)prop.GetValue(item1)); + } + else + { + prop.SetValue(item2, prop.GetValue(different)); + } + + item1.Should().NotBeSameAs(item2); + item1.Should().NotBe(item2); + } + + [Test] + public void metadata_and_db_fields_should_replicate_artist() + { + var item1 = GivenArtist(); + var item2 = GivenArtist(); + + item1.Should().NotBe(item2); + + item1.UseMetadataFrom(item2); + item1.UseDbFieldsFrom(item2); + item1.Should().Be(item2); + } + } +} diff --git a/src/NzbDrone.Core.Test/MusicTests/RefreshAlbumReleaseServiceFixture.cs b/src/NzbDrone.Core.Test/MusicTests/RefreshAlbumReleaseServiceFixture.cs new file mode 100644 index 000000000..9dfaa7af0 --- /dev/null +++ b/src/NzbDrone.Core.Test/MusicTests/RefreshAlbumReleaseServiceFixture.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FizzWare.NBuilder; +using Moq; +using NUnit.Framework; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Exceptions; +using NzbDrone.Core.MetadataSource; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Music; +using NzbDrone.Test.Common; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.History; + +namespace NzbDrone.Core.Test.MusicTests +{ + [TestFixture] + public class RefreshAlbumReleaseServiceFixture : CoreTest + { + private AlbumRelease _release; + private List _tracks; + private ArtistMetadata _metadata; + + [SetUp] + public void Setup() + { + + _release = Builder + .CreateNew() + .With(s => s.Media = new List { new Medium { Number = 1 } }) + .With(s => s.ForeignReleaseId = "xxx-xxx-xxx-xxx") + .With(s => s.Monitored = true) + .With(s => s.TrackCount = 10) + .Build(); + + _metadata = Builder.CreateNew().Build(); + + _tracks = Builder + .CreateListOfSize(10) + .All() + .With(x => x.AlbumReleaseId = _release.Id) + .With(x => x.ArtistMetadata = _metadata) + .With(x => x.ArtistMetadataId = _metadata.Id) + .BuildList(); + + Mocker.GetMock() + .Setup(s => s.GetTracksForRefresh(_release.Id, It.IsAny>())) + .Returns(_tracks); + + } + + [Test] + public void should_update_if_musicbrainz_id_changed_and_no_clash() + { + var newInfo = _release.JsonClone(); + newInfo.ForeignReleaseId = _release.ForeignReleaseId + 1; + newInfo.OldForeignReleaseIds = new List { _release.ForeignReleaseId }; + newInfo.Tracks = _tracks; + + Subject.RefreshEntityInfo(_release, new List { newInfo }, false, false); + + Mocker.GetMock() + .Verify(v => v.UpdateMany(It.Is>(s => s.First().ForeignReleaseId == newInfo.ForeignReleaseId))); + } + + [Test] + public void should_merge_if_musicbrainz_id_changed_and_new_already_exists() + { + var existing = _release; + + var clash = existing.JsonClone(); + clash.Id = 100; + clash.ForeignReleaseId = clash.ForeignReleaseId + 1; + + clash.Tracks = Builder.CreateListOfSize(10) + .All() + .With(x => x.AlbumReleaseId = clash.Id) + .With(x => x.ArtistMetadata = _metadata) + .With(x => x.ArtistMetadataId = _metadata.Id) + .BuildList(); + + Mocker.GetMock() + .Setup(x => x.GetReleaseByForeignReleaseId(clash.ForeignReleaseId, false)) + .Returns(clash); + + Mocker.GetMock() + .Setup(x => x.GetTracksForRefresh(It.IsAny(), It.IsAny>())) + .Returns(_tracks); + + var newInfo = existing.JsonClone(); + newInfo.ForeignReleaseId = _release.ForeignReleaseId + 1; + newInfo.OldForeignReleaseIds = new List { _release.ForeignReleaseId }; + newInfo.Tracks = _tracks; + + Subject.RefreshEntityInfo(new List { clash, existing }, new List { newInfo }, false, false); + + // check old album is deleted + Mocker.GetMock() + .Verify(v => v.DeleteMany(It.Is>(x => x.First().ForeignReleaseId == existing.ForeignReleaseId))); + + // check that clash gets updated + Mocker.GetMock() + .Verify(v => v.UpdateMany(It.Is>(s => s.First().ForeignReleaseId == newInfo.ForeignReleaseId))); + + } + + [Test] + public void child_merge_targets_should_not_be_null_if_target_is_new() + { + var oldTrack = Builder + .CreateNew() + .With(x => x.AlbumReleaseId = _release.Id) + .With(x => x.ArtistMetadata = _metadata) + .With(x => x.ArtistMetadataId = _metadata.Id) + .Build(); + _release.Tracks = new List { oldTrack }; + + var newInfo = _release.JsonClone(); + var newTrack = oldTrack.JsonClone(); + newTrack.ArtistMetadata = _metadata; + newTrack.ArtistMetadataId = _metadata.Id; + newTrack.ForeignTrackId = "new id"; + newTrack.OldForeignTrackIds = new List { oldTrack.ForeignTrackId }; + newInfo.Tracks = new List { newTrack }; + + Mocker.GetMock() + .Setup(s => s.GetTracksForRefresh(_release.Id, It.IsAny>())) + .Returns(new List { oldTrack }); + + Subject.RefreshEntityInfo(_release, new List { newInfo }, false, false); + + Mocker.GetMock() + .Verify(v => v.RefreshTrackInfo(It.IsAny>(), + It.IsAny>(), + It.Is>>(x => x.All(y => y.Item2 != null)), + It.IsAny>(), + It.IsAny>(), + It.IsAny>(), + It.IsAny())); + + Mocker.GetMock() + .Verify(v => v.UpdateMany(It.Is>(s => s.First().ForeignReleaseId == newInfo.ForeignReleaseId))); + + } + } +} diff --git a/src/NzbDrone.Core.Test/MusicTests/RefreshAlbumServiceFixture.cs b/src/NzbDrone.Core.Test/MusicTests/RefreshAlbumServiceFixture.cs index c6e454ca5..d1a44e647 100644 --- a/src/NzbDrone.Core.Test/MusicTests/RefreshAlbumServiceFixture.cs +++ b/src/NzbDrone.Core.Test/MusicTests/RefreshAlbumServiceFixture.cs @@ -9,10 +9,7 @@ using NzbDrone.Core.Exceptions; using NzbDrone.Core.MetadataSource; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Music; -using NzbDrone.Core.Music.Commands; using NzbDrone.Test.Common; -using FluentAssertions; -using NzbDrone.Common.Serializer; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.History; @@ -181,51 +178,6 @@ namespace NzbDrone.Core.Test.MusicTests ExceptionVerification.ExpectedWarns(1); } - [Test] - public void two_equivalent_albums_should_be_equal() - { - var album = Builder.CreateNew().Build(); - var album2 = Builder.CreateNew().Build(); - - ReferenceEquals(album, album2).Should().BeFalse(); - album.Equals(album2).Should().BeTrue(); - } - - [Test] - public void two_equivalent_releases_should_be_equal() - { - var release = Builder.CreateNew().Build(); - var release2 = Builder.CreateNew().Build(); - - ReferenceEquals(release, release2).Should().BeFalse(); - release.Equals(release2).Should().BeTrue(); - - release.Label?.ToJson().Should().Be(release2.Label?.ToJson()); - release.Country?.ToJson().Should().Be(release2.Country?.ToJson()); - release.Media?.ToJson().Should().Be(release2.Media?.ToJson()); - - } - - [Test] - public void two_equivalent_tracks_should_be_equal() - { - var track = Builder.CreateNew().Build(); - var track2 = Builder.CreateNew().Build(); - - ReferenceEquals(track, track2).Should().BeFalse(); - track.Equals(track2).Should().BeTrue(); - } - - [Test] - public void two_equivalent_metadata_should_be_equal() - { - var meta = Builder.CreateNew().Build(); - var meta2 = Builder.CreateNew().Build(); - - ReferenceEquals(meta, meta2).Should().BeFalse(); - meta.Equals(meta2).Should().BeTrue(); - } - [Test] public void should_not_add_duplicate_releases() { diff --git a/src/NzbDrone.Core.Test/MusicTests/RefreshTrackServiceFixture.cs b/src/NzbDrone.Core.Test/MusicTests/RefreshTrackServiceFixture.cs new file mode 100644 index 000000000..7ff5a21f0 --- /dev/null +++ b/src/NzbDrone.Core.Test/MusicTests/RefreshTrackServiceFixture.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FizzWare.NBuilder; +using Moq; +using NUnit.Framework; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Music; +using NzbDrone.Core.MediaFiles; + +namespace NzbDrone.Core.Test.MusicTests +{ + [TestFixture] + public class RefreshTrackServiceFixture : CoreTest + { + private AlbumRelease _release; + private List _allTracks; + + [SetUp] + public void Setup() + { + _release = Builder.CreateNew().Build(); + _allTracks = Builder.CreateListOfSize(20) + .All() + .BuildList(); + } + + [Test] + public void updated_track_should_not_have_null_album_release() + { + var add = new List(); + var update = new List(); + var merge = new List>(); + var delete = new List(); + var upToDate = new List(); + + upToDate.AddRange(_allTracks.Take(10)); + + var toUpdate = _allTracks[10].JsonClone(); + toUpdate.Title = "title to update"; + toUpdate.AlbumRelease = _release; + + update.Add(toUpdate); + + Subject.RefreshTrackInfo(add, update, merge, delete, upToDate, _allTracks, false); + + Mocker.GetMock() + .Verify(v => v.SyncTags(It.Is>(x => x.Count == 1 && + x[0].AlbumRelease != null && + x[0].AlbumRelease.IsLoaded == true))); + + } + } +} diff --git a/src/NzbDrone.Core/Lidarr.Core.csproj b/src/NzbDrone.Core/Lidarr.Core.csproj index f1ab32a5c..7d3bdafa9 100644 --- a/src/NzbDrone.Core/Lidarr.Core.csproj +++ b/src/NzbDrone.Core/Lidarr.Core.csproj @@ -16,6 +16,7 @@ + diff --git a/src/NzbDrone.Core/MediaCover/MediaCover.cs b/src/NzbDrone.Core/MediaCover/MediaCover.cs index bdca37a68..4e5c42b7d 100644 --- a/src/NzbDrone.Core/MediaCover/MediaCover.cs +++ b/src/NzbDrone.Core/MediaCover/MediaCover.cs @@ -1,5 +1,6 @@ using System.IO; using NzbDrone.Common.Extensions; +using Equ; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.MediaCover @@ -24,7 +25,7 @@ namespace NzbDrone.Core.MediaCover Album = 1 } - public class MediaCover : IEmbeddedDocument + public class MediaCover : MemberwiseEquatable, IEmbeddedDocument { private string _url; public string Url diff --git a/src/NzbDrone.Core/Music/Album.cs b/src/NzbDrone.Core/Music/Album.cs index 6eec0f258..2ece54b34 100644 --- a/src/NzbDrone.Core/Music/Album.cs +++ b/src/NzbDrone.Core/Music/Album.cs @@ -1,26 +1,26 @@ using NzbDrone.Common.Extensions; -using NzbDrone.Core.Datastore; using System; using System.Collections.Generic; using Marr.Data; +using Equ; using System.Linq; -using NzbDrone.Common.Serializer; namespace NzbDrone.Core.Music { - public class Album : ModelBase, IEquatable + public class Album : Entity { public Album() { - Genres = new List(); + OldForeignAlbumIds = new List(); + Images = new List(); Links = new List(); + Genres = new List(); + SecondaryTypes = new List(); Ratings = new Ratings(); Artist = new Artist(); - OldForeignAlbumIds = new List(); - } - public const string RELEASE_DATE_FORMAT = "yyyy-MM-dd"; + } // These correspond to columns in the Albums table // These are metadata entries @@ -45,14 +45,19 @@ namespace NzbDrone.Core.Music public bool AnyReleaseOk { get; set; } public DateTime? LastInfoSync { get; set; } public DateTime Added { get; set; } + [MemberwiseEqualityIgnore] public AddArtistOptions AddOptions { get; set; } // These are dynamically queried from other tables + [MemberwiseEqualityIgnore] public LazyLoaded ArtistMetadata { get; set; } + [MemberwiseEqualityIgnore] public LazyLoaded> AlbumReleases { get; set; } + [MemberwiseEqualityIgnore] public LazyLoaded Artist { get; set; } //compatibility properties with old version of Album + [MemberwiseEqualityIgnore] public int ArtistId { get { return Artist?.Value?.Id ?? 0; } set { Artist.Value.Id = value; } } public override string ToString() @@ -60,7 +65,36 @@ namespace NzbDrone.Core.Music return string.Format("[{0}][{1}]", ForeignAlbumId, Title.NullSafe()); } - public void ApplyChanges(Album otherAlbum) + public override void UseMetadataFrom(Album other) + { + ForeignAlbumId = other.ForeignAlbumId; + OldForeignAlbumIds = other.OldForeignAlbumIds; + Title = other.Title; + Overview = other.Overview.IsNullOrWhiteSpace() ? Overview : other.Overview; + Disambiguation = other.Disambiguation; + ReleaseDate = other.ReleaseDate; + Images = other.Images.Any() ? other.Images : Images; + Links = other.Links; + Genres = other.Genres; + AlbumType = other.AlbumType; + SecondaryTypes = other.SecondaryTypes; + Ratings = other.Ratings; + CleanTitle = other.CleanTitle; + } + + public override void UseDbFieldsFrom(Album other) + { + Id = other.Id; + ArtistMetadataId = other.ArtistMetadataId; + ProfileId = other.ProfileId; + Monitored = other.Monitored; + AnyReleaseOk = other.AnyReleaseOk; + LastInfoSync = other.LastInfoSync; + Added = other.Added; + AddOptions = other.AddOptions; + } + + public override void ApplyChanges(Album otherAlbum) { ForeignAlbumId = otherAlbum.ForeignAlbumId; ProfileId = otherAlbum.ProfileId; @@ -68,72 +102,5 @@ namespace NzbDrone.Core.Music Monitored = otherAlbum.Monitored; 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; - } - } } } diff --git a/src/NzbDrone.Core/Music/Artist.cs b/src/NzbDrone.Core/Music/Artist.cs index 75d57df9f..77636b27e 100644 --- a/src/NzbDrone.Core/Music/Artist.cs +++ b/src/NzbDrone.Core/Music/Artist.cs @@ -1,16 +1,14 @@ using Marr.Data; using NzbDrone.Common.Extensions; -using NzbDrone.Core.Datastore; using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Profiles.Metadata; using System; using System.Collections.Generic; -using System.Linq; -using System.Text; +using Equ; namespace NzbDrone.Core.Music { - public class Artist : ModelBase + public class Artist : Entity { public Artist() { @@ -18,8 +16,8 @@ namespace NzbDrone.Core.Music Metadata = new ArtistMetadata(); } + // These correspond to columns in the Artists table public int ArtistMetadataId { get; set; } - public LazyLoaded Metadata { get; set; } public string CleanName { get; set; } public string SortName { get; set; } public bool Monitored { get; set; } @@ -29,25 +27,62 @@ namespace NzbDrone.Core.Music public string RootFolderPath { get; set; } public DateTime Added { get; set; } public int QualityProfileId { get; set; } - public LazyLoaded QualityProfile { get; set; } - public int MetadataProfileId { get; set; } - public LazyLoaded MetadataProfile { get; set; } - public LazyLoaded> Albums { get; set; } + public int MetadataProfileId { get; set; } public HashSet Tags { get; set; } + [MemberwiseEqualityIgnore] public AddArtistOptions AddOptions { get; set; } + // Dynamically loaded from DB + [MemberwiseEqualityIgnore] + public LazyLoaded Metadata { get; set; } + [MemberwiseEqualityIgnore] + public LazyLoaded QualityProfile { get; set; } + [MemberwiseEqualityIgnore] + public LazyLoaded MetadataProfile { get; set; } + [MemberwiseEqualityIgnore] + public LazyLoaded> Albums { get; set; } + + //compatibility properties + [MemberwiseEqualityIgnore] + public string Name { get { return Metadata.Value.Name; } set { Metadata.Value.Name = value; } } + [MemberwiseEqualityIgnore] + public string ForeignArtistId { get { return Metadata.Value.ForeignArtistId; } set { Metadata.Value.ForeignArtistId = value; } } + public override string ToString() { - return string.Format("[{0}][{1}]", Metadata.Value.ForeignArtistId, Metadata.Value.Name.NullSafe()); + return string.Format("[{0}][{1}]", Metadata.Value.ForeignArtistId.NullSafe(), Metadata.Value.Name.NullSafe()); } - public void ApplyChanges(Artist otherArtist) + public override void UseMetadataFrom(Artist other) + { + CleanName = other.CleanName; + SortName = other.SortName; + } + + public override void UseDbFieldsFrom(Artist other) + { + Id = other.Id; + ArtistMetadataId = other.ArtistMetadataId; + Monitored = other.Monitored; + AlbumFolder = other.AlbumFolder; + LastInfoSync = other.LastInfoSync; + Path = other.Path; + RootFolderPath = other.RootFolderPath; + Added = other.Added; + QualityProfileId = other.QualityProfileId; + MetadataProfileId = other.MetadataProfileId; + Tags = other.Tags; + AddOptions = other.AddOptions; + } + + public override void ApplyChanges(Artist otherArtist) { Path = otherArtist.Path; QualityProfileId = otherArtist.QualityProfileId; QualityProfile = otherArtist.QualityProfile; MetadataProfileId = otherArtist.MetadataProfileId; + MetadataProfile = otherArtist.MetadataProfile; Albums = otherArtist.Albums; Tags = otherArtist.Tags; @@ -57,10 +92,5 @@ namespace NzbDrone.Core.Music AlbumFolder = otherArtist.AlbumFolder; } - - //compatibility properties - public string Name { get { return Metadata.Value.Name; } set { Metadata.Value.Name = value; } } - public string ForeignArtistId { get { return Metadata.Value.ForeignArtistId; } set { Metadata.Value.ForeignArtistId = value; } } - } } diff --git a/src/NzbDrone.Core/Music/ArtistMetadata.cs b/src/NzbDrone.Core/Music/ArtistMetadata.cs index 45732c7c4..bb56bf7c9 100644 --- a/src/NzbDrone.Core/Music/ArtistMetadata.cs +++ b/src/NzbDrone.Core/Music/ArtistMetadata.cs @@ -1,13 +1,10 @@ using NzbDrone.Common.Extensions; -using NzbDrone.Common.Serializer; -using NzbDrone.Core.Datastore; -using System; using System.Collections.Generic; using System.Linq; namespace NzbDrone.Core.Music { - public class ArtistMetadata : ModelBase, IEquatable + public class ArtistMetadata : Entity { public ArtistMetadata() { @@ -38,90 +35,21 @@ namespace NzbDrone.Core.Music return string.Format("[{0}][{1}]", ForeignArtistId, Name.NullSafe()); } - public void ApplyChanges(ArtistMetadata otherArtist) + public override void UseMetadataFrom(ArtistMetadata other) { - ForeignArtistId = otherArtist.ForeignArtistId; - OldForeignArtistIds = otherArtist.OldForeignArtistIds; - Name = otherArtist.Name; - Aliases = otherArtist.Aliases; - Overview = otherArtist.Overview.IsNullOrWhiteSpace() ? Overview : otherArtist.Overview; - Disambiguation = otherArtist.Disambiguation; - Type = otherArtist.Type; - Status = otherArtist.Status; - Images = otherArtist.Images.Any() ? otherArtist.Images : Images; - Links = otherArtist.Links; - Genres = otherArtist.Genres; - Ratings = otherArtist.Ratings; - Members = otherArtist.Members; - } - - public bool Equals(ArtistMetadata other) - { - if (other == null) - { - return false; - } - - if (Id == other.Id && - ForeignArtistId == other.ForeignArtistId && - (OldForeignArtistIds?.SequenceEqual(other.OldForeignArtistIds) ?? true) && - Name == other.Name && - (Aliases?.SequenceEqual(other.Aliases) ?? true) && - Overview == other.Overview && - Disambiguation == other.Disambiguation && - Type == other.Type && - Status == other.Status && - Images?.ToJson() == other.Images?.ToJson() && - Links?.ToJson() == other.Links?.ToJson() && - (Genres?.SequenceEqual(other.Genres) ?? true) && - Ratings?.ToJson() == other.Ratings?.ToJson() && - Members?.ToJson() == other.Members?.ToJson()) - { - return true; - } - - return false; - } - - public override bool Equals(object obj) - { - if (obj == null) - { - return false; - } - - var other = obj as ArtistMetadata; - if (other == null) - { - return false; - } - else - { - return Equals(other); - } - } - - public override int GetHashCode() - { - unchecked - { - int hash = 17; - hash = hash * 23 + Id; - hash = hash * 23 + ForeignArtistId.GetHashCode(); - hash = hash * 23 + OldForeignArtistIds.GetHashCode(); - hash = hash * 23 + Name?.GetHashCode() ?? 0; - hash = hash * 23 + Aliases?.GetHashCode() ?? 0; - hash = hash * 23 + Overview?.GetHashCode() ?? 0; - hash = hash * 23 + Disambiguation?.GetHashCode() ?? 0; - hash = hash * 23 + Type?.GetHashCode() ?? 0; - hash = hash * 23 + (int)Status; - hash = hash * 23 + Images?.GetHashCode() ?? 0; - hash = hash * 23 + Links?.GetHashCode() ?? 0; - hash = hash * 23 + Genres?.GetHashCode() ?? 0; - hash = hash * 23 + Ratings?.GetHashCode() ?? 0; - hash = hash * 23 + Members?.GetHashCode() ?? 0; - return hash; - } + ForeignArtistId = other.ForeignArtistId; + OldForeignArtistIds = other.OldForeignArtistIds; + Name = other.Name; + Aliases = other.Aliases; + Overview = other.Overview.IsNullOrWhiteSpace() ? Overview : other.Overview; + Disambiguation = other.Disambiguation; + Type = other.Type; + Status = other.Status; + Images = other.Images.Any() ? other.Images : Images; + Links = other.Links; + Genres = other.Genres; + Ratings = other.Ratings; + Members = other.Members; } } } diff --git a/src/NzbDrone.Core/Music/ArtistMetadataRepository.cs b/src/NzbDrone.Core/Music/ArtistMetadataRepository.cs index 2890f4dee..8ef16f41a 100644 --- a/src/NzbDrone.Core/Music/ArtistMetadataRepository.cs +++ b/src/NzbDrone.Core/Music/ArtistMetadataRepository.cs @@ -39,7 +39,7 @@ namespace NzbDrone.Core.Music var existing = existingMetadata.SingleOrDefault(x => x.ForeignArtistId == meta.ForeignArtistId); if (existing != null) { - meta.Id = existing.Id; + meta.UseDbFieldsFrom(existing); if (!meta.Equals(existing)) { updateMetadataList.Add(meta); diff --git a/src/NzbDrone.Core/Music/Entity.cs b/src/NzbDrone.Core/Music/Entity.cs new file mode 100644 index 000000000..b0dc4d822 --- /dev/null +++ b/src/NzbDrone.Core/Music/Entity.cs @@ -0,0 +1,37 @@ +using NzbDrone.Core.Datastore; +using System; +using Equ; + +namespace NzbDrone.Core.Music +{ + public abstract class Entity : ModelBase, IEquatable + where T : Entity + { + private static readonly MemberwiseEqualityComparer _comparer = + MemberwiseEqualityComparer.ByProperties; + + public virtual void UseDbFieldsFrom(T other) + { + Id = other.Id; + } + + public virtual void UseMetadataFrom(T other) { } + + public virtual void ApplyChanges(T other) { } + + public bool Equals(T other) + { + return _comparer.Equals(this as T, other); + } + + public override bool Equals(object obj) + { + return Equals(obj as T); + } + + public override int GetHashCode() + { + return _comparer.GetHashCode(this as T); + } + } +} diff --git a/src/NzbDrone.Core/Music/Links.cs b/src/NzbDrone.Core/Music/Links.cs index abf47eafc..19df7f5fd 100644 --- a/src/NzbDrone.Core/Music/Links.cs +++ b/src/NzbDrone.Core/Music/Links.cs @@ -1,8 +1,9 @@ +using Equ; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Music { - public class Links : IEmbeddedDocument + public class Links : MemberwiseEquatable, IEmbeddedDocument { public string Url { get; set; } public string Name { get; set; } diff --git a/src/NzbDrone.Core/Music/Medium.cs b/src/NzbDrone.Core/Music/Medium.cs index 02ada9896..63a9a72c4 100644 --- a/src/NzbDrone.Core/Music/Medium.cs +++ b/src/NzbDrone.Core/Music/Medium.cs @@ -1,9 +1,9 @@ -using System.Collections.Generic; +using Equ; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Music { - public class Medium : IEmbeddedDocument + public class Medium : MemberwiseEquatable, IEmbeddedDocument { public int Number { get; set; } public string Name { get; set; } diff --git a/src/NzbDrone.Core/Music/Member.cs b/src/NzbDrone.Core/Music/Member.cs index e948c9936..34f3bcbc2 100644 --- a/src/NzbDrone.Core/Music/Member.cs +++ b/src/NzbDrone.Core/Music/Member.cs @@ -1,9 +1,10 @@ using System.Collections.Generic; +using Equ; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Music { - public class Member : IEmbeddedDocument + public class Member : MemberwiseEquatable, IEmbeddedDocument { public Member() { diff --git a/src/NzbDrone.Core/Music/Ratings.cs b/src/NzbDrone.Core/Music/Ratings.cs index 530e5a168..ae3a0526a 100644 --- a/src/NzbDrone.Core/Music/Ratings.cs +++ b/src/NzbDrone.Core/Music/Ratings.cs @@ -1,8 +1,9 @@ -using NzbDrone.Core.Datastore; +using Equ; +using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Music { - public class Ratings : IEmbeddedDocument + public class Ratings : MemberwiseEquatable, IEmbeddedDocument { public int Votes { get; set; } public decimal Value { get; set; } diff --git a/src/NzbDrone.Core/Music/RefreshAlbumReleaseService.cs b/src/NzbDrone.Core/Music/RefreshAlbumReleaseService.cs index ce96500c1..ef8cec763 100644 --- a/src/NzbDrone.Core/Music/RefreshAlbumReleaseService.cs +++ b/src/NzbDrone.Core/Music/RefreshAlbumReleaseService.cs @@ -54,18 +54,9 @@ namespace NzbDrone.Core.Music { 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; - + + local.UseMetadataFrom(remote); + return UpdateResult.UpdateTags; } diff --git a/src/NzbDrone.Core/Music/RefreshAlbumService.cs b/src/NzbDrone.Core/Music/RefreshAlbumService.cs index bcc214d45..8aa740d07 100644 --- a/src/NzbDrone.Core/Music/RefreshAlbumService.cs +++ b/src/NzbDrone.Core/Music/RefreshAlbumService.cs @@ -120,7 +120,8 @@ namespace NzbDrone.Core.Music QualityProfileId = oldArtist.QualityProfileId, RootFolderPath = oldArtist.RootFolderPath, Monitored = oldArtist.Monitored, - AlbumFolder = oldArtist.AlbumFolder + AlbumFolder = oldArtist.AlbumFolder, + Tags = oldArtist.Tags }; _logger.Debug($"Adding missing parent artist {addArtist}"); _addArtistService.AddArtist(addArtist); @@ -162,27 +163,16 @@ namespace NzbDrone.Core.Music } // Force update and fetch covers if images have changed so that we can write them into tags - if (remote.Images.Any() && !local.Images.Select(x => x.Url).SequenceEqual(remote.Images.Select(x => x.Url))) + if (remote.Images.Any() && !local.Images.SequenceEqual(remote.Images)) { _mediaCoverService.EnsureAlbumCovers(remote); result = UpdateResult.UpdateTags; } - + + local.UseMetadataFrom(remote); + 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(); return result; @@ -274,10 +264,8 @@ namespace NzbDrone.Core.Music { local.AlbumId = entity.Id; local.Album = entity; - remote.Id = local.Id; - remote.Album = entity; - remote.AlbumId = entity.Id; - remote.Monitored = local.Monitored; + + remote.UseDbFieldsFrom(local); } protected override void AddChildren(List children) diff --git a/src/NzbDrone.Core/Music/RefreshArtistService.cs b/src/NzbDrone.Core/Music/RefreshArtistService.cs index 154162aec..5d3dd2fda 100644 --- a/src/NzbDrone.Core/Music/RefreshArtistService.cs +++ b/src/NzbDrone.Core/Music/RefreshArtistService.cs @@ -103,8 +103,8 @@ namespace NzbDrone.Core.Music result = UpdateResult.UpdateTags; } - local.CleanName = remote.CleanName; - local.SortName = remote.SortName; + local.UseMetadataFrom(remote); + local.Metadata = remote.Metadata; local.LastInfoSync = DateTime.UtcNow; try diff --git a/src/NzbDrone.Core/Music/RefreshEntityServiceBase.cs b/src/NzbDrone.Core/Music/RefreshEntityServiceBase.cs index b802bc3bf..574f9140c 100644 --- a/src/NzbDrone.Core/Music/RefreshEntityServiceBase.cs +++ b/src/NzbDrone.Core/Music/RefreshEntityServiceBase.cs @@ -259,7 +259,7 @@ namespace NzbDrone.Core.Music // note the children that will be merged into remoteChild (once added) foreach (var child in mergedChildren) { - sortedChildren.Merged.Add(Tuple.Create(child, existingChild)); + sortedChildren.Merged.Add(Tuple.Create(child, remoteChild)); sortedChildren.Deleted.Remove(child); } diff --git a/src/NzbDrone.Core/Music/RefreshTrackService.cs b/src/NzbDrone.Core/Music/RefreshTrackService.cs index 5e3a3b5b4..cb4ad38f8 100644 --- a/src/NzbDrone.Core/Music/RefreshTrackService.cs +++ b/src/NzbDrone.Core/Music/RefreshTrackService.cs @@ -31,13 +31,11 @@ namespace NzbDrone.Core.Music var updateList = new List(); // for tracks that need updating, just grab the remote track and set db ids - foreach (var trackToUpdate in update) + foreach (var track in update) { - var track = remoteTracks.Single(e => e.ForeignTrackId == trackToUpdate.ForeignTrackId); + var remoteTrack = remoteTracks.Single(e => e.ForeignTrackId == track.ForeignTrackId); + track.UseMetadataFrom(remoteTrack); - // copy across the db keys to the remote track and check if we need to update - track.Id = trackToUpdate.Id; - track.TrackFileId = trackToUpdate.TrackFileId; // make sure title is not null track.Title = track.Title ?? "Unknown"; updateList.Add(track); @@ -60,6 +58,9 @@ namespace NzbDrone.Core.Music } } + _trackService.DeleteMany(delete.Concat(merge.Select(x => x.Item1)).ToList()); + _trackService.UpdateMany(updateList); + var tagsToUpdate = updateList; if (forceUpdateFileTags) { @@ -67,9 +68,6 @@ namespace NzbDrone.Core.Music tagsToUpdate = updateList.Concat(upToDate).ToList(); } _audioTagService.SyncTags(tagsToUpdate); - - _trackService.DeleteMany(delete.Concat(merge.Select(x => x.Item1)).ToList()); - _trackService.UpdateMany(updateList); return delete.Any() || updateList.Any() || merge.Any(); } diff --git a/src/NzbDrone.Core/Music/Release.cs b/src/NzbDrone.Core/Music/Release.cs index 54ece47f6..2ad6936af 100644 --- a/src/NzbDrone.Core/Music/Release.cs +++ b/src/NzbDrone.Core/Music/Release.cs @@ -1,18 +1,19 @@ using NzbDrone.Common.Extensions; -using NzbDrone.Core.Datastore; using System; using System.Collections.Generic; -using System.Linq; using Marr.Data; -using NzbDrone.Common.Serializer; +using Equ; namespace NzbDrone.Core.Music { - public class AlbumRelease : ModelBase, IEquatable + public class AlbumRelease : Entity { public AlbumRelease() { OldForeignReleaseIds = new List(); + Label = new List(); + Country = new List(); + Media = new List(); } // These correspond to columns in the AlbumReleases table @@ -31,7 +32,9 @@ namespace NzbDrone.Core.Music public bool Monitored { get; set; } // These are dynamically queried from other tables + [MemberwiseEqualityIgnore] public LazyLoaded Album { get; set; } + [MemberwiseEqualityIgnore] public LazyLoaded> Tracks { get; set; } public override string ToString() @@ -39,73 +42,27 @@ namespace NzbDrone.Core.Music return string.Format("[{0}][{1}]", ForeignReleaseId, Title.NullSafe()); } - public bool Equals (AlbumRelease other) + public override void UseMetadataFrom(AlbumRelease other) { - if (other == null) - { - return false; - } - - if (Id == other.Id && - AlbumId == other.AlbumId && - ForeignReleaseId == other.ForeignReleaseId && - (OldForeignReleaseIds?.SequenceEqual(other.OldForeignReleaseIds) ?? true) && - Title == other.Title && - Status == other.Status && - Duration == other.Duration && - (Label?.SequenceEqual(other.Label) ?? true) && - Disambiguation == other.Disambiguation && - (Country?.SequenceEqual(other.Country) ?? true) && - ReleaseDate == other.ReleaseDate && - ((Media == null && other.Media == null) || (Media?.ToJson() == other.Media?.ToJson())) && - TrackCount == other.TrackCount && - Monitored == other.Monitored) - { - return true; - } - - return false; + ForeignReleaseId = other.ForeignReleaseId; + OldForeignReleaseIds = other.OldForeignReleaseIds; + Title = other.Title; + Status = other.Status; + Duration = other.Duration; + Label = other.Label; + Disambiguation = other.Disambiguation; + Country = other.Country; + ReleaseDate = other.ReleaseDate; + Media = other.Media; + TrackCount = other.TrackCount; } - public override bool Equals(object obj) + public override void UseDbFieldsFrom(AlbumRelease other) { - if (obj == null) - { - return false; - } - - var other = obj as AlbumRelease; - if (other == null) - { - return false; - } - else - { - return Equals(other); - } - } - - public override int GetHashCode() - { - unchecked - { - int hash = 17; - hash = hash * 23 + Id; - hash = hash * 23 + AlbumId; - hash = hash * 23 + ForeignReleaseId.GetHashCode(); - hash = hash * 23 + OldForeignReleaseIds?.GetHashCode() ?? 0; - hash = hash * 23 + Title?.GetHashCode() ?? 0; - hash = hash * 23 + Status?.GetHashCode() ?? 0; - hash = hash * 23 + Duration; - hash = hash * 23 + Label?.GetHashCode() ?? 0; - hash = hash * 23 + Disambiguation?.GetHashCode() ?? 0; - hash = hash * 23 + Country?.GetHashCode() ?? 0; - hash = hash * 23 + ReleaseDate.GetHashCode(); - hash = hash * 23 + Media?.GetHashCode() ?? 0; - hash = hash * 23 + TrackCount; - hash = hash * 23 + Monitored.GetHashCode(); - return hash; - } + Id = other.Id; + AlbumId = other.AlbumId; + Album = other.Album; + Monitored = other.Monitored; } } } diff --git a/src/NzbDrone.Core/Music/Track.cs b/src/NzbDrone.Core/Music/Track.cs index d0a1bae23..ceaabc3d9 100644 --- a/src/NzbDrone.Core/Music/Track.cs +++ b/src/NzbDrone.Core/Music/Track.cs @@ -1,20 +1,18 @@ -using NzbDrone.Core.Datastore; using NzbDrone.Core.MediaFiles; using Marr.Data; using NzbDrone.Common.Extensions; -using System; -using NzbDrone.Common.Serializer; using System.Collections.Generic; -using System.Linq; +using Equ; namespace NzbDrone.Core.Music { - public class Track : ModelBase, IEquatable + public class Track : Entity { public Track() { OldForeignTrackIds = new List(); OldForeignRecordingIds = new List(); + Ratings = new Ratings(); } // These are model fields @@ -32,17 +30,25 @@ namespace NzbDrone.Core.Music public Ratings Ratings { get; set; } public int MediumNumber { get; set; } public int TrackFileId { get; set; } + + [MemberwiseEqualityIgnore] public bool HasFile => TrackFileId > 0; // These are dynamically queried from the DB + [MemberwiseEqualityIgnore] public LazyLoaded AlbumRelease { get; set; } + [MemberwiseEqualityIgnore] public LazyLoaded ArtistMetadata { get; set; } + [MemberwiseEqualityIgnore] public LazyLoaded TrackFile { get; set; } + [MemberwiseEqualityIgnore] public LazyLoaded Artist { get; set; } // These are retained for compatibility // TODO: Remove set, bodged in because tests expect this to be writable + [MemberwiseEqualityIgnore] public int AlbumId { get { return AlbumRelease?.Value?.Album?.Value?.Id ?? 0; } set { /* empty */ } } + [MemberwiseEqualityIgnore] public Album Album { get; set; } public override string ToString() @@ -50,75 +56,27 @@ namespace NzbDrone.Core.Music return string.Format("[{0}]{1}", ForeignTrackId, Title.NullSafe()); } - public bool Equals(Track other) + public override void UseMetadataFrom(Track other) { - if (other == null) - { - return false; - } - - if (Id == other.Id && - ForeignTrackId == other.ForeignTrackId && - (OldForeignTrackIds?.SequenceEqual(other.OldForeignTrackIds) ?? true) && - ForeignRecordingId == other.ForeignRecordingId && - (OldForeignRecordingIds?.SequenceEqual(other.OldForeignRecordingIds) ?? true) && - AlbumReleaseId == other.AlbumReleaseId && - ArtistMetadataId == other.ArtistMetadataId && - TrackNumber == other.TrackNumber && - AbsoluteTrackNumber == other.AbsoluteTrackNumber && - Title == other.Title && - Duration == other.Duration && - Explicit == other.Explicit && - Ratings?.ToJson() == other.Ratings?.ToJson() && - MediumNumber == other.MediumNumber && - TrackFileId == other.TrackFileId) - { - return true; - } - - return false; + ForeignTrackId = other.ForeignTrackId; + OldForeignTrackIds = other.OldForeignTrackIds; + ForeignRecordingId = other.ForeignRecordingId; + OldForeignRecordingIds = other.OldForeignRecordingIds; + TrackNumber = other.TrackNumber; + AbsoluteTrackNumber = other.AbsoluteTrackNumber; + Title = other.Title; + Duration = other.Duration; + Explicit = other.Explicit; + Ratings = other.Ratings; + MediumNumber = other.MediumNumber; } - public override bool Equals(object obj) + public override void UseDbFieldsFrom(Track other) { - if (obj == null) - { - return false; - } - - var other = obj as Track; - if (other == null) - { - return false; - } - else - { - return Equals(other); - } - } - - public override int GetHashCode() - { - unchecked - { - int hash = 17; - hash = hash * 23 + Id; - hash = hash * 23 + ForeignTrackId.GetHashCode(); - hash = hash * 23 + OldForeignTrackIds?.GetHashCode() ?? 0; - hash = hash * 23 + ForeignRecordingId.GetHashCode(); - hash = hash * 23 + OldForeignRecordingIds?.GetHashCode() ?? 0; - hash = hash * 23 + AlbumReleaseId; - hash = hash * 23 + ArtistMetadataId; - hash = hash * 23 + TrackNumber?.GetHashCode() ?? 0; - hash = hash * 23 + AbsoluteTrackNumber; - hash = hash * 23 + Title?.GetHashCode() ?? 0; - hash = hash * 23 + Duration; - hash = hash * 23 + Explicit.GetHashCode(); - hash = hash * 23 + Ratings?.GetHashCode() ?? 0; - hash = hash * 23 + MediumNumber; - hash = hash * 23 + TrackFileId; - return hash; - } + Id = other.Id; + AlbumReleaseId = other.AlbumReleaseId; + ArtistMetadataId = other.ArtistMetadataId; + TrackFileId = other.TrackFileId; } } }