mirror of
https://github.com/lidarr/lidarr.git
synced 2025-08-20 13:33:34 -07:00
New: Spotify integration
Import playlists, followed artists and saved albums
This commit is contained in:
parent
2f1290d488
commit
d075ea3625
18 changed files with 892 additions and 1 deletions
|
@ -36,7 +36,8 @@ namespace NzbDrone.Core.Annotations
|
|||
Url,
|
||||
Captcha,
|
||||
OAuth,
|
||||
Device
|
||||
Device,
|
||||
Playlist
|
||||
}
|
||||
|
||||
public enum HiddenType
|
||||
|
|
|
@ -6,6 +6,7 @@ namespace NzbDrone.Core.ImportLists
|
|||
{
|
||||
public interface IImportListRepository : IProviderRepository<ImportListDefinition>
|
||||
{
|
||||
void UpdateSettings(ImportListDefinition model);
|
||||
}
|
||||
|
||||
public class ImportListRepository : ProviderRepository<ImportListDefinition>, IImportListRepository
|
||||
|
@ -14,5 +15,10 @@ namespace NzbDrone.Core.ImportLists
|
|||
: base(database, eventAggregator)
|
||||
{
|
||||
}
|
||||
|
||||
public void UpdateSettings(ImportListDefinition model)
|
||||
{
|
||||
SetFields(model, m => m.Settings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
27
src/NzbDrone.Core/ImportLists/Spotify/SpotifyException.cs
Normal file
27
src/NzbDrone.Core/ImportLists/Spotify/SpotifyException.cs
Normal file
|
@ -0,0 +1,27 @@
|
|||
using System;
|
||||
using NzbDrone.Common.Exceptions;
|
||||
|
||||
namespace NzbDrone.Core.ImportLists.Spotify
|
||||
{
|
||||
public class SpotifyException : NzbDroneException
|
||||
{
|
||||
public SpotifyException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public SpotifyException(string message, params object[] args) : base(message, args)
|
||||
{
|
||||
}
|
||||
|
||||
public SpotifyException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class SpotifyAuthorizationException : SpotifyException
|
||||
{
|
||||
public SpotifyAuthorizationException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
using System.Collections.Generic;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using SpotifyAPI.Web;
|
||||
using SpotifyAPI.Web.Enums;
|
||||
|
||||
namespace NzbDrone.Core.ImportLists.Spotify
|
||||
{
|
||||
public class SpotifyFollowedArtistsSettings : SpotifySettingsBase<SpotifyFollowedArtistsSettings>
|
||||
{
|
||||
public override string Scope => "user-follow-read";
|
||||
}
|
||||
|
||||
public class SpotifyFollowedArtists : SpotifyImportListBase<SpotifyFollowedArtistsSettings>
|
||||
{
|
||||
public SpotifyFollowedArtists(IImportListStatusService importListStatusService,
|
||||
IImportListRepository importListRepository,
|
||||
IConfigService configService,
|
||||
IParsingService parsingService,
|
||||
HttpClient httpClient,
|
||||
Logger logger)
|
||||
: base(importListStatusService, importListRepository, configService, parsingService, httpClient, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public override string Name => "Spotify Followed Artists";
|
||||
|
||||
public override IList<ImportListItemInfo> Fetch(SpotifyWebAPI api)
|
||||
{
|
||||
var result = new List<ImportListItemInfo>();
|
||||
|
||||
var followed = Execute(api, (x) => x.GetFollowedArtists(FollowType.Artist, 50));
|
||||
var artists = followed.Artists;
|
||||
while (true)
|
||||
{
|
||||
foreach (var artist in artists.Items)
|
||||
{
|
||||
if (artist.Name.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
result.AddIfNotNull(new ImportListItemInfo
|
||||
{
|
||||
Artist = artist.Name,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!artists.HasNext())
|
||||
break;
|
||||
artists = Execute(api, (x) => x.GetNextPage(artists));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
210
src/NzbDrone.Core/ImportLists/Spotify/SpotifyImportListBase.cs
Normal file
210
src/NzbDrone.Core/ImportLists/Spotify/SpotifyImportListBase.cs
Normal file
|
@ -0,0 +1,210 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using FluentValidation.Results;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Validation;
|
||||
using SpotifyAPI.Web;
|
||||
using SpotifyAPI.Web.Models;
|
||||
|
||||
namespace NzbDrone.Core.ImportLists.Spotify
|
||||
{
|
||||
public abstract class SpotifyImportListBase<TSettings> : ImportListBase<TSettings>
|
||||
where TSettings : SpotifySettingsBase<TSettings>, new()
|
||||
{
|
||||
private IHttpClient _httpClient;
|
||||
private IImportListRepository _importListRepository;
|
||||
|
||||
public SpotifyImportListBase(IImportListStatusService importListStatusService,
|
||||
IImportListRepository importListRepository,
|
||||
IConfigService configService,
|
||||
IParsingService parsingService,
|
||||
HttpClient httpClient,
|
||||
Logger logger)
|
||||
: base(importListStatusService, configService, parsingService, logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_importListRepository = importListRepository;
|
||||
}
|
||||
|
||||
private void RefreshToken()
|
||||
{
|
||||
_logger.Trace("Refreshing Token");
|
||||
|
||||
Settings.Validate().Filter("RefreshToken").ThrowOnError();
|
||||
|
||||
var request = new HttpRequestBuilder(Settings.RenewUri)
|
||||
.AddQueryParam("refresh_token", Settings.RefreshToken)
|
||||
.Build();
|
||||
|
||||
try
|
||||
{
|
||||
var response = _httpClient.Get<Token>(request);
|
||||
|
||||
if (response != null && response.Resource != null)
|
||||
{
|
||||
var token = response.Resource;
|
||||
Settings.AccessToken = token.AccessToken;
|
||||
Settings.Expires = DateTime.UtcNow.AddSeconds(token.ExpiresIn);
|
||||
Settings.RefreshToken = token.RefreshToken != null ? token.RefreshToken : Settings.RefreshToken;
|
||||
|
||||
if (Definition.Id > 0)
|
||||
{
|
||||
_importListRepository.UpdateSettings((ImportListDefinition)Definition);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (HttpException)
|
||||
{
|
||||
_logger.Warn($"Error refreshing spotify access token");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected SpotifyWebAPI GetApi()
|
||||
{
|
||||
Settings.Validate().Filter("AccessToken", "RefreshToken").ThrowOnError();
|
||||
_logger.Trace($"Access token expires at {Settings.Expires}");
|
||||
|
||||
if (Settings.Expires < DateTime.UtcNow.AddMinutes(5))
|
||||
{
|
||||
RefreshToken();
|
||||
}
|
||||
|
||||
return new SpotifyWebAPI
|
||||
{
|
||||
AccessToken = Settings.AccessToken,
|
||||
TokenType = "Bearer"
|
||||
};
|
||||
}
|
||||
|
||||
protected T Execute<T>(SpotifyWebAPI api, Func<SpotifyWebAPI, T> method, bool allowReauth = true) where T : BasicModel
|
||||
{
|
||||
T result = method(api);
|
||||
if (result.HasError())
|
||||
{
|
||||
// If unauthorized, refresh token and try again
|
||||
if (result.Error.Status == 401)
|
||||
{
|
||||
if (allowReauth)
|
||||
{
|
||||
_logger.Debug("Spotify authorization error, refreshing token and retrying");
|
||||
RefreshToken();
|
||||
api.AccessToken = Settings.AccessToken;
|
||||
return Execute(api, method, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new SpotifyAuthorizationException(result.Error.Message);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new SpotifyException("[{0}] {1}", result.Error.Status, result.Error.Message);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public override IList<ImportListItemInfo> Fetch()
|
||||
{
|
||||
using (var api = GetApi())
|
||||
{
|
||||
_logger.Debug("Starting spotify import list sync");
|
||||
var releases = Fetch(api);
|
||||
return CleanupListItems(releases);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract IList<ImportListItemInfo> Fetch(SpotifyWebAPI api);
|
||||
|
||||
protected DateTime ParseSpotifyDate(string date, string precision)
|
||||
{
|
||||
if (date.IsNullOrWhiteSpace() || precision.IsNullOrWhiteSpace())
|
||||
{
|
||||
return default(DateTime);
|
||||
}
|
||||
|
||||
string format;
|
||||
|
||||
switch (precision) {
|
||||
case "year":
|
||||
format = "yyyy";
|
||||
break;
|
||||
case "month":
|
||||
format = "yyyy-MM";
|
||||
break;
|
||||
case "day":
|
||||
default:
|
||||
format = "yyyy-MM-dd";
|
||||
break;
|
||||
}
|
||||
|
||||
return DateTime.TryParseExact(date, format, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime result) ? result : default(DateTime);
|
||||
}
|
||||
|
||||
protected override void Test(List<ValidationFailure> failures)
|
||||
{
|
||||
failures.AddIfNotNull(TestConnection());
|
||||
}
|
||||
|
||||
private ValidationFailure TestConnection()
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var api = GetApi())
|
||||
{
|
||||
var profile = Execute(api, (x) => x.GetPrivateProfile());
|
||||
_logger.Debug($"Connected to spotify profile {profile.DisplayName} [{profile.Id}]");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
catch (SpotifyAuthorizationException ex)
|
||||
{
|
||||
_logger.Warn(ex, "Spotify Authentication Error");
|
||||
return new ValidationFailure(string.Empty, $"Spotify authentication error: {ex.Message}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn(ex, "Unable to connect to Spotify");
|
||||
|
||||
return new ValidationFailure(string.Empty, "Unable to connect to import list, check the log for more details");
|
||||
}
|
||||
}
|
||||
|
||||
public override object RequestAction(string action, IDictionary<string, string> query)
|
||||
{
|
||||
if (action == "startOAuth")
|
||||
{
|
||||
var request = new HttpRequestBuilder(Settings.OAuthUrl)
|
||||
.AddQueryParam("client_id", Settings.ClientId)
|
||||
.AddQueryParam("response_type", "code")
|
||||
.AddQueryParam("redirect_uri", Settings.RedirectUri)
|
||||
.AddQueryParam("scope", Settings.Scope)
|
||||
.AddQueryParam("state", query["callbackUrl"])
|
||||
.AddQueryParam("show_dialog", true)
|
||||
.Build();
|
||||
|
||||
return new {
|
||||
OauthUrl = request.Url.ToString()
|
||||
};
|
||||
}
|
||||
else if (action == "getOAuthToken")
|
||||
{
|
||||
return new {
|
||||
accessToken = query["access_token"],
|
||||
expires = DateTime.UtcNow.AddSeconds(int.Parse(query["expires_in"])),
|
||||
refreshToken = query["refresh_token"],
|
||||
};
|
||||
}
|
||||
|
||||
return new { };
|
||||
}
|
||||
}
|
||||
}
|
130
src/NzbDrone.Core/ImportLists/Spotify/SpotifyPlaylist.cs
Normal file
130
src/NzbDrone.Core/ImportLists/Spotify/SpotifyPlaylist.cs
Normal file
|
@ -0,0 +1,130 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Validation;
|
||||
using SpotifyAPI.Web;
|
||||
using SpotifyAPI.Web.Models;
|
||||
|
||||
namespace NzbDrone.Core.ImportLists.Spotify
|
||||
{
|
||||
public class SpotifyPlaylist : SpotifyImportListBase<SpotifyPlaylistSettings>
|
||||
{
|
||||
public SpotifyPlaylist(IImportListStatusService importListStatusService,
|
||||
IImportListRepository importListRepository,
|
||||
IConfigService configService,
|
||||
IParsingService parsingService,
|
||||
HttpClient httpClient,
|
||||
Logger logger)
|
||||
: base(importListStatusService, importListRepository, configService, parsingService, httpClient, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public override string Name => "Spotify Playlists";
|
||||
|
||||
public override IList<ImportListItemInfo> Fetch(SpotifyWebAPI api)
|
||||
{
|
||||
var result = new List<ImportListItemInfo>();
|
||||
|
||||
foreach (var id in Settings.PlaylistIds)
|
||||
{
|
||||
_logger.Trace($"Processing playlist {id}");
|
||||
|
||||
var playlistTracks = Execute(api, (x) => x.GetPlaylistTracks(id, fields: "next, items(track(name, album(name,artists)))"));
|
||||
while (true)
|
||||
{
|
||||
foreach (var track in playlistTracks.Items)
|
||||
{
|
||||
var fullTrack = track.Track;
|
||||
// From spotify docs: "Note, a track object may be null. This can happen if a track is no longer available."
|
||||
if (fullTrack != null)
|
||||
{
|
||||
var album = fullTrack.Album?.Name;
|
||||
var artist = fullTrack.Album?.Artists?.FirstOrDefault()?.Name ?? fullTrack.Artists.FirstOrDefault()?.Name;
|
||||
|
||||
if (album.IsNotNullOrWhiteSpace() && artist.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
result.AddIfNotNull(new ImportListItemInfo
|
||||
{
|
||||
Artist = artist,
|
||||
Album = album,
|
||||
ReleaseDate = ParseSpotifyDate(fullTrack.Album.ReleaseDate, fullTrack.Album.ReleaseDatePrecision)
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!playlistTracks.HasNextPage())
|
||||
break;
|
||||
playlistTracks = Execute(api, (x) => x.GetNextPage(playlistTracks));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public override object RequestAction(string action, IDictionary<string, string> query)
|
||||
{
|
||||
if (action == "getPlaylists")
|
||||
{
|
||||
if (Settings.AccessToken.IsNullOrWhiteSpace())
|
||||
{
|
||||
return new
|
||||
{
|
||||
playlists = new List<object>()
|
||||
};
|
||||
}
|
||||
|
||||
Settings.Validate().Filter("AccessToken").ThrowOnError();
|
||||
|
||||
using (var api = GetApi())
|
||||
{
|
||||
try
|
||||
{
|
||||
var profile = Execute(api, (x) => x.GetPrivateProfile());
|
||||
var playlistPage = Execute(api, (x) => x.GetUserPlaylists(profile.Id));
|
||||
_logger.Trace($"Got {playlistPage.Total} playlists");
|
||||
|
||||
var playlists = new List<SimplePlaylist>(playlistPage.Total);
|
||||
while (true)
|
||||
{
|
||||
playlists.AddRange(playlistPage.Items);
|
||||
|
||||
if (!playlistPage.HasNextPage())
|
||||
break;
|
||||
playlistPage = Execute(api, (x) => x.GetNextPage(playlistPage));
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
options = new {
|
||||
user = profile.DisplayName,
|
||||
playlists = playlists.OrderBy(p => p.Name)
|
||||
.Select(p => new
|
||||
{
|
||||
id = p.Id,
|
||||
name = p.Name
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn(ex, "Error fetching playlists from Spotify");
|
||||
return new { };
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return base.RequestAction(action, query);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
using System.Collections.Generic;
|
||||
using FluentValidation;
|
||||
using NzbDrone.Core.Annotations;
|
||||
|
||||
namespace NzbDrone.Core.ImportLists.Spotify
|
||||
{
|
||||
public class SpotifyPlaylistSettingsValidator : SpotifySettingsBaseValidator<SpotifyPlaylistSettings>
|
||||
{
|
||||
public SpotifyPlaylistSettingsValidator()
|
||||
: base()
|
||||
{
|
||||
RuleFor(c => c.PlaylistIds).NotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public class SpotifyPlaylistSettings : SpotifySettingsBase<SpotifyPlaylistSettings>
|
||||
{
|
||||
protected override AbstractValidator<SpotifyPlaylistSettings> Validator => new SpotifyPlaylistSettingsValidator();
|
||||
|
||||
public SpotifyPlaylistSettings()
|
||||
{
|
||||
PlaylistIds = new string[] { };
|
||||
}
|
||||
|
||||
public override string Scope => "playlist-read-private";
|
||||
|
||||
[FieldDefinition(1, Label = "Playlists", Type = FieldType.Playlist)]
|
||||
public IEnumerable<string> PlaylistIds { get; set; }
|
||||
}
|
||||
}
|
64
src/NzbDrone.Core/ImportLists/Spotify/SpotifySavedAlbums.cs
Normal file
64
src/NzbDrone.Core/ImportLists/Spotify/SpotifySavedAlbums.cs
Normal file
|
@ -0,0 +1,64 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using SpotifyAPI.Web;
|
||||
|
||||
namespace NzbDrone.Core.ImportLists.Spotify
|
||||
{
|
||||
public class SpotifySavedAlbumsSettings : SpotifySettingsBase<SpotifySavedAlbumsSettings>
|
||||
{
|
||||
public override string Scope => "user-library-read";
|
||||
}
|
||||
|
||||
public class SpotifySavedAlbums : SpotifyImportListBase<SpotifySavedAlbumsSettings>
|
||||
{
|
||||
public SpotifySavedAlbums(IImportListStatusService importListStatusService,
|
||||
IImportListRepository importListRepository,
|
||||
IConfigService configService,
|
||||
IParsingService parsingService,
|
||||
HttpClient httpClient,
|
||||
Logger logger)
|
||||
: base(importListStatusService, importListRepository, configService, parsingService, httpClient, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public override string Name => "Spotify Saved Albums";
|
||||
|
||||
public override IList<ImportListItemInfo> Fetch(SpotifyWebAPI api)
|
||||
{
|
||||
var result = new List<ImportListItemInfo>();
|
||||
|
||||
var albums = Execute(api, (x) => x.GetSavedAlbums(50));
|
||||
_logger.Trace($"Got {albums.Total} saved albums");
|
||||
|
||||
while (true)
|
||||
{
|
||||
foreach (var album in albums.Items)
|
||||
{
|
||||
var artistName = album.Album.Artists.FirstOrDefault()?.Name;
|
||||
var albumName = album.Album.Name;
|
||||
_logger.Trace($"Adding {artistName} - {albumName}");
|
||||
|
||||
if (artistName.IsNotNullOrWhiteSpace() && albumName.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
result.AddIfNotNull(new ImportListItemInfo
|
||||
{
|
||||
Artist = artistName,
|
||||
Album = albumName
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!albums.HasNextPage())
|
||||
break;
|
||||
albums = Execute(api, (x) => x.GetNextPage(albums));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
55
src/NzbDrone.Core/ImportLists/Spotify/SpotifySettingsBase.cs
Normal file
55
src/NzbDrone.Core/ImportLists/Spotify/SpotifySettingsBase.cs
Normal file
|
@ -0,0 +1,55 @@
|
|||
using System;
|
||||
using FluentValidation;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.ImportLists.Spotify
|
||||
{
|
||||
public class SpotifySettingsBaseValidator<TSettings> : AbstractValidator<TSettings>
|
||||
where TSettings : SpotifySettingsBase<TSettings>
|
||||
{
|
||||
public SpotifySettingsBaseValidator()
|
||||
{
|
||||
RuleFor(c => c.AccessToken).NotEmpty();
|
||||
RuleFor(c => c.RefreshToken).NotEmpty();
|
||||
RuleFor(c => c.Expires).NotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public class SpotifySettingsBase<TSettings> : IImportListSettings
|
||||
where TSettings : SpotifySettingsBase<TSettings>
|
||||
{
|
||||
protected virtual AbstractValidator<TSettings> Validator => new SpotifySettingsBaseValidator<TSettings>();
|
||||
|
||||
public SpotifySettingsBase()
|
||||
{
|
||||
BaseUrl = "https://api.spotify.com/v1";
|
||||
SignIn = "startOAuth";
|
||||
}
|
||||
|
||||
public string BaseUrl { get; set; }
|
||||
|
||||
public string OAuthUrl => "https://accounts.spotify.com/authorize";
|
||||
public string RedirectUri => "https://spotify.lidarr.audio/auth";
|
||||
public string RenewUri => "https://spotify.lidarr.audio/renew";
|
||||
public string ClientId => "848082790c32436d8a0405fddca0aa18";
|
||||
public virtual string Scope => "";
|
||||
|
||||
[FieldDefinition(0, Label = "Access Token", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)]
|
||||
public string AccessToken { get; set; }
|
||||
|
||||
[FieldDefinition(0, Label = "Refresh Token", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)]
|
||||
public string RefreshToken { get; set; }
|
||||
|
||||
[FieldDefinition(0, Label = "Expires", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)]
|
||||
public DateTime Expires { get; set; }
|
||||
|
||||
[FieldDefinition(99, Label = "Authenticate with Spotify", Type = FieldType.OAuth)]
|
||||
public string SignIn { get; set; }
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate((TSettings)this));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -289,6 +289,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
|
|||
artist.Metadata = MapArtistMetadata(resource.Artists.Single(x => x.Id == resource.ArtistId));
|
||||
}
|
||||
album.Artist = artist;
|
||||
album.ArtistMetadata = artist.Metadata;
|
||||
|
||||
return album;
|
||||
}
|
||||
|
|
|
@ -569,6 +569,13 @@
|
|||
<Compile Include="ImportLists\LidarrLists\LidarrListsParser.cs" />
|
||||
<Compile Include="ImportLists\LidarrLists\LidarrListsRequestGenerator.cs" />
|
||||
<Compile Include="ImportLists\LidarrLists\LidarrListsSettings.cs" />
|
||||
<Compile Include="ImportLists\Spotify\SpotifyException.cs" />
|
||||
<Compile Include="ImportLists\Spotify\SpotifySettingsBase.cs" />
|
||||
<Compile Include="ImportLists\Spotify\SpotifyImportListBase.cs" />
|
||||
<Compile Include="ImportLists\Spotify\SpotifyPlaylistSettings.cs" />
|
||||
<Compile Include="ImportLists\Spotify\SpotifyPlaylist.cs" />
|
||||
<Compile Include="ImportLists\Spotify\SpotifyFollowedArtists.cs" />
|
||||
<Compile Include="ImportLists\Spotify\SpotifySavedAlbums.cs" />
|
||||
<Compile Include="IndexerSearch\AlbumSearchCommand.cs" />
|
||||
<Compile Include="IndexerSearch\AlbumSearchService.cs" />
|
||||
<Compile Include="IndexerSearch\ArtistSearchCommand.cs" />
|
||||
|
@ -1333,6 +1340,7 @@
|
|||
<PackageReference Include="xmlrpcnet">
|
||||
<Version>2.5.0</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="SpotifyAPI.Web" Version="4.2.0" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<PropertyGroup>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue