Audiobookshelf

Audiobookshelf scans library/audiobooks, knows each book's files and chapters, and - most importantly - remembers each listener's place. It is the one backend where SoundStorm gives every person their own account, because that place is personal.

Provisioning

SoundStorm creates the root user through /init, signs in at /login, creates the library on /audiobooks with /api/libraries and starts its first scan. Two things were verified against 2.36.1 rather than read anywhere:

An account per person

Listening position is keyed by Audiobookshelf's account, so two people sharing one would overwrite each other's places. So provision.TokenFor gives each member their own account, created lazily on first use rather than when they are added: a backend can be down or still provisioning when somebody joins, and first use is a retry that costs nothing to write. The create response carries a token, so there is no second login and no password to keep. The owner uses the shared administrator credential - they already have an account there, and a second would split their own history in two.

Removing somebody deletes their Audiobookshelf account first, because deleting the SoundStorm account drops the record of which Audiobookshelf user was theirs. It is best effort: a backend that is down must not stop somebody being removed.

The calls SoundStorm makes

CallUsed for
/api/libraries/{id}/searchSearch
/api/libraries/{id}/itemsBrowsing (search matches nothing for an empty query); sort=addedAt&desc=1 for Home's newest
/api/items/{id}A book's files, inodes and chapters
/api/items/{id}/file/{ino}Audio bytes
/api/items/{id}/coverCovers, resized
/api/me/progress/{id}Reading and writing the listener's position
/api/me/items-in-progress, /api/meThe Continue row: which books, and how far
POST /api/libraries/{id}/scan"Look now" after an upload

Audio is addressed by inode, not item id. /api/items/{id} has to be fetched to learn it - which is why source.Streamer takes a context: resolving a target is not always local arithmetic.

The browse listing has the same item shape as search but one wrapper shallower. Decoding it with the search struct yields an empty list and looks like an empty library rather than a bug.

Position: sharp edges

Position is written to Audiobookshelf rather than kept by SoundStorm, so a chapter finished in its phone app is where the browser picks up. Its API needs care:

Continue leaves out finished books, ones hidden from continue listening in Audiobookshelf's own UI, missing ones and podcast episodes. A position under half a percent or over 98.5% is "not started" or "finished".

Files and chapters

source.TrackLister reports a book's audio files, each with its StartSeconds on the whole-book clock. source.ChapterLister sends Audiobookshelf's chapter list, which names the marks inside a single m4b as well as one file per chapter. source.AudioLayouter says which chapters fall in which file - what Storyteller's timing conversion rests on. Chapter names come from the chapter list when there is one per file, then the ID3 title, then the file name. Some LibriVox MP3s ship double-encoded tags; those are left alone rather than "repaired" by guessing.

Deleted files

Audiobookshelf sets isMissing on a book whose files are gone and keeps serving it from both search and items. That is defensible - a book on an unplugged drive should not lose its listening position - but it meant a shelf somebody emptied still looked full, with a play button behind every entry (six of seven items were missing in a live library when this was found). There is no "not missing" filter to ask for, so the adapter skips them client-side, on both endpoints, each tested separately.

Version

The image follows latest; everything above was checked against 2.36.1.