New: Add SpotifySavedTracks import list

This commit is contained in:
eljose47 2026-03-04 16:24:55 +00:00
parent f6a3e73705
commit 3533c49a16
3 changed files with 327 additions and 0 deletions

View file

@ -0,0 +1,222 @@
using System.Collections.Generic;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.ImportLists.Spotify;
using NzbDrone.Core.Test.Framework;
using SpotifyAPI.Web;
using SpotifyAPI.Web.Models;
namespace NzbDrone.Core.Test.ImportListTests
{
[TestFixture]
public class SpotifySavedTracksFixture : CoreTest<SpotifySavedTracks>
{
[Test]
public void should_not_throw_if_saved_tracks_is_null()
{
var paging = default(Paging<SavedTrack>);
Mocker.GetMock<ISpotifyProxy>().
Setup(x => x.GetSavedTracks(It.IsAny<SpotifySavedTracks>(),
It.IsAny<SpotifyWebAPI>()))
.Returns(paging);
var result = Subject.Fetch(null);
result.Should().BeEmpty();
}
[Test]
public void should_not_throw_if_saved_track_items_is_null()
{
var savedTracks = new Paging<SavedTrack>
{
Items = null
};
Mocker.GetMock<ISpotifyProxy>().
Setup(x => x.GetSavedTracks(It.IsAny<SpotifySavedTracks>(),
It.IsAny<SpotifyWebAPI>()))
.Returns(savedTracks);
var result = Subject.Fetch(null);
result.Should().BeEmpty();
}
[Test]
public void should_not_throw_if_saved_track_is_null()
{
var savedTracks = new Paging<SavedTrack>
{
Items = new List<SavedTrack>
{
null
}
};
Mocker.GetMock<ISpotifyProxy>().
Setup(x => x.GetSavedTracks(It.IsAny<SpotifySavedTracks>(),
It.IsAny<SpotifyWebAPI>()))
.Returns(savedTracks);
var result = Subject.Fetch(null);
result.Should().BeEmpty();
}
[Test]
public void should_not_throw_if_saved_track_track_is_null()
{
var savedTracks = new Paging<SavedTrack>
{
Items = new List<SavedTrack>
{
new SavedTrack
{
Track = null
}
}
};
Mocker.GetMock<ISpotifyProxy>().
Setup(x => x.GetSavedTracks(It.IsAny<SpotifySavedTracks>(),
It.IsAny<SpotifyWebAPI>()))
.Returns(savedTracks);
var result = Subject.Fetch(null);
result.Should().BeEmpty();
}
[TestCase("Artist", "Album")]
public void should_parse_saved_track(string artistName, string albumName)
{
var savedTracks = new Paging<SavedTrack>
{
Items = new List<SavedTrack>
{
new SavedTrack
{
AddedAt = System.DateTime.Now,
Track = new FullTrack
{
Album = new SimpleAlbum
{
Name = albumName,
Artists = new List<SimpleArtist>
{
new SimpleArtist
{
Name = artistName
}
}
}
}
}
}
};
Mocker.GetMock<ISpotifyProxy>().
Setup(x => x.GetSavedTracks(It.IsAny<SpotifySavedTracks>(),
It.IsAny<SpotifyWebAPI>()))
.Returns(savedTracks);
var result = Subject.Fetch(null);
result.Should().HaveCount(1);
}
[Test]
public void should_not_throw_if_get_next_page_returns_null()
{
var savedTracks = new Paging<SavedTrack>
{
Items = new List<SavedTrack>
{
new SavedTrack
{
AddedAt = System.DateTime.Now,
Track = new FullTrack
{
Album = new SimpleAlbum
{
Name = "Album",
Artists = new List<SimpleArtist>
{
new SimpleArtist
{
Name = "Artist"
}
}
}
}
}
},
Next = "DummyToMakeHasNextTrue"
};
Mocker.GetMock<ISpotifyProxy>().
Setup(x => x.GetSavedTracks(It.IsAny<SpotifySavedTracks>(),
It.IsAny<SpotifyWebAPI>()))
.Returns(savedTracks);
Mocker.GetMock<ISpotifyProxy>()
.Setup(x => x.GetNextPage(It.IsAny<SpotifyFollowedArtists>(),
It.IsAny<SpotifyWebAPI>(),
It.IsAny<Paging<SavedTrack>>()))
.Returns(default(Paging<SavedTrack>));
var result = Subject.Fetch(null);
result.Should().HaveCount(1);
Mocker.GetMock<ISpotifyProxy>()
.Verify(x => x.GetNextPage(It.IsAny<SpotifySavedTracks>(),
It.IsAny<SpotifyWebAPI>(),
It.IsAny<Paging<SavedTrack>>()),
Times.Once());
}
[TestCase(null, "Album")]
[TestCase("Artist", null)]
[TestCase(null, null)]
public void should_skip_bad_artist_or_album_names(string artistName, string albumName)
{
var savedTracks = new Paging<SavedTrack>
{
Items = new List<SavedTrack>
{
new SavedTrack
{
AddedAt = System.DateTime.Now,
Track = new FullTrack
{
Album = new SimpleAlbum
{
Name = albumName,
Artists = new List<SimpleArtist>
{
new SimpleArtist
{
Name = artistName
}
}
}
}
}
}
};
Mocker.GetMock<ISpotifyProxy>().
Setup(x => x.GetSavedTracks(It.IsAny<SpotifySavedTracks>(),
It.IsAny<SpotifyWebAPI>()))
.Returns(savedTracks);
var result = Subject.Fetch(null);
result.Should().BeEmpty();
}
}
}

View file

@ -18,6 +18,8 @@ Paging<SavedAlbum> GetSavedAlbums<TSettings>(SpotifyImportListBase<TSettings> li
where TSettings : SpotifySettingsBase<TSettings>, new();
Paging<PlaylistTrack> GetPlaylistTracks<TSettings>(SpotifyImportListBase<TSettings> list, SpotifyWebAPI api, string id, string fields)
where TSettings : SpotifySettingsBase<TSettings>, new();
Paging<SavedTrack> GetSavedTracks<TSettings>(SpotifyImportListBase<TSettings> list, SpotifyWebAPI api)
where TSettings : SpotifySettingsBase<TSettings>, new();
Paging<T> GetNextPage<T, TSettings>(SpotifyImportListBase<TSettings> list, SpotifyWebAPI api, Paging<T> item)
where TSettings : SpotifySettingsBase<TSettings>, new();
FollowedArtists GetNextPage<TSettings>(SpotifyImportListBase<TSettings> list, SpotifyWebAPI api, FollowedArtists item)
@ -63,6 +65,12 @@ public Paging<PlaylistTrack> GetPlaylistTracks<TSettings>(SpotifyImportListBase<
return Execute(list, api, x => x.GetPlaylistTracks(id, fields: fields));
}
public Paging<SavedTrack> GetSavedTracks<TSettings>(SpotifyImportListBase<TSettings> list, SpotifyWebAPI api)
where TSettings : SpotifySettingsBase<TSettings>, new()
{
return Execute(list, api, x => x.GetSavedTracks());
}
public Paging<T> GetNextPage<T, TSettings>(SpotifyImportListBase<TSettings> list, SpotifyWebAPI api, Paging<T> item)
where TSettings : SpotifySettingsBase<TSettings>, new()
{

View file

@ -0,0 +1,97 @@
using System.Collections.Generic;
using System.Linq;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.MetadataSource;
using NzbDrone.Core.Parser;
using SpotifyAPI.Web;
using SpotifyAPI.Web.Models;
namespace NzbDrone.Core.ImportLists.Spotify
{
public class SpotifySavedTracksSettings : SpotifySettingsBase<SpotifySavedTracksSettings>
{
public override string Scope => "user-library-read";
}
public class SpotifySavedTracks : SpotifyImportListBase<SpotifySavedTracksSettings>
{
public SpotifySavedTracks(ISpotifyProxy spotifyProxy,
IMetadataRequestBuilder requestBuilder,
IImportListStatusService importListStatusService,
IImportListRepository importListRepository,
IConfigService configService,
IParsingService parsingService,
IHttpClient httpClient,
Logger logger)
: base(spotifyProxy, requestBuilder, importListStatusService, importListRepository, configService, parsingService, httpClient, logger)
{
}
public override string Name => "Spotify Saved Tracks";
public override IList<SpotifyImportListItemInfo> Fetch(SpotifyWebAPI api)
{
var result = new List<SpotifyImportListItemInfo>();
var savedTracks = _spotifyProxy.GetSavedTracks(this, api);
// savedTracks may be null if the spotify proxy returns nothing (e.g. user has no saved tracks)
if (savedTracks == null)
{
_logger.Trace("No saved tracks returned");
return result;
}
_logger.Trace($"Got {savedTracks.Total} saved tracks");
while (true)
{
if (savedTracks?.Items == null)
{
return result;
}
foreach (var savedTrack in savedTracks.Items)
{
result.AddIfNotNull(ParseSavedTrack(savedTrack));
}
if (!savedTracks.HasNextPage())
{
break;
}
savedTracks = _spotifyProxy.GetNextPage(this, api, savedTracks);
}
return result;
}
private SpotifyImportListItemInfo ParseSavedTrack(SavedTrack savedTrack)
{
// From spotify docs: "Note, a track object may be null. This can happen if a track is no longer available."
if (savedTrack?.Track?.Album != null)
{
var album = savedTrack.Track.Album;
var albumName = album.Name;
var artistName = album.Artists?.FirstOrDefault()?.Name ?? savedTrack.Track?.Artists?.FirstOrDefault()?.Name;
if (albumName.IsNotNullOrWhiteSpace() && artistName.IsNotNullOrWhiteSpace())
{
return new SpotifyImportListItemInfo
{
Artist = artistName,
Album = album.Name,
AlbumSpotifyId = album.Id,
ReleaseDate = ParseSpotifyDate(album.ReleaseDate, album.ReleaseDatePrecision)
};
}
}
return null;
}
}
}