Sign inSign up

starf0x/my-audiobook-collection

By starf0x

•Updated 1 day ago

Browse and play an audiobook collection organised as Genre / Author / Series / Book

Image
0

10K+

starf0x/my-audiobook-collection repository overview

⁠My Audiobook Collection

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 .m4b and .ogg into 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⁠.

⁠Writing the tags into the files
  • Shows per book which tags the files themselves carry, in green, so metadata that is only in the database is visible as such
  • A Needs tags list of every book whose files miss a required tag — nine of them — with writing and lookup on the spot
  • Fills in what is missing from the Google Books API, or by hand, and writes it back into the MP3s
  • Writes album, title, artist, album artist, genre, year, description, cover art, narrator and a renumbered track number
  • Merges: frames it is not setting are left alone, and a value it does not have is left out rather than written empty
  • A value written by another tagger is picked up by the next scan — what it says, not only that it is there
  • The whole collection in one run, on the server: closing the page does not stop it, and it can be stopped and carried on
  • Two books, or two series, can be written at once, each with its own bar — but never the same book twice at once
⁠Converting to MP3
  • Converts .m4b and .ogg books to MP3 — a chapter becomes a track, named and numbered in order
  • A Needs converting list of every book whose files are not MP3, and ⤳ Convert to MP3… on any cover
  • Keeps what it came from: the originals move to Converted, with their size and date, and you delete them when you are satisfied
  • Cover art the source carried is written beside the audio, so nothing is lost
  • ffmpeg and ffprobe are part of the image — nothing to install, upload or configure
  • Every refusal says why, in words: an older container, a file the tool could not read, or what the tool itself said
⁠Playing it
  • Streams in the browser, remembers the position per user, and marks a book listened when it runs out
  • The player is the app's own: dark, with a yellow line measuring time through the book, not tracks
  • A book keeps playing when you change page — the listening page, the admin page and the Home Assistant page hand it over between them
  • The Play button of the book that is playing is its Pause button, on the card you started it from
  • The cover is a play button too: click the picture to start a book, click it again to pause it
  • Unticking Listened clears the place kept in that book, so it starts from the beginning again
  • ⤓ downloads the whole book — every file of it in one archive, not the single track that is playing
  • Home Assistant reads the collection and carries a book on to any media player in the house
  • And a sensor per listener: every book they have on the go with how far in they are, and the books they finished with how long they took — with a ready-made dashboard card in docs/home-assistant.md⁠
  • Music Assistant can be pointed at it as an Audiobookshelf server: browse the books there, play them on any speaker, and the place syncs both ways
⁠And around all that
  • Three-column interface: genres on the left, authors next to it, books with full metadata on the right
  • One search box for the lot: title, author, genre, series, narrator or a few words from the description
  • Each genre lists its series underneath it, from a series folder, from sibling volume names, or from the tags — and a series published in parts lists its parts under it
  • Says under a series which volume you are missing, from the numbers it already has — no requests, no key
  • And asks Wikidata which volumes a series actually has, so a book you never owned is one you can be told about
  • Opens on shelves of covers: what you were listening to, with how far you are, and what was added last
  • Scans one or more library folders on the server (with a built-in folder browser)
  • Draws a cover for a book that has none, so a shelf is never a row of empty rectangles
  • Files new audiobooks from an import folder into the right genre, author and series
  • Moves a book to another genre, author or series, and deletes one to a trash it keeps for 30 days
  • Says which books it could not read, and which folders it walked past, rather than quietly finding fewer
  • Keeps a copy of its database beside it, daily — your books are on your share, but where everybody is in them is not
  • The page itself, and the covers drawn for books with no art of their own, turn their colours every day
  • Works on a phone: one column at a time, thumb-sized rows, full-screen dialogs, the player across the bottom
  • Runs as a single Docker container, SQLite storage, no external services

⁠Folder layout it expects

/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 containsRead as
audio filesa book
several sub-foldersa 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-folderone book; a series of one has nothing to group
⁠A series published in parts

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:

  • every folder in it holds a book — audio of its own, or disc folders. One that does not means this is something else, and the scan says so on Walked past rather than offering you a series with a hole in it;
  • something beside it holds two or more books. One folder holding one book reads just as well as a book filed a level too deep, so the siblings settle it — and a part with a single book standing next to a fuller one is read as a part too.

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).

⁠Converting .m4b and .ogg to MP3

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.

⁠ffmpeg and ffprobe

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.

⁠Series

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:

  • a series folder — Fantasy / Brandon Sanderson / Mistborn / The Final Empire
  • sibling folder names, when two or more folders under one author differ only by a volume number — The 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.
  • the files themselves, for a book filed straight under its author: the movement name (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 alone

A 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.

⁠Which volumes you are missing

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:

  • It never calls a series finished. A shelf holding books 1 to 5 says exactly that; whether a book 6 was ever published is not something the app can know. Google's Books API will not answer it either — it says whether a series is complete, never which volumes are in it — so this is counted at home, and costs no requests and no key.
  • Books with no number are counted and said. Missing: book 2. 1 book(s) here carry no volume number, so what is missing may be among them — the unnumbered one may well be the gap. Give it a number in Edit metadata and the line settles.
  • A series nobody has numbered says so rather than pretending to a verdict, and a single book on its own gets no line at all.
  • It counts the whole series, not the books on screen. Browsing by author shows one author's share of a shared series; the book another author wrote still counts as one you have.

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.

⁠Does a volume exist that I do not have?

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:

  • Which entry it took, with a link. A name is not an identifier — The Dark Tower is also an album, a film and a video game — so it picks the one whose volumes look like the books you already have, and says which that was.
  • When it could not tell. A series it cannot place is listed with the reason, never quietly counted as complete.
  • When it matched by title. If your books in a series carry no volume numbers, it compares titles instead and says so.

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.

⁠Run it on Unraid

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:

SettingDefaultWhat it is
WebUI Port8523change it if the port is taken; the WebUI link follows
Appdata/mnt/user/appdata/my-audiobook-collection → /datadatabase and cover images
Audiobooks/mnt/user/Audiobooks → /audiobooksthe collection, read/write so tags can be written
Import folderempty → /importwhere new audiobooks arrive; leave empty if you do not import
User ID / Group ID99 / 100who 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 mask000the mode of what it creates. 000 is what an Unraid share normally is; 022 makes it read-only to others
Admin passwordemptyguards 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 keyemptyfor looking up missing metadata; the only place it is set
MA_TOKENemptythe password Music Assistant logs in with. Empty means that whole face of the app is not there
Home Assistant tokenemptyonly for the polling addresses: set it to make /api/ha… ask for it. The page above needs no container setting
Base URLemptyonly behind a reverse proxy: the address other machines reach the app on
BROWSE_ROOTSemptyonly 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 countryUSwhich 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:

  1. The first visit asks you to make an administrator — a name and a password of at least eight characters — unless 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.
  2. Settings ▾ → Library and maintenance → add /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.
  3. Scan library. The bar at the bottom shows how far it is, and the button stays grey until the scan is done. With more than one library folder, the pulldown beside it picks which one to scan, or all of them — each option says what the button will do: Scan all libraries, Scan /audiobooks/Fantasy.

Nobody else gets in until you let them: a visitor asks for an account and you approve it under Accounts — see Who may listen.

⁠Updating

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.

⁠The admin and Google variables

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.

⁠Or with docker compose
docker compose up -d

⁠Run it in an LXC container

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.

⁠On Proxmox VE: one command on the host
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.

⁠Your two folders

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

Tag summary

Content type

Image

Digest

sha256:83a9f3061…

Size

111.6 MB

Last updated

1 day ago

docker pull starf0x/my-audiobook-collection