New: Readarr 0.1

This commit is contained in:
ta264
2020-05-06 21:14:11 +01:00
parent 476f2d6047
commit 08496c82af
911 changed files with 14837 additions and 24442 deletions
@@ -79,7 +79,7 @@ namespace NzbDrone.Core.ImportLists.Exclusions
return;
}
var existingExclusion = _repo.FindByForeignId(message.Artist.ForeignArtistId);
var existingExclusion = _repo.FindByForeignId(message.Artist.ForeignAuthorId);
if (existingExclusion != null)
{
@@ -88,7 +88,7 @@ namespace NzbDrone.Core.ImportLists.Exclusions
var importExclusion = new ImportListExclusion
{
ForeignId = message.Artist.ForeignArtistId,
ForeignId = message.Artist.ForeignAuthorId,
Name = message.Artist.Name
};
@@ -102,7 +102,7 @@ namespace NzbDrone.Core.ImportLists.Exclusions
return;
}
var existingExclusion = _repo.FindByForeignId(message.Album.ForeignAlbumId);
var existingExclusion = _repo.FindByForeignId(message.Album.ForeignBookId);
if (existingExclusion != null)
{
@@ -111,8 +111,8 @@ namespace NzbDrone.Core.ImportLists.Exclusions
var importExclusion = new ImportListExclusion
{
ForeignId = message.Album.ForeignAlbumId,
Name = $"{message.Album.ArtistMetadata.Value.Name} - {message.Album.Title}"
ForeignId = message.Album.ForeignBookId,
Name = $"{message.Album.AuthorMetadata.Value.Name} - {message.Album.Title}"
};
_repo.Insert(importExclusion);
@@ -0,0 +1,149 @@
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.MetadataSource.Goodreads;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.ImportLists.Goodreads
{
public class GoodreadsBookshelf : GoodreadsImportListBase<GoodreadsBookshelfSettings>
{
public GoodreadsBookshelf(IImportListStatusService importListStatusService,
IConfigService configService,
IParsingService parsingService,
IHttpClient httpClient,
Logger logger)
: base(importListStatusService, configService, parsingService, httpClient, logger)
{
}
public override string Name => "Goodreads Bookshelves";
public override IList<ImportListItemInfo> Fetch()
{
return CleanupListItems(Settings.PlaylistIds.SelectMany(x => Fetch(x)).ToList());
}
public IList<ImportListItemInfo> Fetch(string shelf)
{
var reviews = new List<ReviewResource>();
var page = 0;
while (true)
{
var curr = GetReviews(shelf, ++page);
if (curr == null || curr.Count == 0)
{
break;
}
reviews.AddRange(curr);
}
return reviews.Select(x => new ImportListItemInfo
{
Artist = x.Book.Authors.First().Name.CleanSpaces(),
Album = x.Book.TitleWithoutSeries.CleanSpaces(),
AlbumMusicBrainzId = x.Book.Uri.Replace("kca://book/", string.Empty)
}).ToList();
}
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();
var shelves = new List<UserShelfResource>();
var page = 0;
while (true)
{
var curr = GetShelfList(++page);
if (curr == null || curr.Count == 0)
{
break;
}
shelves.AddRange(curr);
}
return new
{
options = new
{
user = Settings.UserName,
playlists = shelves.OrderBy(p => p.Name)
.Select(p => new
{
id = p.Name,
name = p.Name
})
}
};
}
else
{
return base.RequestAction(action, query);
}
}
private IReadOnlyList<UserShelfResource> GetShelfList(int page)
{
try
{
var builder = RequestBuilder()
.SetSegment("route", $"shelf/list.xml")
.AddQueryParam("user_id", Settings.UserId)
.AddQueryParam("page", page);
var httpResponse = OAuthGet(builder);
return httpResponse.Deserialize<PaginatedList<UserShelfResource>>("shelves").List;
}
catch (Exception ex)
{
_logger.Warn(ex, "Error fetching bookshelves from Goodreads");
return new List<UserShelfResource>();
}
}
private IReadOnlyList<ReviewResource> GetReviews(string shelf, int page)
{
try
{
var builder = RequestBuilder()
.SetSegment("route", $"review/list.xml")
.AddQueryParam("v", 2)
.AddQueryParam("id", Settings.UserId)
.AddQueryParam("shelf", shelf)
.AddQueryParam("per_page", 200)
.AddQueryParam("page", page);
var httpResponse = OAuthGet(builder);
return httpResponse.Deserialize<PaginatedList<ReviewResource>>("reviews").List;
}
catch (Exception ex)
{
_logger.Warn(ex, "Error fetching bookshelves from Goodreads");
return new List<ReviewResource>();
}
}
}
}
@@ -0,0 +1,28 @@
using System.Collections.Generic;
using FluentValidation;
using NzbDrone.Core.Annotations;
namespace NzbDrone.Core.ImportLists.Goodreads
{
public class GoodreadsBookshelfSettingsValidator : GoodreadsSettingsBaseValidator<GoodreadsBookshelfSettings>
{
public GoodreadsBookshelfSettingsValidator()
: base()
{
RuleFor(c => c.PlaylistIds).NotEmpty();
}
}
public class GoodreadsBookshelfSettings : GoodreadsSettingsBase<GoodreadsBookshelfSettings>
{
public GoodreadsBookshelfSettings()
{
PlaylistIds = new string[] { };
}
[FieldDefinition(1, Label = "Bookshelves", Type = FieldType.Playlist)]
public IEnumerable<string> PlaylistIds { get; set; }
protected override AbstractValidator<GoodreadsBookshelfSettings> Validator => new GoodreadsBookshelfSettingsValidator();
}
}
@@ -0,0 +1,31 @@
using System;
using NzbDrone.Common.Exceptions;
namespace NzbDrone.Core.ImportLists.Goodreads
{
public class GoodreadsException : NzbDroneException
{
public GoodreadsException(string message)
: base(message)
{
}
public GoodreadsException(string message, params object[] args)
: base(message, args)
{
}
public GoodreadsException(string message, Exception innerException)
: base(message, innerException)
{
}
}
public class GoodreadsAuthorizationException : GoodreadsException
{
public GoodreadsAuthorizationException(string message)
: base(message)
{
}
}
}
@@ -0,0 +1,172 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Web;
using System.Xml.Linq;
using System.Xml.XPath;
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Common.OAuth;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Exceptions;
using NzbDrone.Core.MetadataSource.Goodreads;
using NzbDrone.Core.Parser;
namespace NzbDrone.Core.ImportLists.Goodreads
{
public abstract class GoodreadsImportListBase<TSettings> : ImportListBase<TSettings>
where TSettings : GoodreadsSettingsBase<TSettings>, new()
{
protected readonly IHttpClient _httpClient;
protected GoodreadsImportListBase(IImportListStatusService importListStatusService,
IConfigService configService,
IParsingService parsingService,
IHttpClient httpClient,
Logger logger)
: base(importListStatusService, configService, parsingService, logger)
{
_httpClient = httpClient;
}
public override ImportListType ListType => ImportListType.Goodreads;
public string AccessToken => Settings.AccessToken;
protected HttpRequestBuilder RequestBuilder() => new HttpRequestBuilder("https://www.goodreads.com/{route}")
.AddQueryParam("key", "xQh8LhdTztb9u3cL26RqVg", true)
.AddQueryParam("_nc", "1")
.KeepAlive();
protected override void Test(List<ValidationFailure> failures)
{
failures.AddIfNotNull(TestConnection());
}
private ValidationFailure TestConnection()
{
try
{
GetUser();
return null;
}
catch (Common.Http.HttpException ex)
{
_logger.Warn(ex, "Goodreads Authentication Error");
return new ValidationFailure(string.Empty, $"Goodreads authentication error: {ex.Message}");
}
catch (Exception ex)
{
_logger.Warn(ex, "Unable to connect to Goodreads");
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")
{
if (query["callbackUrl"].IsNullOrWhiteSpace())
{
throw new BadRequestException("QueryParam callbackUrl invalid.");
}
var oAuthRequest = OAuthRequest.ForRequestToken(Settings.ConsumerKey, Settings.ConsumerSecret, query["callbackUrl"]);
oAuthRequest.RequestUrl = Settings.OAuthRequestTokenUrl;
var qscoll = OAuthQuery(oAuthRequest);
var url = string.Format("{0}?oauth_token={1}&oauth_callback={2}", Settings.OAuthUrl, qscoll["oauth_token"], query["callbackUrl"]);
return new
{
OauthUrl = url,
RequestTokenSecret = qscoll["oauth_token_secret"]
};
}
else if (action == "getOAuthToken")
{
if (query["oauth_token"].IsNullOrWhiteSpace())
{
throw new BadRequestException("QueryParam oauth_token invalid.");
}
if (query["requestTokenSecret"].IsNullOrWhiteSpace())
{
throw new BadRequestException("Missing requestTokenSecret.");
}
var oAuthRequest = OAuthRequest.ForAccessToken(Settings.ConsumerKey, Settings.ConsumerSecret, query["oauth_token"], query["requestTokenSecret"], "");
oAuthRequest.RequestUrl = Settings.OAuthAccessTokenUrl;
var qscoll = OAuthQuery(oAuthRequest);
Settings.AccessToken = qscoll["oauth_token"];
Settings.AccessTokenSecret = qscoll["oauth_token_secret"];
var user = GetUser();
return new
{
Settings.AccessToken,
Settings.AccessTokenSecret,
RequestTokenSecret = "",
UserId = user.Item1,
UserName = user.Item2
};
}
return new { };
}
protected Common.Http.HttpResponse OAuthGet(HttpRequestBuilder builder)
{
var auth = OAuthRequest.ForProtectedResource(builder.Method.ToString(), Settings.ConsumerKey, Settings.ConsumerSecret, Settings.AccessToken, Settings.AccessTokenSecret);
var request = builder.Build();
request.LogResponseContent = true;
// we need the url without the query to sign
auth.RequestUrl = request.Url.SetQuery(null).FullUri;
var header = auth.GetAuthorizationHeader(builder.QueryParams.ToDictionary(x => x.Key, x => x.Value));
request.Headers.Add("Authorization", header);
return _httpClient.Get(request);
}
private NameValueCollection OAuthQuery(OAuthRequest oAuthRequest)
{
var auth = oAuthRequest.GetAuthorizationHeader();
var request = new Common.Http.HttpRequest(oAuthRequest.RequestUrl);
request.Headers.Add("Authorization", auth);
var response = _httpClient.Get(request);
return HttpUtility.ParseQueryString(response.Content);
}
private Tuple<string, string> GetUser()
{
var builder = RequestBuilder()
.SetSegment("route", $"api/auth_user")
.AddQueryParam("key", Settings.ConsumerKey, true);
var httpResponse = OAuthGet(builder);
string userId = null;
string userName = null;
var content = httpResponse.Content;
if (!string.IsNullOrWhiteSpace(content))
{
var user = XDocument.Parse(content).XPathSelectElement("GoodreadsResponse/user");
userId = user.AttributeAsString("id");
userName = user.ElementAsString("name");
}
return Tuple.Create(userId, userName);
}
}
}
@@ -0,0 +1,82 @@
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.MetadataSource;
using NzbDrone.Core.MetadataSource.Goodreads;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.ImportLists.Goodreads
{
public class GoodreadsOwnedBooksSettings : GoodreadsSettingsBase<GoodreadsOwnedBooksSettings>
{
}
public class GoodreadsOwnedBooks : GoodreadsImportListBase<GoodreadsOwnedBooksSettings>
{
public GoodreadsOwnedBooks(IImportListStatusService importListStatusService,
IConfigService configService,
IParsingService parsingService,
IHttpClient httpClient,
Logger logger)
: base(importListStatusService, configService, parsingService, httpClient, logger)
{
}
public override string Name => "Goodreads Owned Books";
public override IList<ImportListItemInfo> Fetch()
{
var reviews = new List<OwnedBookResource>();
var page = 0;
while (true)
{
var curr = GetOwned(++page);
if (curr == null || curr.Count == 0)
{
break;
}
reviews.AddRange(curr);
}
var result = reviews.Select(x => new ImportListItemInfo
{
Artist = x.Book.Authors.First().Name.CleanSpaces(),
ArtistMusicBrainzId = x.Book.Authors.First().Id.ToString(),
Album = x.Book.TitleWithoutSeries.CleanSpaces(),
AlbumMusicBrainzId = x.Book.Id.ToString()
}).ToList();
return CleanupListItems(result);
}
private IReadOnlyList<OwnedBookResource> GetOwned(int page)
{
try
{
var builder = RequestBuilder()
.SetSegment("route", $"owned_books/user")
.AddQueryParam("id", Settings.UserId)
.AddQueryParam("page", page);
var httpResponse = OAuthGet(builder);
_logger.Trace("Got:\n{0}", httpResponse.Content);
return httpResponse.Deserialize<PaginatedList<OwnedBookResource>>("reviews").List;
}
catch (Exception ex)
{
_logger.Warn(ex, "Error fetching bookshelves from Goodreads");
return new List<OwnedBookResource>();
}
}
}
}
@@ -0,0 +1,58 @@
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.ImportLists.Goodreads
{
public class GoodreadsSettingsBaseValidator<TSettings> : AbstractValidator<TSettings>
where TSettings : GoodreadsSettingsBase<TSettings>
{
public GoodreadsSettingsBaseValidator()
{
RuleFor(c => c.AccessToken).NotEmpty();
RuleFor(c => c.AccessTokenSecret).NotEmpty();
}
}
public class GoodreadsSettingsBase<TSettings> : IImportListSettings
where TSettings : GoodreadsSettingsBase<TSettings>
{
public GoodreadsSettingsBase()
{
SignIn = "startOAuth";
}
public string BaseUrl { get; set; }
public string ConsumerKey => "xQh8LhdTztb9u3cL26RqVg";
public string ConsumerSecret => "96aDA1lJRcS8KofYbw2jjkRk3wTNKypHAL2GeOgbPZw";
public string OAuthUrl => "https://www.goodreads.com/oauth/authorize";
public string OAuthRequestTokenUrl => "https://www.goodreads.com/oauth/request_token";
public string OAuthAccessTokenUrl => "https://www.goodreads.com/oauth/access_token";
[FieldDefinition(0, Label = "Access Token", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)]
public string AccessToken { get; set; }
[FieldDefinition(0, Label = "Access Token Secret", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)]
public string AccessTokenSecret { get; set; }
[FieldDefinition(0, Label = "Request Token Secret", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)]
public string RequestTokenSecret { get; set; }
[FieldDefinition(0, Label = "User Id", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)]
public string UserId { get; set; }
[FieldDefinition(0, Label = "User Name", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)]
public string UserName { get; set; }
[FieldDefinition(99, Label = "Authenticate with Goodreads", Type = FieldType.OAuth)]
public string SignIn { get; set; }
protected virtual AbstractValidator<TSettings> Validator => new GoodreadsSettingsBaseValidator<TSettings>();
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate((TSettings)this));
}
}
}
@@ -1,8 +0,0 @@
namespace NzbDrone.Core.ImportLists.HeadphonesImport
{
public class HeadphonesImportArtist
{
public string ArtistName { get; set; }
public string ArtistId { get; set; }
}
}
@@ -1,33 +0,0 @@
using System.Collections.Generic;
using NzbDrone.Common.Http;
namespace NzbDrone.Core.ImportLists.HeadphonesImport
{
public class HeadphonesImportRequestGenerator : IImportListRequestGenerator
{
public HeadphonesImportSettings Settings { get; set; }
public int MaxPages { get; set; }
public int PageSize { get; set; }
public HeadphonesImportRequestGenerator()
{
MaxPages = 1;
PageSize = 1000;
}
public virtual ImportListPageableRequestChain GetListItems()
{
var pageableRequests = new ImportListPageableRequestChain();
pageableRequests.Add(GetPagedRequests());
return pageableRequests;
}
private IEnumerable<ImportListRequest> GetPagedRequests()
{
yield return new ImportListRequest(string.Format("{0}/api?cmd=getIndex&apikey={1}", Settings.BaseUrl.TrimEnd('/'), Settings.ApiKey), HttpAccept.Json);
}
}
}
@@ -1,35 +0,0 @@
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.ImportLists.HeadphonesImport
{
public class HeadphonesImportSettingsValidator : AbstractValidator<HeadphonesImportSettings>
{
public HeadphonesImportSettingsValidator()
{
RuleFor(c => c.BaseUrl).ValidRootUrl();
}
}
public class HeadphonesImportSettings : IImportListSettings
{
private static readonly HeadphonesImportSettingsValidator Validator = new HeadphonesImportSettingsValidator();
public HeadphonesImportSettings()
{
BaseUrl = "http://localhost:8181/";
}
[FieldDefinition(0, Label = "Headphones URL")]
public string BaseUrl { get; set; }
[FieldDefinition(1, Label = "API Key")]
public string ApiKey { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}
@@ -6,9 +6,9 @@ namespace NzbDrone.Core.ImportLists
{
public class ImportListSyncCompleteEvent : IEvent
{
public List<Album> ProcessedDecisions { get; private set; }
public List<Book> ProcessedDecisions { get; private set; }
public ImportListSyncCompleteEvent(List<Album> processedDecisions)
public ImportListSyncCompleteEvent(List<Book> processedDecisions)
{
ProcessedDecisions = processedDecisions;
}
@@ -8,6 +8,7 @@ using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.MetadataSource;
using NzbDrone.Core.Music;
using NzbDrone.Core.Music.Commands;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.ImportLists
@@ -17,25 +18,27 @@ namespace NzbDrone.Core.ImportLists
private readonly IImportListFactory _importListFactory;
private readonly IImportListExclusionService _importListExclusionService;
private readonly IFetchAndParseImportList _listFetcherAndParser;
private readonly ISearchForNewAlbum _albumSearchService;
private readonly ISearchForNewArtist _artistSearchService;
private readonly ISearchForNewBook _albumSearchService;
private readonly ISearchForNewAuthor _artistSearchService;
private readonly IArtistService _artistService;
private readonly IAlbumService _albumService;
private readonly IAddArtistService _addArtistService;
private readonly IAddAlbumService _addAlbumService;
private readonly IEventAggregator _eventAggregator;
private readonly IManageCommandQueue _commandQueueManager;
private readonly Logger _logger;
public ImportListSyncService(IImportListFactory importListFactory,
IImportListExclusionService importListExclusionService,
IFetchAndParseImportList listFetcherAndParser,
ISearchForNewAlbum albumSearchService,
ISearchForNewArtist artistSearchService,
ISearchForNewBook albumSearchService,
ISearchForNewAuthor artistSearchService,
IArtistService artistService,
IAlbumService albumService,
IAddArtistService addArtistService,
IAddAlbumService addAlbumService,
IEventAggregator eventAggregator,
IManageCommandQueue commandQueueManager,
Logger logger)
{
_importListFactory = importListFactory;
@@ -48,10 +51,11 @@ namespace NzbDrone.Core.ImportLists
_addArtistService = addArtistService;
_addAlbumService = addAlbumService;
_eventAggregator = eventAggregator;
_commandQueueManager = commandQueueManager;
_logger = logger;
}
private List<Album> SyncAll()
private List<Book> SyncAll()
{
_logger.ProgressInfo("Starting Import List Sync");
@@ -62,7 +66,7 @@ namespace NzbDrone.Core.ImportLists
return ProcessReports(reports);
}
private List<Album> SyncList(ImportListDefinition definition)
private List<Book> SyncList(ImportListDefinition definition)
{
_logger.ProgressInfo(string.Format("Starting Import List Refresh for List {0}", definition.Name));
@@ -73,11 +77,11 @@ namespace NzbDrone.Core.ImportLists
return ProcessReports(reports);
}
private List<Album> ProcessReports(List<ImportListItemInfo> reports)
private List<Book> ProcessReports(List<ImportListItemInfo> reports)
{
var processed = new List<Album>();
var artistsToAdd = new List<Artist>();
var albumsToAdd = new List<Album>();
var processed = new List<Book>();
var artistsToAdd = new List<Author>();
var albumsToAdd = new List<Book>();
_logger.ProgressInfo("Processing {0} list items", reports.Count);
@@ -113,35 +117,52 @@ namespace NzbDrone.Core.ImportLists
}
}
_addArtistService.AddArtists(artistsToAdd);
_addAlbumService.AddAlbums(albumsToAdd);
var addedArtists = _addArtistService.AddArtists(artistsToAdd, false);
var addedAlbums = _addAlbumService.AddAlbums(albumsToAdd, false);
var message = string.Format($"Import List Sync Completed. Items found: {reports.Count}, Artists added: {artistsToAdd.Count}, Albums added: {albumsToAdd.Count}");
_logger.ProgressInfo(message);
var toRefresh = addedArtists.Select(x => x.Id).Concat(addedAlbums.Select(x => x.Author.Value.Id)).Distinct().ToList();
if (toRefresh.Any())
{
_commandQueueManager.Push(new BulkRefreshArtistCommand(toRefresh, true));
}
return processed;
}
private void MapAlbumReport(ImportListItemInfo report)
{
var albumQuery = report.AlbumMusicBrainzId.IsNotNullOrWhiteSpace() ? $"readarr:{report.AlbumMusicBrainzId}" : report.Album;
var mappedAlbum = _albumSearchService.SearchForNewAlbum(albumQuery, report.Artist)
.FirstOrDefault();
Book mappedAlbum;
if (report.AlbumMusicBrainzId.IsNotNullOrWhiteSpace() && int.TryParse(report.AlbumMusicBrainzId, out var goodreadsId))
{
mappedAlbum = _albumSearchService.SearchByGoodreadsId(goodreadsId).FirstOrDefault(x => x.GoodreadsId == goodreadsId);
}
else
{
mappedAlbum = _albumSearchService.SearchForNewBook(report.Album, report.Artist).FirstOrDefault();
}
// Break if we are looking for an album and cant find it. This will avoid us from adding the artist and possibly getting it wrong.
if (mappedAlbum == null)
{
_logger.Trace($"Nothing found for {report.AlbumMusicBrainzId}");
report.AlbumMusicBrainzId = null;
return;
}
report.AlbumMusicBrainzId = mappedAlbum.ForeignAlbumId;
_logger.Trace($"Mapped {report.AlbumMusicBrainzId} to {mappedAlbum}");
report.AlbumMusicBrainzId = mappedAlbum.ForeignBookId;
report.Album = mappedAlbum.Title;
report.Artist = mappedAlbum.ArtistMetadata?.Value?.Name;
report.ArtistMusicBrainzId = mappedAlbum.ArtistMetadata?.Value?.ForeignArtistId;
report.Artist = mappedAlbum.AuthorMetadata?.Value?.Name;
report.ArtistMusicBrainzId = mappedAlbum.AuthorMetadata?.Value?.ForeignAuthorId;
}
private void ProcessAlbumReport(ImportListDefinition importList, ImportListItemInfo report, List<ImportListExclusion> listExclusions, List<Album> albumsToAdd)
private void ProcessAlbumReport(ImportListDefinition importList, ImportListItemInfo report, List<ImportListExclusion> listExclusions, List<Book> albumsToAdd)
{
if (report.AlbumMusicBrainzId == null)
{
@@ -176,23 +197,21 @@ namespace NzbDrone.Core.ImportLists
}
// Append Album if not already in DB or already on add list
if (albumsToAdd.All(s => s.ForeignAlbumId != report.AlbumMusicBrainzId))
if (albumsToAdd.All(s => s.ForeignBookId != report.AlbumMusicBrainzId))
{
var monitored = importList.ShouldMonitor != ImportListMonitorType.None;
var toAdd = new Album
var toAdd = new Book
{
ForeignAlbumId = report.AlbumMusicBrainzId,
ForeignBookId = report.AlbumMusicBrainzId,
Monitored = monitored,
AnyReleaseOk = true,
Artist = new Artist
Author = new Author
{
Monitored = monitored,
RootFolderPath = importList.RootFolderPath,
QualityProfileId = importList.ProfileId,
MetadataProfileId = importList.MetadataProfileId,
Tags = importList.Tags,
AlbumFolder = true,
AddOptions = new AddArtistOptions
{
SearchForMissingAlbums = monitored,
@@ -204,7 +223,7 @@ namespace NzbDrone.Core.ImportLists
if (importList.ShouldMonitor == ImportListMonitorType.SpecificAlbum)
{
toAdd.Artist.Value.AddOptions.AlbumsToMonitor.Add(toAdd.ForeignAlbumId);
toAdd.Author.Value.AddOptions.AlbumsToMonitor.Add(toAdd.ForeignBookId);
}
albumsToAdd.Add(toAdd);
@@ -213,13 +232,13 @@ namespace NzbDrone.Core.ImportLists
private void MapArtistReport(ImportListItemInfo report)
{
var mappedArtist = _artistSearchService.SearchForNewArtist(report.Artist)
var mappedArtist = _artistSearchService.SearchForNewAuthor(report.Artist)
.FirstOrDefault();
report.ArtistMusicBrainzId = mappedArtist?.Metadata.Value?.ForeignArtistId;
report.ArtistMusicBrainzId = mappedArtist?.Metadata.Value?.ForeignAuthorId;
report.Artist = mappedArtist?.Metadata.Value?.Name;
}
private void ProcessArtistReport(ImportListDefinition importList, ImportListItemInfo report, List<ImportListExclusion> listExclusions, List<Artist> artistsToAdd)
private void ProcessArtistReport(ImportListDefinition importList, ImportListItemInfo report, List<ImportListExclusion> listExclusions, List<Author> artistsToAdd)
{
if (report.ArtistMusicBrainzId == null)
{
@@ -245,15 +264,15 @@ namespace NzbDrone.Core.ImportLists
}
// Append Artist if not already in DB or already on add list
if (artistsToAdd.All(s => s.Metadata.Value.ForeignArtistId != report.ArtistMusicBrainzId))
if (artistsToAdd.All(s => s.Metadata.Value.ForeignAuthorId != report.ArtistMusicBrainzId))
{
var monitored = importList.ShouldMonitor != ImportListMonitorType.None;
artistsToAdd.Add(new Artist
artistsToAdd.Add(new Author
{
Metadata = new ArtistMetadata
Metadata = new AuthorMetadata
{
ForeignArtistId = report.ArtistMusicBrainzId,
ForeignAuthorId = report.ArtistMusicBrainzId,
Name = report.Artist
},
Monitored = monitored,
@@ -261,7 +280,6 @@ namespace NzbDrone.Core.ImportLists
QualityProfileId = importList.ProfileId,
MetadataProfileId = importList.MetadataProfileId,
Tags = importList.Tags,
AlbumFolder = true,
AddOptions = new AddArtistOptions
{
SearchForMissingAlbums = monitored,
@@ -274,7 +292,7 @@ namespace NzbDrone.Core.ImportLists
public void Execute(ImportListSyncCommand message)
{
List<Album> processed;
List<Book> processed;
if (message.DefinitionId.HasValue)
{
@@ -2,8 +2,7 @@ namespace NzbDrone.Core.ImportLists
{
public enum ImportListType
{
Spotify,
LastFm,
Goodreads,
Other
}
}
@@ -1,20 +0,0 @@
using System.Collections.Generic;
namespace NzbDrone.Core.ImportLists.LastFm
{
public class LastFmArtistList
{
public List<LastFmArtist> Artist { get; set; }
}
public class LastFmArtistResponse
{
public LastFmArtistList TopArtists { get; set; }
}
public class LastFmArtist
{
public string Name { get; set; }
public string Mbid { get; set; }
}
}
@@ -1,60 +0,0 @@
using System.Collections.Generic;
using System.Net;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.ImportLists.Exceptions;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.ImportLists.LastFm
{
public class LastFmParser : IParseImportListResponse
{
private ImportListResponse _importListResponse;
public IList<ImportListItemInfo> ParseResponse(ImportListResponse importListResponse)
{
_importListResponse = importListResponse;
var items = new List<ImportListItemInfo>();
if (!PreProcess(_importListResponse))
{
return items;
}
var jsonResponse = Json.Deserialize<LastFmArtistResponse>(_importListResponse.Content);
if (jsonResponse == null)
{
return items;
}
foreach (var item in jsonResponse.TopArtists.Artist)
{
items.AddIfNotNull(new ImportListItemInfo
{
Artist = item.Name,
ArtistMusicBrainzId = item.Mbid
});
}
return items;
}
protected virtual bool PreProcess(ImportListResponse importListResponse)
{
if (importListResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
{
throw new ImportListException(importListResponse, "Import List API call resulted in an unexpected StatusCode [{0}]", importListResponse.HttpResponse.StatusCode);
}
if (importListResponse.HttpResponse.Headers.ContentType != null && importListResponse.HttpResponse.Headers.ContentType.Contains("text/json") &&
importListResponse.HttpRequest.Headers.Accept != null && !importListResponse.HttpRequest.Headers.Accept.Contains("text/json"))
{
throw new ImportListException(importListResponse, "Import List responded with html content. Site is likely blocked or unavailable.");
}
return true;
}
}
}
@@ -1,31 +0,0 @@
using NLog;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Parser;
namespace NzbDrone.Core.ImportLists.LastFm
{
public class LastFmTag : HttpImportListBase<LastFmTagSettings>
{
public override string Name => "Last.fm Tag";
public override ImportListType ListType => ImportListType.LastFm;
public override int PageSize => 1000;
public LastFmTag(IHttpClient httpClient, IImportListStatusService importListStatusService, IConfigService configService, IParsingService parsingService, Logger logger)
: base(httpClient, importListStatusService, configService, parsingService, logger)
{
}
public override IImportListRequestGenerator GetRequestGenerator()
{
return new LastFmTagRequestGenerator { Settings = Settings };
}
public override IParseImportListResponse GetParser()
{
return new LastFmParser();
}
}
}
@@ -1,41 +0,0 @@
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.ImportLists.LastFm
{
public class LastFmTagSettingsValidator : AbstractValidator<LastFmTagSettings>
{
public LastFmTagSettingsValidator()
{
RuleFor(c => c.TagId).NotEmpty();
RuleFor(c => c.Count).LessThanOrEqualTo(1000);
}
}
public class LastFmTagSettings : IImportListSettings
{
private static readonly LastFmTagSettingsValidator Validator = new LastFmTagSettingsValidator();
public LastFmTagSettings()
{
BaseUrl = "http://ws.audioscrobbler.com/2.0/?method=tag.gettopartists";
ApiKey = "204c76646d6020eee36bbc51a2fcd810";
Count = 25;
}
public string BaseUrl { get; set; }
public string ApiKey { get; set; }
[FieldDefinition(0, Label = "Last.fm Tag", HelpText = "Tag to pull artists from")]
public string TagId { get; set; }
[FieldDefinition(1, Label = "Count", HelpText = "Number of results to pull from list (Max 1000)", Type = FieldType.Number)]
public int Count { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}
@@ -1,31 +0,0 @@
using NLog;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Parser;
namespace NzbDrone.Core.ImportLists.LastFm
{
public class LastFmUser : HttpImportListBase<LastFmUserSettings>
{
public override string Name => "Last.fm User";
public override ImportListType ListType => ImportListType.LastFm;
public override int PageSize => 1000;
public LastFmUser(IHttpClient httpClient, IImportListStatusService importListStatusService, IConfigService configService, IParsingService parsingService, Logger logger)
: base(httpClient, importListStatusService, configService, parsingService, logger)
{
}
public override IImportListRequestGenerator GetRequestGenerator()
{
return new LastFmUserRequestGenerator { Settings = Settings };
}
public override IParseImportListResponse GetParser()
{
return new LastFmParser();
}
}
}
@@ -1,33 +0,0 @@
using System.Collections.Generic;
using NzbDrone.Common.Http;
namespace NzbDrone.Core.ImportLists.LastFm
{
public class LastFmUserRequestGenerator : IImportListRequestGenerator
{
public LastFmUserSettings Settings { get; set; }
public int MaxPages { get; set; }
public int PageSize { get; set; }
public LastFmUserRequestGenerator()
{
MaxPages = 1;
PageSize = 1000;
}
public virtual ImportListPageableRequestChain GetListItems()
{
var pageableRequests = new ImportListPageableRequestChain();
pageableRequests.Add(GetPagedRequests());
return pageableRequests;
}
private IEnumerable<ImportListRequest> GetPagedRequests()
{
yield return new ImportListRequest(string.Format("{0}&user={1}&limit={2}&api_key={3}&format=json", Settings.BaseUrl.TrimEnd('/'), Settings.UserId, Settings.Count, Settings.ApiKey), HttpAccept.Json);
}
}
}
@@ -1,41 +0,0 @@
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.ImportLists.LastFm
{
public class LastFmSettingsValidator : AbstractValidator<LastFmUserSettings>
{
public LastFmSettingsValidator()
{
RuleFor(c => c.UserId).NotEmpty();
RuleFor(c => c.Count).LessThanOrEqualTo(1000);
}
}
public class LastFmUserSettings : IImportListSettings
{
private static readonly LastFmSettingsValidator Validator = new LastFmSettingsValidator();
public LastFmUserSettings()
{
BaseUrl = "http://ws.audioscrobbler.com/2.0/?method=user.gettopartists";
ApiKey = "204c76646d6020eee36bbc51a2fcd810";
Count = 25;
}
public string BaseUrl { get; set; }
public string ApiKey { get; set; }
[FieldDefinition(0, Label = "Last.fm UserID", HelpText = "Last.fm UserId to pull artists from")]
public string UserId { get; set; }
[FieldDefinition(1, Label = "Count", HelpText = "Number of results to pull from list (Max 1000)", Type = FieldType.Number)]
public int Count { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}
@@ -3,29 +3,29 @@ using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Parser;
namespace NzbDrone.Core.ImportLists.HeadphonesImport
namespace NzbDrone.Core.ImportLists.LazyLibrarianImport
{
public class HeadphonesImport : HttpImportListBase<HeadphonesImportSettings>
public class LazyLibrarianImport : HttpImportListBase<LazyLibrarianImportSettings>
{
public override string Name => "Headphones";
public override string Name => "LazyLibrarian";
public override ImportListType ListType => ImportListType.Other;
public override int PageSize => 1000;
public HeadphonesImport(IHttpClient httpClient, IImportListStatusService importListStatusService, IConfigService configService, IParsingService parsingService, Logger logger)
public LazyLibrarianImport(IHttpClient httpClient, IImportListStatusService importListStatusService, IConfigService configService, IParsingService parsingService, Logger logger)
: base(httpClient, importListStatusService, configService, parsingService, logger)
{
}
public override IImportListRequestGenerator GetRequestGenerator()
{
return new HeadphonesImportRequestGenerator { Settings = Settings };
return new LazyLibrarianImportRequestGenerator { Settings = Settings };
}
public override IParseImportListResponse GetParser()
{
return new HeadphonesImportParser();
return new LazyLibrarianImportParser();
}
}
}
@@ -0,0 +1,11 @@
namespace NzbDrone.Core.ImportLists.LazyLibrarianImport
{
public class LazyLibrarianBook
{
public string BookName { get; set; }
public string BookId { get; set; }
public string BookIsbn { get; set; }
public string AuthorName { get; set; }
public string AuthorId { get; set; }
}
}
@@ -5,9 +5,9 @@ using NzbDrone.Common.Extensions;
using NzbDrone.Core.ImportLists.Exceptions;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.ImportLists.HeadphonesImport
namespace NzbDrone.Core.ImportLists.LazyLibrarianImport
{
public class HeadphonesImportParser : IParseImportListResponse
public class LazyLibrarianImportParser : IParseImportListResponse
{
private ImportListResponse _importListResponse;
@@ -22,7 +22,7 @@ namespace NzbDrone.Core.ImportLists.HeadphonesImport
return items;
}
var jsonResponse = JsonConvert.DeserializeObject<List<HeadphonesImportArtist>>(_importListResponse.Content);
var jsonResponse = JsonConvert.DeserializeObject<List<LazyLibrarianBook>>(_importListResponse.Content);
// no albums were return
if (jsonResponse == null)
@@ -34,8 +34,9 @@ namespace NzbDrone.Core.ImportLists.HeadphonesImport
{
items.AddIfNotNull(new ImportListItemInfo
{
Artist = item.ArtistName,
ArtistMusicBrainzId = item.ArtistId
Artist = item.AuthorName,
Album = item.BookName,
AlbumMusicBrainzId = item.BookId
});
}
@@ -1,16 +1,16 @@
using System.Collections.Generic;
using NzbDrone.Common.Http;
namespace NzbDrone.Core.ImportLists.LastFm
namespace NzbDrone.Core.ImportLists.LazyLibrarianImport
{
public class LastFmTagRequestGenerator : IImportListRequestGenerator
public class LazyLibrarianImportRequestGenerator : IImportListRequestGenerator
{
public LastFmTagSettings Settings { get; set; }
public LazyLibrarianImportSettings Settings { get; set; }
public int MaxPages { get; set; }
public int PageSize { get; set; }
public LastFmTagRequestGenerator()
public LazyLibrarianImportRequestGenerator()
{
MaxPages = 1;
PageSize = 1000;
@@ -27,7 +27,7 @@ namespace NzbDrone.Core.ImportLists.LastFm
private IEnumerable<ImportListRequest> GetPagedRequests()
{
yield return new ImportListRequest(string.Format("{0}&tag={1}&limit={2}&api_key={3}&format=json", Settings.BaseUrl.TrimEnd('/'), Settings.TagId, Settings.Count, Settings.ApiKey), HttpAccept.Json);
yield return new ImportListRequest(string.Format("{0}/api?cmd=getAllBooks&apikey={1}", Settings.BaseUrl.TrimEnd('/'), Settings.ApiKey), HttpAccept.Json);
}
}
}
@@ -0,0 +1,36 @@
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.ImportLists.LazyLibrarianImport
{
public class LazyLibrarianImportSettingsValidator : AbstractValidator<LazyLibrarianImportSettings>
{
public LazyLibrarianImportSettingsValidator()
{
RuleFor(c => c.BaseUrl).IsValidUrl();
RuleFor(c => c.ApiKey).NotEmpty();
}
}
public class LazyLibrarianImportSettings : IImportListSettings
{
private static readonly LazyLibrarianImportSettingsValidator Validator = new LazyLibrarianImportSettingsValidator();
public LazyLibrarianImportSettings()
{
BaseUrl = "http://localhost:5299";
}
[FieldDefinition(0, Label = "Url")]
public string BaseUrl { get; set; }
[FieldDefinition(1, Label = "API Key")]
public string ApiKey { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}
@@ -1,69 +0,0 @@
using System.Collections.Generic;
using NLog;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.MetadataSource;
using NzbDrone.Core.Parser;
using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.ImportLists.ReadarrLists
{
public class ReadarrLists : HttpImportListBase<ReadarrListsSettings>
{
public override string Name => "Readarr Lists";
public override ImportListType ListType => ImportListType.Other;
public override int PageSize => 10;
private readonly IMetadataRequestBuilder _requestBuilder;
public ReadarrLists(IHttpClient httpClient, IImportListStatusService importListStatusService, IConfigService configService, IParsingService parsingService, IMetadataRequestBuilder requestBuilder, Logger logger)
: base(httpClient, importListStatusService, configService, parsingService, logger)
{
_requestBuilder = requestBuilder;
}
public override IEnumerable<ProviderDefinition> DefaultDefinitions
{
get
{
yield return GetDefinition("iTunes Top Albums", GetSettings("itunes/album/top"));
yield return GetDefinition("iTunes New Albums", GetSettings("itunes/album/new"));
yield return GetDefinition("Apple Music Top Albums", GetSettings("apple-music/album/top"));
yield return GetDefinition("Apple Music New Albums", GetSettings("apple-music/album/new"));
yield return GetDefinition("Billboard Top Albums", GetSettings("billboard/album/top"));
yield return GetDefinition("Billboard Top Artists", GetSettings("billboard/artist/top"));
yield return GetDefinition("Last.fm Top Artists", GetSettings("lastfm/artist/top"));
}
}
private ImportListDefinition GetDefinition(string name, ReadarrListsSettings settings)
{
return new ImportListDefinition
{
EnableAutomaticAdd = false,
Name = name,
Implementation = GetType().Name,
Settings = settings
};
}
private ReadarrListsSettings GetSettings(string url)
{
var settings = new ReadarrListsSettings { ListId = url };
return settings;
}
public override IImportListRequestGenerator GetRequestGenerator()
{
return new ReadarrListsRequestGenerator(_requestBuilder) { Settings = Settings };
}
public override IParseImportListResponse GetParser()
{
return new ReadarrListsParser(Settings);
}
}
}
@@ -1,13 +0,0 @@
using System;
namespace NzbDrone.Core.ImportLists.ReadarrLists
{
public class ReadarrListsAlbum
{
public string ArtistName { get; set; }
public string AlbumTitle { get; set; }
public string ArtistId { get; set; }
public string AlbumId { get; set; }
public DateTime? ReleaseDate { get; set; }
}
}
@@ -1,70 +0,0 @@
using System.Collections.Generic;
using System.Net;
using Newtonsoft.Json;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.ImportLists.Exceptions;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.ImportLists.ReadarrLists
{
public class ReadarrListsParser : IParseImportListResponse
{
private readonly ReadarrListsSettings _settings;
private ImportListResponse _importListResponse;
public ReadarrListsParser(ReadarrListsSettings settings)
{
_settings = settings;
}
public IList<ImportListItemInfo> ParseResponse(ImportListResponse importListResponse)
{
_importListResponse = importListResponse;
var items = new List<ImportListItemInfo>();
if (!PreProcess(_importListResponse))
{
return items;
}
var jsonResponse = JsonConvert.DeserializeObject<List<ReadarrListsAlbum>>(_importListResponse.Content);
// no albums were return
if (jsonResponse == null)
{
return items;
}
foreach (var item in jsonResponse)
{
items.AddIfNotNull(new ImportListItemInfo
{
Artist = item.ArtistName,
Album = item.AlbumTitle,
ArtistMusicBrainzId = item.ArtistId,
AlbumMusicBrainzId = item.AlbumId,
ReleaseDate = item.ReleaseDate.GetValueOrDefault()
});
}
return items;
}
protected virtual bool PreProcess(ImportListResponse importListResponse)
{
if (importListResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
{
throw new ImportListException(importListResponse, "Import List API call resulted in an unexpected StatusCode [{0}]", importListResponse.HttpResponse.StatusCode);
}
if (importListResponse.HttpResponse.Headers.ContentType != null && importListResponse.HttpResponse.Headers.ContentType.Contains("text/json") &&
importListResponse.HttpRequest.Headers.Accept != null && !importListResponse.HttpRequest.Headers.Accept.Contains("text/json"))
{
throw new ImportListException(importListResponse, "Import List responded with html content. Site is likely blocked or unavailable.");
}
return true;
}
}
}
@@ -1,36 +0,0 @@
using System.Collections.Generic;
using NzbDrone.Core.MetadataSource;
namespace NzbDrone.Core.ImportLists.ReadarrLists
{
public class ReadarrListsRequestGenerator : IImportListRequestGenerator
{
public ReadarrListsSettings Settings { get; set; }
private readonly IMetadataRequestBuilder _requestBulder;
public ReadarrListsRequestGenerator(IMetadataRequestBuilder requestBuilder)
{
_requestBulder = requestBuilder;
}
public virtual ImportListPageableRequestChain GetListItems()
{
var pageableRequests = new ImportListPageableRequestChain();
pageableRequests.Add(GetPagedRequests());
return pageableRequests;
}
private IEnumerable<ImportListRequest> GetPagedRequests()
{
var request = _requestBulder.GetRequestBuilder()
.Create()
.SetSegment("route", "chart/" + Settings.ListId)
.Build();
yield return new ImportListRequest(request);
}
}
}
@@ -1,33 +0,0 @@
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.ImportLists.ReadarrLists
{
public class ReadarrListsSettingsValidator : AbstractValidator<ReadarrListsSettings>
{
public ReadarrListsSettingsValidator()
{
}
}
public class ReadarrListsSettings : IImportListSettings
{
private static readonly ReadarrListsSettingsValidator Validator = new ReadarrListsSettingsValidator();
public ReadarrListsSettings()
{
BaseUrl = "";
}
public string BaseUrl { get; set; }
[FieldDefinition(0, Label = "List Id", Advanced = true)]
public string ListId { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}
@@ -1,31 +0,0 @@
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)
{
}
}
}
@@ -1,79 +0,0 @@
using System.Collections.Generic;
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 SpotifyFollowedArtistsSettings : SpotifySettingsBase<SpotifyFollowedArtistsSettings>
{
public override string Scope => "user-follow-read";
}
public class SpotifyFollowedArtists : SpotifyImportListBase<SpotifyFollowedArtistsSettings>
{
public SpotifyFollowedArtists(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 Followed Artists";
public override IList<SpotifyImportListItemInfo> Fetch(SpotifyWebAPI api)
{
var result = new List<SpotifyImportListItemInfo>();
var followedArtists = _spotifyProxy.GetFollowedArtists(this, api);
var artists = followedArtists?.Artists;
while (true)
{
if (artists?.Items == null)
{
return result;
}
foreach (var artist in artists.Items)
{
result.AddIfNotNull(ParseFullArtist(artist));
}
if (!artists.HasNext())
{
break;
}
followedArtists = _spotifyProxy.GetNextPage(this, api, followedArtists);
artists = followedArtists?.Artists;
}
return result;
}
private SpotifyImportListItemInfo ParseFullArtist(FullArtist artist)
{
if (artist?.Name.IsNotNullOrWhiteSpace() ?? false)
{
return new SpotifyImportListItemInfo
{
Artist = artist.Name,
ArtistSpotifyId = artist.Id
};
}
return null;
}
}
}
@@ -1,354 +0,0 @@
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;
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;
protected ISpotifyProxy _spotifyProxy;
private readonly IMetadataRequestBuilder _requestBuilder;
protected SpotifyImportListBase(ISpotifyProxy spotifyProxy,
IMetadataRequestBuilder requestBuilder,
IImportListStatusService importListStatusService,
IImportListRepository importListRepository,
IConfigService configService,
IParsingService parsingService,
IHttpClient httpClient,
Logger logger)
: base(importListStatusService, configService, parsingService, logger)
{
_httpClient = httpClient;
_importListRepository = importListRepository;
_spotifyProxy = spotifyProxy;
_requestBuilder = requestBuilder;
}
public override ImportListType ListType => ImportListType.Spotify;
public string AccessToken => Settings.AccessToken;
public 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");
}
}
public 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"
};
}
public override IList<ImportListItemInfo> Fetch()
{
IList<SpotifyImportListItemInfo> releases;
using (var api = GetApi())
{
_logger.Debug("Starting spotify import list sync");
releases = Fetch(api);
}
// map to musicbrainz ids
releases = MapSpotifyReleases(releases);
return CleanupListItems(releases);
}
public abstract IList<SpotifyImportListItemInfo> 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);
}
public IList<SpotifyImportListItemInfo> MapSpotifyReleases(IList<SpotifyImportListItemInfo> 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<string, string> map;
try
{
var httpResponse = _httpClient.Post<List<SpotifyMap>>(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<string, string>();
}
_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<ArtistResource>(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 ReadarrAPI");
}
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<AlbumResource>(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 ReadarrAPI");
}
catch (Exception e)
{
_logger.Error(e);
}
}
protected override void Test(List<ValidationFailure> failures)
{
failures.AddIfNotNull(TestConnection());
}
private ValidationFailure TestConnection()
{
try
{
using (var api = GetApi())
{
var profile = _spotifyProxy.GetPrivateProfile(this, api);
_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 { };
}
}
}
@@ -1,15 +0,0 @@
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);
}
}
}
@@ -1,8 +0,0 @@
namespace NzbDrone.Core.ImportLists.Spotify
{
public class SpotifyMap
{
public string SpotifyId { get; set; }
public string MusicbrainzId { get; set; }
}
}
@@ -1,160 +0,0 @@
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.MetadataSource;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Validation;
using SpotifyAPI.Web;
using SpotifyAPI.Web.Models;
namespace NzbDrone.Core.ImportLists.Spotify
{
public class SpotifyPlaylist : SpotifyImportListBase<SpotifyPlaylistSettings>
{
public SpotifyPlaylist(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 Playlists";
public override IList<SpotifyImportListItemInfo> Fetch(SpotifyWebAPI api)
{
return Settings.PlaylistIds.SelectMany(x => Fetch(api, x)).ToList();
}
public IList<SpotifyImportListItemInfo> Fetch(SpotifyWebAPI api, string playlistId)
{
var result = new List<SpotifyImportListItemInfo>();
_logger.Trace($"Processing playlist {playlistId}");
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)
{
if (playlistTracks?.Items == null)
{
return result;
}
foreach (var playlistTrack in playlistTracks.Items)
{
result.AddIfNotNull(ParsePlaylistTrack(playlistTrack));
}
if (!playlistTracks.HasNextPage())
{
break;
}
playlistTracks = _spotifyProxy.GetNextPage(this, api, playlistTracks);
}
return result;
}
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 SpotifyImportListItemInfo
{
Artist = artistName,
Album = album.Name,
AlbumSpotifyId = album.Id,
ReleaseDate = ParseSpotifyDate(album.ReleaseDate, album.ReleaseDatePrecision)
};
}
}
return null;
}
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 = _spotifyProxy.GetPrivateProfile(this, api);
var playlistPage = _spotifyProxy.GetUserPlaylists(this, api, profile.Id);
_logger.Trace($"Got {playlistPage.Total} playlists");
var playlists = new List<SimplePlaylist>(playlistPage.Total);
while (true)
{
if (playlistPage == null)
{
break;
}
playlists.AddRange(playlistPage.Items);
if (!playlistPage.HasNextPage())
{
break;
}
playlistPage = _spotifyProxy.GetNextPage(this, api, 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);
}
}
}
}
@@ -1,30 +0,0 @@
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; }
}
}
@@ -1,109 +0,0 @@
using System;
using NLog;
using SpotifyAPI.Web;
using SpotifyAPI.Web.Enums;
using SpotifyAPI.Web.Models;
namespace NzbDrone.Core.ImportLists.Spotify
{
public interface ISpotifyProxy
{
PrivateProfile GetPrivateProfile<TSettings>(SpotifyImportListBase<TSettings> list, SpotifyWebAPI api)
where TSettings : SpotifySettingsBase<TSettings>, new();
Paging<SimplePlaylist> GetUserPlaylists<TSettings>(SpotifyImportListBase<TSettings> list, SpotifyWebAPI api, string id)
where TSettings : SpotifySettingsBase<TSettings>, new();
FollowedArtists GetFollowedArtists<TSettings>(SpotifyImportListBase<TSettings> list, SpotifyWebAPI api)
where TSettings : SpotifySettingsBase<TSettings>, new();
Paging<SavedAlbum> GetSavedAlbums<TSettings>(SpotifyImportListBase<TSettings> list, SpotifyWebAPI api)
where TSettings : SpotifySettingsBase<TSettings>, new();
Paging<PlaylistTrack> GetPlaylistTracks<TSettings>(SpotifyImportListBase<TSettings> list, SpotifyWebAPI api, string id, string fields)
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)
where TSettings : SpotifySettingsBase<TSettings>, new();
}
public class SpotifyProxy : ISpotifyProxy
{
private readonly Logger _logger;
public SpotifyProxy(Logger logger)
{
_logger = logger;
}
public PrivateProfile GetPrivateProfile<TSettings>(SpotifyImportListBase<TSettings> list, SpotifyWebAPI api)
where TSettings : SpotifySettingsBase<TSettings>, new()
{
return Execute(list, api, x => x.GetPrivateProfile());
}
public Paging<SimplePlaylist> GetUserPlaylists<TSettings>(SpotifyImportListBase<TSettings> list, SpotifyWebAPI api, string id)
where TSettings : SpotifySettingsBase<TSettings>, new()
{
return Execute(list, api, x => x.GetUserPlaylists(id));
}
public FollowedArtists GetFollowedArtists<TSettings>(SpotifyImportListBase<TSettings> list, SpotifyWebAPI api)
where TSettings : SpotifySettingsBase<TSettings>, new()
{
return Execute(list, api, x => x.GetFollowedArtists(FollowType.Artist, 50));
}
public Paging<SavedAlbum> GetSavedAlbums<TSettings>(SpotifyImportListBase<TSettings> list, SpotifyWebAPI api)
where TSettings : SpotifySettingsBase<TSettings>, new()
{
return Execute(list, api, x => x.GetSavedAlbums(50));
}
public Paging<PlaylistTrack> GetPlaylistTracks<TSettings>(SpotifyImportListBase<TSettings> list, SpotifyWebAPI api, string id, string fields)
where TSettings : SpotifySettingsBase<TSettings>, new()
{
return Execute(list, api, x => x.GetPlaylistTracks(id, fields: fields));
}
public Paging<T> GetNextPage<T, TSettings>(SpotifyImportListBase<TSettings> list, SpotifyWebAPI api, Paging<T> item)
where TSettings : SpotifySettingsBase<TSettings>, new()
{
return Execute(list, api, (x) => x.GetNextPage(item));
}
public FollowedArtists GetNextPage<TSettings>(SpotifyImportListBase<TSettings> list, SpotifyWebAPI api, FollowedArtists item)
where TSettings : SpotifySettingsBase<TSettings>, new()
{
return Execute(list, api, (x) => x.GetNextPage<FollowedArtists, FullArtist>(item.Artists));
}
public T Execute<T, TSettings>(SpotifyImportListBase<TSettings> list, SpotifyWebAPI api, Func<SpotifyWebAPI, T> method, bool allowReauth = true)
where T : BasicModel
where TSettings : SpotifySettingsBase<TSettings>, new()
{
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");
list.RefreshToken();
api.AccessToken = list.AccessToken;
return Execute(list, api, method, false);
}
else
{
throw new SpotifyAuthorizationException(result.Error.Message);
}
}
else
{
throw new SpotifyException("[{0}] {1}", result.Error.Status, result.Error.Message);
}
}
return result;
}
}
}
@@ -1,86 +0,0 @@
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 SpotifySavedAlbumsSettings : SpotifySettingsBase<SpotifySavedAlbumsSettings>
{
public override string Scope => "user-library-read";
}
public class SpotifySavedAlbums : SpotifyImportListBase<SpotifySavedAlbumsSettings>
{
public SpotifySavedAlbums(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 Albums";
public override IList<SpotifyImportListItemInfo> Fetch(SpotifyWebAPI api)
{
var result = new List<SpotifyImportListItemInfo>();
var savedAlbums = _spotifyProxy.GetSavedAlbums(this, api);
_logger.Trace($"Got {savedAlbums?.Total ?? 0} saved albums");
while (true)
{
if (savedAlbums?.Items == null)
{
return result;
}
foreach (var savedAlbum in savedAlbums.Items)
{
result.AddIfNotNull(ParseSavedAlbum(savedAlbum));
}
if (!savedAlbums.HasNextPage())
{
break;
}
savedAlbums = _spotifyProxy.GetNextPage(this, api, savedAlbums);
}
return result;
}
private SpotifyImportListItemInfo ParseSavedAlbum(SavedAlbum savedAlbum)
{
var artistName = savedAlbum?.Album?.Artists?.FirstOrDefault()?.Name;
var albumName = savedAlbum?.Album?.Name;
_logger.Trace($"Adding {artistName} - {albumName}");
if (artistName.IsNotNullOrWhiteSpace() && albumName.IsNotNullOrWhiteSpace())
{
return new SpotifyImportListItemInfo
{
Artist = artistName,
Album = albumName,
AlbumSpotifyId = savedAlbum?.Album?.Id,
ReleaseDate = ParseSpotifyDate(savedAlbum?.Album?.ReleaseDate, savedAlbum?.Album?.ReleaseDatePrecision)
};
}
return null;
}
}
}
@@ -1,55 +0,0 @@
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.readarr.audio/auth";
public string RenewUri => "https://spotify.readarr.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));
}
}
}