feat(audiobookshelf): bootstrap the account, library and scan from the deployer #119

Merged
dank merged 7 commits from feat/audiobookshelf-bootstrap into main 2026-11-02 07:16:45 -08:00
Showing only changes of commit ad9563412d - Show all commits
@@ -153,21 +153,37 @@ the file, not the bind: the same line count grows while the total climbs.
ABS's own single-book layout; formats of one book share a folder), through the ABS's own single-book layout; formats of one book share a folder), through the
journal-guarded mover, then delete the stale item so the old path is not kept, journal-guarded mover, then delete the stale item so the old path is not kept,
then scan. then scan.
- **`?force=1` is not a discovery arm.** Both arms of `POST /libraries/:id/scan` - **`?force=1` is neither a discovery arm nor a repair arm.** Both arms of
walk the disk; force additionally re-scans every item that already exists `POST /libraries/:id/scan` walk the disk, so finding new folders needs neither
(measured 1:33 against 0:04 on ~4,600 items). Finding new folders needs neither, and the deployer's cheap trigger stays on the plain arm. Force additionally
so the deployer's cheap trigger stays on the plain arm. re-scans every item that already exists (measured 1:33 against 0:04 on ~4,600
items) and repaired nothing on a library whose residuals were file-level: item
count, path set and media-less count identical before and after.
- **`DELETE /api/items/:id` is a DB row; `?hard=1` is `fs.remove()`.** In - **`DELETE /api/items/:id` is a DB row; `?hard=1` is `fs.remove()`.** In
`LibraryItemController.delete`, `hardDelete = req.query.hard == 1` and then `LibraryItemController.delete`, `hardDelete = req.query.hard == 1` and then
`fs.remove(libraryItemPath)`. Pointed at a covering item, that path is the whole `fs.remove(libraryItemPath)`. Pointed at a covering item, that path is the whole
swallowed subtree — `hard=1` there deletes every book beneath it off the array. swallowed subtree — `hard=1` there deletes every book beneath it off the array.
Assert the item's `path` first, and never pass `hard`. Assert the item's `path` first, and never pass `hard`.
- **`Invalid data found when processing input` names the file, not the bind.** The - **`Invalid data found when processing input` names the file, not the bind.** The
scanner logs it per file and skips the file — the book never appears — so a scanner logs it per file and skips that file; a book whose files ALL fail never
missing title beside those lines is library debris, not a server fault. Census of becomes an item at all, so the disk-vs-DB folder census shows it as a book with
the refusals on the first full scan: 38 of 43 files were 0 bytes, 3 were WAV no item. That is corrupt media, not a lost book. Never clear a suspect file with
bytes wearing a `.mp3` name, 2 were unreadable. `stat -c %s` before any a lenient probe: `ffprobe -show_entries format=duration` happily returns three
investigation of the server. hours for the WAV bytes wearing a `.mp3` name that the scanner refuses. Read the
log's file paths instead, and `stat -c %s` first — 0-byte files are the common
case, and a truncated download reports `moov atom not found`.
- **A `Disc 01` / `CD 1` subfolder is part of a book, not a book.** The extension
census counts those folders; ABS deliberately does not — its grouping attaches
their files to the ancestor item (measured: 110 such folders under one item
carrying all 192 tracks). Read the basename before believing a disk-vs-DB gap.
- **`.cbr`/`.cbz` group like media and never play.** They sit in the grouping
predicate, so they hide books, but the comic parser throws `ERAR_BAD_ARCHIVE`
(`parseComicMetadata` → `CbrComicBookExtractor`) and the item lands with no
media. A media-less row beside those lines is the file, not the scanner.
- **A scan is finished when a NEW scan uuid completes.** Match
`Library scan "<uuid>" completed in ...`. `podman logs --since 30m` is a sliding
window: the set of visible completion lines changes on its own as old lines age
out, so a poller keyed on that hash reads a mid-scan number and calls it done.
## Verification ## Verification
@@ -178,3 +194,10 @@ stub of the API above: create-once, idempotent re-run, no secret in a URL, refus
by URL when the server does not answer, and the deployer calling it only after the by URL when the server does not answer, and the deployer calling it only after the
health gate) and `tests/media_overlay.bats` (only [[Plex]] and [[Jellyfin]] may health gate) and `tests/media_overlay.bats` (only [[Plex]] and [[Jellyfin]] may
reference the metadata view). reference the metadata view).
Five book folders hold media no scanner can read, so they stay out of the library:
three 2024 Nebula nominees in the `_unsorted` box (two `.m4b` with no `moov` atom,
one 36-file `.mp3` set that is not MPEG audio at all), `Anthony Heald/Bounty
Hunter Wars - Book 1` and `Star Wars Full Cast Audio Drama/Dark Empire III` — the
latter two also exist in readable form under their author folders. Fix by
re-acquiring the files; the scanner names every one of them in its log.