From 4655cbe0c217ee6d336ea6fb0940da822a1ec518 Mon Sep 17 00:00:00 2001 From: ta264 Date: Wed, 16 Oct 2019 21:04:03 +0100 Subject: [PATCH] New: Cache spotify -> musicbrainz mapping --- .../ImportListSyncServiceFixture.cs | 14 +- .../Spotify/SpotifyMappingFixture.cs | 346 ++++++++++++++++++ .../ImportLists/ImportListSyncService.cs | 12 + .../Spotify/SpotifyFollowedArtists.cs | 14 +- .../Spotify/SpotifyImportListBase.cs | 169 ++++++++- .../Spotify/SpotifyImportListItemInfo.cs | 15 + .../ImportLists/Spotify/SpotifyMap.cs | 8 + .../ImportLists/Spotify/SpotifyPlaylist.cs | 22 +- .../ImportLists/Spotify/SpotifySavedAlbums.cs | 14 +- 9 files changed, 590 insertions(+), 24 deletions(-) create mode 100644 src/NzbDrone.Core.Test/ImportListTests/Spotify/SpotifyMappingFixture.cs create mode 100644 src/NzbDrone.Core/ImportLists/Spotify/SpotifyImportListItemInfo.cs create mode 100644 src/NzbDrone.Core/ImportLists/Spotify/SpotifyMap.cs diff --git a/src/NzbDrone.Core.Test/ImportListTests/ImportListSyncServiceFixture.cs b/src/NzbDrone.Core.Test/ImportListTests/ImportListSyncServiceFixture.cs index 17995eeed..20e36bdc9 100644 --- a/src/NzbDrone.Core.Test/ImportListTests/ImportListSyncServiceFixture.cs +++ b/src/NzbDrone.Core.Test/ImportListTests/ImportListSyncServiceFixture.cs @@ -120,9 +120,19 @@ namespace NzbDrone.Core.Test.ImportListTests } [Test] - public void should_not_search_if_album_title_and_album_id() + public void should_search_with_lidarr_id_if_album_id_and_no_artist_id() { - WithAlbum(); + WithAlbumId(); + Subject.Execute(new ImportListSyncCommand()); + + Mocker.GetMock() + .Verify(v => v.SearchForNewAlbum($"lidarr:{_importListReports.First().AlbumMusicBrainzId}", null), Times.Once()); + } + + [Test] + public void should_not_search_if_album_id_and_artist_id() + { + WithArtistId(); WithAlbumId(); Subject.Execute(new ImportListSyncCommand()); diff --git a/src/NzbDrone.Core.Test/ImportListTests/Spotify/SpotifyMappingFixture.cs b/src/NzbDrone.Core.Test/ImportListTests/Spotify/SpotifyMappingFixture.cs new file mode 100644 index 000000000..0e4e3ead2 --- /dev/null +++ b/src/NzbDrone.Core.Test/ImportListTests/Spotify/SpotifyMappingFixture.cs @@ -0,0 +1,346 @@ +using System; +using System.Collections.Generic; +using System.Net; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Common.Cloud; +using NzbDrone.Common.Http; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.ImportLists.Spotify; +using NzbDrone.Core.MetadataSource; +using NzbDrone.Core.MetadataSource.SkyHook.Resource; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Test.Common; + +namespace NzbDrone.Core.Test.ImportListTests +{ + [TestFixture] + // the base import list class is abstract so use the followed artists one + public class SpotifyMappingFixture : CoreTest + { + [SetUp] + public void Setup() + { + Mocker.SetConstant(new LidarrCloudRequestBuilder()); + Mocker.SetConstant(Mocker.Resolve()); + } + + [Test] + public void map_artist_should_return_name_if_id_null() + { + var data = new SpotifyImportListItemInfo + { + Artist = "Adele" + }; + + Subject.MapArtistItem(data); + + data.Artist.Should().Be("Adele"); + data.ArtistMusicBrainzId.Should().BeNull(); + data.Album.Should().BeNull(); + data.AlbumMusicBrainzId.Should().BeNull(); + } + + [Test] + public void map_artist_should_set_id_0_if_no_match() + { + Mocker.GetMock() + .Setup(x => x.Get(It.IsAny())) + .Returns((x) => new HttpResponse(new HttpResponse(x, new HttpHeader(), new byte[0], HttpStatusCode.NotFound))); + + var data = new SpotifyImportListItemInfo + { + Artist = "Adele", + ArtistSpotifyId = "id" + }; + + Subject.MapArtistItem(data); + data.ArtistMusicBrainzId.Should().Be("0"); + } + + [Test] + public void map_artist_should_not_update_id_if_http_throws() + { + Mocker.GetMock() + .Setup(x => x.Get(It.IsAny())) + .Throws(new Exception("Dummy exception")); + + var data = new SpotifyImportListItemInfo + { + Artist = "Adele", + ArtistSpotifyId = "id" + }; + + Subject.MapArtistItem(data); + data.ArtistMusicBrainzId.Should().BeNull(); + + ExceptionVerification.ExpectedErrors(1); + } + + [Test] + public void map_artist_should_work() + { + UseRealHttp(); + + var data = new SpotifyImportListItemInfo + { + Artist = "Adele", + ArtistSpotifyId = "4dpARuHxo51G3z768sgnrY" + }; + + Subject.MapArtistItem(data); + data.Should().NotBeNull(); + data.Artist.Should().Be("Adele"); + data.ArtistMusicBrainzId.Should().Be("cc2c9c3c-b7bc-4b8b-84d8-4fbd8779e493"); + data.Album.Should().BeNull(); + data.AlbumMusicBrainzId.Should().BeNull(); + } + + [Test] + public void map_album_should_return_name_if_uri_null() + { + var data = new SpotifyImportListItemInfo + { + Album = "25", + Artist = "Adele" + }; + + Subject.MapAlbumItem(data); + data.Should().NotBeNull(); + data.Artist.Should().Be("Adele"); + data.ArtistMusicBrainzId.Should().BeNull(); + data.Album.Should().Be("25"); + data.AlbumMusicBrainzId.Should().BeNull(); + } + + [Test] + public void map_album_should_set_id_0_if_no_match() + { + Mocker.GetMock() + .Setup(x => x.Get(It.IsAny())) + .Returns((x) => new HttpResponse(new HttpResponse(x, new HttpHeader(), new byte[0], HttpStatusCode.NotFound))); + + var data = new SpotifyImportListItemInfo + { + Album = "25", + AlbumSpotifyId = "id", + Artist = "Adele" + }; + + Subject.MapAlbumItem(data); + data.AlbumMusicBrainzId.Should().Be("0"); + } + + [Test] + public void map_album_should_not_update_id_if_http_throws() + { + Mocker.GetMock() + .Setup(x => x.Get(It.IsAny())) + .Throws(new Exception("Dummy exception")); + + var data = new SpotifyImportListItemInfo + { + Album = "25", + AlbumSpotifyId = "id", + Artist = "Adele" + }; + + + Subject.MapAlbumItem(data); + data.Should().NotBeNull(); + data.Artist.Should().Be("Adele"); + data.ArtistMusicBrainzId.Should().BeNull(); + data.Album.Should().Be("25"); + data.AlbumMusicBrainzId.Should().BeNull(); + + ExceptionVerification.ExpectedErrors(1); + } + + [Test] + public void map_album_should_work() + { + UseRealHttp(); + + var data = new SpotifyImportListItemInfo + { + Album = "25", + AlbumSpotifyId = "7uwTHXmFa1Ebi5flqBosig", + Artist = "Adele" + }; + + Subject.MapAlbumItem(data); + + data.Should().NotBeNull(); + data.Artist.Should().Be("Adele"); + data.Album.Should().Be("25"); + data.AlbumMusicBrainzId.Should().Be("5537624c-3d2f-4f5c-8099-df916082c85c"); + } + + [Test] + public void map_spotify_releases_should_only_map_album_id_for_album() + { + var data = new List + { + new SpotifyImportListItemInfo + { + Album = "25", + AlbumSpotifyId = "7uwTHXmFa1Ebi5flqBosig", + Artist = "Adele", + ArtistSpotifyId = "4dpARuHxo51G3z768sgnrY" + } + }; + + var map = new List + { + new SpotifyMap + { + SpotifyId = "7uwTHXmFa1Ebi5flqBosig", + MusicbrainzId = "5537624c-3d2f-4f5c-8099-df916082c85c" + }, + new SpotifyMap + { + SpotifyId = "4dpARuHxo51G3z768sgnrY", + MusicbrainzId = "cc2c9c3c-b7bc-4b8b-84d8-4fbd8779e493" + } + }; + + Mocker.GetMock() + .Setup(x => x.Post>(It.IsAny())) + .Returns(r => new HttpResponse>(new HttpResponse(r, new HttpHeader(), map.ToJson()))); + + var result = Subject.MapSpotifyReleases(data); + result[0].AlbumMusicBrainzId.Should().Be("5537624c-3d2f-4f5c-8099-df916082c85c"); + result[0].ArtistMusicBrainzId.Should().BeNull(); + } + + [Test] + public void map_spotify_releases_should_map_artist_id_for_artist() + { + var data = new List + { + new SpotifyImportListItemInfo + { + Artist = "Adele", + ArtistSpotifyId = "4dpARuHxo51G3z768sgnrY" + } + }; + + var map = new List + { + new SpotifyMap + { + SpotifyId = "7uwTHXmFa1Ebi5flqBosig", + MusicbrainzId = "5537624c-3d2f-4f5c-8099-df916082c85c" + }, + new SpotifyMap + { + SpotifyId = "4dpARuHxo51G3z768sgnrY", + MusicbrainzId = "cc2c9c3c-b7bc-4b8b-84d8-4fbd8779e493" + } + }; + + Mocker.GetMock() + .Setup(x => x.Post>(It.IsAny())) + .Returns(r => new HttpResponse>(new HttpResponse(r, new HttpHeader(), map.ToJson()))); + + var result = Subject.MapSpotifyReleases(data); + result[0].ArtistMusicBrainzId.Should().Be("cc2c9c3c-b7bc-4b8b-84d8-4fbd8779e493"); + } + + [Test] + public void map_spotify_releases_should_drop_not_found() + { + var data = new List + { + new SpotifyImportListItemInfo + { + Album = "25", + AlbumSpotifyId = "7uwTHXmFa1Ebi5flqBosig", + Artist = "Adele" + } + }; + + var map = new List + { + new SpotifyMap + { + SpotifyId = "7uwTHXmFa1Ebi5flqBosig", + MusicbrainzId = "0" + } + }; + + Mocker.GetMock() + .Setup(x => x.Post>(It.IsAny())) + .Returns(r => new HttpResponse>(new HttpResponse(r, new HttpHeader(), map.ToJson()))); + + var result = Subject.MapSpotifyReleases(data); + result.Should().BeEmpty(); + } + + [Test] + public void map_spotify_releases_should_catch_exception_from_api() + { + var data = new List + { + new SpotifyImportListItemInfo + { + Album = "25", + AlbumSpotifyId = "7uwTHXmFa1Ebi5flqBosig", + Artist = "Adele" + } + }; + + Mocker.GetMock() + .Setup(x => x.Post>(It.IsAny())) + .Throws(new Exception("Dummy exception")); + + Mocker.GetMock() + .Setup(x => x.Get(It.IsAny())) + .Throws(new Exception("Dummy exception")); + + + var result = Subject.MapSpotifyReleases(data); + result.Should().NotBeNull(); + ExceptionVerification.ExpectedErrors(2); + } + + [Test] + public void map_spotify_releases_should_cope_with_duplicate_spotify_ids() + { + var data = new List + { + new SpotifyImportListItemInfo + { + Album = "25", + AlbumSpotifyId = "7uwTHXmFa1Ebi5flqBosig", + Artist = "Adele" + }, + new SpotifyImportListItemInfo + { + Album = "25", + AlbumSpotifyId = "7uwTHXmFa1Ebi5flqBosig", + Artist = "Adele" + } + }; + + var map = new List + { + new SpotifyMap + { + SpotifyId = "7uwTHXmFa1Ebi5flqBosig", + MusicbrainzId = "5537624c-3d2f-4f5c-8099-df916082c85c" + } + }; + + Mocker.GetMock() + .Setup(x => x.Post>(It.IsAny())) + .Returns(r => new HttpResponse>(new HttpResponse(r, new HttpHeader(), map.ToJson()))); + + var result = Subject.MapSpotifyReleases(data); + result.Should().HaveCount(2); + result[0].AlbumMusicBrainzId.Should().Be("5537624c-3d2f-4f5c-8099-df916082c85c"); + result[1].AlbumMusicBrainzId.Should().Be("5537624c-3d2f-4f5c-8099-df916082c85c"); + } + } +} diff --git a/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs b/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs index ba284096f..438e69596 100644 --- a/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs +++ b/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs @@ -104,6 +104,18 @@ namespace NzbDrone.Core.ImportLists } + // Map artist ID if we only have album ID + if (report.AlbumMusicBrainzId.IsNotNullOrWhiteSpace() && report.ArtistMusicBrainzId.IsNullOrWhiteSpace()) + { + var mappedAlbum = _albumSearchService.SearchForNewAlbum($"lidarr:{report.AlbumMusicBrainzId}", null) + .FirstOrDefault(); + + if (mappedAlbum == null) continue; + + report.Artist = mappedAlbum.ArtistMetadata?.Value?.Name; + report.ArtistMusicBrainzId = mappedAlbum?.ArtistMetadata?.Value?.ForeignArtistId; + } + // Map MBid if we only have a artist name if (report.ArtistMusicBrainzId.IsNullOrWhiteSpace() && report.Artist.IsNotNullOrWhiteSpace()) { diff --git a/src/NzbDrone.Core/ImportLists/Spotify/SpotifyFollowedArtists.cs b/src/NzbDrone.Core/ImportLists/Spotify/SpotifyFollowedArtists.cs index 5897af015..5c75640ae 100644 --- a/src/NzbDrone.Core/ImportLists/Spotify/SpotifyFollowedArtists.cs +++ b/src/NzbDrone.Core/ImportLists/Spotify/SpotifyFollowedArtists.cs @@ -3,6 +3,7 @@ using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; +using NzbDrone.Core.MetadataSource; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using SpotifyAPI.Web; @@ -18,21 +19,22 @@ namespace NzbDrone.Core.ImportLists.Spotify public class SpotifyFollowedArtists : SpotifyImportListBase { public SpotifyFollowedArtists(ISpotifyProxy spotifyProxy, + IMetadataRequestBuilder requestBuilder, IImportListStatusService importListStatusService, IImportListRepository importListRepository, IConfigService configService, IParsingService parsingService, IHttpClient httpClient, Logger logger) - : base(spotifyProxy, importListStatusService, importListRepository, configService, parsingService, httpClient, logger) + : base(spotifyProxy, requestBuilder, importListStatusService, importListRepository, configService, parsingService, httpClient, logger) { } public override string Name => "Spotify Followed Artists"; - public override IList Fetch(SpotifyWebAPI api) + public override IList Fetch(SpotifyWebAPI api) { - var result = new List(); + var result = new List(); var followedArtists = _spotifyProxy.GetFollowedArtists(this, api); var artists = followedArtists?.Artists; @@ -61,12 +63,14 @@ namespace NzbDrone.Core.ImportLists.Spotify return result; } - private ImportListItemInfo ParseFullArtist(FullArtist artist) + private SpotifyImportListItemInfo ParseFullArtist(FullArtist artist) { if (artist?.Name.IsNotNullOrWhiteSpace() ?? false) { - return new ImportListItemInfo { + return new SpotifyImportListItemInfo + { Artist = artist.Name, + ArtistSpotifyId = artist.Id }; } diff --git a/src/NzbDrone.Core/ImportLists/Spotify/SpotifyImportListBase.cs b/src/NzbDrone.Core/ImportLists/Spotify/SpotifyImportListBase.cs index 2a1a57853..8390179a6 100644 --- a/src/NzbDrone.Core/ImportLists/Spotify/SpotifyImportListBase.cs +++ b/src/NzbDrone.Core/ImportLists/Spotify/SpotifyImportListBase.cs @@ -1,11 +1,16 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; +using System.Net; using FluentValidation.Results; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; +using NzbDrone.Common.Serializer; using NzbDrone.Core.Configuration; +using NzbDrone.Core.MetadataSource; +using NzbDrone.Core.MetadataSource.SkyHook.Resource; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Validation; @@ -21,8 +26,10 @@ namespace NzbDrone.Core.ImportLists.Spotify private IImportListRepository _importListRepository; protected ISpotifyProxy _spotifyProxy; + private readonly IMetadataRequestBuilder _requestBuilder; protected SpotifyImportListBase(ISpotifyProxy spotifyProxy, + IMetadataRequestBuilder requestBuilder, IImportListStatusService importListStatusService, IImportListRepository importListRepository, IConfigService configService, @@ -34,6 +41,7 @@ namespace NzbDrone.Core.ImportLists.Spotify _httpClient = httpClient; _importListRepository = importListRepository; _spotifyProxy = spotifyProxy; + _requestBuilder = requestBuilder; } public override ImportListType ListType => ImportListType.Spotify; @@ -93,15 +101,20 @@ namespace NzbDrone.Core.ImportLists.Spotify public override IList Fetch() { + IList releases; using (var api = GetApi()) { _logger.Debug("Starting spotify import list sync"); - var releases = Fetch(api); - return CleanupListItems(releases); + releases = Fetch(api); } + + // map to musicbrainz ids + releases = MapSpotifyReleases(releases); + + return CleanupListItems(releases); } - public abstract IList Fetch(SpotifyWebAPI api); + public abstract IList Fetch(SpotifyWebAPI api); protected DateTime ParseSpotifyDate(string date, string precision) { @@ -128,6 +141,156 @@ namespace NzbDrone.Core.ImportLists.Spotify return DateTime.TryParseExact(date, format, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime result) ? result : default(DateTime); } + public IList MapSpotifyReleases(IList items) + { + // first pass bulk lookup, server won't do search + var spotifyIds = items.Select(x => x.ArtistSpotifyId) + .Concat(items.Select(x => x.AlbumSpotifyId)) + .Where(x => x.IsNotNullOrWhiteSpace()) + .Distinct(); + var httpRequest = _requestBuilder.GetRequestBuilder().Create() + .SetSegment("route", "spotify/lookup") + .Build(); + httpRequest.SetContent(spotifyIds.ToJson()); + httpRequest.Headers.ContentType = "application/json"; + + _logger.Trace($"Requesting maps for:\n{spotifyIds.ToJson()}"); + + Dictionary map; + try + { + var httpResponse = _httpClient.Post>(httpRequest); + var mapList = httpResponse.Resource; + + // Generate a mapping dictionary. + // The API will return 0 to mean it has previously searched and can't find the item. + // null means that it has never been searched before. + map = mapList.Where(x => x.MusicbrainzId.IsNotNullOrWhiteSpace()) + .ToDictionary(x => x.SpotifyId, x => x.MusicbrainzId); + } + catch (Exception e) + { + _logger.Error(e); + map = new Dictionary(); + } + + _logger.Trace("Got mapping:\n{0}", map.ToJson()); + + foreach (var item in items) + { + if (item.AlbumSpotifyId.IsNotNullOrWhiteSpace()) + { + if (map.ContainsKey(item.AlbumSpotifyId)) + { + item.AlbumMusicBrainzId = map[item.AlbumSpotifyId]; + } + else + { + MapAlbumItem(item); + } + } + else if (item.ArtistSpotifyId.IsNotNullOrWhiteSpace()) + { + if (map.ContainsKey(item.ArtistSpotifyId)) + { + item.ArtistMusicBrainzId = map[item.ArtistSpotifyId]; + } + else + { + MapArtistItem(item); + } + } + } + + // Strip out items where mapped to not found + return items.Where(x => x.AlbumMusicBrainzId != "0" && x.ArtistMusicBrainzId != "0").ToList(); + } + + public void MapArtistItem(SpotifyImportListItemInfo item) + { + if (item.ArtistSpotifyId.IsNullOrWhiteSpace()) + { + return; + } + + var httpRequest = _requestBuilder.GetRequestBuilder().Create() + .SetSegment("route", $"spotify/artist/{item.ArtistSpotifyId}") + .Build(); + httpRequest.AllowAutoRedirect = true; + httpRequest.SuppressHttpError = true; + + try + { + var response = _httpClient.Get(httpRequest); + + if (response.HasHttpError) + { + if (response.StatusCode == HttpStatusCode.NotFound) + { + item.ArtistMusicBrainzId = "0"; + return; + } + else + { + throw new HttpException(httpRequest, response); + } + } + + item.ArtistMusicBrainzId = response.Resource.Id; + } + catch (HttpException e) + { + _logger.Warn(e, "Unable to communicate with LidarrAPI"); + } + catch (Exception e) + { + _logger.Error(e); + } + } + + public void MapAlbumItem(SpotifyImportListItemInfo item) + { + if (item.AlbumSpotifyId.IsNullOrWhiteSpace()) + { + return; + } + + var httpRequest = _requestBuilder.GetRequestBuilder().Create() + .SetSegment("route", $"spotify/album/{item.AlbumSpotifyId}") + .Build(); + httpRequest.AllowAutoRedirect = true; + httpRequest.SuppressHttpError = true; + + try + { + var response = _httpClient.Get(httpRequest); + + if (response.HasHttpError) + { + if (response.StatusCode == HttpStatusCode.NotFound) + { + item.AlbumMusicBrainzId = "0"; + return; + } + else + { + throw new HttpException(httpRequest, response); + } + } + + item.ArtistMusicBrainzId = response.Resource.ArtistId; + item.AlbumMusicBrainzId = response.Resource.Id; + } + catch (HttpException e) + { + _logger.Warn(e, "Unable to communicate with LidarrAPI"); + } + catch (Exception e) + { + _logger.Error(e); + } + } + protected override void Test(List failures) { failures.AddIfNotNull(TestConnection()); diff --git a/src/NzbDrone.Core/ImportLists/Spotify/SpotifyImportListItemInfo.cs b/src/NzbDrone.Core/ImportLists/Spotify/SpotifyImportListItemInfo.cs new file mode 100644 index 000000000..a9ccc0c4e --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/Spotify/SpotifyImportListItemInfo.cs @@ -0,0 +1,15 @@ +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.ImportLists.Spotify +{ + public class SpotifyImportListItemInfo : ImportListItemInfo + { + public string ArtistSpotifyId { get; set; } + public string AlbumSpotifyId { get; set; } + + public override string ToString() + { + return string.Format("[{0}] {1}", ArtistSpotifyId, AlbumSpotifyId); + } + } +} diff --git a/src/NzbDrone.Core/ImportLists/Spotify/SpotifyMap.cs b/src/NzbDrone.Core/ImportLists/Spotify/SpotifyMap.cs new file mode 100644 index 000000000..34170b98b --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/Spotify/SpotifyMap.cs @@ -0,0 +1,8 @@ +namespace NzbDrone.Core.ImportLists.Spotify +{ + public class SpotifyMap + { + public string SpotifyId { get; set; } + public string MusicbrainzId { get; set; } + } +} diff --git a/src/NzbDrone.Core/ImportLists/Spotify/SpotifyPlaylist.cs b/src/NzbDrone.Core/ImportLists/Spotify/SpotifyPlaylist.cs index dc8daa267..3c1b80761 100644 --- a/src/NzbDrone.Core/ImportLists/Spotify/SpotifyPlaylist.cs +++ b/src/NzbDrone.Core/ImportLists/Spotify/SpotifyPlaylist.cs @@ -5,8 +5,8 @@ using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; +using NzbDrone.Core.MetadataSource; using NzbDrone.Core.Parser; -using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Validation; using SpotifyAPI.Web; using SpotifyAPI.Web.Models; @@ -16,30 +16,31 @@ namespace NzbDrone.Core.ImportLists.Spotify public class SpotifyPlaylist : SpotifyImportListBase { public SpotifyPlaylist(ISpotifyProxy spotifyProxy, + IMetadataRequestBuilder requestBuilder, IImportListStatusService importListStatusService, IImportListRepository importListRepository, IConfigService configService, IParsingService parsingService, IHttpClient httpClient, Logger logger) - : base(spotifyProxy, importListStatusService, importListRepository, configService, parsingService, httpClient, logger) + : base(spotifyProxy, requestBuilder, importListStatusService, importListRepository, configService, parsingService, httpClient, logger) { } public override string Name => "Spotify Playlists"; - public override IList Fetch(SpotifyWebAPI api) + public override IList Fetch(SpotifyWebAPI api) { return Settings.PlaylistIds.SelectMany(x => Fetch(api, x)).ToList(); } - public IList Fetch(SpotifyWebAPI api, string playlistId) + public IList Fetch(SpotifyWebAPI api, string playlistId) { - var result = new List(); + var result = new List(); _logger.Trace($"Processing playlist {playlistId}"); - var playlistTracks = _spotifyProxy.GetPlaylistTracks(this, api, playlistId, "next, items(track(name, album(name,artists)))"); + var playlistTracks = _spotifyProxy.GetPlaylistTracks(this, api, playlistId, "next, items(track(name, artists(id, name), album(id, name, release_date, release_date_precision, artists(id, name))))"); while (true) { @@ -64,20 +65,23 @@ namespace NzbDrone.Core.ImportLists.Spotify return result; } - private ImportListItemInfo ParsePlaylistTrack(PlaylistTrack playlistTrack) + private SpotifyImportListItemInfo ParsePlaylistTrack(PlaylistTrack playlistTrack) { // From spotify docs: "Note, a track object may be null. This can happen if a track is no longer available." if (playlistTrack?.Track?.Album != null) { var album = playlistTrack.Track.Album; + var albumName = album.Name; var artistName = album.Artists?.FirstOrDefault()?.Name ?? playlistTrack.Track?.Artists?.FirstOrDefault()?.Name; if (albumName.IsNotNullOrWhiteSpace() && artistName.IsNotNullOrWhiteSpace()) { - return new ImportListItemInfo { + return new SpotifyImportListItemInfo + { Artist = artistName, - Album = albumName, + Album = album.Name, + AlbumSpotifyId = album.Id, ReleaseDate = ParseSpotifyDate(album.ReleaseDate, album.ReleaseDatePrecision) }; } diff --git a/src/NzbDrone.Core/ImportLists/Spotify/SpotifySavedAlbums.cs b/src/NzbDrone.Core/ImportLists/Spotify/SpotifySavedAlbums.cs index bc250da45..91bb3e869 100644 --- a/src/NzbDrone.Core/ImportLists/Spotify/SpotifySavedAlbums.cs +++ b/src/NzbDrone.Core/ImportLists/Spotify/SpotifySavedAlbums.cs @@ -4,6 +4,7 @@ using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; +using NzbDrone.Core.MetadataSource; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; using SpotifyAPI.Web; @@ -19,21 +20,22 @@ namespace NzbDrone.Core.ImportLists.Spotify public class SpotifySavedAlbums : SpotifyImportListBase { public SpotifySavedAlbums(ISpotifyProxy spotifyProxy, + IMetadataRequestBuilder requestBuilder, IImportListStatusService importListStatusService, IImportListRepository importListRepository, IConfigService configService, IParsingService parsingService, IHttpClient httpClient, Logger logger) - : base(spotifyProxy, importListStatusService, importListRepository, configService, parsingService, httpClient, logger) + : base(spotifyProxy, requestBuilder, importListStatusService, importListRepository, configService, parsingService, httpClient, logger) { } public override string Name => "Spotify Saved Albums"; - public override IList Fetch(SpotifyWebAPI api) + public override IList Fetch(SpotifyWebAPI api) { - var result = new List(); + var result = new List(); var savedAlbums = _spotifyProxy.GetSavedAlbums(this, api); @@ -62,7 +64,7 @@ namespace NzbDrone.Core.ImportLists.Spotify return result; } - private ImportListItemInfo ParseSavedAlbum(SavedAlbum savedAlbum) + private SpotifyImportListItemInfo ParseSavedAlbum(SavedAlbum savedAlbum) { var artistName = savedAlbum?.Album?.Artists?.FirstOrDefault()?.Name; var albumName = savedAlbum?.Album?.Name; @@ -70,9 +72,11 @@ namespace NzbDrone.Core.ImportLists.Spotify if (artistName.IsNotNullOrWhiteSpace() && albumName.IsNotNullOrWhiteSpace()) { - return new ImportListItemInfo { + return new SpotifyImportListItemInfo + { Artist = artistName, Album = albumName, + AlbumSpotifyId = savedAlbum?.Album?.Id, ReleaseDate = ParseSpotifyDate(savedAlbum?.Album?.ReleaseDate, savedAlbum?.Album?.ReleaseDatePrecision) }; }