I was recently introduced to the excellent Wolf 359 audio drama. It’s the story of the crew of a space station orbiting a distant star, with good humor and interpersonal drama as well as some suspense/horror sprinkled in.

I have a small set of other podcasts as well, first and foremost the great British History Podcast. Up to this point, I had mostly listened on my phone, during train rides, but also via my browser during Saturday morning household chores.

My problem: I used Podcast Addict on my phone, and the podcast’s website on my PC. So there was no syncing of which episodes I had already listened to, and no syncing of listen positions inside an episode.

So that was my main motivator to look into a selfhosted podcast app. In addition, I have a couple of audiobooks from the pandemic lockdown times I listened to while making and eating lunch, which I also wanted to put somewhere that wasn’t Audible.

The possible solutions

There are a number of solutions for selfhosted podcast apps. I had the following requirements in my mind:

  • Needed to sync at least completed episodes
  • Needed a web player, because I’m just used to doing many things in the browser these days
  • Bonus: Sync listening position in a started episode

On my “services to look at” list, I already had Podgrab. But looking at the Github repo, the last commit was from September 2022 - a bit too long ago for my taste. But looking at the open issues, I saw this one advertising an alternative.

It is written in Rust

Well excellent. That universal moniker for quality software. (I’m sorry Rustaceans. I know you really like your language. And I promise I will most likely come around to your language. Once the community has gotten past its proselytizing phase.)

But Rust wasn’t the main problem with it, of course. Instead it was the fact that launching the container locally, it just didn’t work. No player showed up. That might have been due to the fact that it is using websockets, and the protocol upgrade simply didn’t work properly. But it turned me off. So did the rather barebones Web UI. Granted, when you’re looking at the Web UI for a podcast player, you’re doing it wrong. But it still turned me off.

Then, I read about GPodder. It’s a Linux desktop podcast manager, but also a protocol for podcast progress sync. I wasn’t too hot on the “Linux Desktop App” idea, to be honest. And the docs for the selfhosted variant for the gpodder online service didn’t look too confidence-inspiring.

Finally, I decided on Audiobookshelf. At first look, it fulfilled all my requirements and actually worked when I ran it locally.

The winner: Audiobookshelf

Audiobookshelf started its life as a pure audiobook app and only added podcasts a little while ago. But having a combo app suited me quite well, as I also have some audiobooks on Audible I always wanted to get onto my own infrastructure.

It works on the principle of “libraries”, and is entirely file based where the media itself is concerned. If you remove a file for a podcast episode or an audiobook, it will also be removed from your Audiobookshelf library, as ABS doesn’t track library items on its own.

When you open it, you are greeted with a nice interface for both, podcasts and audiobooks.

A screenshot of the audiobookshelf web UI. On the left is a menu, showing the following entries: Home, Library, Series, Collections, Authors. In the header is a search bar on the left. On the right are three symbols, a stylized bar char, an upload button and a cogwheel. The main area has several sections, each showing the covers of audiobooks, with their titles and authors beneath them. The sections are: Recently Added, Recent Series and cut off at the bottom the Recommended section.

As you can see, I tend towards history novels. 😉. I can recommend all of them. Especially Rebecca Gable’s Waringham Saga when you’re into medieval England.

The podcasts page looks very similar:

Another screenshot of the Web UI. This time, the menu on the left of the screen contains these entries: Home, Latest, Library, Search, Queue. The sections in the main area are: Continue Listening, Newest Episodes, Recently Added.

In the search tab, you can either search by keyword (which uses iTunes for the search) or you can enter an RSS feed URL directly. When something is found, ABS will show the available info on the Podcast.

Another screenshot of the Web UI. The details available are the podcasts title and author, the feed URL, Genres, a drop-down for the Type, a field for Language and description, a checkbox to mark the content as explicit, the folder for the podcasts files. Finally, a checkbox to automatically download podcast episodes.

Once a new podcast is added, its page will look pretty blank.

Another screenshot of the Web UI. The page shows the podcasts cover picture, title, genre etc at the top. At the bottom is a section headed Episodes, which helpfully shows No Episodes in the screenshot.

As mentioned before, Audiobookshelf is entirely file based. And because we haven’t downloaded an episode yet, ABS shows that there are no episodes. Which is my biggest complaint at the moment. Because everything is tied to files on disk, I can only see episodes which I downloaded. I can of course fix this by enabling automatic download of new episodes. But: This will mean that my disk will grow pretty full pretty quickly with currently 28 podcasts. I would very much prefer a setup like Podcast Addict, where it shows me all the available episodes, and I can click to download a couple of them.

This behavior also has repercussions for finished episodes. Yes, I can then delete them. But if I haven’t downloaded a number of fresh episodes already, I have no way of knowing where I was, as ABS does not store the finished state after an episode file is deleted. So for now, I have to pay attention and make sure I always download a fresh episode before I delete the last finished episode.

In principle, this isn’t too bad for audio dramas, where I might actually want to keep them for repeat listening. But something like the weekly selfhosted podcast, or Linux after Dark, I don’t care too much about old episodes I have already finished.

But let’s continue the UI tour. To download new episodes, you click the magnifying glass, where you will then be shown a list of the available episodes from the RSS feed.

Another screenshot of the Web UI. It shows a list of available episodes, with their episode number, title, publishing date and first line of description. There is also a search bar at the top.

If you actually have some episodes downloaded, they will be clearly marked in the list.

The bottom of the same list as before, but now there is a green check mark next to the very first episode.

As a pretty nice feature, you can re-export any podcast as an RSS feed, so that you can point e.g. your phone’s podcast app at it. This prevents listening position and finished episodes sync, of course.

As a connaisseur of pretty plots, I also need to mention that there are a couple of stats available on your listening habits.

The stats page shows a line chart with listening time, minutes on the Y axis and date on the X axis. Furthermore, completed items, days listening etc are also shown.

In my defense of the 555 minutes on Wednesday: I took a sick day because I had overextended something in my neck and could barely hold up my head for any length of time. So I found a comfortable position on the couch, donned headphones and started listening. 😅

I’ve also used it during my commute today. It works nicely as a Progressive Web App (PWA) added to my home screen, including player controls on the lock screen. The only thing missing is preloading episodes. The web player streams them directly from the server. Probably not too good for my monthly data volume. Here, re-exporting the RSS feed would work, but then I wouldn’t have playback and finished episodes sync. 🤷

In summary, I’m pretty satisfied with the app. Besides that tiny problem that it is only based on files. But this is of course a legacy of its audiobook roots, and there are several open issues on GitHub to address this.

Setup

And now finally to the setup. As always, I have set ABS up as a Nomad job, using a Ceph CSI RBD volume for storage, Consul connect service mesh for connectivity and Traefik as my proxy.

As ABS does not have any external dependencies, the setup was pretty straightforward. It also does not have many configuration options.

One very important thing to note: In all of the Docker examples, there are specific directories mounted for the audiobook and podcast libraries. Don’t get fooled by this. I did. I spend a considerable amount of time thinking about the fact that it has config options to define the metadata and config dirs, but not the audiobook and podcast library dirs. And tried to wrap my head around how to use a single Ceph CSI volume to supply two different top level directories in the container.

Of course, in hindsight, that was pretty stupid. Because the reason ABS doesn’t have config options for the audiobook and podcast libraries is…that you can freely choose the directory for your libraries when you create them. 🤦

Here’s my Nomad job file:

job "audiobookshelf" {
  datacenters = ["mine"]

  priority = 50

  constraint {
    attribute = "${node.class}"
    value     = "internal"
  }

  group "audiobookshelf" {

    network {
      mode = "bridge"
      port "health" {
        host_network = "local"
        to           = 80
      }
    }

    service {
      name = "audiobookshelf"
      port = 80

      connect {
        sidecar_service {}
      }

      tags = [
        "traefik.enable=true",
        "traefik.consulcatalog.connect=true",
        "traefik.http.routers.audiobookshelf.entrypoints=internal-entry",
        "traefik.http.routers.audiobookshelf.rule=Host(`audio.example.com`)",
      ]

      check {
        type     = "http"
        interval = "30s"
        path     = "/healthcheck"
        timeout  = "3s"
        port     = "health"
      }
    }

    volume "vol-audiobookshelf" {
      type            = "csi"
      source          = "vol-audiobookshelf"
      attachment_mode = "file-system"
      access_mode     = "single-node-writer"
    }

    task "audiobookshelf" {
      driver = "docker"

      config {
        image = "ghcr.io/advplyr/audiobookshelf:2.2.18"
      }

      volume_mount {
        volume      = "vol-audiobookshelf"
        destination = "/hn-data"
      }

      env {
        CONFIG_PATH = "/hn-data/config"
        METADATA_PATH = "/hn-data/metadata"
      }

      resources {
        cpu = 400
        memory = 400
        memory_max = 4096
      }
    }
  }
}

Nothing particularly notable in most of the config. The container listens on port 80 by default. I’m mounting the RBD volume under hn-data and set the metadata and config paths under that mount.

The volume was created via Nomad with this file:

# 0000-5555-fake-id
id = "vol-audiobookshelf"
name = "vol-audiobookshelf"
type = "csi"
plugin_id = "ceph-csi-rbd"
capacity_max = "100G"
capacity_min = "100G"

capability {
  access_mode     = "single-node-writer"
  attachment_mode = "file-system"
}

mount_options {
  fs_type = "ext4"
}

secrets {
  userID  = "mine-id"
  userKey = "really not that interesting"
}

context {
  clusterID = "useless-string-of-numbers-and-letters"
  pool = "homenet-base-bulk"
  imageFeatures = "layering,exclusive-lock,fast-diff,object-map"
}

I definitely need to finish my “Homelab setup” series and finally get to the Nomad and Consul parts, so I have something to link here to explain all the boilerplate.

The only notable thing here is the memory_max config, which grants the container up to 4 GB of memory. This memory max is where the Linux OOM killer gets active, while memory is what Nomad bases its scheduling on. Under normal operation, ABS runs fine with 400 MB. Although it does take as much memory as it can get. But when uploading an audiobook, the memory consumption jumps very high. I’m not sure why that is. During ingestion of e.g. Ken Follet’s Winter of the World, a 31h, 865 MB tome, the consumption jumps straight to 2.5 GB - and seems to stay there. I have not run into any problems with 4 GB yet.

No idea what ABS is doing there, really - they can’t just blindly load the file into memory, right? And even if - the book is just 800 MB.

That’s it for today. I think I like this format of not only describing my app setup, but also giving a little review.