Browse and play an audiobook collection organised as Genre / Author / Series / Book
10K+
A small, self-hosted web app for an audiobook collection that is organised on disk as Genre → Author → Book, Genre → Author → Series → Book, or — for an author who publishes a series in parts — Genre → Author → Series → Part → Book.
It does three things, and it does them properly:
It writes the metadata into your MP3s. Not into a database only its own interface can read — into the files, where every other player will find it.
It turns
.m4band.ogginto MP3, a chapter to a track. So the books that could not be tagged, can be.It plays them, and remembers where each person got to in every book.
The full manual, with screenshots of every part of it, is in the wiki.
.m4b and .ogg books to MP3 — a chapter becomes a track, named and numbered in orderffmpeg and ffprobe are part of the image — nothing to install, upload or configure/audiobooks
├── Fantasy
│ ├── Brandon Sanderson
│ │ └── Mistborn <- series folder
│ │ ├── The Final Empire
│ │ │ ├── 01-chapter.mp3
│ │ │ └── 02-chapter.mp3
│ │ └── The Well of Ascension
│ ├── Jane Deep
│ │ └── The Great Cycle <- series
│ │ ├── First Movement <- part of it
│ │ │ ├── The Opening
│ │ │ └── The Turning
│ │ └── Second Movement
│ │ └── The Return
│ └── Patrick Rothfuss
│ └── The Name of the Wind <- book directly under the author
└── Thriller
└── Stephen King
└── The Shining
How a third-level folder is read:
| It contains | Read as |
|---|---|
| audio files | a book |
| several sub-folders | a series, each sub-folder a book |
sub-folders that are all Disc 01, CD 2, Part 3… | one book, its discs played in order as one track list |
| exactly one sub-folder | one book; a series of one has nothing to group |
Some authors write a series in parts, each part its own run of books. Put the parts under the series and the books under the parts, as Jane Deep is above.
The part is the series — it is what the books are numbered against, what Series to complete counts, and what Music Assistant and Home Assistant report. The series above it groups the parts: in the genre column it is a row of its own with its parts indented under it, and pressing it shows the whole series at once with each part under its own heading.
Two things decide whether a folder of folders is a part rather than a mistake:
One extra level and no more: a book any deeper is still reported as "a folder deeper than the layout reads". And Move… writes four folders, so moving a book out of a part is possible but moving it back in is not — the dialog says so when you open it on one.
Settings has a Genres section to make one: it creates the folder, and adds it as a library folder when your libraries are single genre folders, so the new genre is offered for an import straight away.
If a library folder holds things you do not want scanned, add the genre folders themselves and tick Is a Genre behind each, instead of adding their parent. A scan warns when a folder looks like a genre but is not marked as one, since its authors would otherwise be filed as genres.
Supported files: .mp3 .m4a .m4b .ogg .flac .opus (tag writing is MP3-only, so
everything else can be converted — see below).
Those books play, but their tags cannot be written, so they never leave Needs tags. Needs converting in the left column lists every book whose files are not MP3, and ⤳ Convert to MP3… on a cover does one on the spot.
A chapter becomes a track, which is what this app calls a chapter: an .m4b of
30 chapters becomes 30 MP3s named 01 - <chapter>.mp3, in order, each carrying its
chapter name as its title, so the player lists them and you can jump between them.
A file with no chapters becomes one MP3. The cover art the source carried is
written beside the audio as cover.jpg, where a scan and the tag writer both read
one from.
The files it came from are not deleted: they are moved to a .converted folder
inside the same library folder and listed under Converted in the left column,
with what they were, how big they were and when. Delete them there once you are
happy — one book at a time or all of them.
Converting uses both, and they are part of the image from 2.3.0 on — nothing to install, upload or configure, and always the build that image is for. A container from before that says so when you try: update it to the newest build.
A series shows up under its genre in the left column, with the number of books in it, and opens as one list in reading order. Genres fold their series away: the arrow beside a genre opens and closes it, while the name selects the genre and leaves the list as it was, and the column comes back the way you left it. Every book card names its series next to the cover, and so does every tile on the shelves.
A series published in parts is a row with its parts indented under it, inside the genre's own fold. Pressing a part shows that part; pressing the series above it shows the whole thing at once, each part under its own heading — Series · The Great Cycle · First Movement. A book's number belongs to the part it is in, so Edit metadata numbers it against its own run of books rather than against the whole series.
A series comes from any of three places:
Fantasy / Brandon Sanderson / Mistborn / The Final EmpireThe Dark Tower I, The Dark Tower II, … make the
series The Dark Tower, numbered in that order. One such folder on its own is
just a title that ends in a numeral, and disc folders are never volumes.MVNM), the grouping frame (TIT1) or a SERIES text frame,
which is where audiobook taggers put it, with the movement index deciding the
order. A tag that names the volume rather than the series — The Dark Tower V
— is read as book 5 of The Dark Tower, so it joins the other volumes instead
of standing aloneA series only appears after a scan, since that is when the folders and files are read. Books that were already in the library pick it up from a rescan without their folders having to change.
All three are shown the same way, on the cards and as a heading in the author view. A series that was not a folder never moves a file: the folders stay as they are, and Move… and the Series field still work on the folder alone.
Under the name of a series, on both pages, is a line saying what the collection has of it — Missing: book 3 in red where a number has no book on it, or Book 1 to 5 are all here where nothing between them is absent.
It reads the volume numbers, so it can only say as much as those do:
And when you would rather ask than come across them: Series to complete, in the left column under Maintenance, goes through every series in the collection and shows the ones with a hole. Each gets a series heading — its name, the sentence saying what is gone, a ⧉ copy button and Open in library, which goes to that series where the missing book would land — and under it one card per missing volume, one under the other, the way the rest of the library is drawn. There is no title to put on such a card, since nothing in your files knows what book 3 is called, so it reads Book 3 and says why it is there: your own books run up to book 4, with nothing on 3. It is the same counting as the line under a series head, so it asks Google nothing and costs no quota. Series that carry no volume numbers at all are named there rather than left out: they are the ones no verdict could be given about, and an empty list would otherwise read as nothing missing.
That is a different question, and nothing in your own files can answer it: your numbers say a book is missing between the ones you have, never that a ninth was ever written. Series to complete, in the left column under Maintenance, asks Wikidata — which records the volumes of a series and their order, needs no key and costs no quota, and is sent nothing but the series name.
It goes through every series, slowly and one at a time, because that is somebody's free service; on a large collection it runs for a few minutes, on the server, and you may close the page. What it found is kept, so restarting or updating the container does not throw it away and ask you to spend those minutes again — the pane tells you when it was last asked. The pane keeps the two kinds of answer apart, because they are not equally certain: what is missing between your own books is counted here and exact, and what Wikidata knows and you do not have is a question asked of the world.
A series you will never complete — or one Wikidata confuses with something else — can be put aside with ✕ ignore beside its name. It leaves the list and is not asked of Wikidata again; the line at the foot of the pane names every ignored series, and clicking one there (↺) looks for it again.
It is browsed the way the library is, in three columns like the rest of the
app. The authors column fills with whoever is short of something, with a count
each, and clicking a name narrows the pane to their series; it fills as the check
runs, so an author Wikidata has just found appears while you watch, and the name
you were reading stays chosen. A volume you do not have is drawn as the card it
would be —
cover, title, author, Series · name · book 3 — because what you want is the
thing to go and find. Those cards are dashed and dimmed, and have no Play
button, so a shelf of them never reads as part of your collection. Every card
and every series heading carries a ⧉ copy button that puts
author - title (or author - series) on the clipboard, ready to paste into a
search.
Under that, each card has Wikipedia ↗, which opens in a new tab: the book's page where it has one, and Wikipedia's search for it where it has not, so it never lands on nothing. A card that is only a number — Book 3 — asks about the series instead, which is the page that lists the volumes. Cards Wikidata found also keep On Wikidata, so you can check the entry it picked.
Three things it will tell you rather than hide:
Their numbering is not always yours: Wikidata has The Wind Through the Keyhole as book 8 of The Dark Tower, where it reads fourth-and-a-half. Treat what it finds as worth looking at, not as a verdict.
ca_profile.xml in the root describes this repository for Unraid's Community
Applications.
The template does the whole form for you. Copy
my-My-Audiobook-Collection.xml to
/boot/config/plugins/dockerMan/templates-user/ on the server, then Docker →
Add Container → pick My-Audiobook-Collection under User templates, and
check the paths it filled in:
| Setting | Default | What it is |
|---|---|---|
| WebUI Port | 8523 | change it if the port is taken; the WebUI link follows |
| Appdata | /mnt/user/appdata/my-audiobook-collection → /data | database and cover images |
| Audiobooks | /mnt/user/Audiobooks → /audiobooks | the collection, read/write so tags can be written |
| Import folder | empty → /import | where new audiobooks arrive; leave empty if you do not import |
| User ID / Group ID | 99 / 100 | who the app writes as. Leave these: without them it runs as root, and every folder it creates on your share belongs to root, so you are refused permission to write in it yourself |
| File mode mask | 000 | the mode of what it creates. 000 is what an Unraid share normally is; 022 makes it read-only to others |
| Admin password | empty | guards everything that changes the collection. Leave it empty and the app asks you to make an administrator the first time you open it — see below |
| Google Books API key | empty | for looking up missing metadata; the only place it is set |
MA_TOKEN | empty | the password Music Assistant logs in with. Empty means that whole face of the app is not there |
| Home Assistant token | empty | only for the polling addresses: set it to make /api/ha… ask for it. The page above needs no container setting |
| Base URL | empty | only behind a reverse proxy: the address other machines reach the app on |
BROWSE_ROOTS | empty | only if your audiobooks are somewhere other than /audiobooks, /import, /mnt, /media or /srv: more places the folder browser in Settings may look, comma-separated |
FRAME_ANCESTORS | 'self' | only if you show the app inside a Home Assistant dashboard (a webpage card): add its address, e.g. 'self' http://192.168.2.200:8123. Without it the app refuses to be framed by another site |
| Google country | US | which country's Google catalogue a fresh database starts on. From 2.3.72 it is chosen in Settings, and the setting wins; this only decides where it begins. Series data belongs to a country's catalogue and the US one has the most of it — but if lookups keep saying Google is busy, try your own country instead |
The last three are optional, and the two variables are masked in the form. Then Apply, and open the WebUI:
ADMIN_PASSWORD is set on the container,
in which case it asks for that. The first page, at the bare address, is the
listening one; the page that changes things is at /admin, or one press of
Admin./audiobooks as a library folder (Browse… picks it from
the container's own view of the disk) → Save.
If /audiobooks also holds folders you do not want scanned, add the genre
folders one by one instead and tick Is a Genre behind each.Nobody else gets in until you let them: a visitor asks for an account and you approve it under Accounts — see Who may listen.
Because the container comes from a user template, an update is one click. Unraid
names a user template after the container it belongs to, my-<container name>.xml,
and the <Name> inside the file has to match that name too — that pairing is what
makes the Docker tab treat it as managed. Rename both if you call your container
something else.
Force update is in the Docker tab's Advanced View: switch the toggle at the top right and the container row gains a version column, the context menu a Force update entry. Neither is shown in Basic View.
ADMIN_USER, ADMIN_PASSWORD and GOOGLE_API_KEY live here and nowhere else.
None has a field in Settings, so there is no second copy to drift out of step and
all survive an emptied appdata folder. Leave the password empty and the app asks
you to make an administrator the first time it is opened, kept hashed in its
database; fill it in and the container's password wins — which is also the way
back in if you forget the one made in the app. ADMIN_USER is the name that
administrator goes by.
docker compose up -d
Proxmox, or Unraid's own LXC support, or any Debian or Ubuntu machine with systemd. There is no image here, so the things the container did for you have to be done once: install node and ffmpeg, put the app somewhere, write a settings file, and arrange for it to start at boot. One script does all four.
bash -c "$(curl -fsSL https://raw.githubusercontent.com/Starf0x/my-audiobook-collection/main/ct/my-audiobook-collection.sh)"
This is a Proxmox VE Helper Script, and it behaves exactly like the ones from
community-scripts because it
is one: the engine is theirs, loaded from community-scripts/core. The banner,
Default and Advanced settings, the storage radiolist, the spinners and the
line at the end with the address all come from it, so if you have installed
anything that way before, this is the same thing.
What makes it work for an app that is not in their repository is one line. Their
engine resolves two roots separately — its own files from one, and ct/ and
install/ from another — so this script points the second at this repository,
and the engine fetches this app's installer instead of looking for it in theirs.
Run it again later and it offers to update: it fetches the newest released tag, reinstalls the dependencies and restarts the service.
It makes a privileged container, where theirs default to unprivileged. That
is deliberate — see the id-shifting note below — and var_unprivileged=1 in
front of the command overrides it.
The container cannot see your collection until the host hands it over. On the
host, with <id> the container it just made — it prints these at the end, with
the id filled in:
pct set <id> -mp0 /host/path/Audiobooks,mp=/audiobooks
pct set <id> -mp1 /host/path/Import,mp=/import # only if you import
pct reboot <id>
Then add /audiobooks under Settings → Library folders. Until that mount is
there the app runs perfectly well with nothing to show, which reads as the app
being broken when it is the folder that is missing.
Why privileged. An unprivileged container shifts every id by 100000:
PUID=99 inside becomes 100099 on the host, which owns nothing, so the app is
refused permission to write in a collection whose permissions look perfectly
right from the host. Privileged is the usual answer for a container that writes
to a media share, which is why this one defaults to it
Content type
Image
Digest
sha256:83a9f3061…
Size
111.6 MB
Last updated
1 day ago
docker pull starf0x/my-audiobook-collection