Skip to content

Series: one series per spelling, numbers from titles, paged and searchable Series page - #123

Merged
KodeStar merged 2 commits into
mainfrom
fix/series-grouping-paging
Oct 12, 2026
Merged

KodeStar merged 2 commits into
mainfrom
fix/series-grouping-paging

Conversation

@KodeStar

@KodeStar KodeStar commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

Fixes the Library > Series page reported on 2026-10-12. "He Who Fights with Monsters" showed as three cards ("WIth", "With" and "with"), and the matched card listed as missing books that sat on the other two. The page also lacked numbered paging and search, and Back returned to the top of the first batch.

Docs: KodeStar/audiosilo-docs#123.

What step 1 found

  • Meta is not the cause. data/series/he-who-fights-with-monsters.json on main is one series with 13 works at positions 1-13, no duplicates. The only other series with that key is the German translation (translation_of), which the series rules already leave out.
  • Cause 1, the split (server and console). catalog.Series grouped by the exact series string. seriesFilter, seriesOrderKeys, numberedSeriesMembers and the console's groupSeriesPages were all exact too.
  • Cause 2, the "missing" list. On the real data, 8 books are tagged "He Who Fights With Monsters" with series_index 0 and no ASIN, and their titles carry the number ("He Who Fights with Monsters 4: A LitRPG Adventure ..."). Even on one card they couldn't be placed.
  • Why the 8 weren't matched. The bulk run (match_runs 2) found the right work for every one of them at score 91. Books 2-5, 7, 8 and 10 were held for review because the runner-up, an adjacent volume, scored 86: under the 10-point lead rule. The review items were never applied. So pkg/match/matchrun didn't fail; metaserve separates adjacent volumes by only 5 points even with the number in the title (follow-up for audiosilo-meta's works/match scoring).
  • The duplicate spines (1,1 / 6,6 / 9,9). They don't reproduce on this server's data: each position is held once, in one library. The run's item paths show the series folder was renamed from "WIth" to "With" between the match run and the next index. The server in the screenshot most likely holds both folders, so those are two real copies. Health > Duplicates (same ASIN within a library) is the place to clean those up; the shelf showing one spine per copy is intended.
  • The player. /libraries/{id}/series, /series/books and /books?series= share the same catalog code, so the server fix covers it. audiosilo-frontend never groups series client-side, and its series page already falls back to a loose key (inSeries -> looseKey), so no frontend PR is needed.

The fix

One series, any spelling (server). seriesKey (match.SeriesKey, the key the rails already use: case, accents, punctuation and spacing aside; a name with no letter or number is its own key) groups everywhere:

  • catalog.Series merges counts, positions, lengths and authors. The name is the spelling the matched books use most, else the one most books use, ties alphabetical.
  • Every filter takes every spelling. Catalog.spellings reads the index's spellings (the distinct (library_id, series) of idx_books_series in order, plus more_series names when memberships are on). These feed IN lists, an OR'd FTS phrase prefilter, literal order keys, the next-book query and positionIn.
  • SeriesList and normalizeMoreSeries keep a series once however a list spells it again.
  • A series swap now takes another spelling of a listed series for it, on the server and in the console's draft (one sameSeries).
  • Nothing fuzzier merges: "The Expanse" and "Expanse" stay two series.

Cost. About 20 ms over 50,000 books across all libraries, 9 ms in one. /series/books reads spellings once per batch.

Wire. The JSON is unchanged (CROSS-REPO §15). Shipped players just see fewer, fuller series. Their /books?series=<name> (no memberships) now returns every spelling's books, which is what they need for a merged entry.

A position from the title. bookLayers.scannedFields gives a book with a series and no position the number its title gives (metadata.PositionInTitle). It's deliberately narrow:

  • the name must be followed by a space or comma (optionally "Book", "Vol.", "#"), then 1-3 digits, then the end or a separator;
  • a range ("Dork Diaries 1-12"), a number run on from the name ("Catch-22"), or one followed by more words ("Jack Reacher 61 Hours") gives none;
  • tag positions and edits win.

It's in the resolve step, not the console, so the players and /next benefit too. Books indexed earlier get it from Catalog.PlaceByTitles, which the launcher runs once per database, in the background (server_state title_positions). On the real data it placed 25 books, all correct. A first draft read "Dork Diaries 1-12" as #1; that's now a golden negative case.

Series page (admin-ui).

  • Numbered pages: a new reusable Pager in src/components/ and pagerItems in src/lib/pager.ts, 24 cards a page.
  • A search box finds a series by name or author, compared by series key. It's the Books page's debounced SearchBox, now shared in src/components/.
  • page and q are in the URL. A library switch resets the page.
  • scrollRestoration: true on the router, so Back/Forward restore scroll and a new visit opens at the top.
  • Each card loads its own books through the series filter (as the old MembersCard did), so a jump to page 9 asks for page 9's series only. groupSeriesPages and the series-sorted walk are gone.

Verified in a real browser

Run against a scratch copy of audiosilo-server/data (two books given a "WIth" override, to have three spellings), headless Chromium:

  • One card. A single "He Who Fights with Monsters" card: 13/13, "Complete: you have all 13", 13 spines, no gaps.
  • Paging. The pager shows "1 2 ... 9" and jumping to page 9 works.
  • Search. "fights with" leaves one card.
  • Back. On page 3, opening a book from the 11th card and pressing Back returned to ?library=5&page=3 with the same scroll position (scrollY 3244 and card top 509, before and after). A search also survived opening a book and Back.
  • Library switch. Going from page 3 to All resets to page 1, and LibriVox (no series) shows no pager.
  • Layout. Light and dark mode; at 375 px wide there's no sideways scroll and the pager clears the tab bar.
  • Player endpoints (API on the copy).
    • /libraries/5/series returns one entry with positions 1-13.
    • /series/books returns all 13 books for "WIth" and for lower-case.
    • /books?series= without memberships returns all 13 too.
    • /next from Demo mode #2 ("With") gives Integration #3 ("WIth").

Not driven: the web player's own Series screen. The API it reads was checked instead.

Gates

  • Go: go build ./... && go vet ./... && go test -race ./... && golangci-lint run all pass.
  • Console: scripts/build-admin.sh passes, 679 tests.
  • Flake, unrelated: internal/backup TestStartRunsAtOnce failed once under machine load and passed 3/3 on its own; this change doesn't touch backups.
  • Process: /simplify and /code-review xhigh --fix both ran. Fixes applied include the swap-by-spelling regression, the title pass missing match-retitled books, startup blocking, decomposed (NFD) titles, and wasted more_series scans.

Follow-ups, not in this PR

  • A stored, indexed series_key column (migration + backfill) would replace the per-request spellings read with a seek. Worth it if more series paths come.
  • A Health issue offering to rename a series' spellings to one. Skipped: more than a small change, and the grouping removes most of the pain.
  • metaserve works/match: adjacent volumes score within 5 points even with the volume number in the title, so whole numbered series land in review.
  • A more_series list stored before this change can name one series twice. Reads dedupe it in Go; the next-book SQL may see the second entry. Rare.

…hable Series page

Series names that differ only in case, accents, punctuation or spacing are
one series everywhere: the aggregate (named by its matched books' spelling),
every series filter (admin list, player /books and /series/books), the series
ordering and the next book. A book with a series and no position takes the one
its title gives ("He Who Fights with Monsters 4: ..."); books indexed before
get it once at start.

The admin Series page pages its cards (numbered, in the URL), finds a series by
name or author, loads each card's books by its own series filter, and Back
returns to the same page, search and scroll position.
A series swap takes another spelling of a listed series for it (server and the
console's draft, one sameSeries helper). The title-position pass runs in the
background and finds books whose snapshot or path title numbers them; titles
in decomposed form read too. Spellings skip the more_series lists where no
caller uses them, and the next book reads them only for numbered series.
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