mirror of
https://github.com/jellyfin/jellyfin-plugin-bookshelf.git
synced 2024-11-22 21:29:45 +00:00
Add Google Books external id (#80)
* Add error handling, external ID and ID search to the Google Books provider * Fix CodeQL warnings
This commit is contained in:
parent
fa45821aa9
commit
e31429c87e
@ -1,4 +1,3 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 15
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Plugin.Bookshelf", "Jellyfin.Plugin.Bookshelf\Jellyfin.Plugin.Bookshelf.csproj", "{8D744D83-5403-4BA4-8794-760AF69DAC06}"
|
||||
|
@ -0,0 +1,76 @@
|
||||
using System.Globalization;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Extensions.Json;
|
||||
using MediaBrowser.Common.Net;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for the Google Books providers.
|
||||
/// </summary>
|
||||
public abstract class BaseGoogleBooksProvider
|
||||
{
|
||||
private readonly ILogger<BaseGoogleBooksProvider> _logger;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BaseGoogleBooksProvider"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger{GoogleBooksProvider}"/> interface.</param>
|
||||
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
|
||||
protected BaseGoogleBooksProvider(ILogger<BaseGoogleBooksProvider> logger, IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a result from the Google Books API.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of expected result.</typeparam>
|
||||
/// <param name="url">API URL to call.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The API result.</returns>
|
||||
protected async Task<T?> GetResultFromAPI<T>(string url, CancellationToken cancellationToken)
|
||||
where T : class
|
||||
{
|
||||
var response = await _httpClientFactory
|
||||
.CreateClient(NamedClient.Default)
|
||||
.GetAsync(url, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var errorResponse = await response.Content.ReadFromJsonAsync<ErrorResponse>(JsonDefaults.Options, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (errorResponse != null)
|
||||
{
|
||||
_logger.LogError("Error response from Google Books API: {ErrorMessage} (status code: {StatusCode})", errorResponse.Error.Message, response.StatusCode);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<T>(JsonDefaults.Options, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetch book data from the Google Books API.
|
||||
/// </summary>
|
||||
/// <param name="googleBookId">The volume id.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The API result.</returns>
|
||||
protected async Task<BookResult?> FetchBookData(string googleBookId, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var url = string.Format(CultureInfo.InvariantCulture, GoogleApiUrls.DetailsUrl, googleBookId);
|
||||
|
||||
return await GetResultFromAPI<BookResult>(url, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
15
Jellyfin.Plugin.Bookshelf/Providers/GoogleBooks/Error.cs
Normal file
15
Jellyfin.Plugin.Bookshelf/Providers/GoogleBooks/Error.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
|
||||
namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
|
||||
{
|
||||
internal class Error
|
||||
{
|
||||
public HttpStatusCode Code { get; set; }
|
||||
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
public IEnumerable<ErrorDetails> Errors { get; set; } = Enumerable.Empty<ErrorDetails>();
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
|
||||
{
|
||||
internal class ErrorDetails
|
||||
{
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
public string Domain { get; set; } = string.Empty;
|
||||
|
||||
public string Reason { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
|
||||
{
|
||||
internal class ErrorResponse
|
||||
{
|
||||
public Error Error { get; set; } = new Error();
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Providers;
|
||||
|
||||
namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class GoogleBooksExternalId : IExternalId
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string ProviderName => GoogleBooksConstants.ProviderName;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Key => GoogleBooksConstants.ProviderId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ExternalIdMediaType? Type => null; // TODO: No ExternalIdMediaType value for book
|
||||
|
||||
/// <inheritdoc />
|
||||
public string? UrlFormatString => "https://books.google.com/books?id={0}";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(IHasProviderIds item) => item is Book;
|
||||
}
|
||||
}
|
@ -1,32 +1,35 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Extensions.Json;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Providers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
|
||||
{
|
||||
/// <summary>
|
||||
/// Google books image provider.
|
||||
/// </summary>
|
||||
public class GoogleBooksImageProvider : IRemoteImageProvider
|
||||
public class GoogleBooksImageProvider : BaseGoogleBooksProvider, IRemoteImageProvider
|
||||
{
|
||||
private readonly ILogger<GoogleBooksImageProvider> _logger;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GoogleBooksImageProvider"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger{GoogleBooksProvider}"/> interface.</param>
|
||||
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
|
||||
public GoogleBooksImageProvider(IHttpClientFactory httpClientFactory)
|
||||
public GoogleBooksImageProvider(ILogger<GoogleBooksImageProvider> logger, IHttpClientFactory httpClientFactory)
|
||||
: base(logger, httpClientFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
}
|
||||
|
||||
@ -74,19 +77,6 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
|
||||
return list;
|
||||
}
|
||||
|
||||
private async Task<BookResult?> FetchBookData(string googleBookId, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var url = string.Format(CultureInfo.InvariantCulture, GoogleApiUrls.DetailsUrl, googleBookId);
|
||||
|
||||
var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
|
||||
|
||||
using var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<BookResult>(JsonDefaults.Options, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private List<string> ProcessBookImage(BookResult bookResult)
|
||||
{
|
||||
var images = new List<string>();
|
||||
|
@ -4,12 +4,10 @@ using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Extensions.Json;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
@ -22,7 +20,7 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
|
||||
/// <summary>
|
||||
/// Google books provider.
|
||||
/// </summary>
|
||||
public class GoogleBooksProvider : IRemoteMetadataProvider<Book, BookInfo>
|
||||
public class GoogleBooksProvider : BaseGoogleBooksProvider, IRemoteMetadataProvider<Book, BookInfo>
|
||||
{
|
||||
// convert these characters to whitespace for better matching
|
||||
// there are two dashes with different char codes
|
||||
@ -69,6 +67,7 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
|
||||
public GoogleBooksProvider(
|
||||
ILogger<GoogleBooksProvider> logger,
|
||||
IHttpClientFactory httpClientFactory)
|
||||
: base(logger, httpClientFactory)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_logger = logger;
|
||||
@ -81,38 +80,59 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
|
||||
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(BookInfo searchInfo, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var list = new List<RemoteSearchResult>();
|
||||
|
||||
var searchResults = await GetSearchResultsInternal(searchInfo, cancellationToken).ConfigureAwait(false);
|
||||
if (searchResults is null)
|
||||
Func<BookResult, RemoteSearchResult> getSearchResultFromBook = (BookResult info) =>
|
||||
{
|
||||
return Enumerable.Empty<RemoteSearchResult>();
|
||||
}
|
||||
|
||||
foreach (var result in searchResults.Items)
|
||||
{
|
||||
if (result.VolumeInfo is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var remoteSearchResult = new RemoteSearchResult();
|
||||
|
||||
remoteSearchResult.SetProviderId(GoogleBooksConstants.ProviderId, result.Id);
|
||||
remoteSearchResult.SetProviderId(GoogleBooksConstants.ProviderId, info.Id);
|
||||
remoteSearchResult.SearchProviderName = GoogleBooksConstants.ProviderName;
|
||||
remoteSearchResult.Name = result.VolumeInfo.Title;
|
||||
remoteSearchResult.Overview = WebUtility.HtmlDecode(result.VolumeInfo.Description);
|
||||
remoteSearchResult.ProductionYear = GetYearFromPublishedDate(result.VolumeInfo.PublishedDate);
|
||||
remoteSearchResult.Name = info.VolumeInfo?.Title;
|
||||
remoteSearchResult.Overview = WebUtility.HtmlDecode(info.VolumeInfo?.Description);
|
||||
remoteSearchResult.ProductionYear = GetYearFromPublishedDate(info.VolumeInfo?.PublishedDate);
|
||||
|
||||
if (result.VolumeInfo.ImageLinks?.Thumbnail != null)
|
||||
if (info.VolumeInfo?.ImageLinks?.Thumbnail != null)
|
||||
{
|
||||
remoteSearchResult.ImageUrl = result.VolumeInfo.ImageLinks.Thumbnail;
|
||||
remoteSearchResult.ImageUrl = info.VolumeInfo.ImageLinks.Thumbnail;
|
||||
}
|
||||
|
||||
list.Add(remoteSearchResult);
|
||||
}
|
||||
return remoteSearchResult;
|
||||
};
|
||||
|
||||
return list;
|
||||
var googleBookId = searchInfo.GetProviderId(GoogleBooksConstants.ProviderId);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(googleBookId))
|
||||
{
|
||||
var bookData = await FetchBookData(googleBookId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (bookData == null || bookData.VolumeInfo == null)
|
||||
{
|
||||
return Enumerable.Empty<RemoteSearchResult>();
|
||||
}
|
||||
|
||||
return new[] { getSearchResultFromBook(bookData) };
|
||||
}
|
||||
else
|
||||
{
|
||||
var searchResults = await GetSearchResultsInternal(searchInfo, cancellationToken).ConfigureAwait(false);
|
||||
if (searchResults is null)
|
||||
{
|
||||
return Enumerable.Empty<RemoteSearchResult>();
|
||||
}
|
||||
|
||||
var list = new List<RemoteSearchResult>();
|
||||
foreach (var result in searchResults.Items)
|
||||
{
|
||||
if (result.VolumeInfo is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
list.Add(getSearchResultFromBook(result));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -173,11 +193,7 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
|
||||
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);
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<SearchResult>(JsonDefaults.Options, cancellationToken).ConfigureAwait(false);
|
||||
return await GetResultFromAPI<SearchResult>(url, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<string?> FetchBookId(BookInfo item, CancellationToken cancellationToken)
|
||||
@ -240,19 +256,6 @@ namespace Jellyfin.Plugin.Bookshelf.Providers.GoogleBooks
|
||||
return bookReleaseYear;
|
||||
}
|
||||
|
||||
private async Task<BookResult?> FetchBookData(string googleBookId, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var url = string.Format(CultureInfo.InvariantCulture, GoogleApiUrls.DetailsUrl, googleBookId);
|
||||
|
||||
var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
|
||||
|
||||
using var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<BookResult>(JsonDefaults.Options, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private Book? ProcessBookData(BookResult bookResult, CancellationToken cancellationToken)
|
||||
{
|
||||
if (bookResult.VolumeInfo is null)
|
||||
|
@ -3,6 +3,7 @@ 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
|
||||
@ -21,7 +22,7 @@ namespace Jellyfin.Plugin.Bookshelf.Tests
|
||||
using var client = new HttpClient(mockedMessageHandler);
|
||||
mockedHttpClientFactory.CreateClient(Arg.Any<string>()).Returns(client);
|
||||
|
||||
IRemoteImageProvider provider = new GoogleBooksImageProvider(mockedHttpClientFactory);
|
||||
IRemoteImageProvider provider = new GoogleBooksImageProvider(NullLogger<GoogleBooksImageProvider>.Instance, mockedHttpClientFactory);
|
||||
|
||||
var images = await provider.GetImages(new Book()
|
||||
{
|
||||
|
@ -27,7 +27,7 @@ namespace Jellyfin.Plugin.Bookshelf.Tests
|
||||
#region GetSearchResults
|
||||
|
||||
[Fact]
|
||||
public async Task GetSearchResults_Success()
|
||||
public async Task GetSearchResults_ByName_Success()
|
||||
{
|
||||
var mockedMessageHandler = new MockHttpMessageHandler(new List<(Func<Uri, bool> requestMatcher, MockHttpResponse response)>
|
||||
{
|
||||
@ -65,6 +65,131 @@ namespace Jellyfin.Plugin.Bookshelf.Tests
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSearchResults_ByProviderId_Success()
|
||||
{
|
||||
var mockedMessageHandler = new MockHttpMessageHandler(new List<(Func<Uri, bool> requestMatcher, MockHttpResponse response)>
|
||||
{
|
||||
((Uri uri) => uri.AbsoluteUri.Contains("volumes/49T5twEACAAJ"), new MockHttpResponse(HttpStatusCode.OK, GetEnglishTestVolumeResult()))
|
||||
});
|
||||
|
||||
var mockedHttpClientFactory = Substitute.For<IHttpClientFactory>();
|
||||
using var client = new HttpClient(mockedMessageHandler);
|
||||
mockedHttpClientFactory.CreateClient(Arg.Any<string>()).Returns(client);
|
||||
|
||||
IRemoteMetadataProvider<Book, BookInfo> provider = new GoogleBooksProvider(NullLogger<GoogleBooksProvider>.Instance, mockedHttpClientFactory);
|
||||
|
||||
var results = await provider.GetSearchResults(new BookInfo()
|
||||
{
|
||||
ProviderIds = new Dictionary<string, string>()
|
||||
{
|
||||
{ GoogleBooksConstants.ProviderId, "49T5twEACAAJ" }
|
||||
}
|
||||
}, CancellationToken.None);
|
||||
|
||||
Assert.True(results.All(result => result.SearchProviderName == GoogleBooksConstants.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&imgtk=AFLRE70U9t4z91EAYhiD2AYOR9pzNu86QDKZebNLQo4K3jMaJ748TC5LvCoZGt9ON4pZ54H8RoIRyCB5IveVDmt49QjeJlbJtWLlZoksRHXInrEVmo2476WXKcLhZOjp41Vu_5Lb05oJ&source=gbs_api", first.ImageUrl);
|
||||
Assert.Equal("<b>Adrian Tchaikovksy's award-winning novel <i>Children of Time</i>, is the epic story of humanity's battle for survival on a terraformed planet.</b><b>" +
|
||||
"<br></b>Who will inherit this new Earth?<br><br>" +
|
||||
"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." +
|
||||
"<br><br>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." +
|
||||
"<br><br>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);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSearchResults_ByProviderId_WithInvalidId_ReturnsNoResults()
|
||||
{
|
||||
// API will return a 503 code if the volume id is invalid
|
||||
string errorResponse = @"
|
||||
{
|
||||
""error"": {
|
||||
""code"": 503,
|
||||
""message"": ""Service temporarily unavailable."",
|
||||
""errors"": [
|
||||
{
|
||||
""message"": ""Service temporarily unavailable."",
|
||||
""domain"": ""global"",
|
||||
""reason"": ""backendFailed""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
";
|
||||
var mockedMessageHandler = new MockHttpMessageHandler(new List<(Func<Uri, bool> requestMatcher, MockHttpResponse response)>
|
||||
{
|
||||
((Uri uri) => uri.AbsoluteUri.Contains("volumes/49T55wEACAA"), new MockHttpResponse(HttpStatusCode.NotFound, errorResponse))
|
||||
});
|
||||
|
||||
var mockedHttpClientFactory = Substitute.For<IHttpClientFactory>();
|
||||
using var client = new HttpClient(mockedMessageHandler);
|
||||
mockedHttpClientFactory.CreateClient(Arg.Any<string>()).Returns(client);
|
||||
|
||||
IRemoteMetadataProvider<Book, BookInfo> provider = new GoogleBooksProvider(NullLogger<GoogleBooksProvider>.Instance, mockedHttpClientFactory);
|
||||
|
||||
var results = await provider.GetSearchResults(new BookInfo()
|
||||
{
|
||||
ProviderIds = new Dictionary<string, string>()
|
||||
{
|
||||
{ GoogleBooksConstants.ProviderId, "49T55wEACAA" }
|
||||
}
|
||||
}, CancellationToken.None);
|
||||
|
||||
Assert.Empty(results);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSearchResults_ByProviderId_WithNonExistentId_ReturnsNoResults()
|
||||
{
|
||||
// API will return a 404 code if the volume is not found
|
||||
string errorResponse = @"
|
||||
{
|
||||
""error"": {
|
||||
""code"": 404,
|
||||
""message"": ""The volume ID could not be found."",
|
||||
""errors"": [
|
||||
{
|
||||
""message"": ""The volume ID could not be found."",
|
||||
""domain"": ""global"",
|
||||
""reason"": ""notFound""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
";
|
||||
var mockedMessageHandler = new MockHttpMessageHandler(new List<(Func<Uri, bool> requestMatcher, MockHttpResponse response)>
|
||||
{
|
||||
((Uri uri) => uri.AbsoluteUri.Contains("volumes/49T55wEACAAX"), new MockHttpResponse(HttpStatusCode.NotFound, errorResponse))
|
||||
});
|
||||
|
||||
var mockedHttpClientFactory = Substitute.For<IHttpClientFactory>();
|
||||
using var client = new HttpClient(mockedMessageHandler);
|
||||
mockedHttpClientFactory.CreateClient(Arg.Any<string>()).Returns(client);
|
||||
|
||||
IRemoteMetadataProvider<Book, BookInfo> provider = new GoogleBooksProvider(NullLogger<GoogleBooksProvider>.Instance, mockedHttpClientFactory);
|
||||
|
||||
var results = await provider.GetSearchResults(new BookInfo()
|
||||
{
|
||||
ProviderIds = new Dictionary<string, string>()
|
||||
{
|
||||
{ GoogleBooksConstants.ProviderId, "49T55wEACAAX" }
|
||||
}
|
||||
}, CancellationToken.None);
|
||||
|
||||
Assert.Empty(results);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetMetadata
|
||||
@ -216,7 +341,6 @@ namespace Jellyfin.Plugin.Bookshelf.Tests
|
||||
Assert.Equal("Children of Time", bookInfo.Name);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void GetBookMetadata_WithNameAndDefaultSeriesName_CorrectlyResetSeriesName()
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user