Skip to content

Searching for a book in a 2-in-1 edition never returns anything - #70

Open
JordanFromIT wants to merge 1 commit into
Chaptarr:developfrom
JordanFromIT:fix/omnibus-edition-search-query
Open

Searching for a book in a 2-in-1 edition never returns anything#70
JordanFromIT wants to merge 1 commit into
Chaptarr:developfrom
JordanFromIT:fix/omnibus-edition-search-query

Conversation

@JordanFromIT

Copy link
Copy Markdown

Description

A book whose monitored edition is a 2-in-1 can never be found. The edition title names both works — "A Game of Thrones / A Clash of Kings" — and that whole string is what gets sent to the indexers as the search query. Nothing is named after a compilation, so every indexer returns zero results, every time, for as long as that edition stays selected. The user sees an interactive search that finds nothing and no reason why. This makes the search use the book's own title when the book is one of the works the edition collects; the edition itself is left alone.

For a concrete sense of it: Goodreads lists A Clash of Kings under at least three compilation works — #1-2, #1-4, and a 5-book boxed set — so a library that picks up any of them for the wrong reason silently loses that book. On a real instance, a book pinned to the #1-2 edition returned 0 results across 5 indexers; with this patch the same book, same config, same indexers returned 3.

Why it happens. BookSearchCriteria already narrows a title down to its main section before querying, but it only knows two separators. SplitBookTitle splits on : and (, so "A Clash of Kings (Part Two)" correctly becomes "A Clash of Kings" — while "A Game of Thrones / A Clash of Kings" is passed through whole. GetQueryTitle then replaces the slash with the same + it uses for ordinary word breaks, welding the two titles into one query that matches nothing:

Monitored edition title Query sent to indexers Results
A Clash of Kings (Part Two) A+Clash+of+Kings works today
A Game of Thrones / A Clash of Kings A+Game+of+Thrones+A+Clash+of+Kings always zero

Both rows are the same book by the same author. The only difference is which punctuation the edition happens to use.

Technical detail

ReleaseSearchService.BookSearch picks the single monitored edition and hands its title to the search criteria:

searchSpec.BookTitle = GetSearchBookTitle(book, selectedEdition) ?? string.Empty;

GetSearchBookTitle prefers selectedEdition.Title over book.Title — deliberately, so a pinned edition is what actually gets searched for. That is the right default and is not changed here. The gap is only that a compilation edition names several works, so preferring its title asks for something that does not exist as a release.

Downstream, BookSearchCriteria.BookQuery is GetQueryTitle(GetMainSearchTitle(BookTitle, Author?.Name)). GetMainSearchTitle delegates to Parser.SplitBookTitle, which builds its parts array from IndexOf('(') and IndexOf(':') only and otherwise returns (book, string.Empty) — the title unsplit. GetQueryTitle then runs NonWord.Replace(cleanTitle, "+"), and / is a non-word character, so it collapses to the same separator used between ordinary words.

This adds GetCollectedWorkTitle, called only from GetSearchBookTitle. It splits the edition title on /, normalises each segment (collapse internal whitespace, trim) and compares case-insensitively against the book's own title. On a match it returns book.Title; otherwise it returns null and the caller keeps the edition title exactly as before.

SplitBookTitle itself is untouched. It is shared with ParsingService on the release-parsing side, and teaching it a new separator would change how release names are interpreted too — a much wider blast radius than this bug justifies.

Deliberate choices worth flagging for review:

  • The equality check is strict, and that is the point. A segment only matches when it equals the book title outright. This is what keeps "Stories of Fantasy, Horror/Sci-Fi, and a Man Called Tuf" safe: splitting it yields "Stories of Fantasy, Horror" and "Sci-Fi, and a Man Called Tuf", neither of which is any book's title, so nothing fires and the query is unchanged. A looser "contains" test would have broken that title. There is a test for exactly this case.
  • Only / is handled. Compilations also appear as Book One and Book Two or with &, |, +. Those are far more ambiguous — & and and occur inside ordinary titles constantly — and I did not want to guess at them inside a bugfix. If you would rather see a general separator list, say so and I will extend it.
  • Nothing fires when the metadata disagrees even slightly. If the book row is Clash of Kings and the segment is A Clash of Kings, no match, no change. Conservative on purpose: a wrong fallback would send searches for the wrong book, which is worse than the status quo of sending none.
  • Blast radius is one call site. GetCollectedWorkTitle is new and called only from GetSearchBookTitle, which is called only from BookSearch. Nothing else reads it, no shared parser is modified, and when the edition title has no / the method returns null on its second line — so for the overwhelming majority of libraries this code path is inert.
  • The edition stays pinned. This changes only the outbound query. The user's edition selection, the UI display, and everything persisted are untouched.

Known gap, deliberately not addressed: the release matcher still validates candidates against the monitored edition title. So for a book genuinely pinned to a compilation, this patch makes standalone releases reachable — they now come back instead of not existing — but whether each one is then accepted still depends on that downstream comparison. Closing that properly means teaching the matcher about collected works too, which is a separate change against different code, and I would rather not fold it into a fix this small. Happy to follow up with it if you want them together.

Database Migration

NO. No schema changes, no new columns, no data written. The change reads Book.Title and Edition.Title, both of which already exist, and affects only the string handed to the indexer at search time. Nothing is persisted, so there is nothing to migrate or back-fill, and reverting the commit fully reverts the behaviour.

How was this tested?

Built and tested on Linux, .NET 10, in the mcr.microsoft.com/dotnet/sdk:10.0 image. Ran the same steps build.yml uses, including the guards that run before compilation:

Step Result
git grep merge-conflict markers clean
package.json parses ok
version_guard.py sync Version sync OK for working tree: 0.9.929
version_guard.py monotonic --compare-ref origin/develop Version monotonic OK: 0.9.929
version_guard.py commit-hygiene --compare-ref origin/develop OK for range origin/develop..HEAD
dotnet build src/Chaptarr.NoTests.sln -c Release 0 warnings, 0 errors
dotnet test src/Chaptarr.Core.Test -c Release 2845 passed, 0 failed

Test counts. Clean origin/develop at 5713d83: 2838 passed. This branch: 2845 passed (+7), 0 failing. Both measured from a hard-reset, git cleaned tree with _output/_tests removed, because an earlier run on a dirty tree reported the same number for both and I did not want to quote it.

No frontend files are touched — 2 files changed, both .cs — so the yarn steps that live outside build.yml are not applicable here.

The 7 new tests, in ReleaseSearchServiceTitleSelectionFixture: the spaced separator, the unspaced separator, case and padding differences, the book title being blank, a slash that is part of a phrase (Horror/Sci-Fi), a marketplace listing where no segment matches the book, and one end-to-end assertion that BookQuery comes out as a single work.

Confirmed the tests actually catch the bug by reverting only ReleaseSearchService.cs to its develop version while keeping the new tests, then re-running:

Failed omnibus_edition_should_produce_a_single_work_book_query
  Expected: "A+Clash+of+Kings"
  But was:  "A+Game+of+Thrones+A+Clash+of+Kings"
Failed should_use_book_title_when_selected_edition_is_an_omnibus_containing_it
  Expected: "A Clash of Kings"
  But was:  "A Game of Thrones / A Clash of Kings"
...
Failed!  - Failed: 4, Passed: 10, Total: 14

Restoring the fix returns Passed! - Failed: 0, Passed: 14. The three guard tests pass in both states, which is what confirms the patch does not change behaviour for titles it should leave alone.

End to end, Docker on Linux, against a copy of a real library where a book was pinned to the A Game of Thrones / A Clash of Kings edition. Stock 0.9.929 and a build of this branch, same config, same 5 indexers, run through the interactive-search endpoint:

stock this branch
totalResults 0 3
EPUB releases returned none 2

Queries recorded on the indexer proxy confirm the change reaches the wire:

query sent
before A Game of Thrones A Clash of Kings
after A Clash of Kings

Screenshots (UI changes only)

None — no UI changes.

When a book's monitored edition is a compilation named after several works
("A Game of Thrones / A Clash of Kings"), that whole string is sent to the
indexers as the search query. No release is named after a compilation, so
every indexer returns nothing and the book can never be found.

Fall back to the book's own title when it is one of the slash-separated
works listed in the edition title. Titles where the slash is part of a
phrase ("Horror/Sci-Fi") produce no matching segment and are left alone.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant