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.
pull/6/head
ta264 5 years ago
parent c4578c0b0f
commit 2097bfff94

@ -6,6 +6,7 @@
<ItemGroup>
<PackageReference Include="NBuilder" Version="6.0.0" />
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="4.0.11" />
<PackageReference Include="AutoFixture" Version="4.11.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NzbDrone.Test.Common\Lidarr.Test.Common.csproj" />

@ -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<T>
{
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<T>
{
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<ArtistMetadata>();
var item2 = item1.JsonClone();
item1.Should().NotBeSameAs(item2);
item1.Should().Be(item2);
}
[Test, TestCaseSource(typeof(EqualityPropertySource<ArtistMetadata>), "TestCases")]
public void two_different_artist_metadata_should_not_be_equal(PropertyInfo prop)
{
var item1 = fixture.Create<ArtistMetadata>();
var item2 = item1.JsonClone();
var different = fixture.Create<ArtistMetadata>();
// 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<ArtistMetadata>();
var item2 = fixture.Create<ArtistMetadata>();
item1.Should().NotBe(item2);
item1.UseMetadataFrom(item2);
item1.UseDbFieldsFrom(item2);
item1.Should().Be(item2);
}
private Track GivenTrack()
{
return fixture.Build<Track>()
.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<Track>), "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<AlbumRelease>()
.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<AlbumRelease>), "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<Album>()
.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<Album>), "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<Artist>()
.With(x => x.Metadata, new LazyLoaded<ArtistMetadata>(fixture.Create<ArtistMetadata>()))
.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<Artist>), "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);
}
}
}

@ -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<RefreshAlbumReleaseService>
{
private AlbumRelease _release;
private List<Track> _tracks;
private ArtistMetadata _metadata;
[SetUp]
public void Setup()
{
_release = Builder<AlbumRelease>
.CreateNew()
.With(s => s.Media = new List<Medium> { 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<ArtistMetadata>.CreateNew().Build();
_tracks = Builder<Track>
.CreateListOfSize(10)
.All()
.With(x => x.AlbumReleaseId = _release.Id)
.With(x => x.ArtistMetadata = _metadata)
.With(x => x.ArtistMetadataId = _metadata.Id)
.BuildList();
Mocker.GetMock<ITrackService>()
.Setup(s => s.GetTracksForRefresh(_release.Id, It.IsAny<IEnumerable<string>>()))
.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<string> { _release.ForeignReleaseId };
newInfo.Tracks = _tracks;
Subject.RefreshEntityInfo(_release, new List<AlbumRelease> { newInfo }, false, false);
Mocker.GetMock<IReleaseService>()
.Verify(v => v.UpdateMany(It.Is<List<AlbumRelease>>(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<Track>.CreateListOfSize(10)
.All()
.With(x => x.AlbumReleaseId = clash.Id)
.With(x => x.ArtistMetadata = _metadata)
.With(x => x.ArtistMetadataId = _metadata.Id)
.BuildList();
Mocker.GetMock<IReleaseService>()
.Setup(x => x.GetReleaseByForeignReleaseId(clash.ForeignReleaseId, false))
.Returns(clash);
Mocker.GetMock<ITrackService>()
.Setup(x => x.GetTracksForRefresh(It.IsAny<int>(), It.IsAny<IEnumerable<string>>()))
.Returns(_tracks);
var newInfo = existing.JsonClone();
newInfo.ForeignReleaseId = _release.ForeignReleaseId + 1;
newInfo.OldForeignReleaseIds = new List<string> { _release.ForeignReleaseId };
newInfo.Tracks = _tracks;
Subject.RefreshEntityInfo(new List<AlbumRelease> { clash, existing }, new List<AlbumRelease> { newInfo }, false, false);
// check old album is deleted
Mocker.GetMock<IReleaseService>()
.Verify(v => v.DeleteMany(It.Is<List<AlbumRelease>>(x => x.First().ForeignReleaseId == existing.ForeignReleaseId)));
// check that clash gets updated
Mocker.GetMock<IReleaseService>()
.Verify(v => v.UpdateMany(It.Is<List<AlbumRelease>>(s => s.First().ForeignReleaseId == newInfo.ForeignReleaseId)));
}
[Test]
public void child_merge_targets_should_not_be_null_if_target_is_new()
{
var oldTrack = Builder<Track>
.CreateNew()
.With(x => x.AlbumReleaseId = _release.Id)
.With(x => x.ArtistMetadata = _metadata)
.With(x => x.ArtistMetadataId = _metadata.Id)
.Build();
_release.Tracks = new List<Track> { oldTrack };
var newInfo = _release.JsonClone();
var newTrack = oldTrack.JsonClone();
newTrack.ArtistMetadata = _metadata;
newTrack.ArtistMetadataId = _metadata.Id;
newTrack.ForeignTrackId = "new id";
newTrack.OldForeignTrackIds = new List<string> { oldTrack.ForeignTrackId };
newInfo.Tracks = new List<Track> { newTrack };
Mocker.GetMock<ITrackService>()
.Setup(s => s.GetTracksForRefresh(_release.Id, It.IsAny<IEnumerable<string>>()))
.Returns(new List<Track> { oldTrack });
Subject.RefreshEntityInfo(_release, new List<AlbumRelease> { newInfo }, false, false);
Mocker.GetMock<IRefreshTrackService>()
.Verify(v => v.RefreshTrackInfo(It.IsAny<List<Track>>(),
It.IsAny<List<Track>>(),
It.Is<List<Tuple<Track, Track>>>(x => x.All(y => y.Item2 != null)),
It.IsAny<List<Track>>(),
It.IsAny<List<Track>>(),
It.IsAny<List<Track>>(),
It.IsAny<bool>()));
Mocker.GetMock<IReleaseService>()
.Verify(v => v.UpdateMany(It.Is<List<AlbumRelease>>(s => s.First().ForeignReleaseId == newInfo.ForeignReleaseId)));
}
}
}

@ -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<Album>.CreateNew().Build();
var album2 = Builder<Album>.CreateNew().Build();
ReferenceEquals(album, album2).Should().BeFalse();
album.Equals(album2).Should().BeTrue();
}
[Test]
public void two_equivalent_releases_should_be_equal()
{
var release = Builder<AlbumRelease>.CreateNew().Build();
var release2 = Builder<AlbumRelease>.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<Track>.CreateNew().Build();
var track2 = Builder<Track>.CreateNew().Build();
ReferenceEquals(track, track2).Should().BeFalse();
track.Equals(track2).Should().BeTrue();
}
[Test]
public void two_equivalent_metadata_should_be_equal()
{
var meta = Builder<ArtistMetadata>.CreateNew().Build();
var meta2 = Builder<ArtistMetadata>.CreateNew().Build();
ReferenceEquals(meta, meta2).Should().BeFalse();
meta.Equals(meta2).Should().BeTrue();
}
[Test]
public void should_not_add_duplicate_releases()
{

@ -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<RefreshTrackService>
{
private AlbumRelease _release;
private List<Track> _allTracks;
[SetUp]
public void Setup()
{
_release = Builder<AlbumRelease>.CreateNew().Build();
_allTracks = Builder<Track>.CreateListOfSize(20)
.All()
.BuildList();
}
[Test]
public void updated_track_should_not_have_null_album_release()
{
var add = new List<Track>();
var update = new List<Track>();
var merge = new List<Tuple<Track, Track>>();
var delete = new List<Track>();
var upToDate = new List<Track>();
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<IAudioTagService>()
.Verify(v => v.SyncTags(It.Is<List<Track>>(x => x.Count == 1 &&
x[0].AlbumRelease != null &&
x[0].AlbumRelease.IsLoaded == true)));
}
}
}

@ -16,6 +16,7 @@
<PackageReference Include="xmlrpcnet" Version="2.5.0" />
<PackageReference Include="SpotifyAPI.Web" Version="4.2.0" />
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta0006" />
<PackageReference Include="Equ" Version="2.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Marr.Data\Marr.Data.csproj" />

@ -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<MediaCover>, IEmbeddedDocument
{
private string _url;
public string Url

@ -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<Album>
public class Album : Entity<Album>
{
public Album()
{
Genres = new List<string>();
OldForeignAlbumIds = new List<string>();
Images = new List<MediaCover.MediaCover>();
Links = new List<Links>();
Genres = new List<string>();
SecondaryTypes = new List<SecondaryAlbumType>();
Ratings = new Ratings();
Artist = new Artist();
OldForeignAlbumIds = new List<string>();
}
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> ArtistMetadata { get; set; }
[MemberwiseEqualityIgnore]
public LazyLoaded<List<AlbumRelease>> AlbumReleases { get; set; }
[MemberwiseEqualityIgnore]
public LazyLoaded<Artist> 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,80 +65,42 @@ namespace NzbDrone.Core.Music
return string.Format("[{0}][{1}]", ForeignAlbumId, Title.NullSafe());
}
public void ApplyChanges(Album otherAlbum)
public override void UseMetadataFrom(Album other)
{
ForeignAlbumId = otherAlbum.ForeignAlbumId;
ProfileId = otherAlbum.ProfileId;
AddOptions = otherAlbum.AddOptions;
Monitored = otherAlbum.Monitored;
AnyReleaseOk = otherAlbum.AnyReleaseOk;
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 bool Equals(Album other)
public override void UseDbFieldsFrom(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);
}
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 int GetHashCode()
public override void ApplyChanges(Album otherAlbum)
{
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;
}
ForeignAlbumId = otherAlbum.ForeignAlbumId;
ProfileId = otherAlbum.ProfileId;
AddOptions = otherAlbum.AddOptions;
Monitored = otherAlbum.Monitored;
AnyReleaseOk = otherAlbum.AnyReleaseOk;
}
}
}

@ -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<Artist>
{
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<ArtistMetadata> 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 int MetadataProfileId { get; set; }
public HashSet<int> Tags { get; set; }
[MemberwiseEqualityIgnore]
public AddArtistOptions AddOptions { get; set; }
// Dynamically loaded from DB
[MemberwiseEqualityIgnore]
public LazyLoaded<ArtistMetadata> Metadata { get; set; }
[MemberwiseEqualityIgnore]
public LazyLoaded<QualityProfile> QualityProfile { get; set; }
public int MetadataProfileId { get; set; }
[MemberwiseEqualityIgnore]
public LazyLoaded<MetadataProfile> MetadataProfile { get; set; }
[MemberwiseEqualityIgnore]
public LazyLoaded<List<Album>> Albums { get; set; }
public HashSet<int> Tags { get; set; }
public AddArtistOptions AddOptions { 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; } }
}
}

@ -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<ArtistMetadata>
public class ArtistMetadata : Entity<ArtistMetadata>
{
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;
}
}
}

@ -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);

@ -0,0 +1,37 @@
using NzbDrone.Core.Datastore;
using System;
using Equ;
namespace NzbDrone.Core.Music
{
public abstract class Entity<T> : ModelBase, IEquatable<T>
where T : Entity<T>
{
private static readonly MemberwiseEqualityComparer<T> _comparer =
MemberwiseEqualityComparer<T>.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);
}
}
}

@ -1,8 +1,9 @@
using Equ;
using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.Music
{
public class Links : IEmbeddedDocument
public class Links : MemberwiseEquatable<Links>, IEmbeddedDocument
{
public string Url { get; set; }
public string Name { get; set; }

@ -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<Medium>, IEmbeddedDocument
{
public int Number { get; set; }
public string Name { get; set; }

@ -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<Member>, IEmbeddedDocument
{
public Member()
{

@ -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<Ratings>, IEmbeddedDocument
{
public int Votes { get; set; }
public decimal Value { get; set; }

@ -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;
}

@ -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<AlbumRelease>();
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<AlbumRelease> children)

@ -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

@ -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);
}

@ -31,13 +31,11 @@ namespace NzbDrone.Core.Music
var updateList = new List<Track>();
// 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();
}

@ -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<AlbumRelease>
public class AlbumRelease : Entity<AlbumRelease>
{
public AlbumRelease()
{
OldForeignReleaseIds = new List<string>();
Label = new List<string>();
Country = new List<string>();
Media = new List<Medium>();
}
// 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> Album { get; set; }
[MemberwiseEqualityIgnore]
public LazyLoaded<List<Track>> 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;
}
public override bool Equals(object obj)
{
if (obj == null)
{
return false;
}
var other = obj as AlbumRelease;
if (other == null)
{
return false;
}
else
{
return Equals(other);
}
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 int GetHashCode()
public override void UseDbFieldsFrom(AlbumRelease other)
{
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;
}
}
}

@ -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<Track>
public class Track : Entity<Track>
{
public Track()
{
OldForeignTrackIds = new List<string>();
OldForeignRecordingIds = new List<string>();
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> AlbumRelease { get; set; }
[MemberwiseEqualityIgnore]
public LazyLoaded<ArtistMetadata> ArtistMetadata { get; set; }
[MemberwiseEqualityIgnore]
public LazyLoaded<TrackFile> TrackFile { get; set; }
[MemberwiseEqualityIgnore]
public LazyLoaded<Artist> 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;
}
public override bool Equals(object obj)
{
if (obj == null)
{
return false;
}
var other = obj as Track;
if (other == null)
{
return false;
}
else
{
return Equals(other);
}
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 int GetHashCode()
public override void UseDbFieldsFrom(Track other)
{
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;
}
}
}

Loading…
Cancel
Save