mirror of
https://github.com/Readarr/Readarr.git
synced 2026-04-27 22:56:45 -04:00
New: Readarr 0.1
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -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; }
|
||||
}
|
||||
}
|
||||
+6
-5
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
+5
-5
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user