diff --git a/docs-developers/server/api/reference.md b/docs-developers/server/api/reference.md index 082319c..5a30a15 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,51 @@ 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 spelling to offer, by the rule + [`/series`](#get-apiv1librariesidseries) names a series by (the spelling its + matched books use most, else the one most of its books use, ties alphabetical), + counted over this library's books whose main series it is. So it can differ from + the name `/series` or [`/admin/series`](#get-apiv1adminseries) gives when other + libraries or other-series lists spell it too. The console offers it first. +- `spellings` - every spelling the library's books give it as their main series, + with its books, most books first (ties alphabetical). Ignored books' spellings + are listed too, so a rename leaves none behind. +- `books` - all of the books in that library whose main series it is (a book that + lists it only among its other series is not counted, and a rename doesn't touch + it). +- 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 +4595,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..70ef8ef 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. @@ -13,7 +13,8 @@ schema**, noting which migration added what. The schema is deliberately split in two, and the split is the most important thing to understand before touching it: -- The **rebuildable index** - `books`, `book_files`, `chapters`, `books_fts` - +- The **rebuildable index** - `books`, `book_files`, `chapters`, + `book_more_series`, `books_fts` - is a cache of what the scanner found on disk. It can be dropped and rebuilt from a rescan at any time. `books.id` is an internal artifact of this half: it must **never** appear in the API contract or in durable user state. @@ -76,6 +77,7 @@ erDiagram libraries ||--o{ books : "rebuildable index" books ||--o{ book_files : "" books ||--o{ chapters : "" + books ||--o{ book_more_series : "ON DELETE CASCADE" books ||--|| books_fts : "rowid = books.id" libraries ||--o{ progress : "path-keyed, NO FK to books" libraries ||--o{ bookmarks : "" @@ -203,7 +205,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 +219,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 +368,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 +761,23 @@ 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":"