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:
/loginreturns two tokens.user.accessTokenexpires in an hour;user.tokenis a legacy JWT with no expiry. SoundStorm stores the legacy one on purpose - the other would strand the backend an hour after provisioning without a refresh flow. If a release drops it, that is where the refresh dance goes.isActivemust be sent when creating a user. Without it the account is created inactive:POST /api/usersreturns an ordinary 200 with a token in it, and that token answersUnauthorizedto everything. Nothing in the response hints at a problem.
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
| Call | Used for |
|---|---|
/api/libraries/{id}/search | Search |
/api/libraries/{id}/items | Browsing (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}/cover | Covers, resized |
/api/me/progress/{id} | Reading and writing the listener's position |
/api/me/items-in-progress, /api/me | The 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:
progressis not derived fromcurrentTime. Send one without the other and the player resumes correctly while Audiobookshelf's own shelf keeps showing the old percentage. The adapter computes the fraction.isFinishedis one-way, and sendingfalseis destructive. For a finished book it resetscurrentTimeandprogressto zero - "mark as unfinished". Someone who reached the end and scrubbed back would lose their place, so the flag is only ever sent astrue.- It stores whatever it is given. PATCHing the string
"x"ascurrentTimeis accepted and read back. So nothing leaves SoundStorm that is not a finite time (1e999decodes to+Inf), and progress is decoded leniently so one junk record cannot become a permanent error for that book.
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.