mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-11-10 12:28:27 +00:00
fix(hardcover): read the featured series from a search result (#1589)
Closes #1585. @joeRob2468 traced this one to the parser, and the answer turned out to be sitting in the search response already. A Hardcover search hit carries the book's featured series as `featured_series`, with the same shape as the `featured_book_series` of a full record: the position and a nested series with its id, name and book count. `_parse_search_result` never read it, so a book requested from a search card was stored with `series_name: null` and the `{Series}` folder and the custom script payload came out empty. It now reads it, through a small helper that `_parse_book` uses too, so there is no extra Hardcover call per request (the issue's other option was fetching the full record, which costs one against the rate limit). I checked it against a live search for "Mistborn The Final Empire". The top hit parses to series "The Mistborn Saga: The Original Trilogy", position 1, id 10001, and its search title is still "Mistborn". That last part is on purpose. I left `_compute_search_title` alone, since passing it the series would change what gets searched for everyone and the series name here doesn't equal the "Mistborn" prefix anyway. Two things that behave differently. A search result now has `series_name` set, so anything in the UI that shows it for a full record (the series link, the bigger cover in the release modal) will show it on search results too. I ran the branch and looked at the release modal for the top Mistborn result: it shows "#1 of 3 in The Mistborn Saga: The Original Trilogy" with the View series link. The modal fetches the full record when it opens, so I can't say how much of that is new on this path. Requests already stored keep the empty series they were saved with. Tests: four in a new file (a hit with a series, hits with none or an incomplete one, a hit nested in a `document`, and the search title staying the same), the first and third fail on current main. The metadata and core suites, ruff and basedpyright are clean.
This commit is contained in:
1 parent
d95f663a92
commit
9bd6c51388
2 files changed
+100
-13
No files matched your search
@@ -1046,6 +1046,29 @@ def _compute_search_title(
|
||||
return None
|
||||
|
||||
|
||||
def _featured_series_fields(
|
||||
featured_series: Any,
|
||||
) -> tuple[str | None, str | None, Any, int | None]:
|
||||
"""The ``(id, name, position, book count)`` of a book's featured series.
|
||||
|
||||
A book record carries it as ``featured_book_series`` and a search hit as
|
||||
``featured_series``; both hold the position and a nested ``series``.
|
||||
"""
|
||||
if not isinstance(featured_series, dict):
|
||||
return None, None, None, None
|
||||
|
||||
series_id = None
|
||||
series_name = None
|
||||
series_count = None
|
||||
series_data = featured_series.get("series")
|
||||
if isinstance(series_data, dict):
|
||||
if series_data.get("id") is not None:
|
||||
series_id = str(series_data.get("id"))
|
||||
series_name = series_data.get("name")
|
||||
series_count = series_data.get("primary_books_count")
|
||||
return series_id, series_name, featured_series.get("position"), series_count
|
||||
|
||||
|
||||
@register_provider_kwargs("hardcover")
|
||||
def _hardcover_kwargs() -> dict[str, Any]:
|
||||
"""Provide Hardcover-specific constructor kwargs."""
|
||||
@@ -2959,6 +2982,9 @@ class HardcoverProvider(MetadataProvider):
|
||||
# Extract subtitle if available in search results
|
||||
subtitle = item.get("subtitle")
|
||||
search_title = _compute_search_title(title, subtitle)
|
||||
series_id, series_name, series_position, series_count = _featured_series_fields(
|
||||
item.get("featured_series") or (item.get("document") or {}).get("featured_series")
|
||||
)
|
||||
|
||||
if matched_title and matched_title != title:
|
||||
# The subtitle belongs to the original title, not to the translation.
|
||||
@@ -2977,6 +3003,10 @@ class HardcoverProvider(MetadataProvider):
|
||||
description=full_description,
|
||||
publish_year=publish_year,
|
||||
source_url=source_url,
|
||||
series_id=series_id,
|
||||
series_name=series_name,
|
||||
series_position=series_position,
|
||||
series_count=series_count,
|
||||
display_fields=display_fields,
|
||||
)
|
||||
|
||||
@@ -3058,19 +3088,9 @@ class HardcoverProvider(MetadataProvider):
|
||||
full_description = _combine_headline_description(headline, description)
|
||||
|
||||
# Extract series info from featured_book_series
|
||||
series_id = None
|
||||
series_name = None
|
||||
series_position = None
|
||||
series_count = None
|
||||
featured_series = book.get("featured_book_series")
|
||||
if featured_series:
|
||||
series_position = featured_series.get("position")
|
||||
series_data = featured_series.get("series")
|
||||
if series_data:
|
||||
if series_data.get("id") is not None:
|
||||
series_id = str(series_data.get("id"))
|
||||
series_name = series_data.get("name")
|
||||
series_count = series_data.get("primary_books_count")
|
||||
series_id, series_name, series_position, series_count = _featured_series_fields(
|
||||
book.get("featured_book_series")
|
||||
)
|
||||
|
||||
# Extract titles by language from editions
|
||||
# This allows searching with localized titles when language filter is active
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
from shelfmark.metadata_providers.hardcover import HardcoverProvider
|
||||
|
||||
|
||||
def _provider() -> HardcoverProvider:
|
||||
return HardcoverProvider(api_key="test-token")
|
||||
|
||||
|
||||
def _hit(**overrides):
|
||||
# The shape of a search hit: the featured series is `featured_series`, not the
|
||||
# `featured_book_series` of a full book record.
|
||||
hit = {
|
||||
"id": "369692",
|
||||
"title": "Mistborn: The Final Empire",
|
||||
"subtitle": "The Final Empire",
|
||||
"author_names": ["Brandon Sanderson"],
|
||||
"featured_series": {
|
||||
"position": 1.0,
|
||||
"series": {
|
||||
"id": 10001,
|
||||
"name": "The Mistborn Saga: The Original Trilogy",
|
||||
"primary_books_count": 3,
|
||||
},
|
||||
},
|
||||
"featured_series_position": 1.0,
|
||||
}
|
||||
hit.update(overrides)
|
||||
return hit
|
||||
|
||||
|
||||
def test_a_search_result_carries_the_featured_series():
|
||||
book = _provider()._parse_search_result(_hit())
|
||||
|
||||
assert book is not None
|
||||
assert book.series_id == "10001"
|
||||
assert book.series_name == "The Mistborn Saga: The Original Trilogy"
|
||||
assert book.series_position == 1.0
|
||||
assert book.series_count == 3
|
||||
|
||||
|
||||
def test_a_search_result_without_a_series_has_none():
|
||||
for featured in (None, {}, {"position": 2.0}):
|
||||
book = _provider()._parse_search_result(_hit(featured_series=featured))
|
||||
|
||||
assert book is not None
|
||||
assert book.series_name is None
|
||||
assert book.series_id is None
|
||||
|
||||
|
||||
def test_a_search_result_nested_in_a_document_carries_the_series_too():
|
||||
book = _provider()._parse_search_result({"document": _hit()})
|
||||
|
||||
assert book is not None
|
||||
assert book.series_name == "The Mistborn Saga: The Original Trilogy"
|
||||
|
||||
|
||||
def test_the_search_title_is_unchanged_by_the_series():
|
||||
book = _provider()._parse_search_result(_hit())
|
||||
|
||||
assert book is not None
|
||||
assert book.search_title == "Mistborn"
|
||||
|
||||
|
||||
def test_a_hit_with_a_null_document_and_no_series_is_still_parsed():
|
||||
book = _provider()._parse_search_result(_hit(featured_series=None, document=None))
|
||||
|
||||
assert book is not None
|
||||
assert book.series_name is None
|
||||
Reference in new issue
Block a user