Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 54 additions & 3 deletions docs-developers/server/api/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
54 changes: 43 additions & 11 deletions docs-developers/server/data-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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.
Expand Down Expand Up @@ -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 : ""
Expand Down Expand Up @@ -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 -
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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":"<time>"}`. `catalog.SetSupportChoice` reads and
writes it in one transaction, so a donation is never downgraded by a later
snooze.
Its keys:

- `support_card`: the answer to the admin console's
[support card](web-ui.md#the-support-card), `{"choice":"donated"}` or
`{"choice":"snoozed","until":"<time>"}`. `catalog.SetSupportChoice` reads and
writes it in one transaction, so a donation is never downgraded by a later
snooze.
- `title_positions`: `true` once `catalog.PlaceByTitles` has given the books
indexed before the title rule their series position from their title.
- `series_keys`: the version the stored series keys were computed with
(`seriesKeysVersion`: the key rules' number, then Go's Unicode version and
the Unicode version of x/text's normalization tables). `catalog.KeySeries` keys
every book at start while it is missing or different (first for the books
indexed before 0041, then after an upgrade that reads a name another way),
before the listener; a failure stops the start, as a failed migration does, since
every series filter reads only the keys (see `books.series_key` and
`book_more_series` in [The rebuildable index](#the-rebuildable-index)).

### Community metadata cache

Expand Down Expand Up @@ -844,8 +874,9 @@ A `books` row always holds the **effective** metadata, built in layers:

`bookLayers.resolve` (in `internal/catalog/overrides.go`) is the single
statement of that precedence, and of each field's source. `refreshEffective`
writes its values into the book's metadata columns and chapter titles and
refreshes the book's `books_fts` row from the result; the admin book page shows
writes its values into the book's metadata columns (with `series_key`, and the
book's `book_more_series` rows) and chapter titles and refreshes the book's
`books_fts` row from the result; the admin book page shows
the same resolution as provenance, so the two can't disagree. Every write path calls
it inside its own transaction - `UpsertBook` (each scan or on-demand index),
`SetEnrichment`, and `EditBook`/`EditBooks` (the admin edits, the bulk form
Expand Down Expand Up @@ -946,6 +977,7 @@ The migration history so far:
| 0038 | `more_series` | `books.more_series` (`TEXT NOT NULL DEFAULT '[]'`), the effective list of a book's other series |
| 0039 | `more_series_index` | the partial index `idx_books_more_series` on `books(library_id) WHERE more_series <> '[]'`, so the reads of the books in more than one series (a series' members, `/series` with `memberships=1`) skip the rest |
| 0040 | `server_state` | `server_state` (`key`, `value`, `updated_at`), small server-wide facts that aren't settings; the first key is the support card's answer: see [Server state](#server-state) |
| 0041 | `series_key` | `books.series_key` and `book_more_series`, a series stored by its key so every series filter is an index seek in any spelling; `idx_books_series_key` replaces `idx_books_series`, and `idx_books_more_series` (0039) goes, since nothing reads it any more. Rows indexed before it are keyed at start by `catalog.KeySeries` (server_state `series_keys`), which also puts every stored `more_series` list through the list rules first |

## SQLite choices

Expand Down
1 change: 1 addition & 0 deletions docs-developers/server/scanner.md
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,7 @@ predicate over `books b`, shared by the counts (`IssueCounts`) and the lists
| `split_discs` | it is the **first** disc (by `rel_path`) of a book split across disc folders: `split_parent` is set and that folder has no override of either mode (one settles how it reads). Its fix sets `book` on `split_parent`, which joins the discs |
| `no_cover` | a scan has checked it (`has_cover` not `NULL`) and it has no sidecar image, no embedded art and no custom cover |
| `unmatched` | it has no ASIN and no ISBN. Only offered while community metadata is on |
| `series_spellings` | another book of its library spells its main series another way: the same `series_key`, a different `series` (two range seeks on `idx_books_series_key`, a spelling below its own or above). `SeriesSpellingGroups` lists them per series from the same predicate; the console's fix sets one spelling with a bulk edit |
| `no_chapters` | it is longer than **2 hours** and has at most one chapter. The console shows why community chapters weren't used from the row's `chapters_check` (a failed [community chapter check](community-chapters.md)) |
| `detailed_chapters` | its last community chapter check is `refine` (the community's finer chapters fit) and it has no `chapter_source` choice; picking either source settles it |
| `transcode` | its codec doesn't play in browsers (`media.DirectPlayable`) |
Expand Down
7 changes: 6 additions & 1 deletion docs-developers/server/web-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,12 @@ CSP-sensitive work against a real build served by Go. The console's own gate is
ignored books (`?issue_ignored=true`). Duplicates
(`duplicates.tsx`) render each group side by side from
`GET /admin/issues/duplicates`, the copy worth keeping first, with "They're
different books" (ignore every copy). The category and the ignored view live
different books" (ignore every copy). Series spelled several ways
(`series-spellings.tsx`) is grouped too: one notice per series and library from
`GET /admin/issues/series-spellings`, the suggested spelling picked first in a
select, "Use this spelling" running the Authors page's merge (`useMerge` on `series`:
a bulk `series` edit of the books whose main series is that series, with Undo),
and Ignore / Show again covering every one of the series' books. The category and the ignored view live
in the URL (`?issue=<kind>&ignored=1`). Without `?issue=` the page opens the
first category needing attention and keeps it open, so clearing it shows "All
clear" instead of jumping to the next. "Check again" queues a scan of every
Expand Down
10 changes: 9 additions & 1 deletion docs-users/admin/books.md
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,12 @@ one person. Use the library filter and **Filter authors** (or **Filter
narrators**) to narrow the list. Click a name to open **Books** filtered to that
person, their shared books included.

The page shows 120 people at a time. Use the page numbers under the tiles to
move through a long list. The page and the filter are part of the page's
address, so after you open a name, **Back** returns you to the same page and the
same filter. A new filter, or picking another library, starts again on the first
page.

![Library > Authors](/img/screenshots/admin/authors.png)

### Merging two spellings
Expand Down Expand Up @@ -465,7 +471,9 @@ Monsters") are one series, everywhere: on this page, in the books list
filtered to the series and in the players. The card is named the way your
matched books spell it, or else the way most of its books do. Names that
differ in any other way stay apart ("The Expanse" and "Expanse" are two
series), so fix those with a [bulk edit](#changing-many-books-at-once).
series), so fix those with a [bulk edit](#changing-many-books-at-once). To make
every book of a series spell it the same way, use
[Series spelled several ways](health.md#series-spelled-several-ways) in Health.

A book with a series but no series number takes the number its title gives
when the title is the series' name followed by a number: "He Who Fights with
Expand Down
Loading
Loading