mirror of
https://github.com/jellyfin/jellyfin-plugin-bookshelf.git
synced 2024-11-26 23:20:27 +00:00
Add unit tests for the Google Books provider and fix parsing issues
Fix adding new PersonInfo
This commit is contained in:
parent
d68cb3c8cc
commit
f0defb34b3
3
Jellyfin.Plugin.Bookshelf/Properties/AssemblyInfo.cs
Normal file
3
Jellyfin.Plugin.Bookshelf/Properties/AssemblyInfo.cs
Normal file
@ -0,0 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Jellyfin.Plugin.Bookshelf.Tests")]
|
@ -24,23 +24,37 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
|
||||
/// </summary>
|
||||
public class GoogleBooksProvider : IRemoteMetadataProvider<Book, BookInfo>
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the provider.
|
||||
/// </summary>
|
||||
public const string ProviderName = "Google Books";
|
||||
|
||||
/// <summary>
|
||||
/// Id of the provider.
|
||||
/// </summary>
|
||||
public const string ProviderId = "GoogleBooks";
|
||||
|
||||
// convert these characters to whitespace for better matching
|
||||
// there are two dashes with different char codes
|
||||
private const string Spacers = "/,.:;\\(){}[]+-_=–*";
|
||||
|
||||
private const string Remove = "\"'!`?";
|
||||
|
||||
// first pattern provides the name and the year
|
||||
// alternate option to use series index instead of year
|
||||
// last resort matches the whole string as the name
|
||||
private static readonly Regex[] _nameMatches =
|
||||
{
|
||||
// seriesName (seriesYear) #index (of count) (year), with only seriesName and index required
|
||||
new Regex(@"^(?<seriesName>.+?)((\s\((?<seriesYear>\d{4})\))?)\s#(?<index>\d+)((\s\(of\s(?<count>\d+)\))?)((\s\((?<year>\d{4})\))?)$"),
|
||||
// name (seriesName, #index) (year), with year optional
|
||||
new Regex(@"^(?<name>.+?)\s\((?<seriesName>.+?),\s#(?<index>\d+)\)((\s\((?<year>\d{4})\))?)$"),
|
||||
// index - name (year), with year optional
|
||||
new Regex(@"^(?<index>\d+)\s\-\s(?<name>.+?)((\s\((?<year>\d{4})\))?)$"),
|
||||
// name (year)
|
||||
new Regex(@"(?<name>.*)\((?<year>\d{4})\)"),
|
||||
new Regex(@"(?<index>\d*)\s\-\s(?<name>.*)"),
|
||||
// last resort matches the whole string as the name
|
||||
new Regex(@"(?<name>.*)")
|
||||
};
|
||||
|
||||
private readonly Dictionary<string, string> _replaceEndNumerals = new ()
|
||||
private readonly Dictionary<string, string> _replaceEndNumerals = new()
|
||||
{
|
||||
{ " i", " 1" },
|
||||
{ " ii", " 2" },
|
||||
@ -71,7 +85,7 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "Google Books";
|
||||
public string Name => ProviderName;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(BookInfo searchInfo, CancellationToken cancellationToken)
|
||||
@ -93,7 +107,13 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
|
||||
}
|
||||
|
||||
var remoteSearchResult = new RemoteSearchResult();
|
||||
|
||||
remoteSearchResult.SetProviderId(ProviderId, result.Id);
|
||||
remoteSearchResult.SearchProviderName = ProviderName;
|
||||
remoteSearchResult.Name = result.VolumeInfo.Title;
|
||||
remoteSearchResult.Overview = result.VolumeInfo.Description;
|
||||
remoteSearchResult.ProductionYear = GetYearFromPublishedDate(result.VolumeInfo.PublishedDate);
|
||||
|
||||
if (result.VolumeInfo.ImageLinks?.Thumbnail != null)
|
||||
{
|
||||
remoteSearchResult.ImageUrl = result.VolumeInfo.ImageLinks.Thumbnail;
|
||||
@ -109,13 +129,21 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
|
||||
public async Task<MetadataResult<Book>> GetMetadata(BookInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var metadataResult = new MetadataResult<Book>();
|
||||
metadataResult.HasMetadata = false;
|
||||
|
||||
var googleBookId = info.GetProviderId("GoogleBooks")
|
||||
?? await FetchBookId(info, cancellationToken).ConfigureAwait(false);
|
||||
var metadataResult = new MetadataResult<Book>()
|
||||
{
|
||||
QueriedById = true
|
||||
};
|
||||
|
||||
if (string.IsNullOrEmpty(googleBookId))
|
||||
var googleBookId = info.GetProviderId(ProviderId);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(googleBookId))
|
||||
{
|
||||
googleBookId = await FetchBookId(info, cancellationToken).ConfigureAwait(false);
|
||||
metadataResult.QueriedById = false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(googleBookId))
|
||||
{
|
||||
return metadataResult;
|
||||
}
|
||||
@ -133,9 +161,11 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
|
||||
return metadataResult;
|
||||
}
|
||||
|
||||
ProcessBookMetadata(metadataResult, bookResult);
|
||||
|
||||
metadataResult.Item = bookMetadataResult;
|
||||
metadataResult.QueriedById = true;
|
||||
metadataResult.HasMetadata = true;
|
||||
|
||||
return metadataResult;
|
||||
}
|
||||
|
||||
@ -150,19 +180,16 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// pattern match the filename
|
||||
// year can be included for better results
|
||||
GetBookMetadata(item);
|
||||
|
||||
var url = string.Format(CultureInfo.InvariantCulture, GoogleApiUrls.SearchUrl, WebUtility.UrlEncode(item.Name), 0, 20);
|
||||
var searchString = GetSearchString(item);
|
||||
var url = string.Format(CultureInfo.InvariantCulture, GoogleApiUrls.SearchUrl, WebUtility.UrlEncode(searchString), 0, 20);
|
||||
|
||||
var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
|
||||
|
||||
using var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
#pragma warning disable CA2007
|
||||
#pragma warning disable CA2007
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
#pragma warning restore CA2007
|
||||
#pragma warning restore CA2007
|
||||
|
||||
return await JsonSerializer.DeserializeAsync<SearchResult>(stream, JsonDefaults.Options, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
@ -171,6 +198,10 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// pattern match the filename
|
||||
// year can be included for better results
|
||||
GetBookMetadata(item);
|
||||
|
||||
var searchResults = await GetSearchResultsInternal(item, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (searchResults?.Items == null)
|
||||
@ -178,7 +209,7 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
|
||||
return null;
|
||||
}
|
||||
|
||||
var comparableName = GetComparableName(item.Name);
|
||||
var comparableName = GetComparableName(item.Name, item.SeriesName, item.IndexNumber);
|
||||
foreach (var i in searchResults.Items)
|
||||
{
|
||||
if (i.VolumeInfo is null)
|
||||
@ -193,14 +224,14 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
|
||||
}
|
||||
|
||||
// adjust for google yyyy-mm-dd format
|
||||
var resultYear = i.VolumeInfo.PublishedDate?.Length > 4 ? i.VolumeInfo.PublishedDate[..4] : i.VolumeInfo.PublishedDate;
|
||||
if (!int.TryParse(resultYear, out var bookReleaseYear))
|
||||
var resultYear = GetYearFromPublishedDate(i.VolumeInfo.PublishedDate);
|
||||
if (resultYear == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// allow a one year variance
|
||||
if (Math.Abs(bookReleaseYear - item.Year ?? 0) > 1)
|
||||
if (Math.Abs(resultYear - item.Year ?? 0) > 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@ -211,6 +242,18 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
|
||||
return null;
|
||||
}
|
||||
|
||||
private int? GetYearFromPublishedDate(string? publishedDate)
|
||||
{
|
||||
var resultYear = publishedDate?.Length > 4 ? publishedDate[..4] : publishedDate;
|
||||
|
||||
if (!int.TryParse(resultYear, out var bookReleaseYear))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return bookReleaseYear;
|
||||
}
|
||||
|
||||
private async Task<BookResult?> FetchBookData(string googleBookId, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
@ -221,9 +264,9 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
|
||||
|
||||
using var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
#pragma warning disable CA2007
|
||||
#pragma warning disable CA2007
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
#pragma warning restore CA2007
|
||||
#pragma warning restore CA2007
|
||||
|
||||
return await JsonSerializer.DeserializeAsync<BookResult>(stream, JsonDefaults.Options, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
@ -240,58 +283,88 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
|
||||
|
||||
book.Name = bookResult.VolumeInfo.Title;
|
||||
book.Overview = bookResult.VolumeInfo.Description;
|
||||
try
|
||||
book.ProductionYear = GetYearFromPublishedDate(bookResult.VolumeInfo.PublishedDate);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(bookResult.VolumeInfo.Publisher))
|
||||
{
|
||||
book.ProductionYear = bookResult.VolumeInfo.PublishedDate?.Length > 4
|
||||
? Convert.ToInt32(bookResult.VolumeInfo.PublishedDate[..4], CultureInfo.InvariantCulture)
|
||||
: Convert.ToInt32(bookResult.VolumeInfo.PublishedDate, CultureInfo.InvariantCulture);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
_logger.LogError("Error parsing date");
|
||||
book.AddStudio(bookResult.VolumeInfo.Publisher);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(bookResult.VolumeInfo.Publisher))
|
||||
{
|
||||
book.Studios = book.Studios.Append(bookResult.VolumeInfo.Publisher).ToArray();
|
||||
}
|
||||
HashSet<string> categories = new HashSet<string>();
|
||||
|
||||
var tags = new List<string>();
|
||||
if (!string.IsNullOrEmpty(bookResult.VolumeInfo.MainCategory))
|
||||
// Categories are from the BISAC list (https://www.bisg.org/complete-bisac-subject-headings-list)
|
||||
// Keep the first one (most general) as genre, and add the rest as tags (while dropping the "General" tag)
|
||||
foreach (var category in bookResult.VolumeInfo.Categories)
|
||||
{
|
||||
tags.Add(bookResult.VolumeInfo.MainCategory);
|
||||
}
|
||||
|
||||
if (bookResult.VolumeInfo.Categories is { Count: > 0 })
|
||||
{
|
||||
foreach (var category in bookResult.VolumeInfo.Categories)
|
||||
foreach (var subCategory in category.Split('/', StringSplitOptions.TrimEntries))
|
||||
{
|
||||
tags.Add(category);
|
||||
if (subCategory == "General")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
categories.Add(subCategory);
|
||||
}
|
||||
}
|
||||
|
||||
if (tags.Count > 0)
|
||||
if (categories.Count > 0)
|
||||
{
|
||||
tags.AddRange(book.Tags);
|
||||
book.Tags = tags.ToArray();
|
||||
book.AddGenre(categories.First());
|
||||
foreach (var category in categories.Skip(1))
|
||||
{
|
||||
book.AddTag(category);
|
||||
}
|
||||
}
|
||||
|
||||
// google rates out of five so convert to ten
|
||||
book.CommunityRating = bookResult.VolumeInfo.AverageRating * 2;
|
||||
|
||||
if (!string.IsNullOrEmpty(bookResult.Id))
|
||||
if (bookResult.VolumeInfo.AverageRating.HasValue)
|
||||
{
|
||||
book.SetProviderId("GoogleBooks", bookResult.Id);
|
||||
// google rates out of five so convert to ten
|
||||
book.CommunityRating = bookResult.VolumeInfo.AverageRating.Value * 2;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(bookResult.Id))
|
||||
{
|
||||
book.SetProviderId(ProviderId, bookResult.Id);
|
||||
}
|
||||
|
||||
return book;
|
||||
}
|
||||
|
||||
private string GetComparableName(string? name)
|
||||
private void ProcessBookMetadata(MetadataResult<Book> metadataResult, BookResult bookResult)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
if (bookResult.VolumeInfo == null)
|
||||
{
|
||||
return string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var author in bookResult.VolumeInfo.Authors)
|
||||
{
|
||||
metadataResult.AddPerson(new PersonInfo
|
||||
{
|
||||
Name = author,
|
||||
Type = "Author",
|
||||
});
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(bookResult.VolumeInfo.Language))
|
||||
{
|
||||
metadataResult.ResultLanguage = bookResult.VolumeInfo.Language;
|
||||
}
|
||||
}
|
||||
|
||||
private string GetComparableName(string? name, string? seriesName = null, int? index = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(seriesName) && index != null)
|
||||
{
|
||||
// We searched by series name and index, so use that
|
||||
name = $"{seriesName} {index}";
|
||||
}
|
||||
else
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
name = name.ToLower(CultureInfo.InvariantCulture);
|
||||
@ -341,7 +414,11 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
|
||||
return name.Trim();
|
||||
}
|
||||
|
||||
private void GetBookMetadata(BookInfo item)
|
||||
/// <summary>
|
||||
/// Extract metadata from the file name.
|
||||
/// </summary>
|
||||
/// <param name="item">The info item.</param>
|
||||
internal void GetBookMetadata(BookInfo item)
|
||||
{
|
||||
foreach (var regex in _nameMatches)
|
||||
{
|
||||
@ -351,6 +428,11 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reset the name, since we'll get it from parsing
|
||||
// Don't reset the series name, since it may be set from the parent folder's name
|
||||
// We'll just override it if we find it in the file name
|
||||
item.Name = string.Empty;
|
||||
|
||||
// catch return value because user may want to index books from zero
|
||||
// but zero is also the return value from int.TryParse failure
|
||||
var result = int.TryParse(match.Groups["index"].Value, out var index);
|
||||
@ -359,7 +441,15 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
|
||||
item.IndexNumber = index;
|
||||
}
|
||||
|
||||
item.Name = match.Groups["name"].Value.Trim();
|
||||
if (match.Groups.ContainsKey("name"))
|
||||
{
|
||||
item.Name = match.Groups["name"].Value.Trim();
|
||||
}
|
||||
|
||||
if (match.Groups.ContainsKey("seriesName"))
|
||||
{
|
||||
item.SeriesName = match.Groups["seriesName"].Value.Trim();
|
||||
}
|
||||
|
||||
// might as well catch the return value here as well
|
||||
result = int.TryParse(match.Groups["year"].Value, out var year);
|
||||
@ -369,5 +459,42 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the search string for the item.
|
||||
/// If the item is part of a series, use the series name and the issue name or index.
|
||||
/// Otherwise, use the book name and year.
|
||||
/// </summary>
|
||||
/// <param name="item">BookInfo item.</param>
|
||||
/// <returns>The search query.</returns>
|
||||
internal string GetSearchString(BookInfo item)
|
||||
{
|
||||
string result = string.Empty;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(item.SeriesName))
|
||||
{
|
||||
result = item.SeriesName;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(item.Name))
|
||||
{
|
||||
result = $"{result} {item.Name}";
|
||||
}
|
||||
else if (item.IndexNumber.HasValue)
|
||||
{
|
||||
result = $"{result} {item.IndexNumber.Value}";
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(item.Name))
|
||||
{
|
||||
result = item.Name;
|
||||
|
||||
if (item.Year.HasValue)
|
||||
{
|
||||
result = $"{result} {item.Year.Value}";
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
@ -61,6 +61,12 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
|
||||
/// Gets or sets the average rating.
|
||||
/// </summary>
|
||||
[JsonPropertyName("averageRating")]
|
||||
public float AverageRating { get; set; }
|
||||
public float? AverageRating { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the language.
|
||||
/// </summary>
|
||||
[JsonPropertyName("language")]
|
||||
public string? Language { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,82 @@
|
||||
{
|
||||
"kind": "books#volume",
|
||||
"id": "49T5twEACAAJ",
|
||||
"etag": "erLq86KB0Vg",
|
||||
"selfLink": "https://www.googleapis.com/books/v1/volumes/49T5twEACAAJ",
|
||||
"volumeInfo": {
|
||||
"title": "Children of Time",
|
||||
"authors": [
|
||||
"Adrian Tchaikovsky"
|
||||
],
|
||||
"publisher": "Orbit",
|
||||
"publishedDate": "2018-12-11",
|
||||
"description": "\u003cb\u003eAdrian Tchaikovksy's award-winning novel \u003ci\u003eChildren of Time\u003c/i\u003e, is the epic story of humanity's battle for survival on a terraformed planet.\u003c/b\u003e\u003cb\u003e\u003cbr\u003e\u003c/b\u003eWho will inherit this new Earth?\u003cbr\u003e\u003cbr\u003eThe last remnants of the human race left a dying Earth, desperate to find a new home among the stars. Following in the footsteps of their ancestors, they discover the greatest treasure of the past age - a world terraformed and prepared for human life.\u003cbr\u003e\u003cbr\u003eBut all is not right in this new Eden. In the long years since the planet was abandoned, the work of its architects has borne disastrous fruit. The planet is not waiting for them, pristine and unoccupied. New masters have turned it from a refuge into mankind's worst nightmare.\u003cbr\u003e\u003cbr\u003eNow two civilizations are on a collision course, both testing the boundaries of what they will do to survive. As the fate of humanity hangs in the balance, who are the true heirs of this new Earth?span",
|
||||
"industryIdentifiers": [
|
||||
{
|
||||
"type": "ISBN_10",
|
||||
"identifier": "0316452505"
|
||||
},
|
||||
{
|
||||
"type": "ISBN_13",
|
||||
"identifier": "9780316452502"
|
||||
}
|
||||
],
|
||||
"readingModes": {
|
||||
"text": false,
|
||||
"image": false
|
||||
},
|
||||
"pageCount": 640,
|
||||
"printedPageCount": 640,
|
||||
"dimensions": {
|
||||
"height": "21.00 cm",
|
||||
"width": "14.00 cm",
|
||||
"thickness": "4.10 cm"
|
||||
},
|
||||
"printType": "BOOK",
|
||||
"categories": [
|
||||
"Fiction / Science Fiction / Alien Contact",
|
||||
"Fiction / Science Fiction / Genetic Engineering",
|
||||
"Fiction / Science Fiction / Hard Science Fiction",
|
||||
"Fiction / Science Fiction / Space Exploration",
|
||||
"Fiction / Science Fiction / Space Opera"
|
||||
],
|
||||
"averageRating": 4,
|
||||
"ratingsCount": 49,
|
||||
"maturityRating": "NOT_MATURE",
|
||||
"allowAnonLogging": false,
|
||||
"contentVersion": "preview-1.0.0",
|
||||
"panelizationSummary": {
|
||||
"containsEpubBubbles": false,
|
||||
"containsImageBubbles": false
|
||||
},
|
||||
"imageLinks": {
|
||||
"smallThumbnail": "http://books.google.com/books/content?id=49T5twEACAAJ&printsec=frontcover&img=1&zoom=5&imgtk=AFLRE73LHhsrURPw3qT1X-4OnfxY0-tUYmAUe4Z2GQR2R0X2Z2xMc28QU4T77PPDOHIwIk0WUy_MhXUCJH3k8TNZQEjJONM82_3R7fA1CkL57Idz9KmnDEYRLdRh5dyk0F0dgO8US2cN&source=gbs_api",
|
||||
"thumbnail": "http://books.google.com/books/content?id=49T5twEACAAJ&printsec=frontcover&img=1&zoom=1&imgtk=AFLRE70U9t4z91EAYhiD2AYOR9pzNu86QDKZebNLQo4K3jMaJ748TC5LvCoZGt9ON4pZ54H8RoIRyCB5IveVDmt49QjeJlbJtWLlZoksRHXInrEVmo2476WXKcLhZOjp41Vu_5Lb05oJ&source=gbs_api"
|
||||
},
|
||||
"language": "en",
|
||||
"previewLink": "http://books.google.fr/books?id=49T5twEACAAJ&hl=&source=gbs_api",
|
||||
"infoLink": "https://play.google.com/store/books/details?id=49T5twEACAAJ&source=gbs_api",
|
||||
"canonicalVolumeLink": "https://play.google.com/store/books/details?id=49T5twEACAAJ"
|
||||
},
|
||||
"saleInfo": {
|
||||
"country": "FR",
|
||||
"saleability": "NOT_FOR_SALE",
|
||||
"isEbook": false
|
||||
},
|
||||
"accessInfo": {
|
||||
"country": "FR",
|
||||
"viewability": "NO_PAGES",
|
||||
"embeddable": false,
|
||||
"publicDomain": false,
|
||||
"textToSpeechPermission": "ALLOWED",
|
||||
"epub": {
|
||||
"isAvailable": false
|
||||
},
|
||||
"pdf": {
|
||||
"isAvailable": false
|
||||
},
|
||||
"webReaderLink": "http://play.google.com/books/reader?id=49T5twEACAAJ&hl=&source=gbs_api",
|
||||
"accessViewStatus": "NONE",
|
||||
"quoteSharingAllowed": false
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
{
|
||||
"kind": "books#volume",
|
||||
"id": "G7utDwAAQBAJ",
|
||||
"etag": "CFZojkd8PV4",
|
||||
"selfLink": "https://www.googleapis.com/books/v1/volumes/G7utDwAAQBAJ",
|
||||
"volumeInfo": {
|
||||
"title": "Dans la toile du temps",
|
||||
"authors": [
|
||||
"Adrian Tchaikovsky"
|
||||
],
|
||||
"publisher": "Editions Gallimard",
|
||||
"publishedDate": "2019-10-03T00:00:00+02:00",
|
||||
"description": "La Terre est au plus mal... Ses derniers habitants n’ont plus qu’un seul espoir : coloniser le \"Monde de Kern\", une planète lointaine, spécialement terraformée pour l’espèce humaine. Mais sur ce \"monde vert\" paradisiaque, tout ne s’est pas déroulé comme les scientifiques s’y attendaient. Une autre espèce que celle qui était prévue, aidée par un nanovirus, s’est parfaitement adaptée à ce nouvel environnement et elle n’a pas du tout l’intention de laisser sa place. Le choc de deux civilisations aussi différentes que possible semble inévitable. Qui seront donc les héritiers de l’ancienne Terre ? Qui sortira vainqueur du piège tendu par la toile du temps ? Premier roman de l’auteur paru en France, Dans la toile du temps s’inscrit dans la lignée du cycle Élévation de David Brin. Il nous fait découvrir l’évolution d’une civilisation radicalement autre et sa confrontation inévitable avec l’espèce humaine. Le roman a reçu le prix Arthur C. Clarke en 2016",
|
||||
"industryIdentifiers": [
|
||||
{
|
||||
"type": "ISBN_10",
|
||||
"identifier": "2072853311"
|
||||
},
|
||||
{
|
||||
"type": "ISBN_13",
|
||||
"identifier": "9782072853319"
|
||||
}
|
||||
],
|
||||
"readingModes": {
|
||||
"text": true,
|
||||
"image": true
|
||||
},
|
||||
"pageCount": 704,
|
||||
"printedPageCount": 478,
|
||||
"printType": "BOOK",
|
||||
"categories": [
|
||||
"Fiction / Science Fiction / General"
|
||||
],
|
||||
"maturityRating": "NOT_MATURE",
|
||||
"allowAnonLogging": true,
|
||||
"contentVersion": "1.1.1.0.preview.3",
|
||||
"panelizationSummary": {
|
||||
"containsEpubBubbles": false,
|
||||
"containsImageBubbles": false
|
||||
},
|
||||
"imageLinks": {
|
||||
"smallThumbnail": "http://books.google.com/books/publisher/content?id=G7utDwAAQBAJ&printsec=frontcover&img=1&zoom=5&edge=curl&imgtk=AFLRE71Zhj6v9aMLeOtTnqEjCcUstOUtv8pU3R3ipmb2zOe8ga2XZcKEUNYKzKQhSjwhdXCCWH8j0357XzhN2xCGo4X44XVZ6QadsAKgv-I8bNBmRbie1tmcIKn8huVrjJLGh3ytd5iO&source=gbs_api",
|
||||
"thumbnail": "http://books.google.com/books/publisher/content?id=G7utDwAAQBAJ&printsec=frontcover&img=1&zoom=1&edge=curl&imgtk=AFLRE73iXAAA6Bipi-q6HwR1kz5-XegugreP1A2Mbu63gh2TQKdI1lOCoRg9EuW7sFt2RjQgDbAXaHQlBPe8TBY2mo0i2ngWotY1eAvIusIEaCLRD18wl0baMruHUs4b3QvBF56gznpu&source=gbs_api",
|
||||
"small": "http://books.google.com/books/publisher/content?id=G7utDwAAQBAJ&printsec=frontcover&img=1&zoom=2&edge=curl&imgtk=AFLRE71Xief9ehNn38lxcE4zm0gkG9A2v_B7DVXF-dG4rm9ZWM7eZLLbt7hKOikmOBnt_2yClMeKTcTRawMX1Vyk0aK3wn5ynUUpMf9NF39gN-HQvGgI_zCOTHI666iNvDfhUyZL8sIB&source=gbs_api",
|
||||
"medium": "http://books.google.com/books/publisher/content?id=G7utDwAAQBAJ&printsec=frontcover&img=1&zoom=3&edge=curl&imgtk=AFLRE70WLksH5tj5q8gFMNUenY-rFrrr5Ff3r5TDt9Uhx6fiPQxm8lE2rhwr-HxUvbIMtXP-CHpV5JAagggBeyO8UjEjuRNZDSkIfABBLneNh9eLdIcBXS-esCeTXc-AyI76tQ0PAsPS&source=gbs_api",
|
||||
"large": "http://books.google.com/books/publisher/content?id=G7utDwAAQBAJ&printsec=frontcover&img=1&zoom=4&edge=curl&imgtk=AFLRE701An2VOoqqEQ7XZre802y67seosY2vxTgZtotvslfTUGuq6Z1zb_HtCnezKKh2PZuonHdeOjLCIpNn6Ns8GtwGgEdQPeLUoZhL-AZqqJShKvVyc5CVRXjeE9cl1oubIT7c_Tis&source=gbs_api",
|
||||
"extraLarge": "http://books.google.com/books/publisher/content?id=G7utDwAAQBAJ&printsec=frontcover&img=1&zoom=6&edge=curl&imgtk=AFLRE70zvRYbN6L3AM1H-SFdT_b8RDDGh6SfKIC_erPvfkI3QnpI_sFSIyOjXKgLJqbxVttwKVw12OUkxkPGjlAekXU7tTbpS7OcUQ_XbxhKaIsoC6ekr32GtMzZ5WkHbGu6rRpdIYVQ&source=gbs_api"
|
||||
},
|
||||
"language": "fr",
|
||||
"previewLink": "http://books.google.fr/books?id=G7utDwAAQBAJ&hl=&source=gbs_api",
|
||||
"infoLink": "https://play.google.com/store/books/details?id=G7utDwAAQBAJ&source=gbs_api",
|
||||
"canonicalVolumeLink": "https://play.google.com/store/books/details?id=G7utDwAAQBAJ"
|
||||
},
|
||||
"layerInfo": {
|
||||
"layers": [
|
||||
{
|
||||
"layerId": "geo",
|
||||
"volumeAnnotationsVersion": "3"
|
||||
}
|
||||
]
|
||||
},
|
||||
"saleInfo": {
|
||||
"country": "FR",
|
||||
"saleability": "NOT_FOR_SALE",
|
||||
"isEbook": false
|
||||
},
|
||||
"accessInfo": {
|
||||
"country": "FR",
|
||||
"viewability": "PARTIAL",
|
||||
"embeddable": true,
|
||||
"publicDomain": false,
|
||||
"textToSpeechPermission": "ALLOWED",
|
||||
"epub": {
|
||||
"isAvailable": true,
|
||||
"acsTokenLink": "http://books.google.fr/books/download/Dans_la_toile_du_temps-sample-epub.acsm?id=G7utDwAAQBAJ&format=epub&output=acs4_fulfillment_token&dl_type=sample&source=gbs_api"
|
||||
},
|
||||
"pdf": {
|
||||
"isAvailable": true,
|
||||
"acsTokenLink": "http://books.google.fr/books/download/Dans_la_toile_du_temps-sample-pdf.acsm?id=G7utDwAAQBAJ&format=pdf&output=acs4_fulfillment_token&dl_type=sample&source=gbs_api"
|
||||
},
|
||||
"webReaderLink": "http://play.google.com/books/reader?id=G7utDwAAQBAJ&hl=&source=gbs_api",
|
||||
"accessViewStatus": "SAMPLE",
|
||||
"quoteSharingAllowed": false
|
||||
}
|
||||
}
|
@ -0,0 +1,156 @@
|
||||
{
|
||||
"kind": "books#volumes",
|
||||
"totalItems": 1499,
|
||||
"items": [
|
||||
{
|
||||
"kind": "books#volume",
|
||||
"id": "49T5twEACAAJ",
|
||||
"etag": "xf91z5Lu3rE",
|
||||
"selfLink": "https://www.googleapis.com/books/v1/volumes/49T5twEACAAJ",
|
||||
"volumeInfo": {
|
||||
"title": "Children of Time",
|
||||
"authors": [
|
||||
"Adrian Tchaikovsky"
|
||||
],
|
||||
"publisher": "Orbit",
|
||||
"publishedDate": "2018-12-11",
|
||||
"description": "Adrian Tchaikovksy's award-winning novel Children of Time, is the epic story of humanity's battle for survival on a terraformed planet. Who will inherit this new Earth? The last remnants of the human race left a dying Earth, desperate to find a new home among the stars. Following in the footsteps of their ancestors, they discover the greatest treasure of the past age - a world terraformed and prepared for human life. But all is not right in this new Eden. In the long years since the planet was abandoned, the work of its architects has borne disastrous fruit. The planet is not waiting for them, pristine and unoccupied. New masters have turned it from a refuge into mankind's worst nightmare. Now two civilizations are on a collision course, both testing the boundaries of what they will do to survive. As the fate of humanity hangs in the balance, who are the true heirs of this new Earth?span",
|
||||
"industryIdentifiers": [
|
||||
{
|
||||
"type": "ISBN_10",
|
||||
"identifier": "0316452505"
|
||||
},
|
||||
{
|
||||
"type": "ISBN_13",
|
||||
"identifier": "9780316452502"
|
||||
}
|
||||
],
|
||||
"readingModes": {
|
||||
"text": false,
|
||||
"image": false
|
||||
},
|
||||
"pageCount": 0,
|
||||
"printType": "BOOK",
|
||||
"categories": [
|
||||
"Fiction"
|
||||
],
|
||||
"averageRating": 4,
|
||||
"ratingsCount": 49,
|
||||
"maturityRating": "NOT_MATURE",
|
||||
"allowAnonLogging": false,
|
||||
"contentVersion": "preview-1.0.0",
|
||||
"panelizationSummary": {
|
||||
"containsEpubBubbles": false,
|
||||
"containsImageBubbles": false
|
||||
},
|
||||
"imageLinks": {
|
||||
"smallThumbnail": "http://books.google.com/books/content?id=49T5twEACAAJ&printsec=frontcover&img=1&zoom=5&source=gbs_api",
|
||||
"thumbnail": "http://books.google.com/books/content?id=49T5twEACAAJ&printsec=frontcover&img=1&zoom=1&source=gbs_api"
|
||||
},
|
||||
"language": "en",
|
||||
"previewLink": "http://books.google.fr/books?id=49T5twEACAAJ&dq=children+of+time+2015&hl=&cd=1&source=gbs_api",
|
||||
"infoLink": "http://books.google.fr/books?id=49T5twEACAAJ&dq=children+of+time+2015&hl=&source=gbs_api",
|
||||
"canonicalVolumeLink": "https://books.google.com/books/about/Children_of_Time.html?hl=&id=49T5twEACAAJ"
|
||||
},
|
||||
"saleInfo": {
|
||||
"country": "FR",
|
||||
"saleability": "NOT_FOR_SALE",
|
||||
"isEbook": false
|
||||
},
|
||||
"accessInfo": {
|
||||
"country": "FR",
|
||||
"viewability": "NO_PAGES",
|
||||
"embeddable": false,
|
||||
"publicDomain": false,
|
||||
"textToSpeechPermission": "ALLOWED",
|
||||
"epub": {
|
||||
"isAvailable": false
|
||||
},
|
||||
"pdf": {
|
||||
"isAvailable": false
|
||||
},
|
||||
"webReaderLink": "http://play.google.com/books/reader?id=49T5twEACAAJ&hl=&source=gbs_api",
|
||||
"accessViewStatus": "NONE",
|
||||
"quoteSharingAllowed": false
|
||||
},
|
||||
"searchInfo": {
|
||||
"textSnippet": "Adrian Tchaikovksy's award-winning novel Children of Time, is the epic story of humanity's battle for survival on a terraformed planet."
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "books#volume",
|
||||
"id": "G7utDwAAQBAJ",
|
||||
"etag": "CD3eFflJQF0",
|
||||
"selfLink": "https://www.googleapis.com/books/v1/volumes/G7utDwAAQBAJ",
|
||||
"volumeInfo": {
|
||||
"title": "Dans la toile du temps",
|
||||
"authors": [
|
||||
"Adrian Tchaikovsky"
|
||||
],
|
||||
"publisher": "Editions Gallimard",
|
||||
"publishedDate": "2019-10-03T00:00:00+02:00",
|
||||
"description": "La Terre est au plus mal... Ses derniers habitants n’ont plus qu’un seul espoir : coloniser le \"Monde de Kern\", une planète lointaine, spécialement terraformée pour l’espèce humaine. Mais sur ce \"monde vert\" paradisiaque, tout ne s’est pas déroulé comme les scientifiques s’y attendaient. Une autre espèce que celle qui était prévue, aidée par un nanovirus, s’est parfaitement adaptée à ce nouvel environnement et elle n’a pas du tout l’intention de laisser sa place. Le choc de deux civilisations aussi différentes que possible semble inévitable. Qui seront donc les héritiers de l’ancienne Terre ? Qui sortira vainqueur du piège tendu par la toile du temps ? Premier roman de l’auteur paru en France, Dans la toile du temps s’inscrit dans la lignée du cycle Élévation de David Brin. Il nous fait découvrir l’évolution d’une civilisation radicalement autre et sa confrontation inévitable avec l’espèce humaine. Le roman a reçu le prix Arthur C. Clarke en 2016",
|
||||
"industryIdentifiers": [
|
||||
{
|
||||
"type": "ISBN_13",
|
||||
"identifier": "9782072853319"
|
||||
},
|
||||
{
|
||||
"type": "ISBN_10",
|
||||
"identifier": "2072853311"
|
||||
}
|
||||
],
|
||||
"readingModes": {
|
||||
"text": true,
|
||||
"image": true
|
||||
},
|
||||
"pageCount": 478,
|
||||
"printType": "BOOK",
|
||||
"categories": [
|
||||
"Fiction"
|
||||
],
|
||||
"maturityRating": "NOT_MATURE",
|
||||
"allowAnonLogging": true,
|
||||
"contentVersion": "1.1.1.0.preview.3",
|
||||
"panelizationSummary": {
|
||||
"containsEpubBubbles": false,
|
||||
"containsImageBubbles": false
|
||||
},
|
||||
"imageLinks": {
|
||||
"smallThumbnail": "http://books.google.com/books/content?id=G7utDwAAQBAJ&printsec=frontcover&img=1&zoom=5&edge=curl&source=gbs_api",
|
||||
"thumbnail": "http://books.google.com/books/content?id=G7utDwAAQBAJ&printsec=frontcover&img=1&zoom=1&edge=curl&source=gbs_api"
|
||||
},
|
||||
"language": "fr",
|
||||
"previewLink": "http://books.google.fr/books?id=G7utDwAAQBAJ&printsec=frontcover&dq=children+of+time+2015&hl=&cd=2&source=gbs_api",
|
||||
"infoLink": "http://books.google.fr/books?id=G7utDwAAQBAJ&dq=children+of+time+2015&hl=&source=gbs_api",
|
||||
"canonicalVolumeLink": "https://books.google.com/books/about/Dans_la_toile_du_temps.html?hl=&id=G7utDwAAQBAJ"
|
||||
},
|
||||
"saleInfo": {
|
||||
"country": "FR",
|
||||
"saleability": "NOT_FOR_SALE",
|
||||
"isEbook": false
|
||||
},
|
||||
"accessInfo": {
|
||||
"country": "FR",
|
||||
"viewability": "PARTIAL",
|
||||
"embeddable": true,
|
||||
"publicDomain": false,
|
||||
"textToSpeechPermission": "ALLOWED",
|
||||
"epub": {
|
||||
"isAvailable": true,
|
||||
"acsTokenLink": "http://books.google.fr/books/download/Dans_la_toile_du_temps-sample-epub.acsm?id=G7utDwAAQBAJ&format=epub&output=acs4_fulfillment_token&dl_type=sample&source=gbs_api"
|
||||
},
|
||||
"pdf": {
|
||||
"isAvailable": true,
|
||||
"acsTokenLink": "http://books.google.fr/books/download/Dans_la_toile_du_temps-sample-pdf.acsm?id=G7utDwAAQBAJ&format=pdf&output=acs4_fulfillment_token&dl_type=sample&source=gbs_api"
|
||||
},
|
||||
"webReaderLink": "http://play.google.com/books/reader?id=G7utDwAAQBAJ&hl=&source=gbs_api",
|
||||
"accessViewStatus": "SAMPLE",
|
||||
"quoteSharingAllowed": false
|
||||
},
|
||||
"searchInfo": {
|
||||
"textSnippet": "Qui sortira vainqueur du piège tendu par la toile du temps ? Premier roman de l’auteur paru en France, Dans la toile du temps s’inscrit dans la lignée du cycle Élévation de David Brin."
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,410 @@
|
||||
using System.Net;
|
||||
using Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks;
|
||||
using Jellyfin.Plugin.Bookshelf.Tests.Http;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NSubstitute;
|
||||
|
||||
namespace Jellyfin.Plugin.Bookshelf.Tests
|
||||
{
|
||||
public class GoogleBooksProviderTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the content of a fixture file.
|
||||
/// </summary>
|
||||
/// <param name="fileName">Name of the fixture file.</param>
|
||||
/// <returns>The file's content.</returns>
|
||||
/// <exception cref="FileNotFoundException">If the file does not exist.</exception>
|
||||
private string GetFixture(string fileName)
|
||||
{
|
||||
var filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Fixtures", fileName);
|
||||
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
throw new FileNotFoundException($"The fixture file '{filePath}' was not found.");
|
||||
}
|
||||
|
||||
return File.ReadAllText(filePath);
|
||||
}
|
||||
|
||||
// From the query 'https://www.googleapis.com/books/v1/volumes?q=children+of+time+2015'
|
||||
private string GetTestSearchResult() => GetFixture("google-books-volume-search.json");
|
||||
|
||||
private string GetEnglishTestVolumeResult() => GetFixture("google-books-single-volume-en.json");
|
||||
private string GetFrenchTestVolumeResult() => GetFixture("google-books-single-volume-fr.json");
|
||||
|
||||
private bool HasGoogleId(string id, Dictionary<string, string> providerIds)
|
||||
{
|
||||
return providerIds.Count == 1
|
||||
&& providerIds.ContainsKey(GoogleBooksProvider.ProviderId)
|
||||
&& providerIds[GoogleBooksProvider.ProviderId] == id;
|
||||
}
|
||||
|
||||
#region GetSearchResults
|
||||
|
||||
[Fact]
|
||||
public async Task GetSearchResults_Success()
|
||||
{
|
||||
var mockedMessageHandler = new MockHttpMessageHandler(new List<(Func<Uri, bool> requestMatcher, MockHttpResponse response)>
|
||||
{
|
||||
((Uri uri) => uri.AbsoluteUri.Contains("volumes?q="), new MockHttpResponse(HttpStatusCode.OK, GetTestSearchResult())),
|
||||
});
|
||||
|
||||
var mockedHttpClientFactory = Substitute.For<IHttpClientFactory>();
|
||||
mockedHttpClientFactory.CreateClient(Arg.Any<string>()).Returns(new HttpClient(mockedMessageHandler));
|
||||
|
||||
IRemoteMetadataProvider<Book, BookInfo> provider = new GoogleBooksProvider(NullLogger<GoogleBooksProvider>.Instance, mockedHttpClientFactory);
|
||||
|
||||
var results = await provider.GetSearchResults(new BookInfo() { Name = "Children of Time" }, CancellationToken.None);
|
||||
|
||||
Assert.True(results.All(result => result.SearchProviderName == GoogleBooksProvider.ProviderName));
|
||||
|
||||
Assert.Collection(
|
||||
results,
|
||||
first =>
|
||||
{
|
||||
Assert.Equal("Children of Time", first.Name);
|
||||
Assert.True(HasGoogleId("49T5twEACAAJ", first.ProviderIds));
|
||||
Assert.Equal("http://books.google.com/books/content?id=49T5twEACAAJ&printsec=frontcover&img=1&zoom=1&source=gbs_api", first.ImageUrl);
|
||||
Assert.Equal("Adrian Tchaikovksy's award-winning novel Children of Time, is the epic story of humanity's battle for survival on a terraformed planet. Who will inherit this new Earth? The last remnants of the human race left a dying Earth, desperate to find a new home among the stars. Following in the footsteps of their ancestors, they discover the greatest treasure of the past age - a world terraformed and prepared for human life. But all is not right in this new Eden. In the long years since the planet was abandoned, the work of its architects has borne disastrous fruit. The planet is not waiting for them, pristine and unoccupied. New masters have turned it from a refuge into mankind's worst nightmare. Now two civilizations are on a collision course, both testing the boundaries of what they will do to survive. As the fate of humanity hangs in the balance, who are the true heirs of this new Earth?span", first.Overview);
|
||||
Assert.Equal(2018, first.ProductionYear);
|
||||
|
||||
},
|
||||
second =>
|
||||
{
|
||||
Assert.Equal("Dans la toile du temps", second.Name);
|
||||
Assert.True(HasGoogleId("G7utDwAAQBAJ", second.ProviderIds));
|
||||
Assert.Equal("http://books.google.com/books/content?id=G7utDwAAQBAJ&printsec=frontcover&img=1&zoom=1&edge=curl&source=gbs_api", second.ImageUrl);
|
||||
Assert.Equal("La Terre est au plus mal... Ses derniers habitants n’ont plus qu’un seul espoir : coloniser le \"Monde de Kern\", une planète lointaine, spécialement terraformée pour l’espèce humaine. Mais sur ce \"monde vert\" paradisiaque, tout ne s’est pas déroulé comme les scientifiques s’y attendaient. Une autre espèce que celle qui était prévue, aidée par un nanovirus, s’est parfaitement adaptée à ce nouvel environnement et elle n’a pas du tout l’intention de laisser sa place. Le choc de deux civilisations aussi différentes que possible semble inévitable. Qui seront donc les héritiers de l’ancienne Terre ? Qui sortira vainqueur du piège tendu par la toile du temps ? Premier roman de l’auteur paru en France, Dans la toile du temps s’inscrit dans la lignée du cycle Élévation de David Brin. Il nous fait découvrir l’évolution d’une civilisation radicalement autre et sa confrontation inévitable avec l’espèce humaine. Le roman a reçu le prix Arthur C. Clarke en 2016", second.Overview);
|
||||
Assert.Equal(2019, second.ProductionYear);
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetMetadata
|
||||
|
||||
[Fact]
|
||||
public async Task GetMetadata_MatchesByName_Success()
|
||||
{
|
||||
var mockedMessageHandler = new MockHttpMessageHandler(new List<(Func<Uri, bool> requestMatcher, MockHttpResponse response)>
|
||||
{
|
||||
((Uri uri) => uri.AbsoluteUri.Contains("volumes?q="), new MockHttpResponse(HttpStatusCode.OK, GetTestSearchResult())),
|
||||
((Uri uri) => uri.AbsoluteUri.Contains("volumes/49T5twEACAAJ"), new MockHttpResponse(HttpStatusCode.OK, GetEnglishTestVolumeResult()))
|
||||
});
|
||||
|
||||
var mockedHttpClientFactory = Substitute.For<IHttpClientFactory>();
|
||||
mockedHttpClientFactory.CreateClient(Arg.Any<string>()).Returns(new HttpClient(mockedMessageHandler));
|
||||
|
||||
IRemoteMetadataProvider<Book, BookInfo> provider = new GoogleBooksProvider(NullLogger<GoogleBooksProvider>.Instance, mockedHttpClientFactory);
|
||||
|
||||
var metadataResult = await provider.GetMetadata(new BookInfo() { Name = "Children of Time" }, CancellationToken.None);
|
||||
|
||||
Assert.False(metadataResult.QueriedById);
|
||||
Assert.True(metadataResult.HasMetadata);
|
||||
Assert.Equal("en", metadataResult.ResultLanguage);
|
||||
|
||||
Assert.Collection(metadataResult.People,
|
||||
p =>
|
||||
{
|
||||
Assert.Equal("Adrian Tchaikovsky", p.Name);
|
||||
Assert.Equal("Author", p.Type);
|
||||
});
|
||||
|
||||
Assert.True(HasGoogleId("49T5twEACAAJ", metadataResult.Item.ProviderIds));
|
||||
Assert.Equal("Children of Time", metadataResult.Item.Name);
|
||||
Assert.Collection(metadataResult.Item.Studios,
|
||||
s =>
|
||||
{
|
||||
Assert.Equal("Orbit", s);
|
||||
});
|
||||
Assert.Equal(2018, metadataResult.Item.ProductionYear);
|
||||
Assert.Equal("\u003cb\u003eAdrian Tchaikovksy's award-winning novel \u003ci\u003eChildren of Time\u003c/i\u003e, is the epic story of humanity's battle for survival on a terraformed planet." +
|
||||
"\u003c/b\u003e\u003cb\u003e\u003cbr\u003e\u003c/b\u003eWho will inherit this new Earth?" +
|
||||
"\u003cbr\u003e\u003cbr\u003eThe last remnants of the human race left a dying Earth, desperate to find a new home among the stars. " +
|
||||
"Following in the footsteps of their ancestors, they discover the greatest treasure of the past age - a world terraformed and prepared for human life." +
|
||||
"\u003cbr\u003e\u003cbr\u003eBut all is not right in this new Eden. In the long years since the planet was abandoned, the work of its architects has borne disastrous fruit. " +
|
||||
"The planet is not waiting for them, pristine and unoccupied. New masters have turned it from a refuge into mankind's worst nightmare." +
|
||||
"\u003cbr\u003e\u003cbr\u003eNow two civilizations are on a collision course, both testing the boundaries of what they will do to survive. " +
|
||||
"As the fate of humanity hangs in the balance, who are the true heirs of this new Earth?span", metadataResult.Item.Overview);
|
||||
Assert.Collection(metadataResult.Item.Genres,
|
||||
genre => Assert.Equal("Fiction", genre));
|
||||
Assert.Collection(metadataResult.Item.Tags,
|
||||
tag => Assert.Equal("Science Fiction", tag),
|
||||
tag => Assert.Equal("Alien Contact", tag),
|
||||
tag => Assert.Equal("Genetic Engineering", tag),
|
||||
tag => Assert.Equal("Hard Science Fiction", tag),
|
||||
tag => Assert.Equal("Space Exploration", tag),
|
||||
tag => Assert.Equal("Space Opera", tag)
|
||||
);
|
||||
Assert.Equal(8, metadataResult.Item.CommunityRating);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetMetadata_MatchesByProviderId_Success()
|
||||
{
|
||||
var mockedMessageHandler = new MockHttpMessageHandler(new List<(Func<Uri, bool> requestMatcher, MockHttpResponse response)>
|
||||
{
|
||||
((Uri uri) => uri.AbsoluteUri.Contains("volumes/G7utDwAAQBAJ"), new MockHttpResponse(HttpStatusCode.OK, GetFrenchTestVolumeResult()))
|
||||
});
|
||||
|
||||
var mockedHttpClientFactory = Substitute.For<IHttpClientFactory>();
|
||||
mockedHttpClientFactory.CreateClient(Arg.Any<string>()).Returns(new HttpClient(mockedMessageHandler));
|
||||
|
||||
IRemoteMetadataProvider<Book, BookInfo> provider = new GoogleBooksProvider(NullLogger<GoogleBooksProvider>.Instance, mockedHttpClientFactory);
|
||||
|
||||
var metadataResult = await provider.GetMetadata(new BookInfo()
|
||||
{
|
||||
Name = "Children of Time",
|
||||
ProviderIds = { { GoogleBooksProvider.ProviderId, "G7utDwAAQBAJ" } }
|
||||
}, CancellationToken.None);
|
||||
|
||||
Assert.True(metadataResult.QueriedById);
|
||||
Assert.True(metadataResult.HasMetadata);
|
||||
Assert.Equal("fr", metadataResult.ResultLanguage);
|
||||
|
||||
Assert.Collection(metadataResult.People,
|
||||
p =>
|
||||
{
|
||||
Assert.Equal("Adrian Tchaikovsky", p.Name);
|
||||
Assert.Equal("Author", p.Type);
|
||||
});
|
||||
|
||||
Assert.True(HasGoogleId("G7utDwAAQBAJ", metadataResult.Item.ProviderIds));
|
||||
Assert.Equal("Dans la toile du temps", metadataResult.Item.Name);
|
||||
Assert.Collection(metadataResult.Item.Studios,
|
||||
s =>
|
||||
{
|
||||
Assert.Equal("Editions Gallimard", s);
|
||||
});
|
||||
Assert.Equal(2019, metadataResult.Item.ProductionYear);
|
||||
Assert.Equal("La Terre est au plus mal... Ses derniers habitants n’ont plus qu’un seul espoir : coloniser le \"Monde de Kern\", une planète lointaine, spécialement terraformée pour l’espèce humaine. " +
|
||||
"Mais sur ce \"monde vert\" paradisiaque, tout ne s’est pas déroulé comme les scientifiques s’y attendaient. " +
|
||||
"Une autre espèce que celle qui était prévue, aidée par un nanovirus, s’est parfaitement adaptée à ce nouvel environnement et elle n’a pas du tout l’intention de laisser sa place. " +
|
||||
"Le choc de deux civilisations aussi différentes que possible semble inévitable. " +
|
||||
"Qui seront donc les héritiers de l’ancienne Terre ? Qui sortira vainqueur du piège tendu par la toile du temps ? " +
|
||||
"Premier roman de l’auteur paru en France, Dans la toile du temps s’inscrit dans la lignée du cycle Élévation de David Brin. " +
|
||||
"Il nous fait découvrir l’évolution d’une civilisation radicalement autre et sa confrontation inévitable avec l’espèce humaine. " +
|
||||
"Le roman a reçu le prix Arthur C. Clarke en 2016", metadataResult.Item.Overview);
|
||||
Assert.Collection(metadataResult.Item.Genres,
|
||||
genre => Assert.Equal("Fiction", genre));
|
||||
Assert.Collection(metadataResult.Item.Tags,
|
||||
tag => Assert.Equal("Science Fiction", tag));
|
||||
Assert.Null(metadataResult.Item.CommunityRating);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetMetadata_MatchesByNameWithYearVariance_SkipsResult()
|
||||
{
|
||||
var mockedMessageHandler = new MockHttpMessageHandler(new List<(Func<Uri, bool> requestMatcher, MockHttpResponse response)>
|
||||
{
|
||||
((Uri uri) => uri.AbsoluteUri.Contains("volumes?q="), new MockHttpResponse(HttpStatusCode.OK, GetTestSearchResult())),
|
||||
((Uri uri) => uri.AbsoluteUri.Contains("volumes/49T5twEACAAJ"), new MockHttpResponse(HttpStatusCode.OK, GetEnglishTestVolumeResult()))
|
||||
});
|
||||
|
||||
var mockedHttpClientFactory = Substitute.For<IHttpClientFactory>();
|
||||
mockedHttpClientFactory.CreateClient(Arg.Any<string>()).Returns(new HttpClient(mockedMessageHandler));
|
||||
|
||||
IRemoteMetadataProvider<Book, BookInfo> provider = new GoogleBooksProvider(NullLogger<GoogleBooksProvider>.Instance, mockedHttpClientFactory);
|
||||
|
||||
var metadataResult = await provider.GetMetadata(new BookInfo() { Name = "Children of Time (2015)" }, CancellationToken.None);
|
||||
|
||||
Assert.False(metadataResult.HasMetadata);
|
||||
Assert.Null(metadataResult.Item);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetBookMetadata
|
||||
|
||||
[Fact]
|
||||
public void GetBookMetadata_WithName_CorrectlyMatchesFileName()
|
||||
{
|
||||
GoogleBooksProvider provider = new GoogleBooksProvider(NullLogger<GoogleBooksProvider>.Instance, Substitute.For<IHttpClientFactory>());
|
||||
|
||||
var bookInfo = new BookInfo() { Name = "Children of Time" };
|
||||
provider.GetBookMetadata(bookInfo);
|
||||
|
||||
Assert.Equal("Children of Time", bookInfo.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetBookMetadata_WithNameAndYear_CorrectlyMatchesFileName()
|
||||
{
|
||||
GoogleBooksProvider provider = new GoogleBooksProvider(NullLogger<GoogleBooksProvider>.Instance, Substitute.For<IHttpClientFactory>());
|
||||
|
||||
var bookInfo = new BookInfo() { Name = "Children of Time (2015)" };
|
||||
provider.GetBookMetadata(bookInfo);
|
||||
|
||||
Assert.Equal("Children of Time", bookInfo.Name);
|
||||
Assert.Equal(2015, bookInfo.Year);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetBookMetadata_WithIndexAndName_CorrectlyMatchesFileName()
|
||||
{
|
||||
GoogleBooksProvider provider = new GoogleBooksProvider(NullLogger<GoogleBooksProvider>.Instance, Substitute.For<IHttpClientFactory>());
|
||||
|
||||
var bookInfo = new BookInfo() { Name = "1 - Children of Time" };
|
||||
provider.GetBookMetadata(bookInfo);
|
||||
|
||||
Assert.Equal("Children of Time", bookInfo.Name);
|
||||
Assert.Equal(1, bookInfo.IndexNumber);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetBookMetadata_WithIndexAndNameInFolder_CorrectlyMatchesFileName()
|
||||
{
|
||||
GoogleBooksProvider provider = new GoogleBooksProvider(NullLogger<GoogleBooksProvider>.Instance, Substitute.For<IHttpClientFactory>());
|
||||
|
||||
// The series can already be identified from the folder name
|
||||
var bookInfo = new BookInfo() { SeriesName = "Children of Time", Name = "2 - Children of Ruin" };
|
||||
provider.GetBookMetadata(bookInfo);
|
||||
|
||||
Assert.Equal("Children of Time", bookInfo.SeriesName);
|
||||
Assert.Equal("Children of Ruin", bookInfo.Name);
|
||||
Assert.Equal(2, bookInfo.IndexNumber);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetBookMetadata_WithIndexdNameAndYear_CorrectlyMatchesFileName()
|
||||
{
|
||||
GoogleBooksProvider provider = new GoogleBooksProvider(NullLogger<GoogleBooksProvider>.Instance, Substitute.For<IHttpClientFactory>());
|
||||
|
||||
var bookInfo = new BookInfo() { Name = "1 - Children of Time (2015)" };
|
||||
provider.GetBookMetadata(bookInfo);
|
||||
|
||||
Assert.Equal("Children of Time", bookInfo.Name);
|
||||
Assert.Equal(1, bookInfo.IndexNumber);
|
||||
Assert.Equal(2015, bookInfo.Year);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetBookMetadata_WithComicFormat_CorrectlyMatchesFileName()
|
||||
{
|
||||
GoogleBooksProvider provider = new GoogleBooksProvider(NullLogger<GoogleBooksProvider>.Instance, Substitute.For<IHttpClientFactory>());
|
||||
|
||||
// Complete format
|
||||
var bookInfo = new BookInfo() { Name = "Children of Time (2015) #2 (of 3) (2019)" };
|
||||
provider.GetBookMetadata(bookInfo);
|
||||
|
||||
Assert.Empty(bookInfo.Name);
|
||||
Assert.Equal("Children of Time", bookInfo.SeriesName);
|
||||
Assert.Equal(2, bookInfo.IndexNumber);
|
||||
Assert.Equal(2019, bookInfo.Year);
|
||||
|
||||
// Without series year
|
||||
bookInfo = new BookInfo() { Name = "Children of Time #2 (of 3) (2019)" };
|
||||
provider.GetBookMetadata(bookInfo);
|
||||
|
||||
Assert.Empty(bookInfo.Name);
|
||||
Assert.Equal("Children of Time", bookInfo.SeriesName);
|
||||
Assert.Equal(2, bookInfo.IndexNumber);
|
||||
Assert.Equal(2019, bookInfo.Year);
|
||||
|
||||
// Without total count
|
||||
bookInfo = new BookInfo() { Name = "Children of Time #2 (2019)" };
|
||||
provider.GetBookMetadata(bookInfo);
|
||||
|
||||
Assert.Empty(bookInfo.Name);
|
||||
Assert.Equal("Children of Time", bookInfo.SeriesName);
|
||||
Assert.Equal(2, bookInfo.IndexNumber);
|
||||
Assert.Equal(2019, bookInfo.Year);
|
||||
|
||||
// With only issue number
|
||||
bookInfo = new BookInfo() { Name = "Children of Time #2" };
|
||||
provider.GetBookMetadata(bookInfo);
|
||||
|
||||
Assert.Empty(bookInfo.Name);
|
||||
Assert.Equal("Children of Time", bookInfo.SeriesName);
|
||||
Assert.Equal(2, bookInfo.IndexNumber);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetBookMetadata_WithGoodreadsFormat_CorrectlyMatchesFileName()
|
||||
{
|
||||
GoogleBooksProvider provider = new GoogleBooksProvider(NullLogger<GoogleBooksProvider>.Instance, Substitute.For<IHttpClientFactory>());
|
||||
|
||||
// Goodreads format
|
||||
var bookInfo = new BookInfo() { Name = "Children of Ruin (Children of Time, #2)" };
|
||||
provider.GetBookMetadata(bookInfo);
|
||||
|
||||
Assert.Equal("Children of Ruin", bookInfo.Name);
|
||||
Assert.Equal("Children of Time", bookInfo.SeriesName);
|
||||
Assert.Equal(2, bookInfo.IndexNumber);
|
||||
|
||||
// Goodreads format with year added
|
||||
bookInfo = new BookInfo() { Name = "Children of Ruin (Children of Time, #2) (2019)" };
|
||||
provider.GetBookMetadata(bookInfo);
|
||||
|
||||
Assert.Equal("Children of Ruin", bookInfo.Name);
|
||||
Assert.Equal("Children of Time", bookInfo.SeriesName);
|
||||
Assert.Equal(2, bookInfo.IndexNumber);
|
||||
Assert.Equal(2019, bookInfo.Year);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetSearchString
|
||||
|
||||
[Fact]
|
||||
public void GetSearchString_WithSeriesAndName_ReturnsCorrectString()
|
||||
{
|
||||
GoogleBooksProvider provider = new GoogleBooksProvider(NullLogger<GoogleBooksProvider>.Instance, Substitute.For<IHttpClientFactory>());
|
||||
var bookInfo = new BookInfo()
|
||||
{
|
||||
SeriesName = "Invincible",
|
||||
Name = "Eight is Enough",
|
||||
IndexNumber = 2,
|
||||
Year = 2004
|
||||
};
|
||||
var searchString = provider.GetSearchString(bookInfo);
|
||||
|
||||
Assert.Equal("Invincible Eight is Enough", searchString);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSearchString_WithSeriesAndIndex_ReturnsCorrectString()
|
||||
{
|
||||
GoogleBooksProvider provider = new GoogleBooksProvider(NullLogger<GoogleBooksProvider>.Instance, Substitute.For<IHttpClientFactory>());
|
||||
var bookInfo = new BookInfo()
|
||||
{
|
||||
SeriesName = "Invincible",
|
||||
IndexNumber = 2
|
||||
};
|
||||
var searchString = provider.GetSearchString(bookInfo);
|
||||
|
||||
Assert.Equal("Invincible 2", searchString);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSearchString_WithNameAndYear_ReturnsCorrectString()
|
||||
{
|
||||
GoogleBooksProvider provider = new GoogleBooksProvider(NullLogger<GoogleBooksProvider>.Instance, Substitute.For<IHttpClientFactory>());
|
||||
var bookInfo = new BookInfo()
|
||||
{
|
||||
Name = "Eight is Enough",
|
||||
Year = 2004
|
||||
};
|
||||
var searchString = provider.GetSearchString(bookInfo);
|
||||
|
||||
Assert.Equal("Eight is Enough 2004", searchString);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSearchString_WithOnlyName_ReturnsCorrectString()
|
||||
{
|
||||
GoogleBooksProvider provider = new GoogleBooksProvider(NullLogger<GoogleBooksProvider>.Instance, Substitute.For<IHttpClientFactory>());
|
||||
var bookInfo = new BookInfo()
|
||||
{
|
||||
Name = "Eight is Enough",
|
||||
};
|
||||
var searchString = provider.GetSearchString(bookInfo);
|
||||
|
||||
Assert.Equal("Eight is Enough", searchString);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
namespace Jellyfin.Plugin.Bookshelf.Tests.Http
|
||||
{
|
||||
/// <summary>
|
||||
/// HttpMessageHandler that returns a mocked response.
|
||||
/// </summary>
|
||||
internal class MockHttpMessageHandler : HttpMessageHandler
|
||||
{
|
||||
private readonly List<(Func<Uri, bool> RequestMatcher, MockHttpResponse Response)> _messageHandlers;
|
||||
|
||||
public MockHttpMessageHandler(List<(Func<Uri, bool> requestMatcher, MockHttpResponse response)> messageHandlers)
|
||||
{
|
||||
_messageHandlers = messageHandlers;
|
||||
}
|
||||
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (request.RequestUri == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(request.RequestUri));
|
||||
}
|
||||
|
||||
var response = _messageHandlers.FirstOrDefault(x => x.RequestMatcher(request.RequestUri)).Response;
|
||||
|
||||
if (response == null)
|
||||
{
|
||||
throw new InvalidOperationException($"No response found for request {request.RequestUri}");
|
||||
}
|
||||
|
||||
return Task.FromResult(new HttpResponseMessage
|
||||
{
|
||||
StatusCode = response.StatusCode,
|
||||
Content = new StringContent(response.Response)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
using System.Net;
|
||||
|
||||
namespace Jellyfin.Plugin.Bookshelf.Tests.Http
|
||||
{
|
||||
internal record MockHttpResponse(HttpStatusCode StatusCode, string Response);
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
@ -31,4 +32,16 @@
|
||||
<ProjectReference Include="..\..\Jellyfin.Plugin.Bookshelf\Jellyfin.Plugin.Bookshelf.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="Fixtures\google-books-single-volume-en.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Fixtures\google-books-single-volume-fr.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Fixtures\google-books-volume-search.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
1
tests/Jellyfin.Plugin.Bookshelf.Tests/Usings.cs
Normal file
1
tests/Jellyfin.Plugin.Bookshelf.Tests/Usings.cs
Normal file
@ -0,0 +1 @@
|
||||
global using Xunit;
|
Loading…
Reference in New Issue
Block a user