Audiobookshelf on Kubernetes
Migrating a Plex Media Server to Kubernetes, was a significant improvement for the maintenance of the Plex Media Server I use to listen to podcasts and audiobooks, to keep me company while I play games, but after all these years Plex remains a very insufficient and deficient application for audiobooks.
Enter audiobookshelf (because Emby and Jellyfin are also not great)
Installation on Kubernetes
Audiobookshelf
configuration
requires writeable directories mounted at
/config and /metadata for database, cache, etc.
# useradd -d /home/k8s/audiobookshelf -s /usr/sbin/nologin audiobookshelf
# mkdir /home/k8s/audiobookshelf/config /home/k8s/audiobookshelf/metadata
# chown -R audiobookshelf.audiobookshelf /home/k8s/audiobookshelf
# ls -dln /home/k8s/audiobookshelf/
drwxr-xr-x 1 1006 1006 28 Feb 27 22:47 /home/k8s/audiobookshelf/
Note the UID/GID (1006) to be used in the Kubernetes
deployment securityContext later.
Docker Compose suggests mounting audiobooks and podcasts as separate volumes, so the following Kubernetes deployment will do so, even though it would also work to have it all under a single volume.
Create the following audiobookshelf.yaml and deploy
it:
$ kubectl apply -f audiobookshelf.yaml
namespace/audiobookshelf created
persistentvolume/audiobookshelf-pv-config created
persistentvolume/audiobookshelf-pv-metadata created
persistentvolume/audiobookshelf-pv-audiobooks created
persistentvolume/audiobookshelf-pv-podcasts created
persistentvolumeclaim/audiobookshelf-pvc-config created
persistentvolumeclaim/audiobookshelf-pvc-metadata created
persistentvolumeclaim/audiobookshelf-pvc-audiobooks created
persistentvolumeclaim/audiobookshelf-pvc-podcasts created
deployment.apps/audiobookshelf created
service/audiobookshelf created
ingress.networking.k8s.io/audiobookshelf-ingress created
$ kubectl -n audiobookshelf get service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
audiobookshelf NodePort 10.102.115.191 <none> 13378:31378/TCP 20s
cm-acme-http-solver-jh67q NodePort 10.106.142.67 <none> 8089:30126/TCP 2m39s
$ kubectl -n audiobookshelf describe ingress audiobookshelf-ingress
Name: audiobookshelf-ingress
Labels: <none>
Namespace: audiobookshelf
Address:
Ingress Class: nginx
Default backend: <default>
TLS:
tls-secret terminates aus.ssl.uu.am
Rules:
Host Path Backends
---- ---- --------
aus.ssl.uu.am
/ audiobookshelf:31378 (10.244.0.202:13378)
Annotations: acme.cert-manager.io/http01-edit-in-place: true
cert-manager.io/cluster-issuer: letsencrypt-prod
cert-manager.io/issue-temporary-certificate: true
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Sync 26s nginx-ingress-controller Scheduled for sync
Normal CreateCertificate 26s cert-manager-ingress-shim Successfully created Certificate "tls-secret"
The server is now available at http://192.168.0.6:31378
NGinx should also make it available at https://aus.ssl.uu.am
Deployment
Kubernetes deployment: audiobookshelf.yaml
| audiobookshelf.yaml | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 | |
The above deployment is based on audiobookshelf Docker documentation and Audiobookshelf Helm Chart By TrueCharts.
Troubleshooting
Contrary to what
Configuration
would suggest, the server will by default try to listen on port 80.
To override this the env variable PORT must be set:
Otherwise, when running as a non-privileged user, this will cause it to crash-loop:
$ kubectl get all -n audiobookshelf
NAME READY STATUS RESTARTS AGE
pod/audiobookshelf-754d55cd68-z4br6 0/1 CrashLoopBackOff 4 (59s ago) 3m9s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/audiobookshelf-tcp NodePort 10.97.42.11 <none> 13378:32164/TCP 10m
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/audiobookshelf 0/1 1 0 3m9s
NAME DESIRED CURRENT READY AGE
replicaset.apps/audiobookshelf-754d55cd68 1 1 0 3m9s
$ kubectl -n audiobookshelf describe pod audiobookshelf-754d55cd68-z4br6
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 5m56s default-scheduler Successfully assigned audiobookshelf/audiobookshelf-754d55cd68-z4br6 to lexicon
Normal Pulled 5m29s kubelet Successfully pulled image "ghcr.io/advplyr/audiobookshelf:latest" in 25.769s (25.769s including waiting)
Normal Pulled 5m26s kubelet Successfully pulled image "ghcr.io/advplyr/audiobookshelf:latest" in 661ms (661ms including waiting)
Normal Pulled 5m9s kubelet Successfully pulled image "ghcr.io/advplyr/audiobookshelf:latest" in 659ms (659ms including waiting)
Normal Created 4m39s (x4 over 5m29s) kubelet Created container audiobookshelf
Normal Started 4m39s (x4 over 5m29s) kubelet Started container audiobookshelf
Normal Pulled 4m39s kubelet Successfully pulled image "ghcr.io/advplyr/audiobookshelf:latest" in 752ms (752ms including waiting)
Normal Pulling 3m48s (x5 over 5m55s) kubelet Pulling image "ghcr.io/advplyr/audiobookshelf:latest"
Normal Pulled 3m47s kubelet Successfully pulled image "ghcr.io/advplyr/audiobookshelf:latest" in 634ms (634ms including waiting)
Warning BackOff 47s (x22 over 5m25s) kubelet Back-off restarting failed container audiobookshelf in pod audiobookshelf-754d55cd68-z4br6_audiobookshelf(147c22bc-5f0b-4ed8-a881-478748234776)
$ kubectl -n audiobookshelf logs audiobookshelf-754d55cd68-z4br6
Config /config /metadata
[2024-02-27 22:29:14.925] INFO: === Starting Server ===
[2024-02-27 22:29:14.939] INFO: [Server] Init v2.8.0
[2024-02-27 22:29:14.967] INFO: [Database] Initializing db at "/config/absdatabase.sqlite"
[2024-02-27 22:29:14.998] INFO: [Database] Db connection was successful
[2024-02-27 22:29:15.065] INFO: [Database] Db initialized with models: user, library, libraryFolder, book, podcast, podcastEpisode, libraryItem, mediaProgress, series, bookSeries, author, bookAuthor, collection, collectionBook, playlist, playlistMediaItem, device, playbackSession, feed, feedEpisode, setting, customMetadataProvider
[2024-02-27 22:29:15.080] INFO: [LogManager] Init current daily log filename: 2024-02-27.txt
[2024-02-27 22:29:15.084] INFO: [BackupManager] 0 Backups Found
[2024-02-27 22:29:15.085] INFO: [BackupManager] Auto Backups are disabled
Warning: connect.session() MemoryStore is not
designed for a production environment, as it will leak
memory, and will not scale past a single process.
[2024-02-27 22:29:15.095] FATAL: [Server] Uncaught exception origin: uncaughtException, error: Error: listen EACCES: permission denied 0.0.0.0:80
at Server.setupListenHandle [as _listen2] (node:net:1855:21)
at listenInCluster (node:net:1920:12)
at Server.listen (node:net:2008:7)
at Server.start (/server/Server.js:319:17) {
code: 'EACCES',
errno: -13,
syscall: 'listen',
address: '0.0.0.0',
port: 80
} (Server.js:160)
node:events:496
throw er; // Unhandled 'error' event
^
Error: listen EACCES: permission denied 0.0.0.0:80
at Server.setupListenHandle [as _listen2] (node:net:1855:21)
at listenInCluster (node:net:1920:12)
at Server.listen (node:net:2008:7)
at Server.start (/server/Server.js:319:17)
Emitted 'error' event on Server instance at:
at emitErrorNT (node:net:1899:8)
at process.processTicksAndRejections (node:internal/process/task_queues:82:21) {
code: 'EACCES',
errno: -13,
syscall: 'listen',
address: '0.0.0.0',
port: 80
}
Node.js v20.11.1
Configuration
Once the service is running, accessing the web interface allows
creating the first (root) user. Additional users can (and should)
be created, so they can each have their own progress saved throughout
books and podcatss. Users can also have access to different sets of
libraries, although by default they will have access to all libraries.
Mobile app
To complete the self-hosted audiobook service, there is also the mobile app advplyr/audiobookshelf-app.
Audiobookshelf Android app can easily be installed from Google Play directly. The iOS app cannot simply be installed, because Apple has a hard limit of 10k beta testers. Alternatively, plappa can be used to stream audiobooks and podcasts from AudioBookShelf, Emby and Jellyfin. Downloading content and listening offline requires an in-app purchase.
Better Features
There is a lot in Audiobookshelf that is better, or much better, than in Plex:
- Saving the progress through books and podcats, per user.
- Podcats with a separate feed for pre/after show are somehow corrected merged together.
- REST API that may allow implementing some of the missing features below.
Matching Metadata
One of my favorite features of audiobookshelf is the ability to fetch book metadata and thus more clearly tell different versions of a book a part, e.g. abridged vs unabridged:
After selecting one matching book, metadata can be edited or even left out. This is very useful when an exact match cannot be found, but a close enough match can be used to fetch the matching parts of its metadata while at the same time manually editing or adding as needed. After importing metadata, there are further options to add or edit book metadata in the Details tab:
I find it particularly useful to add or edit:
- Explict, because this is seem to never be provided by matching metadata and in any case it can be a matter of opinion.
- Language, if not correctly matched (usually it is).
- Series. A book can have a different places in different series or subseries, and you can create your own series too.
The Series field is interesting to then navigate books by series:
PDF Reader
When there is a PDF file along with audio files, this can be read from the browser by clicking the corresponding icon e.g. this is (half) a page of Nick Offerman's Good Clean Fun:
Podcast Publication Dates
At least when using a separate tool for downloading podcast
episodes, they have have only a year of publication,
from the TYER ID3 frame, so they show up with their release date
being as YYYY-01-01 and they cannot be properly sorted by
release date. This could be fixed by allowing Audiobookshelf
to download RSS feeds directly (have not tried), but also by
updating the separate tool to set the
TDAT and TIME frames.
Once these are set in the MP3 files, audiobookshell will
correctly parse and display them:
Podcast Progress
Just like with Audiobooks, keep track of which podcasts episodes each user has listed to is a central feature. One little detail I really like about how this is done, is the yellow or black circle on the podcast cover indicating whether all episodes have been listed to already (black circle) or otherwise how many are yet to be listened to (yellow circle):
Missing Features
After a few days listening to books and podcasts, a few features turn out to be missing or not working as expected. Most of these are already in the issue tracker:
- Enhancement: Display podcast episode images #1573
- Enhancement: Ability to edit Series similar to tags #1604
- UI: Sort a Series by publication year if no sort sequence number present #1674
- Enhancement Set custom order for podcast library #1792
- Enhancement: Enable Custom datetime in Podcast episode File Name #1869
- Enhancement: More Flexible Library Structure #2208
- Bug: PDF Reader is flickering #2279
- Enhancement: Same book, different narrators #2396
- Enhancement: Display Chapter Art while playing. #2660
- Provide ability to queue audiobooks and podcast episodes #416
- Ability to mark several Podcast episodes by first marking and then dragging down with two fingers in IOS #685
Batch Updates
Not only is progress saved automatically while listening, podcasts
can also be updated to mark episodes are finished. What is missing
here is the ability to select multiple episodes easily and quickly,
for instance with Shift+click the last episode of a range when the
first one is already selected. It would help to even just be able
to mark all episodes as finished, even if only to then mark a few
as unfinished.
Collections
The option to remove books to collections seems to be missing and there doesn't seem to be any way to remove collections at all.
Playlists
Playlists have the same problematic limitations as Collections: no way option to delete them, or remove episodes from them.
This is the one and only thing I still use Plex for. I like to listen to podcasts in by their publication date, especially because I listen to several podcasts that cross-reference and even invite on each other. I have not found a good way to do this with Audiobookshelf, given the no-remove-option limitations, so I'm still using Plex.








