mirror of
https://github.com/Readarr/Readarr.git
synced 2026-04-26 22:46:37 -04:00
New: Use Goodreads directly, allow multiple editions of a book (new DB required)
This commit is contained in:
+1
-1
@@ -142,7 +142,7 @@ namespace NzbDrone.Core.MetadataSource.Goodreads
|
||||
|
||||
try
|
||||
{
|
||||
return new DateTime(publicationYear.Value, publicationMonth.Value, publicationDay.Value);
|
||||
return new DateTime(publicationYear.Value, publicationMonth.Value, publicationDay.Value, 0, 0, 0, DateTimeKind.Utc);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Net;
|
||||
using NzbDrone.Core.Exceptions;
|
||||
|
||||
namespace NzbDrone.Core.MetadataSource.Goodreads
|
||||
{
|
||||
public class GoodreadsException : NzbDroneClientException
|
||||
{
|
||||
public GoodreadsException(string message)
|
||||
: base(HttpStatusCode.ServiceUnavailable, message)
|
||||
{
|
||||
}
|
||||
|
||||
public GoodreadsException(string message, params object[] args)
|
||||
: base(HttpStatusCode.ServiceUnavailable, message, args)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,764 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Books;
|
||||
using NzbDrone.Core.Exceptions;
|
||||
using NzbDrone.Core.MediaCover;
|
||||
using NzbDrone.Core.Parser;
|
||||
|
||||
namespace NzbDrone.Core.MetadataSource.Goodreads
|
||||
{
|
||||
public class GoodreadsProxy : IProvideAuthorInfo, ISearchForNewAuthor, IProvideBookInfo, ISearchForNewBook, ISearchForNewEntity
|
||||
{
|
||||
private static readonly RegexReplace FullSizeImageRegex = new RegexReplace(@"\._[SU][XY]\d+_.jpg$",
|
||||
".jpg",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex DuplicateSpacesRegex = new Regex(@"\s{2,}", RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex NoPhotoRegex = new Regex(@"/nophoto/(book|user)/",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly Logger _logger;
|
||||
private readonly IAuthorService _authorService;
|
||||
private readonly IBookService _bookService;
|
||||
private readonly IEditionService _editionService;
|
||||
private readonly IHttpRequestBuilderFactory _requestBuilder;
|
||||
private readonly IHttpRequestBuilderFactory _searchBuilder;
|
||||
private readonly ICached<HashSet<string>> _cache;
|
||||
|
||||
public GoodreadsProxy(IHttpClient httpClient,
|
||||
IAuthorService authorService,
|
||||
IBookService bookService,
|
||||
IEditionService editionService,
|
||||
Logger logger,
|
||||
ICacheManager cacheManager)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_authorService = authorService;
|
||||
_bookService = bookService;
|
||||
_editionService = editionService;
|
||||
_cache = cacheManager.GetCache<HashSet<string>>(GetType());
|
||||
_logger = logger;
|
||||
|
||||
_requestBuilder = new HttpRequestBuilder("https://www.goodreads.com/{route}")
|
||||
.AddQueryParam("key", new string("gSuM2Onzl6sjMU25HY1Xcd".Reverse().ToArray()))
|
||||
.AddQueryParam("_nc", "1")
|
||||
.SetHeader("User-Agent", "Dalvik/1.6.0 (Linux; U; Android 4.1.2; GT-I9100 Build/JZO54K)")
|
||||
.KeepAlive()
|
||||
.CreateFactory();
|
||||
|
||||
_searchBuilder = new HttpRequestBuilder("https://www.goodreads.com/book/auto_complete")
|
||||
.AddQueryParam("format", "json")
|
||||
.SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36")
|
||||
.KeepAlive()
|
||||
.CreateFactory();
|
||||
}
|
||||
|
||||
public HashSet<string> GetChangedArtists(DateTime startTime)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public Author GetAuthorInfo(string foreignAuthorId)
|
||||
{
|
||||
_logger.Debug("Getting Author details GoodreadsId of {0}", foreignAuthorId);
|
||||
|
||||
var httpRequest = _requestBuilder.Create()
|
||||
.SetSegment("route", $"author/show/{foreignAuthorId}.xml")
|
||||
.AddQueryParam("exclude_books", "true")
|
||||
.Build();
|
||||
|
||||
httpRequest.AllowAutoRedirect = true;
|
||||
httpRequest.SuppressHttpError = true;
|
||||
|
||||
var httpResponse = _httpClient.Get(httpRequest);
|
||||
|
||||
if (httpResponse.HasHttpError)
|
||||
{
|
||||
if (httpResponse.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
throw new AuthorNotFoundException(foreignAuthorId);
|
||||
}
|
||||
else if (httpResponse.StatusCode == HttpStatusCode.BadRequest)
|
||||
{
|
||||
throw new BadRequestException(foreignAuthorId);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new HttpException(httpRequest, httpResponse);
|
||||
}
|
||||
}
|
||||
|
||||
var resource = httpResponse.Deserialize<AuthorResource>();
|
||||
var author = new Author
|
||||
{
|
||||
Metadata = MapAuthor(resource)
|
||||
};
|
||||
author.CleanName = Parser.Parser.CleanAuthorName(author.Metadata.Value.Name);
|
||||
author.SortName = Parser.Parser.NormalizeTitle(author.Metadata.Value.Name);
|
||||
|
||||
// we can only get a rating from the author list page...
|
||||
var listResource = GetAuthorBooksPageResource(foreignAuthorId, 10, 1);
|
||||
var authorResource = listResource.List.First().Authors.First(a => a.Id.ToString() == foreignAuthorId);
|
||||
author.Metadata.Value.Ratings = new Ratings
|
||||
{
|
||||
Votes = authorResource.RatingsCount ?? 0,
|
||||
Value = authorResource.AverageRating ?? 0
|
||||
};
|
||||
|
||||
return author;
|
||||
}
|
||||
|
||||
public Author GetAuthorAndBooks(string foreignAuthorId, double minPopularity = 0)
|
||||
{
|
||||
var author = GetAuthorInfo(foreignAuthorId);
|
||||
|
||||
var bookList = GetAuthorBooks(foreignAuthorId, minPopularity);
|
||||
var books = bookList.Select(x => GetBookInfo(x.Editions.Value.First().ForeignEditionId).Item2).ToList();
|
||||
|
||||
var existingAuthor = _authorService.FindById(foreignAuthorId);
|
||||
if (existingAuthor != null)
|
||||
{
|
||||
var existingEditions = _editionService.GetEditionsByAuthor(existingAuthor.Id);
|
||||
var extraEditionIds = existingEditions.Select(x => x.ForeignEditionId).Except(books.Select(x => x.Editions.Value.First().ForeignEditionId));
|
||||
|
||||
_logger.Debug($"Getting data for extra editions {extraEditionIds.ConcatToString()}");
|
||||
var extraEditions = extraEditionIds.Select(x => GetBookInfo(x));
|
||||
|
||||
var bookDict = books.ToDictionary(x => x.ForeignBookId);
|
||||
foreach (var edition in extraEditions)
|
||||
{
|
||||
var b = edition.Item2;
|
||||
|
||||
if (bookDict.TryGetValue(b.ForeignBookId, out var book))
|
||||
{
|
||||
book.Editions.Value.Add(b.Editions.Value.First());
|
||||
}
|
||||
else
|
||||
{
|
||||
bookDict.Add(b.ForeignBookId, b);
|
||||
}
|
||||
}
|
||||
|
||||
books = bookDict.Values.ToList();
|
||||
}
|
||||
|
||||
books.ForEach(x => x.AuthorMetadata = author.Metadata.Value);
|
||||
author.Books = books;
|
||||
|
||||
author.Series = GetAuthorSeries(foreignAuthorId, author.Books);
|
||||
|
||||
return author;
|
||||
}
|
||||
|
||||
private List<Book> GetAuthorBooks(string foreignAuthorId, double minPopularity)
|
||||
{
|
||||
var perPage = 100;
|
||||
var page = 0;
|
||||
|
||||
var result = new List<Book>();
|
||||
List<Book> current;
|
||||
IEnumerable<Book> filtered;
|
||||
|
||||
do
|
||||
{
|
||||
current = GetAuthorBooksPage(foreignAuthorId, perPage, ++page);
|
||||
filtered = current.Where(x => x.Editions.Value.First().Ratings.Popularity >= minPopularity);
|
||||
result.AddRange(filtered);
|
||||
}
|
||||
while (current.Count == perPage && filtered.Any());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<Book> GetAuthorBooksPage(string foreignAuthorId, int perPage, int page)
|
||||
{
|
||||
var resource = GetAuthorBooksPageResource(foreignAuthorId, perPage, page);
|
||||
|
||||
var books = resource?.List.Where(x => x.Authors.First().Id.ToString() == foreignAuthorId)
|
||||
.Select(MapBook)
|
||||
.ToList() ??
|
||||
new List<Book>();
|
||||
|
||||
books.ForEach(x => x.CleanTitle = x.Title.CleanAuthorName());
|
||||
|
||||
return books;
|
||||
}
|
||||
|
||||
private AuthorBookListResource GetAuthorBooksPageResource(string foreignAuthorId, int perPage, int page)
|
||||
{
|
||||
_logger.Debug("Getting Author Books with GoodreadsId of {0}", foreignAuthorId);
|
||||
|
||||
var httpRequest = _requestBuilder.Create()
|
||||
.SetSegment("route", $"author/list/{foreignAuthorId}.xml")
|
||||
.AddQueryParam("per_page", perPage)
|
||||
.AddQueryParam("page", page)
|
||||
.AddQueryParam("sort", "popularity")
|
||||
.Build();
|
||||
|
||||
httpRequest.AllowAutoRedirect = true;
|
||||
httpRequest.SuppressHttpError = true;
|
||||
|
||||
var httpResponse = _httpClient.Get(httpRequest);
|
||||
|
||||
if (httpResponse.HasHttpError)
|
||||
{
|
||||
if (httpResponse.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
throw new AuthorNotFoundException(foreignAuthorId);
|
||||
}
|
||||
else if (httpResponse.StatusCode == HttpStatusCode.BadRequest)
|
||||
{
|
||||
throw new BadRequestException(foreignAuthorId);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new HttpException(httpRequest, httpResponse);
|
||||
}
|
||||
}
|
||||
|
||||
return httpResponse.Deserialize<AuthorBookListResource>();
|
||||
}
|
||||
|
||||
private List<Series> GetAuthorSeries(string foreignAuthorId, List<Book> books)
|
||||
{
|
||||
_logger.Debug("Getting Author Series with GoodreadsId of {0}", foreignAuthorId);
|
||||
|
||||
var httpRequest = _requestBuilder.Create()
|
||||
.SetSegment("route", $"series/list/{foreignAuthorId}.xml")
|
||||
.Build();
|
||||
|
||||
httpRequest.AllowAutoRedirect = true;
|
||||
httpRequest.SuppressHttpError = true;
|
||||
|
||||
var httpResponse = _httpClient.Get(httpRequest);
|
||||
|
||||
if (httpResponse.HasHttpError)
|
||||
{
|
||||
if (httpResponse.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
throw new AuthorNotFoundException(foreignAuthorId);
|
||||
}
|
||||
else if (httpResponse.StatusCode == HttpStatusCode.BadRequest)
|
||||
{
|
||||
throw new BadRequestException(foreignAuthorId);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new HttpException(httpRequest, httpResponse);
|
||||
}
|
||||
}
|
||||
|
||||
var resource = httpResponse.Deserialize<AuthorSeriesListResource>();
|
||||
|
||||
var result = new List<Series>();
|
||||
var bookDict = books.ToDictionary(x => x.ForeignBookId);
|
||||
|
||||
// only take series where there are some works
|
||||
foreach (var seriesResource in resource.List.Where(x => x.Works.Any()))
|
||||
{
|
||||
var series = MapSeries(seriesResource);
|
||||
series.LinkItems = new List<SeriesBookLink>();
|
||||
|
||||
var works = seriesResource.Works
|
||||
.Where(x => x.BestBook.AuthorId.ToString() == foreignAuthorId &&
|
||||
bookDict.ContainsKey(x.Id.ToString()));
|
||||
foreach (var work in works)
|
||||
{
|
||||
series.LinkItems.Value.Add(new SeriesBookLink
|
||||
{
|
||||
Book = bookDict[work.Id.ToString()],
|
||||
Series = series,
|
||||
IsPrimary = true,
|
||||
Position = work.UserPosition
|
||||
});
|
||||
}
|
||||
|
||||
if (series.LinkItems.Value.Any())
|
||||
{
|
||||
result.Add(series);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public HashSet<string> GetChangedBooks(DateTime startTime)
|
||||
{
|
||||
return _cache.Get("ChangedBooks", () => GetChangedBooksUncached(startTime), TimeSpan.FromMinutes(30));
|
||||
}
|
||||
|
||||
private HashSet<string> GetChangedBooksUncached(DateTime startTime)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public Tuple<string, Book, List<AuthorMetadata>> GetBookInfo(string foreignEditionId)
|
||||
{
|
||||
_logger.Debug("Getting Book with GoodreadsId of {0}", foreignEditionId);
|
||||
|
||||
var httpRequest = _requestBuilder.Create()
|
||||
.SetSegment("route", $"api/book/basic_book_data/{foreignEditionId}")
|
||||
.AddQueryParam("format", "xml")
|
||||
.Build();
|
||||
|
||||
httpRequest.AllowAutoRedirect = true;
|
||||
httpRequest.SuppressHttpError = true;
|
||||
|
||||
var httpResponse = _httpClient.Get(httpRequest);
|
||||
|
||||
if (httpResponse.HasHttpError)
|
||||
{
|
||||
if (httpResponse.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
throw new BookNotFoundException(foreignEditionId);
|
||||
}
|
||||
else if (httpResponse.StatusCode == HttpStatusCode.BadRequest)
|
||||
{
|
||||
throw new BadRequestException(foreignEditionId);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new HttpException(httpRequest, httpResponse);
|
||||
}
|
||||
}
|
||||
|
||||
var resource = httpResponse.Deserialize<BookResource>();
|
||||
|
||||
var book = MapBook(resource);
|
||||
book.CleanTitle = Parser.Parser.CleanAuthorName(book.Title);
|
||||
|
||||
var authors = resource.Authors.SelectList(MapAuthor);
|
||||
book.AuthorMetadata = authors.First();
|
||||
|
||||
return new Tuple<string, Book, List<AuthorMetadata>>(resource.Authors.First().Id.ToString(), book, authors);
|
||||
}
|
||||
|
||||
public List<Author> SearchForNewAuthor(string title)
|
||||
{
|
||||
var books = SearchForNewBook(title, null);
|
||||
|
||||
return books.Select(x => x.Author.Value).ToList();
|
||||
}
|
||||
|
||||
public List<Book> SearchForNewBook(string title, string author)
|
||||
{
|
||||
try
|
||||
{
|
||||
var lowerTitle = title.ToLowerInvariant();
|
||||
|
||||
var split = lowerTitle.Split(':');
|
||||
var prefix = split[0];
|
||||
|
||||
if (split.Length == 2 && new[] { "readarr", "readarrid", "goodreads", "isbn", "asin" }.Contains(prefix))
|
||||
{
|
||||
var slug = split[1].Trim();
|
||||
|
||||
if (slug.IsNullOrWhiteSpace() || slug.Any(char.IsWhiteSpace))
|
||||
{
|
||||
return new List<Book>();
|
||||
}
|
||||
|
||||
if (prefix == "goodreads" || prefix == "readarr" || prefix == "readarrid")
|
||||
{
|
||||
var isValid = int.TryParse(slug, out var searchId);
|
||||
if (!isValid)
|
||||
{
|
||||
return new List<Book>();
|
||||
}
|
||||
|
||||
return SearchByGoodreadsId(searchId);
|
||||
}
|
||||
else if (prefix == "isbn")
|
||||
{
|
||||
return SearchByIsbn(slug);
|
||||
}
|
||||
else if (prefix == "asin")
|
||||
{
|
||||
return SearchByAsin(slug);
|
||||
}
|
||||
}
|
||||
|
||||
var q = title.ToLower().Trim();
|
||||
if (author != null)
|
||||
{
|
||||
q += " " + author;
|
||||
}
|
||||
|
||||
return SearchByField("all", q);
|
||||
}
|
||||
catch (HttpException)
|
||||
{
|
||||
throw new GoodreadsException("Search for '{0}' failed. Unable to communicate with Goodreads.", title);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn(ex, ex.Message);
|
||||
throw new GoodreadsException("Search for '{0}' failed. Invalid response received from Goodreads.", title);
|
||||
}
|
||||
}
|
||||
|
||||
public List<Book> SearchByIsbn(string isbn)
|
||||
{
|
||||
return SearchByField("isbn", isbn);
|
||||
}
|
||||
|
||||
public List<Book> SearchByAsin(string asin)
|
||||
{
|
||||
return SearchByField("isbn", asin);
|
||||
}
|
||||
|
||||
public List<Book> SearchByGoodreadsId(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var remote = GetBookInfo(id.ToString());
|
||||
|
||||
var book = _bookService.FindById(remote.Item2.ForeignBookId);
|
||||
var result = book ?? remote.Item2;
|
||||
|
||||
var edition = _editionService.GetEditionByForeignEditionId(remote.Item2.Editions.Value.Single(x => x.Monitored).ForeignEditionId);
|
||||
if (edition != null)
|
||||
{
|
||||
result.Editions = new List<Edition> { edition };
|
||||
}
|
||||
|
||||
var author = _authorService.FindById(remote.Item1);
|
||||
if (author == null)
|
||||
{
|
||||
author = new Author
|
||||
{
|
||||
CleanName = Parser.Parser.CleanAuthorName(remote.Item2.AuthorMetadata.Value.Name),
|
||||
Metadata = remote.Item2.AuthorMetadata.Value
|
||||
};
|
||||
}
|
||||
|
||||
result.Author = author;
|
||||
|
||||
return new List<Book> { result };
|
||||
}
|
||||
catch (BookNotFoundException)
|
||||
{
|
||||
return new List<Book>();
|
||||
}
|
||||
}
|
||||
|
||||
public List<Book> SearchByField(string field, string query)
|
||||
{
|
||||
try
|
||||
{
|
||||
var httpRequest = _searchBuilder.Create()
|
||||
.AddQueryParam("q", query)
|
||||
.Build();
|
||||
|
||||
var result = _httpClient.Get<List<SearchJsonResource>>(httpRequest);
|
||||
|
||||
return result.Resource.SelectList(MapJsonSearchResult);
|
||||
}
|
||||
catch (HttpException)
|
||||
{
|
||||
throw new GoodreadsException("Search for {0} '{1}' failed. Unable to communicate with Goodreads.", field, query);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn(ex, ex.Message);
|
||||
throw new GoodreadsException("Search for {0} '{1}' failed. Invalid response received from Goodreads.", field, query);
|
||||
}
|
||||
}
|
||||
|
||||
public List<object> SearchForNewEntity(string title)
|
||||
{
|
||||
var books = SearchForNewBook(title, null);
|
||||
|
||||
var result = new List<object>();
|
||||
foreach (var book in books)
|
||||
{
|
||||
var author = book.Author.Value;
|
||||
|
||||
if (!result.Contains(author))
|
||||
{
|
||||
result.Add(author);
|
||||
}
|
||||
|
||||
result.Add(book);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static AuthorMetadata MapAuthor(AuthorResource resource)
|
||||
{
|
||||
var author = new AuthorMetadata
|
||||
{
|
||||
ForeignAuthorId = resource.Id.ToString(),
|
||||
TitleSlug = resource.Id.ToString(),
|
||||
Name = resource.Name.CleanSpaces(),
|
||||
Overview = resource.About,
|
||||
Gender = resource.Gender,
|
||||
Hometown = resource.Hometown,
|
||||
Born = resource.BornOnDate,
|
||||
Died = resource.DiedOnDate,
|
||||
Status = resource.DiedOnDate < DateTime.UtcNow ? AuthorStatusType.Ended : AuthorStatusType.Continuing
|
||||
};
|
||||
|
||||
if (!NoPhotoRegex.IsMatch(resource.LargeImageUrl))
|
||||
{
|
||||
author.Images.Add(new MediaCover.MediaCover
|
||||
{
|
||||
Url = FullSizeImageRegex.Replace(resource.LargeImageUrl),
|
||||
CoverType = MediaCoverTypes.Poster
|
||||
});
|
||||
}
|
||||
|
||||
author.Links.Add(new Links { Url = resource.Link, Name = "Goodreads" });
|
||||
|
||||
return author;
|
||||
}
|
||||
|
||||
private static AuthorMetadata MapAuthor(AuthorSummaryResource resource)
|
||||
{
|
||||
var author = new AuthorMetadata
|
||||
{
|
||||
ForeignAuthorId = resource.Id.ToString(),
|
||||
Name = resource.Name.CleanSpaces(),
|
||||
TitleSlug = resource.Id.ToString()
|
||||
};
|
||||
|
||||
if (resource.RatingsCount.HasValue)
|
||||
{
|
||||
author.Ratings = new Ratings
|
||||
{
|
||||
Votes = resource.RatingsCount ?? 0,
|
||||
Value = resource.AverageRating ?? 0
|
||||
};
|
||||
}
|
||||
|
||||
if (!NoPhotoRegex.IsMatch(resource.ImageUrl))
|
||||
{
|
||||
author.Images.Add(new MediaCover.MediaCover
|
||||
{
|
||||
Url = FullSizeImageRegex.Replace(resource.ImageUrl),
|
||||
CoverType = MediaCoverTypes.Poster
|
||||
});
|
||||
}
|
||||
|
||||
return author;
|
||||
}
|
||||
|
||||
private static Series MapSeries(SeriesResource resource)
|
||||
{
|
||||
var series = new Series
|
||||
{
|
||||
ForeignSeriesId = resource.Id.ToString(),
|
||||
Title = resource.Title,
|
||||
Description = resource.Description,
|
||||
Numbered = resource.IsNumbered,
|
||||
WorkCount = resource.SeriesWorksCount,
|
||||
PrimaryWorkCount = resource.PrimaryWorksCount
|
||||
};
|
||||
|
||||
return series;
|
||||
}
|
||||
|
||||
private static Book MapBook(BookResource resource)
|
||||
{
|
||||
var book = new Book
|
||||
{
|
||||
ForeignBookId = resource.Work.Id.ToString(),
|
||||
Title = (resource.Work.OriginalTitle ?? resource.Title).CleanSpaces(),
|
||||
TitleSlug = resource.Id.ToString(),
|
||||
ReleaseDate = resource.Work.OriginalPublicationDate ?? resource.PublicationDate,
|
||||
Ratings = new Ratings { Votes = resource.Work.RatingsCount, Value = resource.Work.AverageRating },
|
||||
AnyEditionOk = true
|
||||
};
|
||||
|
||||
if (resource.EditionsUrl != null)
|
||||
{
|
||||
book.Links.Add(new Links { Url = resource.EditionsUrl, Name = "Goodreads Editions" });
|
||||
}
|
||||
|
||||
var edition = new Edition
|
||||
{
|
||||
ForeignEditionId = resource.Id.ToString(),
|
||||
TitleSlug = resource.Id.ToString(),
|
||||
Isbn13 = resource.Isbn13,
|
||||
Asin = resource.Asin ?? resource.KindleAsin,
|
||||
Title = resource.TitleWithoutSeries,
|
||||
Language = resource.LanguageCode,
|
||||
Overview = resource.Description,
|
||||
Format = resource.Format,
|
||||
IsEbook = resource.IsEbook,
|
||||
Disambiguation = resource.EditionInformation,
|
||||
Publisher = resource.Publisher,
|
||||
PageCount = resource.Pages,
|
||||
ReleaseDate = resource.PublicationDate,
|
||||
Ratings = new Ratings { Votes = resource.RatingsCount, Value = resource.AverageRating },
|
||||
Monitored = true
|
||||
};
|
||||
|
||||
if (resource.ImageUrl.IsNotNullOrWhiteSpace() && !NoPhotoRegex.IsMatch(resource.ImageUrl))
|
||||
{
|
||||
edition.Images.Add(new MediaCover.MediaCover
|
||||
{
|
||||
Url = FullSizeImageRegex.Replace(resource.ImageUrl),
|
||||
CoverType = MediaCoverTypes.Cover
|
||||
});
|
||||
}
|
||||
|
||||
edition.Links.Add(new Links { Url = resource.Url, Name = "Goodreads Book" });
|
||||
|
||||
book.Editions = new List<Edition> { edition };
|
||||
|
||||
Debug.Assert(!book.Editions.Value.Any() || book.Editions.Value.Count(x => x.Monitored) == 1, "one edition monitored");
|
||||
|
||||
return book;
|
||||
}
|
||||
|
||||
private Book MapSearchResult(WorkResource resource)
|
||||
{
|
||||
var book = _bookService.FindById(resource.Id.ToString());
|
||||
if (resource.BestBook != null)
|
||||
{
|
||||
var edition = _editionService.GetEditionByForeignEditionId(resource.BestBook.Id.ToString());
|
||||
|
||||
if (edition == null)
|
||||
{
|
||||
edition = new Edition
|
||||
{
|
||||
ForeignEditionId = resource.BestBook.Id.ToString(),
|
||||
Title = resource.BestBook.Title,
|
||||
TitleSlug = resource.BestBook.Id.ToString(),
|
||||
Ratings = new Ratings { Votes = resource.RatingsCount, Value = resource.AverageRating },
|
||||
};
|
||||
}
|
||||
|
||||
edition.Monitored = true;
|
||||
edition.ManualAdd = true;
|
||||
|
||||
if (resource.BestBook.ImageUrl.IsNotNullOrWhiteSpace() && !NoPhotoRegex.IsMatch(resource.BestBook.ImageUrl))
|
||||
{
|
||||
edition.Images.Add(new MediaCover.MediaCover
|
||||
{
|
||||
Url = FullSizeImageRegex.Replace(resource.BestBook.ImageUrl),
|
||||
CoverType = MediaCoverTypes.Cover
|
||||
});
|
||||
}
|
||||
|
||||
if (book == null)
|
||||
{
|
||||
book = new Book
|
||||
{
|
||||
ForeignBookId = resource.Id.ToString(),
|
||||
Title = resource.BestBook.Title,
|
||||
TitleSlug = resource.Id.ToString(),
|
||||
ReleaseDate = resource.OriginalPublicationDate,
|
||||
Ratings = new Ratings { Votes = resource.RatingsCount, Value = resource.AverageRating },
|
||||
AnyEditionOk = true
|
||||
};
|
||||
}
|
||||
|
||||
book.Editions = new List<Edition> { edition };
|
||||
|
||||
var authorId = resource.BestBook.AuthorId.ToString();
|
||||
var author = _authorService.FindById(authorId);
|
||||
|
||||
if (author == null)
|
||||
{
|
||||
author = new Author
|
||||
{
|
||||
CleanName = Parser.Parser.CleanAuthorName(resource.BestBook.AuthorName),
|
||||
Metadata = new AuthorMetadata()
|
||||
{
|
||||
ForeignAuthorId = resource.BestBook.AuthorId.ToString(),
|
||||
Name = resource.BestBook.AuthorName,
|
||||
TitleSlug = resource.BestBook.AuthorId.ToString()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
book.Author = author;
|
||||
book.AuthorMetadata = book.Author.Value.Metadata.Value;
|
||||
book.CleanTitle = book.Title.CleanAuthorName();
|
||||
}
|
||||
|
||||
return book;
|
||||
}
|
||||
|
||||
private Book MapJsonSearchResult(SearchJsonResource resource)
|
||||
{
|
||||
var book = _bookService.FindById(resource.WorkId.ToString());
|
||||
var edition = _editionService.GetEditionByForeignEditionId(resource.BookId.ToString());
|
||||
|
||||
if (edition == null)
|
||||
{
|
||||
edition = new Edition
|
||||
{
|
||||
ForeignEditionId = resource.BookId.ToString(),
|
||||
Title = resource.BookTitleBare,
|
||||
TitleSlug = resource.BookId.ToString(),
|
||||
Ratings = new Ratings { Votes = resource.RatingsCount, Value = resource.AverageRating },
|
||||
PageCount = resource.PageCount,
|
||||
Overview = resource.Description?.Html ?? string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
edition.Monitored = true;
|
||||
edition.ManualAdd = true;
|
||||
|
||||
if (resource.ImageUrl.IsNotNullOrWhiteSpace() && !NoPhotoRegex.IsMatch(resource.ImageUrl))
|
||||
{
|
||||
edition.Images.Add(new MediaCover.MediaCover
|
||||
{
|
||||
Url = FullSizeImageRegex.Replace(resource.ImageUrl),
|
||||
CoverType = MediaCoverTypes.Cover
|
||||
});
|
||||
}
|
||||
|
||||
if (book == null)
|
||||
{
|
||||
book = new Book
|
||||
{
|
||||
ForeignBookId = resource.WorkId.ToString(),
|
||||
Title = resource.BookTitleBare,
|
||||
TitleSlug = resource.WorkId.ToString(),
|
||||
Ratings = new Ratings { Votes = resource.RatingsCount, Value = resource.AverageRating },
|
||||
AnyEditionOk = true
|
||||
};
|
||||
}
|
||||
|
||||
book.Editions = new List<Edition> { edition };
|
||||
|
||||
var authorId = resource.Author.Id.ToString();
|
||||
var author = _authorService.FindById(authorId);
|
||||
|
||||
if (author == null)
|
||||
{
|
||||
author = new Author
|
||||
{
|
||||
CleanName = Parser.Parser.CleanAuthorName(resource.Author.Name),
|
||||
Metadata = new AuthorMetadata()
|
||||
{
|
||||
ForeignAuthorId = resource.Author.Id.ToString(),
|
||||
Name = DuplicateSpacesRegex.Replace(resource.Author.Name, " "),
|
||||
TitleSlug = resource.Author.Id.ToString()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
book.Author = author;
|
||||
book.AuthorMetadata = book.Author.Value.Metadata.Value;
|
||||
book.CleanTitle = book.Title.CleanAuthorName();
|
||||
|
||||
return book;
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
-8
@@ -24,6 +24,8 @@ namespace NzbDrone.Core.MetadataSource.Goodreads
|
||||
/// </summary>
|
||||
public string Title { get; private set; }
|
||||
|
||||
public string TitleWithoutSeries { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The description of this book.
|
||||
/// </summary>
|
||||
@@ -64,11 +66,6 @@ namespace NzbDrone.Core.MetadataSource.Goodreads
|
||||
/// </summary>
|
||||
public string ImageUrl { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The small cover image for this book.
|
||||
/// </summary>
|
||||
public string SmallImageUrl { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The date this book was published.
|
||||
/// </summary>
|
||||
@@ -124,6 +121,8 @@ namespace NzbDrone.Core.MetadataSource.Goodreads
|
||||
/// </summary>
|
||||
public string Url { get; private set; }
|
||||
|
||||
public string EditionsUrl { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The aggregate information for this work across all editions of the book.
|
||||
/// </summary>
|
||||
@@ -171,14 +170,16 @@ namespace NzbDrone.Core.MetadataSource.Goodreads
|
||||
{
|
||||
Id = element.ElementAsLong("id");
|
||||
Title = element.ElementAsString("title");
|
||||
TitleWithoutSeries = element.ElementAsString("title_without_series");
|
||||
Isbn = element.ElementAsString("isbn");
|
||||
Isbn13 = element.ElementAsString("isbn13");
|
||||
Asin = element.ElementAsString("asin");
|
||||
KindleAsin = element.ElementAsString("kindle_asin");
|
||||
MarketplaceId = element.ElementAsString("marketplace_id");
|
||||
CountryCode = element.ElementAsString("country_code");
|
||||
ImageUrl = element.ElementAsString("image_url");
|
||||
SmallImageUrl = element.ElementAsString("small_image_url");
|
||||
ImageUrl = element.ElementAsString("large_image_url") ??
|
||||
element.ElementAsString("image_url") ??
|
||||
element.ElementAsString("small_image_url");
|
||||
PublicationDate = element.ElementAsMultiDateField("publication");
|
||||
Publisher = element.ElementAsString("publisher");
|
||||
LanguageCode = element.ElementAsString("language_code");
|
||||
@@ -190,7 +191,8 @@ namespace NzbDrone.Core.MetadataSource.Goodreads
|
||||
EditionInformation = element.ElementAsString("edition_information");
|
||||
RatingsCount = element.ElementAsInt("ratings_count");
|
||||
TextReviewsCount = element.ElementAsInt("text_reviews_count");
|
||||
Url = element.ElementAsString("url");
|
||||
Url = element.ElementAsString("link");
|
||||
EditionsUrl = element.ElementAsString("editions_url");
|
||||
ReviewsWidget = element.ElementAsString("reviews_widget");
|
||||
|
||||
var workElement = element.Element("work");
|
||||
@@ -0,0 +1,85 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NzbDrone.Core.MetadataSource.Goodreads
|
||||
{
|
||||
public class SearchJsonResource
|
||||
{
|
||||
[JsonProperty("imageUrl")]
|
||||
public string ImageUrl { get; set; }
|
||||
|
||||
[JsonProperty("bookId")]
|
||||
public int BookId { get; set; }
|
||||
|
||||
[JsonProperty("workId")]
|
||||
public int WorkId { get; set; }
|
||||
|
||||
[JsonProperty("bookUrl")]
|
||||
public string BookUrl { get; set; }
|
||||
|
||||
[JsonProperty("from_search")]
|
||||
public bool FromSearch { get; set; }
|
||||
|
||||
[JsonProperty("from_srp")]
|
||||
public bool FromSrp { get; set; }
|
||||
|
||||
[JsonProperty("qid")]
|
||||
public string Qid { get; set; }
|
||||
|
||||
[JsonProperty("rank")]
|
||||
public int Rank { get; set; }
|
||||
|
||||
[JsonProperty("title")]
|
||||
public string Title { get; set; }
|
||||
|
||||
[JsonProperty("bookTitleBare")]
|
||||
public string BookTitleBare { get; set; }
|
||||
|
||||
[JsonProperty("numPages")]
|
||||
public int PageCount { get; set; }
|
||||
|
||||
[JsonProperty("avgRating")]
|
||||
public decimal AverageRating { get; set; }
|
||||
|
||||
[JsonProperty("ratingsCount")]
|
||||
public int RatingsCount { get; set; }
|
||||
|
||||
[JsonProperty("author")]
|
||||
public AuthorJsonResource Author { get; set; }
|
||||
|
||||
[JsonProperty("kcrPreviewUrl")]
|
||||
public string KcrPreviewUrl { get; set; }
|
||||
|
||||
[JsonProperty("description")]
|
||||
public DescriptionJsonResource Description { get; set; }
|
||||
}
|
||||
|
||||
public class AuthorJsonResource
|
||||
{
|
||||
[JsonProperty("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonProperty("isGoodreadsAuthor")]
|
||||
public bool IsGoodreadsAuthor { get; set; }
|
||||
|
||||
[JsonProperty("profileUrl")]
|
||||
public string ProfileUrl { get; set; }
|
||||
|
||||
[JsonProperty("worksListUrl")]
|
||||
public string WorksListUrl { get; set; }
|
||||
}
|
||||
|
||||
public class DescriptionJsonResource
|
||||
{
|
||||
[JsonProperty("html")]
|
||||
public string Html { get; set; }
|
||||
|
||||
[JsonProperty("truncated")]
|
||||
public bool Truncated { get; set; }
|
||||
|
||||
[JsonProperty("fullContentUrl")]
|
||||
public string FullContentUrl { get; set; }
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -125,7 +125,7 @@ namespace NzbDrone.Core.MetadataSource.Goodreads
|
||||
var originalPublicationDay = element.ElementAsInt("original_publication_day");
|
||||
if (originalPublicationYear != 0)
|
||||
{
|
||||
OriginalPublicationDate = new DateTime(originalPublicationYear, Math.Max(originalPublicationMonth, 1), Math.Max(originalPublicationDay, 1));
|
||||
OriginalPublicationDate = new DateTime(originalPublicationYear, Math.Max(originalPublicationMonth, 1), Math.Max(originalPublicationDay, 1), 0, 0, 0, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
OriginalTitle = element.ElementAsString("original_title");
|
||||
@@ -7,6 +7,7 @@ namespace NzbDrone.Core.MetadataSource
|
||||
public interface IProvideAuthorInfo
|
||||
{
|
||||
Author GetAuthorInfo(string readarrId);
|
||||
Author GetAuthorAndBooks(string readarrId, double minPopularity = 0);
|
||||
HashSet<string> GetChangedArtists(DateTime startTime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,5 @@ namespace NzbDrone.Core.MetadataSource
|
||||
List<Book> SearchByIsbn(string isbn);
|
||||
List<Book> SearchByAsin(string asin);
|
||||
List<Book> SearchByGoodreadsId(int goodreadsId);
|
||||
List<Book> SearchForNewAlbumByRecordingIds(List<string> recordingIds);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using NLog;
|
||||
@@ -12,7 +13,7 @@ using NzbDrone.Core.MediaCover;
|
||||
|
||||
namespace NzbDrone.Core.MetadataSource.SkyHook
|
||||
{
|
||||
public class SkyHookProxy : IProvideAuthorInfo, ISearchForNewAuthor, IProvideBookInfo, ISearchForNewBook, ISearchForNewEntity
|
||||
public class SkyHookProxy
|
||||
{
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly Logger _logger;
|
||||
@@ -85,6 +86,8 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
|
||||
|
||||
public Tuple<string, Book, List<AuthorMetadata>> GetBookInfo(string foreignBookId)
|
||||
{
|
||||
return null;
|
||||
/*
|
||||
_logger.Debug("Getting Book with ReadarrAPI.MetadataID of {0}", foreignBookId);
|
||||
|
||||
var httpRequest = _requestBuilder.GetRequestBuilder().Create()
|
||||
@@ -115,11 +118,12 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
|
||||
var b = httpResponse.Resource;
|
||||
var book = MapBook(b);
|
||||
|
||||
var authors = httpResponse.Resource.AuthorMetadata.SelectList(MapAuthor);
|
||||
var authorid = GetAuthorId(b);
|
||||
book.AuthorMetadata = authors.First(x => x.ForeignAuthorId == authorid);
|
||||
// var authors = httpResponse.Resource.AuthorMetadata.SelectList(MapAuthor);
|
||||
var authorid = GetAuthorId(b).ToString();
|
||||
|
||||
return new Tuple<string, Book, List<AuthorMetadata>>(authorid, book, authors);
|
||||
// book.AuthorMetadata = authors.First(x => x.ForeignAuthorId == authorid);
|
||||
return new Tuple<string, Book, List<AuthorMetadata>>(authorid, book, null);
|
||||
*/
|
||||
}
|
||||
|
||||
public List<Author> SearchForNewAuthor(string title)
|
||||
@@ -233,11 +237,6 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
|
||||
}
|
||||
}
|
||||
|
||||
public List<Book> SearchForNewAlbumByRecordingIds(List<string> recordingIds)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<object> SearchForNewEntity(string title)
|
||||
{
|
||||
var books = SearchForNewBook(title, null);
|
||||
@@ -260,10 +259,10 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
|
||||
|
||||
private Author MapAuthor(AuthorResource resource)
|
||||
{
|
||||
var metadata = MapAuthor(resource.AuthorMetadata.First(x => x.ForeignId == resource.ForeignId));
|
||||
var metadata = MapAuthor(resource.AuthorMetadata.First(x => x.GoodreadsId == resource.GoodreadsId));
|
||||
|
||||
var books = resource.Books
|
||||
.Where(x => GetAuthorId(x) == resource.ForeignId)
|
||||
var books = resource.Works
|
||||
.Where(x => GetAuthorId(x) == resource.GoodreadsId)
|
||||
.Select(MapBook)
|
||||
.ToList();
|
||||
|
||||
@@ -291,50 +290,26 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
|
||||
var seriesDict = series.ToDictionary(x => x.ForeignSeriesId);
|
||||
|
||||
// only take series where there are some works
|
||||
foreach (var s in resource.Series.Where(x => x.BookLinks.Any()))
|
||||
foreach (var s in resource.Series.Where(x => x.Works.Any()))
|
||||
{
|
||||
if (seriesDict.TryGetValue(s.ForeignId, out var curr))
|
||||
if (seriesDict.TryGetValue(s.GoodreadsId.ToString(), out var curr))
|
||||
{
|
||||
curr.LinkItems = s.BookLinks.Where(x => bookDict.ContainsKey(x.BookId)).Select(l => new SeriesBookLink
|
||||
curr.LinkItems = s.Works.Where(x => bookDict.ContainsKey(x.GoodreadsId.ToString())).Select(l => new SeriesBookLink
|
||||
{
|
||||
Book = bookDict[l.BookId],
|
||||
Book = bookDict[l.GoodreadsId.ToString()],
|
||||
Series = curr,
|
||||
IsPrimary = l.Primary
|
||||
IsPrimary = l.Primary,
|
||||
Position = l.Position
|
||||
}).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var b in resource.Books)
|
||||
{
|
||||
if (bookDict.TryGetValue(b.ForeignId, out var curr))
|
||||
{
|
||||
curr.SeriesLinks = b.SeriesLinks.Where(l => seriesDict.ContainsKey(l.SeriesId)).Select(l => new SeriesBookLink
|
||||
{
|
||||
Series = seriesDict[l.SeriesId],
|
||||
Position = l.Position,
|
||||
Book = curr
|
||||
}).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
_ = series.SelectMany(x => x.LinkItems.Value)
|
||||
.Join(books.SelectMany(x => x.SeriesLinks.Value),
|
||||
sl => Tuple.Create(sl.Series.Value.ForeignSeriesId, sl.Book.Value.ForeignBookId),
|
||||
bl => Tuple.Create(bl.Series.Value.ForeignSeriesId, bl.Book.Value.ForeignBookId),
|
||||
(sl, bl) =>
|
||||
{
|
||||
sl.Position = bl.Position;
|
||||
bl.IsPrimary = sl.IsPrimary;
|
||||
return sl;
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
private static AuthorMetadata MapAuthor(AuthorSummaryResource resource)
|
||||
{
|
||||
var author = new AuthorMetadata
|
||||
{
|
||||
ForeignAuthorId = resource.ForeignId,
|
||||
GoodreadsId = resource.GoodreadsId,
|
||||
ForeignAuthorId = resource.GoodreadsId.ToString(),
|
||||
TitleSlug = resource.TitleSlug,
|
||||
Name = resource.Name.CleanSpaces(),
|
||||
Overview = resource.Description,
|
||||
@@ -350,7 +325,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
|
||||
});
|
||||
}
|
||||
|
||||
author.Links.Add(new Links { Url = resource.WebUrl, Name = "Goodreads" });
|
||||
author.Links.Add(new Links { Url = resource.Url, Name = "Goodreads" });
|
||||
|
||||
return author;
|
||||
}
|
||||
@@ -359,7 +334,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
|
||||
{
|
||||
var series = new Series
|
||||
{
|
||||
ForeignSeriesId = resource.ForeignId,
|
||||
ForeignSeriesId = resource.GoodreadsId.ToString(),
|
||||
Title = resource.Title,
|
||||
Description = resource.Description
|
||||
};
|
||||
@@ -367,37 +342,95 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
|
||||
return series;
|
||||
}
|
||||
|
||||
private static Book MapBook(BookResource resource)
|
||||
private static Book MapBook(WorkResource resource)
|
||||
{
|
||||
var book = new Book
|
||||
{
|
||||
ForeignBookId = resource.ForeignId,
|
||||
ForeignWorkId = resource.WorkForeignId,
|
||||
GoodreadsId = resource.GoodreadsId,
|
||||
ForeignBookId = resource.GoodreadsId.ToString(),
|
||||
Title = resource.Title,
|
||||
TitleSlug = resource.TitleSlug,
|
||||
CleanTitle = Parser.Parser.CleanAuthorName(resource.Title),
|
||||
ReleaseDate = resource.ReleaseDate,
|
||||
};
|
||||
|
||||
book.Links.Add(new Links { Url = resource.Url, Name = "Goodreads Editions" });
|
||||
|
||||
if (resource.Books != null)
|
||||
{
|
||||
book.Editions = resource.Books.Select(x => MapEdition(x)).ToList();
|
||||
|
||||
// monitor the most rated release
|
||||
var mostPopular = book.Editions.Value.OrderByDescending(x => x.Ratings.Votes).FirstOrDefault();
|
||||
if (mostPopular != null)
|
||||
{
|
||||
mostPopular.Monitored = true;
|
||||
|
||||
// fix work title if missing
|
||||
if (book.Title.IsNullOrWhiteSpace())
|
||||
{
|
||||
book.Title = mostPopular.Title;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
book.Editions = new List<Edition>();
|
||||
}
|
||||
|
||||
Debug.Assert(!book.Editions.Value.Any() || book.Editions.Value.Count(x => x.Monitored) == 1, "one edition monitored");
|
||||
|
||||
book.AnyEditionOk = true;
|
||||
|
||||
var ratingCount = book.Editions.Value.Sum(x => x.Ratings.Votes);
|
||||
|
||||
if (ratingCount > 0)
|
||||
{
|
||||
book.Ratings = new Ratings
|
||||
{
|
||||
Votes = ratingCount,
|
||||
Value = book.Editions.Value.Sum(x => x.Ratings.Votes * x.Ratings.Value) / ratingCount
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
book.Ratings = new Ratings { Votes = 0, Value = 0 };
|
||||
}
|
||||
|
||||
return book;
|
||||
}
|
||||
|
||||
private static Edition MapEdition(BookResource resource)
|
||||
{
|
||||
var edition = new Edition
|
||||
{
|
||||
ForeignEditionId = resource.GoodreadsId.ToString(),
|
||||
TitleSlug = resource.TitleSlug,
|
||||
Isbn13 = resource.Isbn13,
|
||||
Asin = resource.Asin,
|
||||
Title = resource.Title.CleanSpaces(),
|
||||
Language = resource.Language,
|
||||
Publisher = resource.Publisher,
|
||||
CleanTitle = Parser.Parser.CleanAuthorName(resource.Title),
|
||||
Overview = resource.Description,
|
||||
Format = resource.Format,
|
||||
IsEbook = resource.IsEbook,
|
||||
Disambiguation = resource.EditionInformation,
|
||||
Publisher = resource.Publisher,
|
||||
PageCount = resource.NumPages ?? 0,
|
||||
ReleaseDate = resource.ReleaseDate,
|
||||
Ratings = new Ratings { Votes = resource.RatingCount, Value = (decimal)resource.AverageRating }
|
||||
};
|
||||
|
||||
if (resource.ImageUrl.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
book.Images.Add(new MediaCover.MediaCover
|
||||
edition.Images.Add(new MediaCover.MediaCover
|
||||
{
|
||||
Url = resource.ImageUrl,
|
||||
CoverType = MediaCoverTypes.Cover
|
||||
});
|
||||
}
|
||||
|
||||
book.Links.Add(new Links { Url = resource.WebUrl, Name = "Goodreads" });
|
||||
edition.Links.Add(new Links { Url = resource.Url, Name = "Goodreads Book" });
|
||||
|
||||
return book;
|
||||
return edition;
|
||||
}
|
||||
|
||||
private List<Book> MapSearchResult(BookSearchResource resource)
|
||||
@@ -406,25 +439,25 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
|
||||
|
||||
var result = new List<Book>();
|
||||
|
||||
foreach (var b in resource.Books)
|
||||
foreach (var b in resource.Works)
|
||||
{
|
||||
var book = _bookService.FindById(b.ForeignId);
|
||||
var book = _bookService.FindById(b.GoodreadsId.ToString());
|
||||
if (book == null)
|
||||
{
|
||||
book = MapBook(b);
|
||||
|
||||
var authorid = GetAuthorId(b);
|
||||
|
||||
if (authorid == null)
|
||||
if (authorid == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var author = _authorService.FindById(authorid);
|
||||
var author = _authorService.FindById(authorid.ToString());
|
||||
|
||||
if (author == null)
|
||||
{
|
||||
var authorMetadata = metadata[authorid];
|
||||
var authorMetadata = metadata[authorid.ToString()];
|
||||
|
||||
author = new Author
|
||||
{
|
||||
@@ -447,9 +480,9 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
|
||||
return result;
|
||||
}
|
||||
|
||||
private string GetAuthorId(BookResource b)
|
||||
private int GetAuthorId(WorkResource b)
|
||||
{
|
||||
return b.Contributors.FirstOrDefault()?.ForeignId;
|
||||
return b.Books.First().Contributors.FirstOrDefault()?.GoodreadsId ?? 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,6 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
|
||||
{
|
||||
public class AuthorResource : BulkResource
|
||||
{
|
||||
public string ForeignId { get; set; }
|
||||
public int GoodreadsId { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,13 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
|
||||
{
|
||||
public class AuthorSummaryResource
|
||||
{
|
||||
public string ForeignId { get; set; }
|
||||
public int GoodreadsId { get; set; }
|
||||
public string TitleSlug { get; set; }
|
||||
public string Name { get; set; }
|
||||
|
||||
public string Description { get; set; }
|
||||
public string ImageUrl { get; set; }
|
||||
public string ProfileUri { get; set; }
|
||||
public string WebUrl { get; set; }
|
||||
public string Url { get; set; }
|
||||
|
||||
public int ReviewCount { get; set; }
|
||||
public int RatingsCount { get; set; }
|
||||
|
||||
@@ -5,36 +5,25 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
|
||||
{
|
||||
public class BookResource
|
||||
{
|
||||
public string ForeignId { get; set; }
|
||||
public int GoodreadsId { get; set; }
|
||||
public string TitleSlug { get; set; }
|
||||
public string Asin { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Isbn13 { get; set; }
|
||||
public long Rvn { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string Publisher { get; set; }
|
||||
public string Language { get; set; }
|
||||
public string DisplayGroup { get; set; }
|
||||
public string Format { get; set; }
|
||||
public string EditionInformation { get; set; }
|
||||
public string Publisher { get; set; }
|
||||
public string ImageUrl { get; set; }
|
||||
public string KindleMappingStatus { get; set; }
|
||||
public string Marketplace { get; set; }
|
||||
public bool IsEbook { get; set; }
|
||||
public int? NumPages { get; set; }
|
||||
public int ReviewsCount { get; set; }
|
||||
public int RatingCount { get; set; }
|
||||
public double AverageRating { get; set; }
|
||||
public IList<BookSeriesLinkResource> SeriesLinks { get; set; } = new List<BookSeriesLinkResource>();
|
||||
public string WebUrl { get; set; }
|
||||
public string WorkForeignId { get; set; }
|
||||
public string Url { get; set; }
|
||||
public DateTime? ReleaseDate { get; set; }
|
||||
|
||||
public List<ContributorResource> Contributors { get; set; } = new List<ContributorResource>();
|
||||
public List<AuthorSummaryResource> AuthorMetadata { get; set; } = new List<AuthorSummaryResource>();
|
||||
}
|
||||
|
||||
public class BookSeriesLinkResource
|
||||
{
|
||||
public string SeriesId { get; set; }
|
||||
public string Position { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
|
||||
public class BulkResource
|
||||
{
|
||||
public List<AuthorSummaryResource> AuthorMetadata { get; set; } = new List<AuthorSummaryResource>();
|
||||
public List<BookResource> Books { get; set; }
|
||||
public List<WorkResource> Works { get; set; }
|
||||
public List<SeriesResource> Series { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
|
||||
{
|
||||
public class ContributorResource
|
||||
{
|
||||
public string ForeignId { get; set; }
|
||||
public int GoodreadsId { get; set; }
|
||||
public string Role { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,16 +4,17 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
|
||||
{
|
||||
public class SeriesResource
|
||||
{
|
||||
public string ForeignId { get; set; }
|
||||
public int GoodreadsId { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string Description { get; set; }
|
||||
|
||||
public List<SeriesBookLinkResource> BookLinks { get; set; }
|
||||
public List<SeriesWorkLinkResource> Works { get; set; }
|
||||
}
|
||||
|
||||
public class SeriesBookLinkResource
|
||||
public class SeriesWorkLinkResource
|
||||
{
|
||||
public string BookId { get; set; }
|
||||
public int GoodreadsId { get; set; }
|
||||
public string Position { get; set; }
|
||||
public bool Primary { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NzbDrone.Core.MetadataSource.SkyHook
|
||||
{
|
||||
public class WorkResource
|
||||
{
|
||||
public int GoodreadsId { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string TitleSlug { get; set; }
|
||||
public string Url { get; set; }
|
||||
public DateTime? ReleaseDate { get; set; }
|
||||
public List<BookResource> Books { get; set; } = new List<BookResource>();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user