diff --git a/shelfmark/core/library_index.py b/shelfmark/core/library_index.py index 95dcb007..ced2646f 100644 --- a/shelfmark/core/library_index.py +++ b/shelfmark/core/library_index.py @@ -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: diff --git a/tests/core/test_library_index.py b/tests/core/test_library_index.py index e58169dd..f4006c0e 100644 --- a/tests/core/test_library_index.py +++ b/tests/core/test_library_index.py @@ -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"})]