diff --git a/src/NzbDrone.Core/ImportLists/Goodreads/Series/GoodreadsSeriesImportList.cs b/src/NzbDrone.Core/ImportLists/Goodreads/Series/GoodreadsSeriesImportList.cs new file mode 100644 index 000000000..7352a10ba --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/Goodreads/Series/GoodreadsSeriesImportList.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Net; +using FluentValidation.Results; +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.Parser.Model; + +namespace NzbDrone.Core.ImportLists.Goodreads +{ + public class GoodreadsSeriesImportList : ImportListBase + { + private readonly IProvideSeriesInfo _seriesInfo; + + public override string Name => "Goodreads Series"; + public override ImportListType ListType => ImportListType.Goodreads; + + public GoodreadsSeriesImportList(IProvideSeriesInfo seriesInfo, + IImportListStatusService importListStatusService, + IConfigService configService, + IParsingService parsingService, + Logger logger) + : base(importListStatusService, configService, parsingService, logger) + { + _seriesInfo = seriesInfo; + } + + public override IList Fetch() + { + var result = new List(); + + try + { + var series = _seriesInfo.GetSeriesInfo(Settings.SeriesId); + + foreach (var work in series.Works) + { + result.Add(new ImportListItemInfo + { + BookGoodreadsId = work.Id.ToString(), + Book = work.OriginalTitle, + EditionGoodreadsId = work.BestBook.Id.ToString(), + Author = work.BestBook.AuthorName, + AuthorGoodreadsId = work.BestBook.AuthorId.ToString() + }); + } + + _importListStatusService.RecordSuccess(Definition.Id); + } + catch + { + _importListStatusService.RecordFailure(Definition.Id); + } + + return CleanupListItems(result); + } + + protected override void Test(List failures) + { + failures.AddIfNotNull(TestConnection()); + } + + private ValidationFailure TestConnection() + { + try + { + _seriesInfo.GetSeriesInfo(Settings.SeriesId); + return null; + } + catch (HttpException e) + { + _logger.Warn(e, "Goodreads API Error"); + if (e.Response.StatusCode == HttpStatusCode.NotFound) + { + return new ValidationFailure(nameof(Settings.SeriesId), $"Series {Settings.SeriesId} not found"); + } + + return new ValidationFailure(nameof(Settings.SeriesId), $"Could not get series data"); + } + 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"); + } + } + } +} diff --git a/src/NzbDrone.Core/ImportLists/Goodreads/Series/GoodreadsSeriesImportListSettings.cs b/src/NzbDrone.Core/ImportLists/Goodreads/Series/GoodreadsSeriesImportListSettings.cs new file mode 100644 index 000000000..6e3b5dacf --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/Goodreads/Series/GoodreadsSeriesImportListSettings.cs @@ -0,0 +1,34 @@ +using FluentValidation; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.ImportLists.Goodreads +{ + public class GoodreadsSeriesImportListValidator : AbstractValidator + { + public GoodreadsSeriesImportListValidator() + { + RuleFor(c => c.SeriesId).GreaterThan(0); + } + } + + public class GoodreadsSeriesImportListSettings : IImportListSettings + { + private static readonly GoodreadsSeriesImportListValidator Validator = new (); + + public GoodreadsSeriesImportListSettings() + { + BaseUrl = "www.goodreads.com"; + } + + public string BaseUrl { get; set; } + + [FieldDefinition(0, Label = "Series ID", HelpText = "Goodreads series ID")] + public int SeriesId { get; set; } + + public NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate(this)); + } + } +} diff --git a/src/NzbDrone.Core/MetadataSource/Goodreads/GoodreadsProxy.cs b/src/NzbDrone.Core/MetadataSource/Goodreads/GoodreadsProxy.cs index b8b772759..898be1e77 100644 --- a/src/NzbDrone.Core/MetadataSource/Goodreads/GoodreadsProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/Goodreads/GoodreadsProxy.cs @@ -17,7 +17,7 @@ using NzbDrone.Core.Parser; namespace NzbDrone.Core.MetadataSource.Goodreads { - public class GoodreadsProxy : IProvideBookInfo + public class GoodreadsProxy : IProvideBookInfo, IProvideSeriesInfo { private static readonly RegexReplace FullSizeImageRegex = new RegexReplace(@"\._[SU][XY]\d+_.jpg$", ".jpg", @@ -307,8 +307,35 @@ namespace NzbDrone.Core.MetadataSource.Goodreads return result; } + public SeriesResource GetSeriesInfo(int foreignSeriesId, bool useCache = true) { + _logger.Debug("Getting Series with GoodreadsId of {0}", foreignSeriesId); + var httpRequest = _requestBuilder.Create() + .SetSegment("route", $"series/{foreignSeriesId}") + .AddQueryParam("format", "xml") + .Build(); + + httpRequest.AllowAutoRedirect = true; + httpRequest.SuppressHttpError = true; + + var httpResponse = _cachedHttpClient.Get(httpRequest, useCache, TimeSpan.FromDays(7)); + + if (httpResponse.HasHttpError) + { + if (httpResponse.StatusCode == HttpStatusCode.BadRequest) + { + throw new BadRequestException(foreignSeriesId.ToString()); + } + else + { + throw new HttpException(httpRequest, httpResponse); + } + } + + var resource = httpResponse.Deserialize(); + + return resource.Series; } private bool TryGetBookInfo(string foreignEditionId, bool useCache, out Tuple> result) diff --git a/src/NzbDrone.Core/MetadataSource/Goodreads/Resources/ShowSeriesResource.cs b/src/NzbDrone.Core/MetadataSource/Goodreads/Resources/ShowSeriesResource.cs new file mode 100644 index 000000000..3590ef2b5 --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/Goodreads/Resources/ShowSeriesResource.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Xml.Linq; + +namespace NzbDrone.Core.MetadataSource.Goodreads +{ + /// + /// This class models the best book in a work, as defined by the Goodreads API. + /// + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public sealed class ShowSeriesResource : GoodreadsResource + { + public override string ElementName => "series"; + + public SeriesResource Series { get; private set; } + + public override void Parse(XElement element) + { + Series = new SeriesResource(); + Series.Parse(element); + + Series.Works = element.ParseChildren("series_works", "series_work"); + } + } +} diff --git a/src/NzbDrone.Core/MetadataSource/IProvideSeriesInfo.cs b/src/NzbDrone.Core/MetadataSource/IProvideSeriesInfo.cs new file mode 100644 index 000000000..a8fa2e824 --- /dev/null +++ b/src/NzbDrone.Core/MetadataSource/IProvideSeriesInfo.cs @@ -0,0 +1,9 @@ +using NzbDrone.Core.MetadataSource.Goodreads; + +namespace NzbDrone.Core.MetadataSource +{ + public interface IProvideSeriesInfo + { + SeriesResource GetSeriesInfo(int id, bool useCache = true); + } +}