Skip to content

fix(importer): library scan keeps the best title match - #2978

Merged
vavallee merged 5 commits into
vavallee:mainfrom
mvanhorn:fix/2941-library-scan-best-title-match
Oct 13, 2026
Merged

vavallee merged 5 commits into
vavallee:mainfrom
mvanhorn:fix/2941-library-scan-best-title-match

Conversation

@mvanhorn

@mvanhorn mvanhorn commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

Summary

tryReconcileTitle now returns a hit with its score once the existing gates pass. reconcileByTitle collects every hit, sorts by score, and claims the leader when it is the only hit, when its normalised title is an exact match strictly ahead of the runner-up, or when it leads by at least 0.05. A closer pair is left unmatched, and a failed write applies the same rule to the hits that remain. The user guide and changelog describe that preference.

A library scan can attach a file to the wrong wanted book for the same author. The first title that scores at least 0.85 takes the file, so a shorter partial title that appears earlier in the library receives it and the book with the exact title stays without a file. reconcileByTitle walks the author's wanted books in library order, and tryReconcileTitle claims a book as soon as it clears the title gates, including the 0.85 Jaro-Winkler floor. Later candidates are never compared with that first score, so an earlier partial match is kept even when a later title matches exactly.

Closes #2941

How it was verified

go test ./internal/importer/... succeeded.

Checklist

  • Every commit carries a Signed-off-by that matches its author (git commit -s), see Sign your work. Commits authored by a coding agent fail this check; author them as yourself.
  • Changelog fragment added as changelog.d/<issue-number>-<slug>.md, not an edit to CHANGELOG.md, see changelog.d/README.md
  • Tests added or updated
  • docs/DEPLOYMENT.md updated if env vars, config, or upgrade path changed
  • Wiki pages under docs/ updated if user-facing behaviour changed
  • No new dependency, or its licence is permissive (Bindery is MIT; GPL, AGPL, SSPL and BUSL are not accepted) and THIRD_PARTY_LICENSES.md is regenerated

Test plan

  • make check (or the individual go test ./cmd/... ./internal/... and cd web && npm run build steps)
    Not claimed: make check is a different command from the local runs that passed, gofmt, go vet and go test.

go test ./internal/importer/... succeeded.

AI was used for assistance.

tryReconcileTitle now returns a hit with its score once the existing
gates pass. reconcileByTitle collects every hit, sorts by score, and
claims the leader when it is the only hit, when its normalised title is
an exact match strictly ahead of the runner-up, or when it leads by at
least 0.05. A closer pair is left unmatched, and a failed write applies
the same rule to the hits that remain. The user guide and changelog
describe that preference.

Fixes vavallee#2941

Signed-off-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
@github-actions github-actions Bot added the bindery-notified Discord notification already sent for this PR label Oct 4, 2026
@codecov

codecov Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@vavallee vavallee left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for jumping on this so fast @mvanhorn! The ranking is a clear improvement, the Dutch Harry Potter case is fixed, and the five new tests all fail on main and pass here. It still merges cleanly with today's main.

@foobarbigtime's two follow ups both hold, and I wrote failing tests for each:

  1. The better match drops out of the comparison. A book that already has a file of that format, or was claimed earlier in this scan, never becomes a hit. So "Harry Potter" is the only hit and takes the file. I hit this with two copies in one scan, with the exact book already imported from an earlier scan, and with "…kind script.epub" being walked before the real file. Could those books stay in the ranking as blocked hits? If one of them leads, or is within the margin of the leader, set claimBlocked and return false so the file stays Unmatched. Books that aren't candidates can come from the same pool suggestionCatalogue uses.

  2. Fallback after a write error. When claimTitleHit fails, just return false rather than moving on to hits[1:]. Repro:

database.Exec(fmt.Sprintf(`CREATE TRIGGER f BEFORE INSERT ON book_files
  WHEN NEW.book_id = %d BEGIN SELECT RAISE(ABORT,'boom'); END`, exact.ID))
e.s.ScanLibrary(e.ctx) // "Harry Potter" ends up with the epub

One small thing: the changelog fragment should end with a thanks to foobarbigtime for the report and the follow up. That's how the other entries read.

With those two fixes and tests for them, happy to merge. Thanks again!

Keep existing and newly claimed format slots in the title comparison so
weaker matches cannot take duplicate files. Stop after a title claim write
fails instead of trying a runner-up.

Cover the reported cases with regressions and thank @foobarbigtime for
the report and follow up.

Signed-off-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
@mvanhorn

mvanhorn commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

Fixed in 1801ad0. Already-imported and already-claimed books stay in the ranking and block a weaker or near-tied match, and a failed write no longer falls through to the runner-up. Both of your repro cases are now regression tests, and the changelog fragment ends with thanks to @foobarbigtime. The new tests, gofmt, and golangci-lint on the importer pass locally.

vavallee added a commit that referenced this pull request Oct 6, 2026
… first (#2941) (#3033)

* fix(importer): bind an existing file to the best title match, not the first (#2941)

FindExisting answered with the first library file whose title cleared
titleWordsMatch, so "Harry Potter" took an untracked "Harry Potter en
het vervloekte kind.epub" on the add author path, skipped its search,
and left the exact book unbound once the file was owned.

FindExisting now scores every matching file by Jaro-Winkler on the
normalised titles, using the rule the library scan adopts in #2978: the
best file answers when it is the only title, when it is an exact
normalised match strictly ahead, or when it leads by 0.05. A closer
pair answers nothing so the book stays Wanted. Files that differ only
in digits (tracks, parts, formats) or share a numbered book folder are
one book. Supplement ranking and the volume veto apply as before.

FindExistingAmong adds the other direction: the chosen file is held to
the same rule against the author's other catalogue titles, so a file
titled exactly as another book is not taken by a shorter one. The add
paths pass the author's books as rivals through handleNewWantedBook.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9
Signed-off-by: vavallee <vavallee@protonmail.com>

* fix(importer): withdraw an existing file only for an exact rival title (#2941)

Review of the first pass found the scored rival rule withdrew real
matches. Any catalogue row sharing two words with the file and scoring
within 0.05 of the wanted title blocked the bind, so "Project Hail Mary
A Novel.epub" was refused to "Project Hail Mary" over a "Proyecto Hail
Mary" row, "Mistborn.epub" to "Mistborn: The Final Empire" over the
rest of the series, and the owned book was downloaded again. A rival
now withdraws a file only when its normalised title is exactly the
file's, which still keeps the Harry Potter case from the report fixed.
Excluded rows are no longer rivals.

A title group now answers with its best scoring member, so "Dune" gets
"Dune.epub" rather than "Dune 2.epub" walked before it.

The author sync passes the catalogue it already holds instead of
listing the author again for every bound book. Other callers read the
listing once per new book, only when a file was found.

Also corrects the Scanner.FindExisting godoc and widens the changelog
fragment to every add path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9
Signed-off-by: vavallee <vavallee@protonmail.com>

* fix(importer): drop rival titled files before ranking existing files (#2941)

A file titled exactly as another of the author's books was withdrawn
after ranking, so when it outscored the wanted book's own file nothing
answered: "The Way of Kings" found nothing beside "The Way of Kings
Prime.epub", and "Dune" nothing beside "Dune Messiah.epub", where main
bound the right file. Such files now leave the candidate set before
bestExistingFile ranks, and the next best file can answer.

Because dropping a file can also settle a near tie, the add path now
always asks the rival aware lookup instead of only after a plain
lookup found something.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9
Signed-off-by: vavallee <vavallee@protonmail.com>

---------

Signed-off-by: vavallee <vavallee@protonmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@vavallee

Copy link
Copy Markdown
Owner

Hey @mvanhorn, thanks for the update on this one, and sorry it has waited on me. This PR now conflicts with main in scanner.go (three hunks, from #2990, #3003 and #3107). Could you merge main into the branch? Once that is in I will take another look at the changes since my last review.

One thing to check while you resolve it: #3003 made the scan refuse a file that another book already tracks, so it is worth looking at how claimBlocked interacts with that rule.

vavallee and others added 3 commits October 13, 2026 00:21
Resolve internal/importer/scanner.go against the work that landed on main
since 1801ad0.

- The too-small gate (vavallee#2944) moves into claimTitleHit, after the ranking
  and claim checks and before AddBookFile.
- fillCoverFromFile (vavallee#3094) stays after the reconcile log in claimTitleHit.
- The recordUnmatchedUnits callback takes main's (rep, ev) signature and
  returns (candidates, reason).
- The suggestion catalogue is built on first use again, shared by the title
  tier and the callback, instead of eagerly.
- titleReconcileMargin is now findExistingMargin, one constant for the scan
  and the add path.
- claimBlocked is set only when an earlier file of the pass claimed the
  book, as on main. A Skipped book in the ranking still blocks the hit but no
  longer turns a tiny sidecar into an already tracked file, which dropped its
  too small row.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9
Signed-off-by: vavallee <vavallee@protonmail.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9
Signed-off-by: vavallee <vavallee@protonmail.com>
…the scan (vavallee#2941)

A blocked hit (Skipped, or already holding a file) within the 0.05 margin
sent the file back to Unmatched even when the wanted book's title was an
exact match, so the wanted book stayed Wanted and was downloaded again.
The scan now claims an exact, unblocked hit that is strictly ahead of every
other hit, the same exact strictly ahead rule FindExisting uses. A blocked
best, a tie, or a non exact best with a blocked hit in the margin still
refuse.

A claimed book whose root check turns a file away now still sets
claimBlocked, so the file stays a sidecar as on main (vavallee#2188).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9
Signed-off-by: vavallee <vavallee@protonmail.com>

@vavallee vavallee left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good after the merge with main and the exact title fix, thanks.

@vavallee
vavallee merged commit 0fe7ccd into vavallee:main Oct 13, 2026
45 of 46 checks passed
@mvanhorn

Copy link
Copy Markdown
Contributor Author

Appreciate the review here, @vavallee. The library scan now keeps the best scoring title match and leaves close ties unmatched.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bindery-notified Discord notification already sent for this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Library scan reconciles a file onto the first title match ≥ 0.85 instead of the best (exact) match

2 participants