diff --git a/shelfmark/metadata_providers/googlebooks.py b/shelfmark/metadata_providers/googlebooks.py index 726de39b..72929d28 100644 --- a/shelfmark/metadata_providers/googlebooks.py +++ b/shelfmark/metadata_providers/googlebooks.py @@ -47,6 +47,11 @@ _HTTP_STATUS_NOT_FOUND = HTTPStatus.NOT_FOUND GOOGLE_BOOKS_BASE_URL = "https://www.googleapis.com/books/v1" + +class _GoogleBooksRequestError(Exception): + """Raised when Google Books does not return a usable API response.""" + + # Sort mapping - Google only supports "relevance" and "newest" SORT_MAPPING: dict[SortOrder, str | None] = { SortOrder.RELEVANCE: None, # Default, no param needed @@ -117,7 +122,10 @@ class GoogleBooksProvider(MetadataProvider): f"{options.query}:{options.search_type.value}:{options.sort.value}:" f"{options.language}:{options.limit}:{options.page}:{fields_key}" ) - return self._search_cached(cache_key, options) + try: + return self._search_cached(cache_key, options) + except _GoogleBooksRequestError: + return [] @cacheable( ttl_key="METADATA_CACHE_SEARCH_TTL", @@ -166,18 +174,19 @@ class GoogleBooksProvider(MetadataProvider): if options.language: params["langRestrict"] = options.language + result = self._make_request("/volumes", params) + if result is None: + raise _GoogleBooksRequestError + books: list[BookMetadata] = [] try: - result = self._make_request("/volumes", params) - if result: - items = result.get("items", []) + items = result.get("items", []) + for item in items: + book = self._parse_volume(item) + if book: + books.append(book) - for item in items: - book = self._parse_volume(item) - if book: - books.append(book) - - logger.info("Google Books search '%s' returned %s results", query, len(books)) + logger.info("Google Books search '%s' returned %s results", query, len(books)) except Exception: logger.exception("Google Books search error") diff --git a/tests/metadata/test_googlebooks_parse.py b/tests/metadata/test_googlebooks_parse.py index 6708c35f..9557e724 100644 --- a/tests/metadata/test_googlebooks_parse.py +++ b/tests/metadata/test_googlebooks_parse.py @@ -1,6 +1,59 @@ +import requests + +from shelfmark.core.cache import get_metadata_cache +from shelfmark.metadata_providers import MetadataSearchOptions from shelfmark.metadata_providers.googlebooks import GoogleBooksProvider +class _GoogleBooksResponse: + def __init__(self, payload): + self._payload = payload + + def raise_for_status(self): + return None + + def json(self): + return self._payload + + +class _FlakyGoogleBooksSession: + def __init__(self): + self.calls = 0 + + def get(self, *args, **kwargs): + self.calls += 1 + if self.calls == 1: + raise requests.Timeout + return _GoogleBooksResponse( + { + "items": [ + { + "id": "volume-1", + "volumeInfo": { + "title": "Recovered Book", + "authors": ["Alice Author"], + }, + } + ] + } + ) + + +def test_googlebooks_search_does_not_cache_request_failures(): + get_metadata_cache().clear() + provider = GoogleBooksProvider(api_key="test-key") + session = _FlakyGoogleBooksSession() + provider.session = session + options = MetadataSearchOptions(query="Recovered Book") + + assert provider.search(options) == [] + + result = provider.search(options) + + assert session.calls == 2 + assert [book.title for book in result] == ["Recovered Book"] + + class TestGoogleBooksParseVolume: def test_parse_volume_returns_metadata_for_valid_payload(self): provider = GoogleBooksProvider(api_key="test-key")