fix(library): match a shelf title against the full title when the search title is only the series (#1587)

Fixes #1584. Thanks to @joeRob2468 for the write-up, which pinned this
down to the line.

Hardcover cuts a title like "Mistborn: The Final Empire" down to
"Mistborn" for searching when it can't tell the series prefix from a
title prefix, and search results don't carry the series. The library
check then matched the shelf title "Mistborn: The Final Empire" against
"Mistborn", saw "final empire" as extra words and took it for a longer,
different work (the "Dune" vs "Dune Messiah" rule), so the book got no
In library badge.

`match_entries` now tries the search title first as before and, if that
finds nothing and the full title is different, tries the full title too.
The full title is the more specific of the two so it should rarely add
false positives. Another book in the same series still doesn't match,
there is a test for that, and the Dune and Dune Messiah test passes
unchanged. The matching loop moved into a small helper to run it twice,
nothing in it changed.

This only touches the library check. The same short search title also
feeds release searches and auto-download matching, which I left alone
here because loosening or tightening those changes what gets searched
and downloaded for everyone.

Tests fail on current main without the change. The library tests, ruff
and basedpyright are clean.
This commit is contained in:
splitsec2 authored and GitHub committed 2026-10-26 01:30:33 -06:00
1 parent e3fd7898f6
commit 46b9e7d8a3
2 files changed
+44 -1

No files matched your search

+20 -1
View File
@@ -162,8 +162,27 @@ def match_entries(book: BookMetadata, entries: list[LibraryEntry]) -> str | None
if external_id in entry.external_ids or entry.isbns & book_isbns:
return "owned"
title = book.search_title or book.title
surname = author_surname(book.search_author or (book.authors[0] if book.authors else ""))
# The search title is the looser of the two, so it goes first. Hardcover cuts "Series:
# Book Title" down to the series name when it cannot tell that prefix from a title, and a
# shelf title with the book's own words then reads as a longer, different work. The full
# title is the more specific of the two and names the book as the shelf does.
titles = [book.search_title or book.title]
if book.title and book.title not in titles:
titles.append(book.title)
collection: str | None = None
for title in titles:
held = _held_as(title, surname, entries)
if held == "owned":
return "owned"
collection = collection or held
return collection
def _held_as(title: str, surname: str | None, entries: list[LibraryEntry]) -> str | None:
"""How the shelf holds a book of this ``title``: ``"owned"``, ``"collection"`` or None."""
collection: str | None = None
title_toks = significant_tokens(title)
for entry in entries:
+24
View File
@@ -444,6 +444,30 @@ def test_the_series_and_volume_a_shelf_title_repeats_do_not_block_a_match() -> N
assert library_index.match_entries(book, shelf) == "owned"
def test_the_full_title_matches_when_the_search_title_is_only_the_series() -> None:
# Hardcover searches "Mistborn: The Final Empire" as "Mistborn" when it cannot tell the
# series prefix from a title prefix, and the shelf title then reads as a longer work.
shelf = [_shelf_entry({"mistborn", "the", "final", "empire"}, {"brandon", "sanderson"})]
book = _book(
title="Mistborn: The Final Empire",
search_title="Mistborn",
authors=["Brandon Sanderson"],
)
assert library_index.match_entries(book, shelf) == "owned"
def test_the_full_title_does_not_match_another_book_in_the_series() -> None:
shelf = [_shelf_entry({"mistborn", "the", "well", "of", "ascension"}, {"brandon", "sanderson"})]
book = _book(
title="Mistborn: The Final Empire",
search_title="Mistborn",
authors=["Brandon Sanderson"],
)
assert library_index.match_entries(book, shelf) is None
def test_an_edition_word_does_not_block_a_match() -> None:
shelf = [_shelf_entry({"dune", "illustrated", "edition"}, {"frank", "herbert"})]