mirror of
https://github.com/Lidarr/Lidarr
synced 2026-01-29 10:58:39 +01:00
Merge b526c76fee into f6a3e73705
This commit is contained in:
commit
c4d50ed37b
14 changed files with 906 additions and 9 deletions
|
|
@ -0,0 +1,250 @@
|
|||
using System.Linq;
|
||||
using System.Net;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NLog;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.ImportLists;
|
||||
using NzbDrone.Core.ImportLists.Discogs;
|
||||
using NzbDrone.Core.ImportLists.Exceptions;
|
||||
|
||||
namespace NzbDrone.Core.Test.ImportListTests.Discogs;
|
||||
|
||||
[TestFixture]
|
||||
public class DiscogsListsFixture
|
||||
{
|
||||
private readonly HttpHeader _defaultHeaders = new () { ContentType = "application/json" };
|
||||
|
||||
private DiscogsListsParser _parser;
|
||||
private Mock<IHttpClient> _httpClient;
|
||||
private DiscogsListsSettings _settings;
|
||||
private Logger _logger;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_httpClient = new Mock<IHttpClient>();
|
||||
_settings = new DiscogsListsSettings { Token = "token", ListId = "123", BaseUrl = "https://api.discogs.com" };
|
||||
_logger = LogManager.GetCurrentClassLogger();
|
||||
_parser = new DiscogsListsParser(_settings, _httpClient.Object, _logger);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_parse_release_items()
|
||||
{
|
||||
const string resourceUrl = "https://api.discogs.com/releases/3";
|
||||
GivenReleaseDetails(resourceUrl, BuildReleaseResponse("Josh Wink", "Profound Sounds Vol. 1"));
|
||||
|
||||
var response = BuildListResponse(@"{
|
||||
""items"": [
|
||||
{
|
||||
""type"": ""release"",
|
||||
""id"": 3,
|
||||
""display_title"": ""Josh Wink - Profound Sounds Vol. 1"",
|
||||
""resource_url"": ""https://api.discogs.com/releases/3""
|
||||
}
|
||||
]
|
||||
}");
|
||||
|
||||
var items = _parser.ParseResponse(response);
|
||||
|
||||
items.Should().HaveCount(1);
|
||||
items.First().Artist.Should().Be("Josh Wink");
|
||||
items.First().Album.Should().Be("Profound Sounds Vol. 1");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_parse_artist_items()
|
||||
{
|
||||
const string resourceUrl = "https://api.discogs.com/artists/3227";
|
||||
GivenArtistDetails(resourceUrl, BuildArtistResponse("Silent Phase"));
|
||||
|
||||
var response = BuildListResponse(@"{
|
||||
""items"": [
|
||||
{
|
||||
""type"": ""artist"",
|
||||
""id"": 3227,
|
||||
""display_title"": ""Silent Phase"",
|
||||
""resource_url"": ""https://api.discogs.com/artists/3227""
|
||||
}
|
||||
]
|
||||
}");
|
||||
|
||||
var items = _parser.ParseResponse(response);
|
||||
|
||||
items.Should().HaveCount(1);
|
||||
items.First().Artist.Should().Be("Silent Phase");
|
||||
items.First().Album.Should().BeNull();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_parse_mixed_release_and_artist_items()
|
||||
{
|
||||
const string releaseUrl = "https://api.discogs.com/releases/4674";
|
||||
const string artistUrl = "https://api.discogs.com/artists/3227";
|
||||
GivenReleaseDetails(releaseUrl, BuildReleaseResponse("Silent Phase", "The Rewired Mixes"));
|
||||
GivenArtistDetails(artistUrl, BuildArtistResponse("Silent Phase"));
|
||||
|
||||
var response = BuildListResponse(@"{
|
||||
""items"": [
|
||||
{
|
||||
""type"": ""release"",
|
||||
""id"": 4674,
|
||||
""display_title"": ""Silent Phase - The Rewired Mixes"",
|
||||
""resource_url"": ""https://api.discogs.com/releases/4674""
|
||||
},
|
||||
{
|
||||
""type"": ""artist"",
|
||||
""id"": 3227,
|
||||
""display_title"": ""Silent Phase"",
|
||||
""resource_url"": ""https://api.discogs.com/artists/3227""
|
||||
}
|
||||
]
|
||||
}");
|
||||
|
||||
var items = _parser.ParseResponse(response);
|
||||
|
||||
items.Should().HaveCount(2);
|
||||
items.First().Artist.Should().Be("Silent Phase");
|
||||
items.First().Album.Should().Be("The Rewired Mixes");
|
||||
items.Last().Artist.Should().Be("Silent Phase");
|
||||
items.Last().Album.Should().BeNull();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_ignore_non_release_and_non_artist_items()
|
||||
{
|
||||
var response = BuildListResponse(@"{
|
||||
""items"": [
|
||||
{
|
||||
""type"": ""label"",
|
||||
""id"": 7,
|
||||
""display_title"": ""Ignore me"",
|
||||
""resource_url"": ""https://api.discogs.com/labels/7""
|
||||
}
|
||||
]
|
||||
}");
|
||||
|
||||
var items = _parser.ParseResponse(response);
|
||||
|
||||
items.Should().BeEmpty();
|
||||
_httpClient.Verify(c => c.Execute(It.IsAny<HttpRequest>()), Times.Never);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_skip_release_when_details_fail()
|
||||
{
|
||||
var response = BuildListResponse(@"{
|
||||
""items"": [
|
||||
{
|
||||
""type"": ""release"",
|
||||
""id"": 3,
|
||||
""display_title"": ""Josh Wink - Profound Sounds Vol. 1"",
|
||||
""resource_url"": ""https://api.discogs.com/releases/3""
|
||||
}
|
||||
]
|
||||
}");
|
||||
|
||||
_httpClient.Setup(c => c.Execute(It.IsAny<HttpRequest>()))
|
||||
.Returns(new HttpResponse(new HttpRequest("https://api.discogs.com/releases/3"), _defaultHeaders, string.Empty, HttpStatusCode.NotFound));
|
||||
|
||||
var items = _parser.ParseResponse(response);
|
||||
|
||||
items.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_skip_artist_when_details_fail()
|
||||
{
|
||||
var response = BuildListResponse(@"{
|
||||
""items"": [
|
||||
{
|
||||
""type"": ""artist"",
|
||||
""id"": 3227,
|
||||
""display_title"": ""Silent Phase"",
|
||||
""resource_url"": ""https://api.discogs.com/artists/3227""
|
||||
}
|
||||
]
|
||||
}");
|
||||
|
||||
_httpClient.Setup(c => c.Execute(It.IsAny<HttpRequest>()))
|
||||
.Returns(new HttpResponse(new HttpRequest("https://api.discogs.com/artists/3227"), _defaultHeaders, string.Empty, HttpStatusCode.NotFound));
|
||||
|
||||
var items = _parser.ParseResponse(response);
|
||||
|
||||
items.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_skip_artist_when_name_is_missing()
|
||||
{
|
||||
const string resourceUrl = "https://api.discogs.com/artists/3227";
|
||||
GivenArtistDetails(resourceUrl, @"{ ""id"": 3227 }");
|
||||
|
||||
var response = BuildListResponse(@"{
|
||||
""items"": [
|
||||
{
|
||||
""type"": ""artist"",
|
||||
""id"": 3227,
|
||||
""display_title"": ""Silent Phase"",
|
||||
""resource_url"": ""https://api.discogs.com/artists/3227""
|
||||
}
|
||||
]
|
||||
}");
|
||||
|
||||
var items = _parser.ParseResponse(response);
|
||||
|
||||
items.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_throw_when_discogs_returns_html()
|
||||
{
|
||||
var response = BuildListResponse("<html></html>", contentType: "text/html");
|
||||
|
||||
_parser.Invoking(p => p.ParseResponse(response))
|
||||
.Should().Throw<ImportListException>()
|
||||
.WithMessage("*HTML content*");
|
||||
}
|
||||
|
||||
private ImportListResponse BuildListResponse(string content, HttpStatusCode statusCode = HttpStatusCode.OK, string contentType = "application/json")
|
||||
{
|
||||
var httpRequest = new HttpRequest("https://api.discogs.com/lists/123");
|
||||
var importListRequest = new ImportListRequest(httpRequest);
|
||||
var headers = new HttpHeader { ContentType = contentType };
|
||||
var httpResponse = new HttpResponse(httpRequest, headers, content, statusCode);
|
||||
|
||||
return new ImportListResponse(importListRequest, httpResponse);
|
||||
}
|
||||
|
||||
private void GivenReleaseDetails(string resourceUrl, string payload, HttpStatusCode statusCode = HttpStatusCode.OK)
|
||||
{
|
||||
_httpClient.Setup(c => c.Execute(It.Is<HttpRequest>(r => r.Url.FullUri == resourceUrl)))
|
||||
.Returns(new HttpResponse(new HttpRequest(resourceUrl), _defaultHeaders, payload, statusCode));
|
||||
}
|
||||
|
||||
private void GivenArtistDetails(string resourceUrl, string payload, HttpStatusCode statusCode = HttpStatusCode.OK)
|
||||
{
|
||||
_httpClient.Setup(c => c.Execute(It.Is<HttpRequest>(r => r.Url.FullUri == resourceUrl)))
|
||||
.Returns(new HttpResponse(new HttpRequest(resourceUrl), _defaultHeaders, payload, statusCode));
|
||||
}
|
||||
|
||||
private static string BuildReleaseResponse(string artist, string title)
|
||||
{
|
||||
return $@"{{
|
||||
""title"": ""{title}"",
|
||||
""artists"": [
|
||||
{{ ""name"": ""{artist}"", ""id"": 3 }}
|
||||
]
|
||||
}}";
|
||||
}
|
||||
|
||||
private static string BuildArtistResponse(string name)
|
||||
{
|
||||
return $@"{{
|
||||
""name"": ""{name}"",
|
||||
""id"": 3227
|
||||
}}";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
using System.Linq;
|
||||
using System.Net;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.ImportLists;
|
||||
using NzbDrone.Core.ImportLists.Discogs;
|
||||
using NzbDrone.Core.ImportLists.Exceptions;
|
||||
|
||||
namespace NzbDrone.Core.Test.ImportListTests.Discogs;
|
||||
|
||||
[TestFixture]
|
||||
public class DiscogsWantlistFixture
|
||||
{
|
||||
private readonly HttpHeader _defaultHeaders = new () { ContentType = "application/json" };
|
||||
|
||||
private DiscogsWantlistParser _parser;
|
||||
private Mock<IHttpClient> _httpClient;
|
||||
private DiscogsWantlistSettings _settings;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_httpClient = new Mock<IHttpClient>();
|
||||
_settings = new DiscogsWantlistSettings { Token = "token", Username = "user", BaseUrl = "https://api.discogs.com" };
|
||||
_parser = new DiscogsWantlistParser(_settings, _httpClient.Object);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_parse_wantlist_items()
|
||||
{
|
||||
var response = BuildWantlistResponse(@"{
|
||||
""wants"": [
|
||||
{
|
||||
""basic_information"": {
|
||||
""id"": 3,
|
||||
""title"": ""Profound Sounds Vol. 1"",
|
||||
""resource_url"": ""https://api.discogs.com/releases/3"",
|
||||
""artists"": [
|
||||
{ ""name"": ""Josh Wink"", ""id"": 123 }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}");
|
||||
|
||||
var items = _parser.ParseResponse(response);
|
||||
|
||||
items.Should().HaveCount(1);
|
||||
items.First().Artist.Should().Be("Josh Wink");
|
||||
items.First().Album.Should().Be("Profound Sounds Vol. 1");
|
||||
_httpClient.Verify(c => c.Execute(It.IsAny<HttpRequest>()), Times.Never);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_skip_entries_without_artists()
|
||||
{
|
||||
var response = BuildWantlistResponse(@"{
|
||||
""wants"": [
|
||||
{
|
||||
""basic_information"": {
|
||||
""id"": 3,
|
||||
""title"": ""Missing artists""
|
||||
}
|
||||
}
|
||||
]
|
||||
}");
|
||||
|
||||
var items = _parser.ParseResponse(response);
|
||||
|
||||
items.Should().BeEmpty();
|
||||
_httpClient.Verify(c => c.Execute(It.IsAny<HttpRequest>()), Times.Never);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_skip_entries_without_title()
|
||||
{
|
||||
var response = BuildWantlistResponse(@"{
|
||||
""wants"": [
|
||||
{
|
||||
""basic_information"": {
|
||||
""id"": 3,
|
||||
""artists"": [
|
||||
{ ""name"": ""Test Artist"", ""id"": 123 }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}");
|
||||
|
||||
var items = _parser.ParseResponse(response);
|
||||
|
||||
items.Should().BeEmpty();
|
||||
_httpClient.Verify(c => c.Execute(It.IsAny<HttpRequest>()), Times.Never);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_skip_entries_with_null_basic_information()
|
||||
{
|
||||
var response = BuildWantlistResponse(@"{
|
||||
""wants"": [
|
||||
{
|
||||
""basic_information"": null
|
||||
}
|
||||
]
|
||||
}");
|
||||
|
||||
var items = _parser.ParseResponse(response);
|
||||
|
||||
items.Should().BeEmpty();
|
||||
_httpClient.Verify(c => c.Execute(It.IsAny<HttpRequest>()), Times.Never);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_throw_when_discogs_returns_html()
|
||||
{
|
||||
var response = BuildWantlistResponse("<html></html>", contentType: "text/html");
|
||||
|
||||
_parser.Invoking(p => p.ParseResponse(response))
|
||||
.Should().Throw<ImportListException>()
|
||||
.WithMessage("*HTML content*");
|
||||
}
|
||||
|
||||
private ImportListResponse BuildWantlistResponse(string content, HttpStatusCode statusCode = HttpStatusCode.OK, string contentType = "application/json")
|
||||
{
|
||||
var httpRequest = new HttpRequest("https://api.discogs.com/users/user/wants");
|
||||
var importListRequest = new ImportListRequest(httpRequest);
|
||||
var headers = new HttpHeader { ContentType = contentType };
|
||||
var httpResponse = new HttpResponse(httpRequest, headers, content, statusCode);
|
||||
|
||||
return new ImportListResponse(importListRequest, httpResponse);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.ImportLists;
|
||||
using NzbDrone.Core.ImportLists.Discogs;
|
||||
using NzbDrone.Core.ImportLists.LidarrLists;
|
||||
using NzbDrone.Core.Lifecycle;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
|
@ -19,6 +20,7 @@ public void Setup()
|
|||
_importLists = new List<IImportList>();
|
||||
|
||||
_importLists.Add(Mocker.Resolve<LidarrLists>());
|
||||
_importLists.Add(Mocker.Resolve<DiscogsLists>());
|
||||
|
||||
Mocker.SetConstant<IEnumerable<IImportList>>(_importLists);
|
||||
}
|
||||
|
|
|
|||
58
src/NzbDrone.Core/ImportLists/Discogs/DiscogsApi.cs
Normal file
58
src/NzbDrone.Core/ImportLists/Discogs/DiscogsApi.cs
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NzbDrone.Core.ImportLists.Discogs;
|
||||
|
||||
public class DiscogsListResponse
|
||||
{
|
||||
public List<DiscogsListItem> Items { get; set; }
|
||||
}
|
||||
|
||||
public class DiscogsListItem
|
||||
{
|
||||
public string Type { get; set; }
|
||||
public int Id { get; set; }
|
||||
[JsonProperty("display_title")]
|
||||
public string DisplayTitle { get; set; }
|
||||
[JsonProperty("resource_url")]
|
||||
public string ResourceUrl { get; set; }
|
||||
public string Uri { get; set; }
|
||||
}
|
||||
|
||||
public class DiscogsReleaseResponse
|
||||
{
|
||||
public string Title { get; set; }
|
||||
public List<DiscogsReleaseArtist> Artists { get; set; }
|
||||
}
|
||||
|
||||
public class DiscogsReleaseArtist
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public int Id { get; set; }
|
||||
}
|
||||
|
||||
public class DiscogsArtistResponse
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public int Id { get; set; }
|
||||
}
|
||||
|
||||
public class DiscogsWantlistResponse
|
||||
{
|
||||
public List<DiscogsWantlistItem> Wants { get; set; }
|
||||
}
|
||||
|
||||
public class DiscogsWantlistItem
|
||||
{
|
||||
[JsonProperty("basic_information")]
|
||||
public DiscogsBasicInformation BasicInformation { get; set; }
|
||||
}
|
||||
|
||||
public class DiscogsBasicInformation
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Title { get; set; }
|
||||
[JsonProperty("resource_url")]
|
||||
public string ResourceUrl { get; set; }
|
||||
public List<DiscogsReleaseArtist> Artists { get; set; }
|
||||
}
|
||||
35
src/NzbDrone.Core/ImportLists/Discogs/DiscogsLists.cs
Normal file
35
src/NzbDrone.Core/ImportLists/Discogs/DiscogsLists.cs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
using System;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Parser;
|
||||
|
||||
namespace NzbDrone.Core.ImportLists.Discogs
|
||||
{
|
||||
public class DiscogsLists : HttpImportListBase<DiscogsListsSettings>
|
||||
{
|
||||
public override string Name => "Discogs Lists";
|
||||
public override ImportListType ListType => ImportListType.Discogs;
|
||||
public override TimeSpan MinRefreshInterval => TimeSpan.FromHours(12);
|
||||
public override TimeSpan RateLimit => TimeSpan.FromSeconds(3);
|
||||
|
||||
public DiscogsLists(IHttpClient httpClient,
|
||||
IImportListStatusService importListStatusService,
|
||||
IConfigService configService,
|
||||
IParsingService parsingService,
|
||||
Logger logger)
|
||||
: base(httpClient, importListStatusService, configService, parsingService, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public override IImportListRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
return new DiscogsListsRequestGenerator(Settings);
|
||||
}
|
||||
|
||||
public override IParseImportListResponse GetParser()
|
||||
{
|
||||
return new DiscogsListsParser(Settings, _httpClient, _logger);
|
||||
}
|
||||
}
|
||||
}
|
||||
81
src/NzbDrone.Core/ImportLists/Discogs/DiscogsListsParser.cs
Normal file
81
src/NzbDrone.Core/ImportLists/Discogs/DiscogsListsParser.cs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.ImportLists.Discogs;
|
||||
|
||||
public class DiscogsListsParser(DiscogsListsSettings settings, IHttpClient httpClient, Logger logger)
|
||||
: IParseImportListResponse
|
||||
{
|
||||
private readonly DiscogsListsSettings _settings = settings;
|
||||
private readonly IHttpClient _httpClient = httpClient;
|
||||
private readonly Logger _logger = logger;
|
||||
|
||||
public IList<ImportListItemInfo> ParseResponse(ImportListResponse importListResponse)
|
||||
{
|
||||
var items = new List<ImportListItemInfo>();
|
||||
|
||||
if (!PreProcess(importListResponse))
|
||||
{
|
||||
return items;
|
||||
}
|
||||
|
||||
var jsonResponse = Json.Deserialize<DiscogsListResponse>(importListResponse.Content);
|
||||
|
||||
if (jsonResponse?.Items == null)
|
||||
{
|
||||
return items;
|
||||
}
|
||||
|
||||
foreach (var item in jsonResponse.Items)
|
||||
{
|
||||
if (item.ResourceUrl.IsNullOrWhiteSpace())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ImportListItemInfo itemInfo = null;
|
||||
|
||||
if (item.Type == "release")
|
||||
{
|
||||
itemInfo = FetchReleaseDetails(item.ResourceUrl);
|
||||
}
|
||||
else if (item.Type == "artist")
|
||||
{
|
||||
itemInfo = FetchArtistDetails(item.ResourceUrl);
|
||||
}
|
||||
|
||||
items.AddIfNotNull(itemInfo);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Discogs API call resulted in an unexpected exception for {0} type item", item.Type ?? "unknown");
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private bool PreProcess(ImportListResponse importListResponse)
|
||||
{
|
||||
DiscogsParserHelper.EnsureValidResponse(importListResponse,
|
||||
"Discogs API responded with HTML content. List may be too large or API may be unavailable.");
|
||||
return true;
|
||||
}
|
||||
|
||||
private ImportListItemInfo FetchReleaseDetails(string resourceUrl)
|
||||
{
|
||||
return DiscogsParserHelper.FetchReleaseDetails(_httpClient, _settings.Token, resourceUrl);
|
||||
}
|
||||
|
||||
private ImportListItemInfo FetchArtistDetails(string resourceUrl)
|
||||
{
|
||||
return DiscogsParserHelper.FetchArtistDetails(_httpClient, _settings.Token, resourceUrl);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
using System.Collections.Generic;
|
||||
using NzbDrone.Common.Http;
|
||||
|
||||
namespace NzbDrone.Core.ImportLists.Discogs
|
||||
{
|
||||
public class DiscogsListsRequestGenerator : IImportListRequestGenerator
|
||||
{
|
||||
private readonly DiscogsListsSettings _settings;
|
||||
|
||||
public DiscogsListsRequestGenerator(DiscogsListsSettings settings)
|
||||
{
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
public virtual ImportListPageableRequestChain GetListItems()
|
||||
{
|
||||
var pageableRequests = new ImportListPageableRequestChain();
|
||||
pageableRequests.Add(GetPagedRequests());
|
||||
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
private IEnumerable<ImportListRequest> GetPagedRequests()
|
||||
{
|
||||
var request = new HttpRequestBuilder(_settings.BaseUrl.TrimEnd('/'))
|
||||
.Resource($"/lists/{_settings.ListId}")
|
||||
.SetHeader("Authorization", $"Discogs token={_settings.Token}")
|
||||
.Build();
|
||||
|
||||
yield return new ImportListRequest(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
using FluentValidation;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.ImportLists.Discogs
|
||||
{
|
||||
public class DiscogsListsSettingsValidator : AbstractValidator<DiscogsListsSettings>
|
||||
{
|
||||
public DiscogsListsSettingsValidator()
|
||||
{
|
||||
RuleFor(c => c.Token).NotEmpty();
|
||||
RuleFor(c => c.ListId).NotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public class DiscogsListsSettings : IImportListSettings
|
||||
{
|
||||
private static readonly DiscogsListsSettingsValidator Validator = new ();
|
||||
|
||||
public DiscogsListsSettings()
|
||||
{
|
||||
BaseUrl = "https://api.discogs.com";
|
||||
}
|
||||
|
||||
public string BaseUrl { get; set; }
|
||||
|
||||
[FieldDefinition(0, Label = "Token", Privacy = PrivacyLevel.ApiKey, HelpText = "Discogs Personal Access Token")]
|
||||
public string Token { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "List ID", HelpText = "ID of the Discogs list to import")]
|
||||
public string ListId { get; set; }
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
82
src/NzbDrone.Core/ImportLists/Discogs/DiscogsParserHelper.cs
Normal file
82
src/NzbDrone.Core/ImportLists/Discogs/DiscogsParserHelper.cs
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
using System.Linq;
|
||||
using System.Net;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.ImportLists.Exceptions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.ImportLists.Discogs;
|
||||
|
||||
internal static class DiscogsParserHelper
|
||||
{
|
||||
public static ImportListItemInfo FetchReleaseDetails(IHttpClient httpClient, string token, string resourceUrl)
|
||||
{
|
||||
var request = new HttpRequestBuilder(resourceUrl)
|
||||
.SetHeader("Authorization", $"Discogs token={token}")
|
||||
.Build();
|
||||
|
||||
var response = httpClient.Execute(request);
|
||||
|
||||
if (response.StatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var releaseResponse = Json.Deserialize<DiscogsReleaseResponse>(response.Content);
|
||||
|
||||
if (releaseResponse?.Artists?.Any() != true || releaseResponse.Title.IsNullOrWhiteSpace())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ImportListItemInfo
|
||||
{
|
||||
Artist = releaseResponse.Artists.First().Name,
|
||||
Album = releaseResponse.Title
|
||||
};
|
||||
}
|
||||
|
||||
public static ImportListItemInfo FetchArtistDetails(IHttpClient httpClient, string token, string resourceUrl)
|
||||
{
|
||||
var request = new HttpRequestBuilder(resourceUrl)
|
||||
.SetHeader("Authorization", $"Discogs token={token}")
|
||||
.Build();
|
||||
|
||||
var response = httpClient.Execute(request);
|
||||
|
||||
if (response.StatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var artistResponse = Json.Deserialize<DiscogsArtistResponse>(response.Content);
|
||||
|
||||
if (artistResponse?.Name.IsNullOrWhiteSpace() == true)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ImportListItemInfo
|
||||
{
|
||||
Artist = artistResponse.Name,
|
||||
Album = null // Artists don't have a specific album, just the artist name
|
||||
};
|
||||
}
|
||||
|
||||
public static void EnsureValidResponse(ImportListResponse importListResponse, string htmlContentMessage)
|
||||
{
|
||||
if (importListResponse.HttpResponse.StatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
throw new ImportListException(importListResponse,
|
||||
"Discogs API call resulted in an unexpected StatusCode [{0}]",
|
||||
importListResponse.HttpResponse.StatusCode);
|
||||
}
|
||||
|
||||
if (importListResponse.HttpResponse.Headers.ContentType != null &&
|
||||
importListResponse.HttpResponse.Headers.ContentType.Contains("text/html"))
|
||||
{
|
||||
throw new ImportListException(importListResponse, htmlContentMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
36
src/NzbDrone.Core/ImportLists/Discogs/DiscogsWantlist.cs
Normal file
36
src/NzbDrone.Core/ImportLists/Discogs/DiscogsWantlist.cs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
using System;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Parser;
|
||||
|
||||
namespace NzbDrone.Core.ImportLists.Discogs
|
||||
{
|
||||
public class DiscogsWantlist : HttpImportListBase<DiscogsWantlistSettings>
|
||||
{
|
||||
public override string Name => "Discogs Wantlist";
|
||||
public override ImportListType ListType => ImportListType.Discogs;
|
||||
public override TimeSpan MinRefreshInterval => TimeSpan.FromHours(12);
|
||||
public override TimeSpan RateLimit => TimeSpan.FromSeconds(3); // Conservative rate limiting to avoid 429 errors when fetching many releases
|
||||
public override int PageSize => 50; // Discogs API supports pagination with page and per_page parameters
|
||||
|
||||
public DiscogsWantlist(IHttpClient httpClient,
|
||||
IImportListStatusService importListStatusService,
|
||||
IConfigService configService,
|
||||
IParsingService parsingService,
|
||||
Logger logger)
|
||||
: base(httpClient, importListStatusService, configService, parsingService, logger)
|
||||
{
|
||||
}
|
||||
|
||||
public override IImportListRequestGenerator GetRequestGenerator()
|
||||
{
|
||||
return new DiscogsWantlistRequestGenerator { Settings = Settings };
|
||||
}
|
||||
|
||||
public override IParseImportListResponse GetParser()
|
||||
{
|
||||
return new DiscogsWantlistParser(Settings, _httpClient);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.ImportLists.Discogs;
|
||||
|
||||
public class DiscogsWantlistParser : IParseImportListResponse
|
||||
{
|
||||
private readonly DiscogsWantlistSettings _settings;
|
||||
private readonly IHttpClient _httpClient;
|
||||
|
||||
public DiscogsWantlistParser(DiscogsWantlistSettings settings, IHttpClient httpClient)
|
||||
{
|
||||
_settings = settings;
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public IList<ImportListItemInfo> ParseResponse(ImportListResponse importListResponse)
|
||||
{
|
||||
var items = new List<ImportListItemInfo>();
|
||||
|
||||
if (!PreProcess(importListResponse))
|
||||
{
|
||||
return items;
|
||||
}
|
||||
|
||||
var jsonResponse = Json.Deserialize<DiscogsWantlistResponse>(importListResponse.Content);
|
||||
|
||||
if (jsonResponse?.Wants == null)
|
||||
{
|
||||
return items;
|
||||
}
|
||||
|
||||
foreach (var want in jsonResponse.Wants)
|
||||
{
|
||||
var basicInfo = want?.BasicInformation;
|
||||
|
||||
if (basicInfo == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// The wantlist API includes artists and title in basic_information, so no need to fetch release details
|
||||
// If you want is artists.First().Name and title, then fetching the release details is redundant according to their API.
|
||||
if (basicInfo.Artists?.Any() != true || basicInfo.Title.IsNullOrWhiteSpace())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
items.AddIfNotNull(new ImportListItemInfo
|
||||
{
|
||||
Artist = basicInfo.Artists.First().Name,
|
||||
Album = basicInfo.Title
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private bool PreProcess(ImportListResponse importListResponse)
|
||||
{
|
||||
DiscogsParserHelper.EnsureValidResponse(importListResponse,
|
||||
"Discogs API responded with HTML content. Wantlist may be too large or API may be unavailable.");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
using System.Collections.Generic;
|
||||
using NzbDrone.Common.Http;
|
||||
|
||||
namespace NzbDrone.Core.ImportLists.Discogs
|
||||
{
|
||||
public class DiscogsWantlistRequestGenerator : IImportListRequestGenerator
|
||||
{
|
||||
public DiscogsWantlistSettings Settings { get; set; }
|
||||
|
||||
public int MaxPages { get; set; }
|
||||
public int PageSize { get; set; }
|
||||
|
||||
public DiscogsWantlistRequestGenerator()
|
||||
{
|
||||
MaxPages = 10; // Allow fetching up to 10 pages
|
||||
PageSize = 50; // Discogs API supports pagination with page and per_page parameters (max 100 per page)
|
||||
}
|
||||
|
||||
public virtual ImportListPageableRequestChain GetListItems()
|
||||
{
|
||||
var pageableRequests = new ImportListPageableRequestChain();
|
||||
pageableRequests.Add(GetPagedRequests());
|
||||
return pageableRequests;
|
||||
}
|
||||
|
||||
private IEnumerable<ImportListRequest> GetPagedRequests()
|
||||
{
|
||||
for (var page = 1; page <= MaxPages; page++)
|
||||
{
|
||||
var request = new HttpRequestBuilder(Settings.BaseUrl.TrimEnd('/'))
|
||||
.Resource($"/users/{Settings.Username}/wants")
|
||||
.AddQueryParam("page", page)
|
||||
.AddQueryParam("per_page", PageSize)
|
||||
.SetHeader("Authorization", $"Discogs token={Settings.Token}")
|
||||
.Build();
|
||||
|
||||
yield return new ImportListRequest(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
using FluentValidation;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.ImportLists.Discogs
|
||||
{
|
||||
public class DiscogsWantlistSettingsValidator : AbstractValidator<DiscogsWantlistSettings>
|
||||
{
|
||||
public DiscogsWantlistSettingsValidator()
|
||||
{
|
||||
RuleFor(c => c.Token).NotEmpty();
|
||||
RuleFor(c => c.Username).NotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public class DiscogsWantlistSettings : IImportListSettings
|
||||
{
|
||||
private static readonly DiscogsWantlistSettingsValidator Validator = new DiscogsWantlistSettingsValidator();
|
||||
|
||||
public DiscogsWantlistSettings()
|
||||
{
|
||||
BaseUrl = "https://api.discogs.com";
|
||||
}
|
||||
|
||||
public string BaseUrl { get; set; }
|
||||
|
||||
[FieldDefinition(0, Label = "Token", Privacy = PrivacyLevel.ApiKey, HelpText = "Discogs Personal Access Token")]
|
||||
public string Token { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "Username", HelpText = "Discogs username whose wantlist to import")]
|
||||
public string Username { get; set; }
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
namespace NzbDrone.Core.ImportLists
|
||||
namespace NzbDrone.Core.ImportLists;
|
||||
|
||||
public enum ImportListType
|
||||
{
|
||||
public enum ImportListType
|
||||
{
|
||||
Program,
|
||||
Spotify,
|
||||
LastFm,
|
||||
Other,
|
||||
Advanced
|
||||
}
|
||||
Program,
|
||||
Spotify,
|
||||
LastFm,
|
||||
Discogs,
|
||||
Other,
|
||||
Advanced
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue