Skip to content

fix(series): skip split edition parts of owned books and apply metadata profile filters to series fill (#3048, #2208) - #3109

Merged
vavallee merged 4 commits into
mainfrom
fix/wanted-series-filters
Oct 8, 2026
Merged

vavallee merged 4 commits into
mainfrom
fix/wanted-series-filters

Conversation

@vavallee

@vavallee vavallee commented Oct 8, 2026 •

Copy link
Copy Markdown
Owner

Closes #3048
Closes #2208

#3048: split edition parts filled before #2915 stay Wanted

Root cause. Fill's re-queue loop (ListBooksInSeries in Fill) marks every non imported series book wanted and monitored, and the scheduled sweep searches every monitored wanted book. Neither knows about split editions, so the "The Way of Kings, Part 1/2" rows a pre #2915 fill created go back on Wanted on every fill and get searched.

Fix.

  • seriesmatch.SplitEditionPartOf: the fix(series): count split editions of an owned book as covered, not missing (#2524) #2915 two signal rule for stored rows. The position is fractional under the whole's integer position, and the title is the whole's title plus a Part N marker. A subtitle after the marker is ignored.
  • BookRepo.ListCoveredSplitEditionParts: SQL self join on series_books, with the rule applied in Go. A part counts as covered when it is not imported and its whole is imported or monitored, not excluded, and has the same owner (null safe), so one user's library never changes another user's sweep.
  • Fill skips covered parts when it re-queues. wantedSearchQueue skips them in the sweep. Rows are left exactly as they are.
  • POST /series/{id}/split-parts/unmonitor (admin) unmonitors them in one click. Nothing is deleted and imported books are never touched. GET /series and /series/{id} carry splitEditionPartBookIds, and the series card badges those rows, leaves them out of the missing count, and shows Unmonitor N split parts when any are still monitored.

Coordination with #2915. #2915 is still open (changes requested), so this PR doesn't reuse its helpers. The rule lives in a new file, internal/seriesmatch/split_edition.go, with distinct names (SplitEditionPartOf, a private splitEditionMarkerRe/fractionalFloor). It doesn't touch match.go, importer/volume.go, buildHardcoverDiff or the diff response type. Both PRs can merge in either order with no textual or compile conflict. Once both are in, SplitEditionPartOf can be collapsed onto #2915's IntegerFloorPosition + SplitPartOf in a small follow up. There is one deliberate difference: #2915 treats any Present row as the whole. Here the whole has to be imported or monitored, because a wanted, unmonitored whole means nothing is being sought and the parts may be what the user wants.

For whichever of #2915 and this PR lands second:

#2208: metadata profile filters and series fill

Already fixed, part 1 (retroactive). #2247 (7917b7ec, 2026-09-04, "feat(author): add safe catalogue reconciliation (#2208)") added More → Reconcile catalogue…. It applies every profile filter to stored rows, with a preview and the provider lookups that needs. This PR doesn't duplicate it. The docs now point to it from the filter description.

Root cause, part 2. ensureHardcoverCatalogBook never consulted a metadata profile, and Fill's re-queue loop re-monitored stored rows the profile rejects (which also undid any hand unmonitoring).

Fix.

  • SeriesHandler.WithMetadataProfiles, wired in main.go.
  • Bulk fill (Fill gaps / add all) screens each book it would create with catalogBookProfileFilter. That covers SkipPartBooks, SkipMissingDate, MinEditionCount (unknown passes), the language allow list (a rejection is rechecked against the provider's edition evidence, as the sync does; a blank language passes whatever the unknown language setting says, because Hardcover series entries carry none and it is resolved later; a failed evidence lookup also passes), and MinPages/SkipMissingISBN via an edition lookup made only when one of them is on (a failed lookup enforces nothing). A brand new author is screened before the author row is created. Books already in the library are linked regardless, matching the sync's "existing books are maintained" rule.
  • The re-queue loop skips stored rows that are unmonitored, not imported, and rejected by SkipPartBooks or SkipMissingDate. Those are the only filters a stored row can be judged by without a provider call. An already monitored row is queued as before and isn't counted in skippedByProfile. There's no stored signal for "added by hand", and a monitored row is being sought by the sweep anyway.
  • The single row add is not screened, deliberately. It's an explicit pick, the same exemption a single work add has (AddBook 404s forever for specific OpenLibrary works when the author already exists (e.g. OL82537W, Chamber of Secrets) — poll waits for a row the catalogue sync refuses to create #1612). This is documented.
  • The Fill response adds skippedByProfile and skippedSplitParts, and the series card says what was skipped.

Fail before evidence

Run on origin/main source with the new tests applied. For internal/api, a test only shim gave the two new entry points (WithMetadataProfiles, UnmonitorSplitEditionParts) no op bodies so the tests compile against main's behaviour:

--- FAIL: TestWantedSearchQueue_SkipsSplitEditionPartsOfAnOwnedBook
    "The Way of Kings, Part 1" was queued for search although the whole book is imported
    "The Way of Kings, Part 2" was queued for search although the whole book is imported
--- FAIL: TestSeriesFillDoesNotRequeueSplitEditionPartsOfAnOwnedBook
    queued = 4, want 2 (Edgedancer and the lone Oathbringer part only)
    fill re-monitored a split part of a book already imported
--- FAIL: TestSeriesUnmonitorSplitEditionParts
--- FAIL: TestSeriesGetMarksSplitEditionParts
--- FAIL: TestSeriesFillAppliesMetadataProfileFilters
    fill created "Caliban's War" although the metadata profile filters it out
    fill created "The Expanse Books 1-3" although the metadata profile filters it out
    fill re-monitored a stored box set the metadata profile filters out

Review follow up (1c0bf1c0), run against the previous branch head:

--- FAIL: TestCatalogBookProfileFilterLanguage/blank_language,_no_evidence_provider     = "language", want ""
--- FAIL: TestCatalogBookProfileFilterLanguage/blank_language,_evidence_lookup_fails    = "language", want ""
--- FAIL: TestCatalogBookProfileFilterLanguage/foreign_language,_evidence_lookup_fails  = "language", want ""
--- FAIL: TestSeriesFillAppliesMetadataProfileFilters (unknown fails profile)
    hc:leviathan-wakes passes every filter but was not created
    response = map[queued:0 skippedByProfile:6 ...], want queued 3 and skippedByProfile 3

TestSplitEditionPartOf, TestListCoveredSplitEditionParts and the SeriesPage vitest case cover new code that doesn't exist on main.

Checks

  • gofmt -l ., go vet ./...
  • golangci-lint v2.11.4 on touched packages: 0 issues
  • go test -p 2 on internal/api, internal/db, internal/scheduler, internal/seriesmatch, internal/models, cmd/bindery
  • cd web && npm ci && npm run lint && npm run typecheck && npx vitest run && npm run build (lint shows only the existing warnings)
  • Docs: docs/Hardcover-Series-Wiki.md, docs/User-Guide-Wiki.md, docs/API.md
  • Changelog fragments: changelog.d/3048-split-edition-parts.md, changelog.d/2208-series-fill-profile-filters.md
  • No migration.

🤖 Generated with Claude Code

https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9

vavallee and others added 2 commits October 7, 2026 23:29
…ries fill (#3048, #2208)

Fill and the wanted sweep skip a stored book that is a split edition part
of a whole already in the library, using the same two signal rule as #2915.
A new series endpoint unmonitors those parts. Series fill now screens the
books it creates and re queues through the author's metadata profile, and
two admin endpoints preview and unmonitor wanted books a profile filter
turned on later would have kept out. Nothing is deleted.

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>
…and profile filters (#3048, #2208)

Series page marks split edition parts, leaves them out of the missing
count, offers a one click unmonitor and reports what a fill skipped.
Drops the profile level retroactive endpoints from the first commit:
per author Reconcile catalogue (#2247) already applies every filter to
stored rows. Adds the db test for the covered split part query, docs and
changelog fragments.

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>
@codecov

codecov Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

vavallee and others added 2 commits October 8, 2026 00:49
…nting hand monitored books as skipped (#2208)

Review follow up. Hardcover series entries carry no language, so a
profile with unknown languages set to fail rejected every book of an add
all whenever OpenLibrary was primary. A blank language now passes during
a fill, and a failed language evidence lookup passes too, the same
unknown passes rule MinPages and SkipMissingISBN use. Only a definitive
"not allowed" rejects.

The re-queue loop now screens only unmonitored stored rows. A monitored
book is already being sought, often because it was added by hand, so it
is queued as before and no longer reported as skipped on every fill.

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>
…anted-series-filters

Signed-off-by: vavallee <vavallee@protonmail.com>
@vavallee
vavallee marked this pull request as ready for review October 8, 2026 03:56
@vavallee
vavallee enabled auto-merge (squash) October 8, 2026 03:56
@vavallee
vavallee merged commit 233cb1d into main Oct 8, 2026
41 checks passed
gchahcg pushed a commit to gchahcg/bindery that referenced this pull request Oct 10, 2026
…avallee#2524)

vavallee#3109 landed on main while this was open and introduced its own copy of
the same two-signal rule (seriesmatch.SplitEditionPartOf,
db.ListCoveredSplitEditionParts) for rows already stored, asking
whichever PR landed second to line up with the other.

- splitEditionWhole now calls seriesmatch.SplitEditionPartOf instead of
  the separate IntegerFloorPosition/SplitPartOf pair, which are
  removed; HasPartMarker/StripPartMarker keep their own tests but now
  share split_edition.go's splitEditionMarkerRe instead of a second
  copy of the same regex.
- The eligibility gate ListCoveredSplitEditionParts applies to a stored
  whole (not excluded, imported or monitored) now gates a Present row's
  eligibility to cover a catalogue-diff part too, via a new
  eligibleSplitEditionWhole helper cross-referenced from both sides of
  the SQL/Go boundary, with a test for each half of the gate (excluded,
  wanted-and-unmonitored, wanted-and-monitored).
- Merged the wiki's two split-edition paragraphs into one and dropped
  the em dashes.

Review feedback from vavallee.

Signed-off-by: gch ahcg <gchahcg@proton.me>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant