Skip to content

Borrow chapter names from a source whose numbering was verified to match - #85

Closed
Squeaks72 wants to merge 7 commits into
AngeloSha:mainfrom
Squeaks72:pr/borrow-chapter-names
Closed

Squeaks72 wants to merge 7 commits into
AngeloSha:mainfrom
Squeaks72:pr/borrow-chapter-names

Conversation

@Squeaks72

@Squeaks72 Squeaks72 commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #84 (chapter names). Review that first; this diff includes its commits until it merges. Only the last two commits belong to this PR.

Summary

Many sources publish no chapter titles at all. Some answer "Chapter 1", "Chapter 2" for the whole of One Piece, while another source has had "Romance Dawn" all along. This PR lets an admin borrow those names from another source, off by default. It can be turned on server-wide (Admin → Settings → Updates & schedules) or per series (a checkbox in the Sources & translations sheet).

The danger is the numbering, not the names. Matching a name to a chapter by number is only sound if both sources number the work the same way. When they don't, every borrowed name is wrong, and wrong in a way that looks right. So bff/src/lib/borrowNames.ts is deliberately conservative:

  • Donor choice. A donor must match the series title exactly after case and punctuation are folded (normTitle, from the per-source searchAll map). A fuzzy match would add a second way to be wrong, and the numbering check cannot catch it. A title that folds to nothing (entirely non-Latin script) is never searched, because it would "match" every other such title.
  • Numbering check. A donor must pass the same rule the app already uses before following a second source (assess / verdict / followable from lib/fill.ts: at least 90% of the numbers we hold, and the verdict agreeing). A donor that fails is not used at all.
  • What gets written. Only chapters whose title is still just their number are written, and that is re-checked in the UPDATE itself. Every borrowed name records its donor in the new lib_books.title_source column. The chapter's own source naming a chapter later (the landed stamp or the listing heal from Show chapter names, and say which chapter Continue opens #84) replaces the borrowed name and clears the mark.
  • Switching off. Turning it off for a series removes that series' borrowed names, resetting them to the title the chapter's file gives, which is exactly what the scanner derives. Turning off the server switch does the same for every series that follows it; a series switched on for itself keeps its names.
  • Traffic. It runs after the listing is written on each source check. It makes no request once every live chapter has a name. The matched donor is remembered (lib_series.name_donor). A search that found no donor is remembered for a week instead of re-running a cross-source search every few hours.

Schema: server_settings.borrow_names (default false), lib_series.borrow_names (null follows the server), lib_series.name_donor, lib_books.title_source. All are ADD COLUMN IF NOT EXISTS.

API: borrowNames on PATCH /api/admin/settings and PATCH /api/admin/series/:id (nullable there), borrow_names on the settings row, and borrowNames / borrowNamesEffective on the admin view of GET /api/series/:id. The checkbox has to show what actually applies, including the server's setting. All are documented in bff/openapi.yaml. There are no new routes, so the docs/api.md route list is unchanged. It works the same on Desktop: none of these routes are desktop-hidden, and the switch lives next to the existing "Look for failed chapters on other sources" switch.

A known limit, stated plainly: the coverage check proves the donor lists the numbers we hold. It cannot detect a donor that lists the same range with a one-chapter offset, such as a prologue counted as chapter 1. The exact-title requirement, the opt-in default and the recorded title_source (removable in bulk by switching off) are the mitigations. That limit is the main reason this stays off by default.

Type

  • Fix
  • Feature
  • Docs
  • Refactor / chore

Testing

Re-run after rebasing onto v0.45.0 (91b41aa): bff 1867 tests, 1854 pass, 9 fail, 4 skipped — the same 9 embedded-Postgres entrypoint failures main has in this environment (main: 1857 / 1844 / 9 / 4), nothing new; web 432/432; both typechecks OK. The detail below is from the first run, on v0.44.0.

I ran the upstream CI setup in node:24-alpine against a real postgres:16-alpine (TEST_DATABASE_URL set), with the same baseline as upstream main (a8bc502).

upstream main this branch (with chapter-names beneath it)
bff tsc --noEmit OK OK
bff tests 1834 tests: 1821 pass, 9 fail, 4 skip 1844 tests: 1831 pass, 9 fail, 4 skip
web tsc --noEmit OK OK
web tests 421 / 421 422 / 422

The same 9 failures appear on both sides. They are the entrypointEmbedded tests, which need an embedded Postgres binary that this sandbox lacks. There are no new failures. The full run first caught predicateHygiene flagging a hand-written deleted_at IS NULL. The series lookup now uses visibleToAll('s'), and I re-ran predicateHygiene, borrowNames, chapterNames, seriesListing, updater and openapiCoverage: all 74 tests passed. openapiCoverage passes with the schema additions.

New: bff/test/borrowNames.int.test.ts, using fake sources for a correct donor, a donor with numbers shifted to 101–110, and a look-alike title with matching numbers. It pins:

  • nothing is asked while the feature is off
  • only the correct donor is used, and a chapter it does not name either stays unnamed
  • a series whose only candidates are the shifted and look-alike sources gets nothing, and its search is not repeated on the next check
  • the own source's listing name replaces a borrowed name and clears the mark
  • switching off resets to the file's title. Chapter 10 reads "Chapter 10": an earlier trim(trailing '0' ...) reset turned it into "Chapter 1".
  • the server switch going off clears only the series that follow it

The new UI strings are translated in all eight locale files. npm run test:i18n is a Puppeteer walk against a running server, so it could not run in this sandbox. The locale-parity unit test passes.

Checklist

  • Focused scope (one topic)
  • Matches the existing TypeScript style
  • Docs updated if user-facing (docs/USAGE.md)
  • No new default sources hardcoded to specific sites (the engine system reaches sites by URL)

🤖 Generated with Claude Code

Squeaks72 and others added 7 commits September 24, 2026 10:27
A downloaded chapter's file is named from its number alone, so the scanner's
title for it was the number said twice. Sources do give real names, and the
downloader already wrote them into each CBZ's ComicInfo -- nothing read them
back. setBookMeta now stores the landed copy's name, filtered so a source that
also just says "Chapter 12" does not replace one restatement with another,
and rows show it beside the number.

The series page also names the chapter the Continue button opens, with the
page position when one is part-read.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A chapter downloaded before the downloader learned to record the source's name
is stuck titled after its own file, which is named from the number alone. The
listing written on each check is the same data for the same chapters and is
already in hand, so replaceListing now fills in any book title that is still a
bare restatement of its number.

Only from a listing title that is not itself one: plenty of sources genuinely
call every chapter "Chapter 12", and swapping one restatement for another is
churn. A title set by hand never matches the pattern, so an override is safe.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The sweep is how most chapters arrive, and its Landed record carried the
group and the source but not the chapter's name, so a chapter fetched
overnight stayed "Chapter 12" until the next check's listing heal. It now
passes the name to setBookMeta like the add and download paths do.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
persistScan's upsert re-derived every book title from its filename, and it
runs on every add, every sweep and every manual scan -- so each time anything
anywhere was fetched, every name in the library went back to "Chapter 12"
until that series' next check healed it. The scan now keeps the row's title
when the filename says nothing but the number and the row holds a real name;
a file that carries a name of its own still wins.

Tests: the name filter, the landed stamp, the listing heal and the rescan.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Plenty of sources publish no chapter titles -- Weeb Central answers "Chapter 1"
for the whole of One Piece -- while another source has had "Romance Dawn" all
along. Same work's names, different source.

The hazard is numbering, not names: matching by number is only sound if both
sources number the work identically, and past a divergence every borrowed name
is wrong in the way that looks right. So this reuses the one rule the app
already applies before taking chapters from a second source -- 90% of the
numbers we hold listed there, and the verdict agreeing -- and a donor that
fails is not used at all. Donors come from the per-source search map, on an
exact title match (normTitle); a fuzzy match would be a second way to be
wrong that the numbering check cannot catch.

Every borrowed name records its donor in lib_books.title_source, so a bad one
is identifiable and clearable in bulk; a chapter that already has a real name
is never touched, and the chapter's own source naming it later wins. Off per
server and per series, because it is outbound traffic to a source that
carries nothing else for you; switching it off takes the names back to what
each file says. A search that finds no donor is remembered for a week rather
than repeated on every check.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
OpenAPI: borrow_names on the settings row, borrowNames on the settings and
series PATCH bodies (the latter is additionalProperties: false), and
borrowNames / borrowNamesEffective on the admin view of a series.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
AngeloSha added a commit that referenced this pull request Sep 25, 2026
…oading

Read a chapter before adding a series (#91), a source order for new chapters
(#93, first half), swapping in a preferred group's copy once it exists (#93,
second half; closes #81), chapter names borrowed from a source that numbers the
work the same way (#85) and a compact chapter list (#88) -- @Squeaks72's ideas,
rebuilt where the originals could not be merged. Every running download,
including the server's own sweeps, repairs and bulk fetches, now shows with a
Cancel (#82).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AngeloSha added a commit to Squeaks72/uchiyomi that referenced this pull request Sep 25, 2026
… (rebuild of AngeloSha#85)

Some sources publish no chapter titles at all -- every row reads "Ch. 12" --
while another source has had "Romance Dawn" all along. The nightly repair gets
a seventh step that takes the names from that other source. Off by default, per
server and per series: it is traffic to sources that carry nothing else for you.

@Squeaks72's idea and its shape. Rebuilt on the hunt's own parts, because the
first version:
- searched through searchAll, which REPORTS to source health, so a lookup only
  for names could put a series' own source into a cooldown and stop real
  downloads. Nothing here reports; each search and lookup is bounded and a
  failure is just a source that did not answer;
- filtered donors by neither language nor the adult rule, so a Spanish "One
  Piece" matched and named the chapters in Spanish. The hunt's sweepAllowedFor
  applies, a source in another language is never asked, and a multi-language
  donor's chapters in another language are never used;
- matched names by a floored number with no minimum, so a donor one chapter off
  named every later chapter wrongly. A donor must now pass judgeCandidate --
  the identity and both-way numbering check an automatic follow must pass --
  and names are matched by exact number;
- trusted a remembered donor the admin had since disabled. It is re-judged.

Names go to chapter_name only, never `title`, marked with the donor in
chapter_name_source: the chapter's own source replaces a borrowed name the
moment it supplies one (the listing heal and setBookMeta), and switching the
setting off takes back exactly what was borrowed.

Each rule was proved by reintroducing its bug. Two assertions that first passed
vacuously -- the language and adult filters, which the good donor's being found
first made unreachable -- now have a test that takes that donor away.

Co-authored-by: Squeaks72 <Squeaks72@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 900601235e5f7865d742aebdce17b013caa559e2)
@AngeloSha

Copy link
Copy Markdown
Owner

Thanks, this is a clever idea and I do want it. I closed this version though, because a few of the problems need it restructured rather than patched:

  • It searches through searchAll, which reports slow and failing sources to source health. So a lookup that's only for names can put someone's main source into a cooldown and stop real downloads. The failure hunt goes out of its way never to do that.
  • Donors aren't filtered by adult rating or language, and normTitle strips a title to [a-z0-9], so a Spanish "One Piece" matches and you get Spanish chapter names.
  • The numbering check floors both sides and has no minimum, so a donor one chapter off, or a different work with the same title, can pass.
  • There's no limit on how much it searches, and the remembered donor ignores a source the admin disabled.

Since #84 keeps names in their own column, I rebuilt this on the existing hunt pipeline (adult rule, own-language order, budgets, no health reporting). It shipped in v0.47.0 as Borrow chapter names from other sources, off by default, with credit to you in the changelog.

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.

2 participants