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 f541566714 - Show all commits
@@ -121,13 +121,30 @@ the file, not the bind: the same line count grows while the total climbs.
itself.** The door is `GET /api/libraries/<id>/items?limit=1` → `total`; a poller itself.** The door is `GET /api/libraries/<id>/items?limit=1` → `total`; a poller
that guesses a plausible name (`numItems`) prints `0` forever beside a scan that that guesses a plausible name (`numItems`) prints `0` forever beside a scan that
is working. An out-of-range `offset` returns the last item rather than an empty is working. An out-of-range `offset` returns the last item rather than an empty
page, so a census that loops "until empty" invents its own total — two runs page, so a census that loops "until empty" invents its own total: `limit=500`
reported exactly 1,500 and 4,000 for one library — and `total` itself moves while against a `total` of 4,628 hands back exactly 5,000 rows — the collected count
a scan runs. Stop on `total`, and take the count after the pass settles. is `ceil(total / limit) × limit`, which is why two runs reported "exactly 1,500"
and "exactly 4,000" for one library. Stop on `total`, count *unique ids*, and
take the number after the pass settles (`total` itself moves during a scan).
- **A fresh scan is the only thing that notices new audio.** `inotify` does not - **A fresh scan is the only thing that notices new audio.** `inotify` does not
cross a rootless podman bind — measured for [[Jellyfin]], whose page owns that cross a rootless podman bind — measured for [[Jellyfin]], whose page owns that
law — so the in-container watcher sees nothing a host process files onto the law — so the in-container watcher sees nothing a host process files onto the
array, and the deployer's re-run of the bootstrapper is the cheap scan trigger. array, and the deployer's re-run of the bootstrapper is the cheap scan trigger.
- **A book is the shallowest folder that holds audio — and that item owns the
subtree beneath it.** A folder with audio in its own file list becomes ONE book
and the scanner folds everything below it into that book's tracks instead of
making items: 6 stray files at the root of `_unsorted` turned the whole
collection dump into a single item of 5,693 tracks, titled after whichever file
supplied metadata, with 542 real books inside it. So "hundreds of books are
missing" is a layout fault, not a scan fault — and no scan, forced or not,
subdivides an existing item. Census the layout the server's way: a directory is
a book folder iff its *own* files include an audio extension.
- **`DELETE /api/items/:id` is a DB row; `?hard=1` is `fs.remove()`.** Read
`LibraryItemController.delete`: `hardDelete = req.query.hard == 1` and then
`fs.remove(libraryItemPath)`. Pointed at a covering item, that path is the whole
swallowed subtree — `hard=1` there deletes every audiobook under it off the
array. Un-swallowing is: file the loose root files with the journal-guarded
mover, then delete the stale covering item with NO query string.
- **`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 the file — the book never appears — so a
missing title beside those lines is library debris, not a server fault. Census of missing title beside those lines is library debris, not a server fault. Census of