From 0d426910f57fc3f5fae4ab569efe9dc371ad43a9 Mon Sep 17 00:00:00 2001 From: KodeStar Date: Mon, 12 Oct 2026 23:41:54 +0100 Subject: [PATCH 1/2] Series keys, Health's series spelled several ways, Authors/Narrators pages Docs for audiosilo-server's stored series key (books.series_key, book_more_series, migration 0041, KeySeries), the Health issue and its endpoint, and the paged Authors and Narrators lists; health-issues and authors screenshots regenerated. --- docs-developers/server/api/reference.md | 52 +++++++++++++++++- docs-developers/server/data-model.md | 43 ++++++++++++--- docs-developers/server/scanner.md | 1 + docs-users/admin/books.md | 11 +++- docs-users/admin/health.md | 26 +++++++++ static/img/screenshots/admin/authors.png | Bin 132842 -> 132311 bytes .../img/screenshots/admin/health-issues.png | Bin 122625 -> 102009 bytes 7 files changed, 121 insertions(+), 12 deletions(-) diff --git a/docs-developers/server/api/reference.md b/docs-developers/server/api/reference.md index 082319c..25aeb37 100644 --- a/docs-developers/server/api/reference.md +++ b/docs-developers/server/api/reference.md @@ -3322,7 +3322,7 @@ what the first does). | `min_duration` · `max_duration` | number (seconds) | - | inclusive bounds; `0` means no bound | | `added_after` | date | - | inclusive lower bound on `added_at`: `YYYY-MM-DD` (used as is) or an RFC 3339 time (any offset; converted to UTC before comparing) | | `added_before` | date | - | exclusive upper bound, same formats | -| `issue` | string | - | one [Health issue](#get-apiv1adminissues) kind's books: `scan_error`, `suspect`, `split_discs` (one row per split book: its first disc), `no_cover`, `unmatched`, `no_chapters`, `detailed_chapters` or `transcode` (not `duplicate`, which comes as groups from [`/admin/issues/duplicates`](#get-apiv1adminissuesduplicates)). Books an admin ignored for that kind are left out. Any other value is `400 unknown issue` | +| `issue` | string | - | one [Health issue](#get-apiv1adminissues) kind's books: `scan_error`, `suspect`, `split_discs` (one row per split book: its first disc), `no_cover`, `unmatched`, `series_spellings`, `no_chapters`, `detailed_chapters` or `transcode` (not `duplicate`, which comes as groups from [`/admin/issues/duplicates`](#get-apiv1adminissuesduplicates)). Books an admin ignored for that kind are left out. Any other value is `400 unknown issue` | | `issue_ignored` | `true`\|`false` | - | with `issue`, list **only** the books an admin ignored for it; without `issue` it is `400 issue_ignored needs an issue` | | `sort` | string | `title` | `title` \| `author` \| `surname` \| `series` \| `narrator` \| `published` \| `added` \| `duration` \| `size` | | `order` | string | `asc` | `asc` \| `desc` | @@ -4468,7 +4468,13 @@ a scan last finished. - `categories` - one entry per kind, always in this order: `scan_error`, `suspect`, `split_discs`, `duplicate`, `no_cover`, `unmatched`, - `no_chapters`, `detailed_chapters`, `transcode`. `detailed_chapters` is a + `series_spellings`, `no_chapters`, `detailed_chapters`, `transcode`. + `series_spellings` is a book whose main series another book of its library + spells another way (the same series in a different case, accent, punctuation or + spacing; see [`/series`](#get-apiv1librariesidseries)); every book of such a + series is counted, and + [`/admin/issues/series-spellings`](#get-apiv1adminissuesseries-spellings) lists + them per series. `detailed_chapters` is a book whose last [community chapter check](../community-chapters.md) found finer community chapters that fit (`refine`) and whose chapter source no admin has chosen; the console's fix is a bulk edit with @@ -4534,6 +4540,46 @@ different libraries are deliberate and never grouped). - At most 500 groups (counted after that filter, so every ignored group can be reached), ordered by library and the kept copy's path. +### `GET /api/v1/admin/issues/series-spellings` + +The series a library's books spell more than one way, one entry per series and +library, from the same rule as the `series_spellings` count. + +| Query param | Type | Notes | +|---|---|---| +| `library_id` | int | one library; a non-positive or non-integer id is `400 invalid library_id` | +| `ignored` | `true` | return the series with a book an admin ignored for `series_spellings` (instead of those with a book not ignored) | + +```json +{ + "groups": [ + { + "library_id": 5, + "suggested": "He Who Fights with Monsters", + "spellings": [ + { "name": "He Who Fights With Monsters", "books": 8 }, + { "name": "He Who Fights with Monsters", "books": 5 } + ], + "books": 13 + } + ] +} +``` + +- `suggested` - the name [`/series`](#get-apiv1librariesidseries) gives the + series: the spelling its matched books use most, else the one most of its books + use, ties alphabetical. The console offers it first. +- `spellings` - every spelling of the series in that library, with its books, most + books first (ties alphabetical). Ignored books' spellings are listed too, so a + rename leaves none behind. +- `books` - all of the series' books in that library. +- A series can be in both views while some of its books are ignored and some not. + At most 200 entries, ordered by `suggested` (case-insensitive), then library. + +The console's fix is a [bulk edit](#post-apiv1adminbooksbulk) setting `series` on +the books of the other spellings, as an author merge is; ignoring a series is a +`series_spellings` ignore of every one of its books. + ### `POST /api/v1/admin/issues/ignore` · `DELETE /api/v1/admin/issues/ignore` `POST` stops listing books under one issue kind; `DELETE` lists them again (the @@ -4544,7 +4590,7 @@ console's Undo and "Show again"). For duplicates, ignore every copy of a group { "kind": "no_cover", "books": [ { "library_id": 1, "path": "Sun Tzu/The Art of War" } ] } ``` -- `kind` - one of the nine kinds above. +- `kind` - one of the ten kinds above. - `books` - 1 to 1000 `{library_id, path}` refs. The books needn't be indexed: the rows are path-keyed durable state (`issue_ignores`), so an ignore survives rescans and rebuilds and moves with the book. A ref in a library diff --git a/docs-developers/server/data-model.md b/docs-developers/server/data-model.md index 6ab719c..1f8a9ee 100644 --- a/docs-developers/server/data-model.md +++ b/docs-developers/server/data-model.md @@ -4,7 +4,7 @@ description: "The SQLite schema behind audiosilo-server: the rebuildable index v --- The schema lives in `internal/store/migrations/` as numbered SQL files -(`0001_init.sql` … `0040_server_state.sql`), embedded into the binary and +(`0001_init.sql` … `0041_series_key.sql`), embedded into the binary and applied by `store.Open` at startup. This page documents the **resulting current schema**, noting which migration added what. @@ -203,7 +203,7 @@ covered in [Auth & security](auth-and-security.md#authorization-shares--scope). `scan_error_file`, `scan_error_detail` and `suspect_parts` in 0017; `split_parent` in 0022; `cover_art` and `cover_color` in 0023; `chapters_source`, `chapters_fit`, `scanned_chapters` and `chapters_hash` in 0035; `released` and `released_checked` -in 0037)* - one row per book, +in 0037; `series_key` in 0041)* - one row per book, `UNIQUE (library_id, rel_path)`. Columns: `is_folder` (folder book vs single-file book), identity metadata (`title`, `author`, `series`, `series_index`, `narrator`), `duration`, `asin`/`isbn` (optional external ids - @@ -217,7 +217,14 @@ that sets or reverts `series` to one of these swaps the two, writing the list and `series_index` as overrides (a value the scan already gives is reverted to it instead; the console sends the values itself, so its swap writes them as edits); -the full-text index's series column holds every series name), `released` (the date the file's tags give, `YYYY[-MM[-DD]]` via +the full-text index's series column holds every series name), `series_key` (the +main series' key, `catalog.seriesKey`: `match.SeriesKey`, so case, accents, +punctuation and spacing aside, or the name itself when it has no letter or digit; +`''` for no series), written by Go in `refreshEffective` beside `series` and never +computed in SQL, so every spelling of a series is found by one index seek +(`idx_books_series_key` on `(library_id, series_key, series, series_index)`, which +replaced 0001's `idx_books_series`; `series` sits before `series_index` so a series' +spellings are adjacent for Health's `series_spellings`), `released` (the date the file's tags give, `YYYY[-MM[-DD]]` via `metadata.ReleaseDate`: usually the recording's, so it is a separate scanned column, never `published`, read only by the admin list's release-date sort as the fallback for a book with no `published`) and `released_checked` (`1` once a @@ -359,6 +366,15 @@ admin state like `book_overrides`, kept apart from it because it is not a metadata value (no part of `edited` / `edited_fields`); moved with the book's other edits, never copied onto a joined book. +**`book_more_series`** *(0041)* - a book's other series by key: one row per +series in `more_series` that isn't its main one (`book_id` → `books` `ON DELETE +CASCADE`, `library_id`, `series_key`, `position`; primary key `(book_id, +series_key)`, `WITHOUT ROWID`, index `(series_key, library_id, position)`). +Rewritten from the effective values by `refreshEffective` (`keyOtherSeries`), as +`books.series_key` is, so a series filter with memberships, the series order and +the next book find a book through its list by an index seek, and the rails' +`SeriesBooks` without reading every list. + **`books_fts`** *(0001)* - see [FTS design](#fts-design-books_fts) below. ### Durable per-user state (path-keyed, no FK to books) @@ -743,11 +759,21 @@ every other table. `catalog/server_state.go` reads and writes a key `putServerState`), taking a querier or execer so a caller can read and write one key in a single transaction. -One key so far, `support_card`: the answer to the admin console's -[support card](web-ui.md#the-support-card), `{"choice":"donated"}` or -`{"choice":"snoozed","until":"