Latest Posts (15 found)
Jason Tucker 2 weeks ago

Getting My iCloud Photos Onto My Own Storage with ImmiBridge

I've had Immich running on my Unraid server for a while now, mostly sitting there as a "someday I'll actually use this" project. The someday finally happened when I found ImmiBridge , a macOS app built specifically to pull photos out of Apple Photos and push them into a self hosted Immich instance. Apple has my entire photo library, decades of it, sitting in iCloud, a place I don't control (other than making sure the auto pay clears each month). I have my photos go to Google Photos and iCloud but I really want this stuff to also live on my servers as well. ImmiBridge closes that gap by copying everything down to a Mac and syncing it straight into Immich, which lives on hardware I control. Setup on the Immich side was nothing new if you've run it before. It's already deployed as a container on my Unraid box, backed by its own Postgres database and a library folder on the array. The only real prep was generating an API key from Immich's user settings so ImmiBridge had something to authenticate with. On the Mac side, ImmiBridge asks for Photos access, then lets you point it at a destination. You can export to a local folder organized by date, sync to an Immich server, or do both. I went straight for the Immich path since that's the whole point of the exercise. Then I just let it run, with the help of Amphetamine and MacOS App that keeps your mac awake. If you do any vibe coding projects there is a bunch of these apps out there, I've been using Amphetamine before any of this AI craze. My library is not small. Over the course of a weekend, ImmiBridge pulled and synced roughly 51,000 photos and videos from iCloud into Immich. That number includes Live Photos, which get exported as a paired still image and video rather than getting flattened into something lossy. It also handles duplicate detection using SHA1 checksums, so nothing gets uploaded twice if a sync gets interrupted and restarted. A few things stood out while I was watching it work. The Test Connection button didn't work for me, I kept getting errors due to not using TLS since I was just hitting the ip address of the server with port from my laptop to the Unraid server. Maybe an exclude TLS option would be nice? I spend about 10 mins trying various things before realizing I should just hit start and see what happens. I also ran into a connection issue with an early build against a specific server version, which turned out to be a version mismatch rather than anything wrong with my Immich setup. Worth checking your server version against what the app expects if a connection attempt just sits there. The one feature I keep wanting is a way to exclude screenshots from the sync. Fourteen years of iPhone screenshots is not something I need cluttering up a photo library that's supposed to be about actual photos. I posted the request in the project's community thread, so hopefully that lands in a future release. I'd also like to see if there is a way to exclude junk photos. I know it sounds weird but if you work in any trade you are going to have photos of ducts, above ceiling tiles and macro images of the top of a screw that you may or may not have stripped. I'm not sure how to fix that other than delete those images but I have a feeling at least 1000 of them are of work and household related stuff before a jaunt to Lowe's or Home Depot to buy a replacement. So far, no data loss, no corrupted files, and nothing that made me question moving forward with the rest of my library. If you're already running Immich and you're on a Mac, ImmiBridge is worth trying before you build your own export pipeline from scratch. Once thing you may notice in the screenshot is that I have a lot of skipped and errored images. I did some digging and since I have a lot of these images on the server already I'm mainly just filling in the blanks with the ones that were missed between my various backup sessions using different tools to do so. What are you using to do all this? I'd love to hear about it in the comments below.

0 views
Jason Tucker 2 weeks ago

What My Nest Hub Does Besides Show Family Photos

There's a Google Nest Hub sitting on a shelf under the living room TV, Google hasn't killed off this device (yet) so lets make it do more than show photos. Out of the box it's a photo frame with a clock on it, which is fine, but a screen that's already sitting there, already networked, already close to the action, felt like it should be doing more. It lives there muted, isn't really used for smart home stuff, it could be with one of the Home Assistant apps (docker containers) but I haven't explored that too much. I've seen plenty of home theater builds where people wire up a display that shows what's currently playing , the title, the poster art, how much longer it runs. I don't have a home theater. I have a living room. But I liked the idea anyway, mostly for one specific reason: when I start something on the Apple TV, I want to glance over and see when it's going to be done. If I pause it and start it back up later, I want that same glance to show me fresh info, not what was true an hour ago. The other thing I wanted from that same shelf display had nothing to do with video at all. Most of the cameras around the house already show up as HomeKit notifications in the corner of the Apple TV screen, so those are covered. The driveway PTZ camera is the odd one out. It catches a lot of things that aren't really notification-worthy, someone walking past on the sidewalk, the neighbor's cat cutting through the yard, kids messing around near the roses out front, so it's never wired into HomeKit alerts. I still wanted an easy way to glance at it without grabbing a phone, just not as an interruption on the TV itself. Also, our TV isn't on 24/7, we watch non linear tv so we don't have the news just running non stop or a tv show we dont care about. With the TV off you could glance at this screen on the shelf and see whats going on. Perfect. Two jobs, one screen, and neither one has anything to do with family photos or what this device was even designed for, I love stuff like this. When someone or something shows up on the driveway camera, the shelf display should switch over to a low-res feed for a minute or two, then go back to normal on its own. No notification, no phone, just a glance if I happen to be in the room. When something starts playing on the living room Apple TV, whether that's a movie through Moonfin, a show, or a YouTube video, the same display should flash a card: the title, the channel or show it belongs to, and a projected finish time. If I pause and resume, it should refresh with an updated time, not just repeat the same stale card from when I first hit play. Home Assistant already had entities for both halves of this. A media_player tracking the Apple TV over AirPlay, and a media_player for the Nest Hub through the Cast integration. On paper, wiring the driveway camera side up was trivial, one action that plays a camera stream to a cast target. The now-playing card looked just as simple. It wasn't. The driveway camera automation worked on the first try: The now-playing card did not. My first attempt looked just as reasonable, a media_player.play_media call pointed at a generated thumbnail: I wired that into an automation that triggered on the Apple TV's media_title changing, pulled the artwork, and cast it with a title and subtitle overlay. Home Assistant reported success every single time. The media player's state cycled through idle, buffering, playing. The title updated. Everything about the API said it worked. The Nest Hub's screen didn't change. At all. The obvious suspect was the image itself, maybe the thumbnail was broken or the fetch was failing silently. I checked. It wasn't. Next I suspected the Cast integration's URL validation, which turned out to be real but unrelated. Home Assistant's Cast component refuses to cast a media_content_id that resolves to its own host, whether that's a cloud URL or a plain LAN address: That fix is real and worth keeping on its own merits. Reference local files through media-source://media_source/local/ instead of building a URL by hand, and Home Assistant resolves it into a signed, cast-safe URL automatically. That killed the error message. It did not put anything on the screen. I kept narrowing it down. Real Jellyfin artwork over a plain HTTP URL, nothing. A photoMediaMetadata type instead of the generic one, nothing. The simplest possible test, a public image URL with zero moving parts: Home Assistant logged a clean success again. The Nest Hub sat there showing its ambient screensaver like nothing had happened. At that point there was only one conclusion left. This specific device does not render generic image casts, on this Home Assistant version, full stop. Every layer I'd been debugging, metadata types, signed URLs, content types, was real, but none of it was the actual problem. The driveway camera automation had been working the entire time, on the same Nest Hub, through the same Cast integration. I'd been treating it as a separate feature instead of a clue, because casting a camera stream feels conceptually different from casting a photo. It isn't, not to the device. A camera stream is video. That's the whole difference. This Nest Hub renders video casts fine and just does nothing visible with a plain image, no matter how it's addressed or labeled. Somewhere in the Cast "Default Media Receiver" on this device, static images and video streams take separate code paths, and only one of them is wired up to actually paint pixels. The fix, once I stopped fighting it, was almost funny. Stop casting an image, and cast a 30 second silent video of the image instead. I think 30 seconds is plenty of time to see whats playing, what the time it will be done is. ffmpeg was already sitting on the Home Assistant host, bundled in for camera and streaming support. That meant the whole now-playing card could be one generated clip: thumbnail on the left, channel name and title on the right, and a big, clearly labeled projected finish time pinned to the bottom. Cast that as video/mp4, and the Nest Hub showed it immediately. media_duration finally reported a real number instead of 0. This device I suppose only likes video casted to it, so we did just that. The text gets written to files first, then read back with drawtext's textfile option, instead of sitting inline in the filter graph. Real titles contain colons, quotes, and apostrophes that would otherwise collide with ffmpeg's own filter syntax. That one decision avoided a whole second debugging session. The thumbnail-left, text-right layout worked fine for YouTube, where the source thumbnail is already roughly square. It looked wrong for anything coming through Moonfin, though. Movie and show posters are portrait, and cropping one down into a small square just showed an oddly zoomed sliver of the art. Jellyfin and Moonfin content gets a different treatment now: a full-bleed backdrop image filling the whole frame, with the movie or show's logo graphic centered near the bottom over a dark scrim. When an item doesn't have a logo in Jellyfin, which happens more than you'd expect, the title gets drawn as bold text in that same spot instead. Picking the right artwork took its own fallback chain. Episodes rarely carry their own backdrop or logo, so the automation walks up to the season, then the series, until it finds one. Movies have neither a parent nor a series to fall back to, so they just use their own artwork directly. Deciding whether an item qualifies for this treatment at all turned into the hardest bug of the whole project. Even once the hero layout worked, it only showed up sometimes. The automation would flip between the backdrop treatment and the plain classic layout across nearly identical Jellyfin sessions, with no obvious pattern and nothing useful in the logs. This is the kind of bug that eats a weekend for a reason that has nothing to do with ffmpeg or Home Assistant at all. The automation decides which layout to use with one boolean: An earlier version of that line just chained the three checks together with a plain "and." That seemed fine, but "and" in this language doesn't actually hand back true or false, it hands back whichever value made the decision. In this case, that was a Jellyfin ID, not a clean yes or no. Most of the time that didn't matter, since the ID still counted as "something" wherever it showed up. But one specific check in Home Assistant is pickier about what counts as true, and an ID string doesn't qualify. So the same value passed one check and quietly failed another a few lines later. Forcing it into a real true or false fixed it for good. That's what "sometimes works, sometimes doesn't" really was: one value being read two different ways by two parts of the automation that never agreed on what counted as true. shell_command doesn't run through a real shell. I assumed Home Assistant's shell_command integration passed commands through /bin/sh, the way you'd expect. It doesn't. For templated commands it execs the rendered, split argument list directly. A plain redirect using the greater-than sign silently did nothing, no error, no file, no clue. I stopped relying on shell syntax entirely and wrote text out from a oneliner Python instead: A family member was watch a show on Jellyfin at their house and it threw this code off a bit. It was reporting the show they were watching not the show I was watching and their AppleTV was named "Apple TV" as well. Two Jellyfin clients were both named "Apple TV" and both running Moonfin for tvOS, one on the LAN, one connecting in over Tailscale from somewhere else. The only field that told them apart was ipAddress. Pinning the lookup to this Apple TV's reserved address fixed it for good: Once there were three visual layouts to choose between, I ran into a wall: these shell command templates don't support if/else logic, just simple substitution. So instead of one command that branches internally, there are three separate commands now, one per layout, and the automation picks which one to call. ffmpeg also choked when I fed it two images at once for the hero layout, a backdrop and a logo composited together. It would just hang. The fix was to flatten the images into a single frame first, then loop that one frame into the finished clip as a separate step. A fresh video starting actually trips two triggers at once. Without a small delay on both, one would fire and finish before the other had even started, so the second run would cast right on top of the first. Giving both triggers the same five-second wait fixed it. And a couple smaller ones: filenames need to be unique per run, since a superseded automation run keeps writing files in the background even after being canceled, and those files could clobber a newer run's output. Tagging every filename with the run's own ID took care of it. Both automations target the same media_player, so they need to stay out of each other's way. The now-playing automation checks that nobody's currently at the driveway before it takes over the display, so a movie card can't interrupt a driveway alert that's already showing: The driveway automation is the simpler of the two, since a camera stream is already video and never ran into the image problem at all: The now-playing automation is longer, mostly because it tries to figure out a good title, channel, and thumbnail no matter what's playing, Jellyfin through Moonfin, a YouTube video, or anything else the Apple TV reports. The load bearing part is really just three steps buried inside it: write the channel, title, and finish time out to text files, composite them into a video with ffmpeg, then cast that video with play_media. Everything else, the Jellyfin session lookup, the YouTube thumbnail fallback, is optional flourish on top of that core trick. A few notes if you're adapting either of these. The IP addresses and device ID are specific to this household and need to change for anyone reusing them. The two DejaVuSans font paths point at fonts bundled with other, unrelated HACS integrations, since there's no dedicated system font directory on this Home Assistant OS install. It works, but it's not something I'd call the right way to do it, just what was already sitting on the disk. Start something through Moonfin and the shelf display switches to the full backdrop treatment. When the movie or show has a logo, it sits centered near the bottom over the artwork. Without one, the title takes its place instead. YouTube and anything else keeps the original layout, thumbnail on the left, channel and title on the right. Either way, the same "Ends @ 1:47PM" line sits at the bottom, and it's gone again after about thirty seconds, cleaning up its own temp files as it goes. Pause the video and start it again later, and the same trigger fires again with a fresh finish time, since the automation reads the Apple TV's current position and duration each time rather than remembering the first calculation. Walk up the driveway while something's playing, and the driveway feed wins instead, then hands the screen back once things are quiet. None of the individual pieces here are exotic. ffmpeg's drawtext filter, media-source:// URIs, a REST session lookup, are all fairly ordinary Home Assistant tools. What made the difference was realizing this device already had a working video cast sitting right next to the broken image cast, and that the two behaving differently was the whole answer. A photo frame turned out to be a fine screen for showing more than photos, once I stopped asking it to show a photo. We sometimes build things just to see if we can, sometimes we build things because its fun. Sure some of this is redundant such as the end time for a Jellyfin video but YouTube doesnt display this so why not. Have you built anything like this? I'd love to hear it in the comments below! Make a quick free acocunt here and leave a comment. It keeps the spammers out and lets me know people care about my adventures in technology.

0 views
Jason Tucker 1 months ago

Exploring the world of Proxmox and installing Home Assistant OS on it

For the last few years I've been running Unraid on my Dell server for all of my media storage. There is a lot in there, immich photos, tv movie "linux isos" and a lot of docker containers for the various acquisition and management tools needed to move all the data around on the server and organize it. All of which has been Docker based with no VMs going on. I started to look into Proxmox for the desktop "server" that I have Home Assistant running on. I had HA running on HAOS and as Home Assistant writes about it: This isn't a tutorial on how to install Proxmox VE, there are other places to find such things Home Assistant: Proxmox VE Quick Start Guide is a great place to start. I'm just going to give you an overview of what I did mainly so I can read this again in a few months when I may end up redoing everything, who knows! So I wanted to try my hand at Proxmox VE and moving over Home Assistant to this platform didn't look to be too hard. I started off by doing a fresh backup of my HAOS install and then saving the encryption key by downloading the emergency kit and storing it somewhere safe. Home Assistant backups are encrypted so you want to make sure you have that key somewhere safe. After that I downloaded the ISO for Proxmox VE 9.2.6 and put it on a usb drive. My method of choice for this is putting Ventoy on a usb with a bunch of isos on it and storing it on top of my server in case I ever need to repair it I at least have a place to start. I put the drive in and went into the BIOS to make sure all of the virtualization options ( Intel VT-d or AMD IOMMU ) were enabled and then booted the usb and started installing Proxmox VE. This being my first time in Proxmox VE, but not my first time in a hypervisor, I knew that I was going to have to do a lot of reading and learning. I started out by watching WunderTech's Proxmox VE video to get myself up to speed. One takeaway I had was getting my backups and snapshots in order and setting up a NFS share on my Unraid server so Proxmox VE could see it. I then watched one of my favorite creators on YouTube, Techno Tim and his video on Proxmox VE as well. One thing I've learned is there are 2 camps when it comes to Proxmox, the folks that use it in homelab and set up the Proxmox Post Install Script and then the enterprise folks that just pay the fee to have the support they need. Me in this instance I'm the former so we loaded up SSH and ran the script and pressed Y a bunch of times to get us to the end. The write up on Derek's blog is quite helpful in using the helper scripts to get Home Assistant up and running. Once I was done with that I restored from backup and had a working Proxmox VE with Home Assistant on it. On the surface Proxmox VE is a great platform and simple to set up. I know it's quite deep when you get into the weeds. I'm going to next be exploring Home Assistant Backup vs Proxmox Backup reading this post on just that How To Backup Home Assistant In Proxmox so I can start using Snapshots and Backups and what the difference is between the two in Proxmox VE. One thing I'm really looking forward to is messing around with the recent Quartermaster integration with Proxmox VE which is more and more becoming an amazing Homelab remote control in your pocket. Here are the command line steps I did to get Quartermaster set up in Proxmox VE. If you ever need to redo this (e.g. lost the secret), you'd delete and recreate the token: pveum user token remove root@pam quartermaster pveum user token add root@pam quartermaster --comment 'Quartermaster client' Here is what it looks like in Quartermaster when it's all done. In the comments, what are you doing with Proxmox? What should I do next? Remember, you can leave a comment just by signing up for free for my blog, not obligated to pay for anything. Created the API token (secret is only ever shown in this output) pveum user token add root@pam quartermaster --comment 'Quartermaster client' Granted PVEAuditor + PVEVMUser on path / with propagate enabled pveum acl modify / --tokens 'root@pam!quartermaster' --roles PVEAuditor,PVEVMUser --propagate 1 Verified the ACL entries pveum acl list | grep quartermaster

0 views
Jason Tucker 2 months ago

Quartermaster: A Homelab Remote That Doesn't Pick a Side

Most homelab remote apps pick a lane. You get one thing for Plex, another for the *arr stack, and a third for the servers underneath. Quartermaster, built by UK developer Lewis Egley , tries to cover all three from a single native iOS app. I have a bunch of mobile apps that have tried to do this and none come close to what Lewis has done. What is a Quartermaster? A quartermaster is an official responsible for managing and distributing supplies and provisions (such as food, uniforms, and equipment) in the army, or for handling navigation and steering duties aboard a ship. Think of Q in James Bond but for this its how you are going to interact with your homelab via it's new Quartermaster. The pitch is currently thirty services split into two halves. One half is entertainment. The other is what the app calls the "Command Centre", everything that keeps the entertainment half running. The home screen leads with an Up Next row for the week ahead, then Recently Added underneath. It reads as a single calm screen instead of a wall of separate widgets, which is usually where these apps end up once you've connected more than two or three services. Radarr and Sonarr get full management inside the app: browse, search, add, edit, monitor. Lidarr gets a lighter touch, connection and live status rather than editing. My library runs all three, and the Radarr and Sonarr depth is the part I actually use daily. Movies and show detail screens pull ratings from multiple sources into one line. There's a taste match score too, computed on the device from your own library and request history rather than pulled from somewhere else. Quality and file size sit next to the monitoring toggle, so a request doesn't need a separate screen. Download clients get proper treatment as well. SABnzbd gets a full queue and history. NZBGet, qBittorrent, Transmission and Deluge show live status, with Transmission and Deluge going further into queue management: adding, pausing and removing torrents directly from the phone. Bazarr surfaces a wanted list and lets you grab subtitles without opening a browser tab. Prowlarr and NZBHydra2 get connection and live status, which covers what most people actually need from an indexer manager on a phone screen. For media servers, Plex, Jellyfin and Emby all get sessions, now playing, and recently added. Tautulli goes further, with full activity history and the ability to terminate a session remotely. That last part matters more than it sounds. Someone transcoding a 4K file from three states away over hotel wifi is exactly the kind of thing you want to kill from your phone, not from a laptop you don't have open. I run Jellyfin now, not Plex, so Tautulli isn't part of my stack anymore. What I'd actually want in its place is Streamystats support. If you're running Jellyfin, that's the equivalent tool, and right now it's missing from the list. Reading and photo libraries round out this half. Komga and Kavita cover comics and ebooks. Immich covers photos, including job status and storage. Audiobookshelf shows library and listening progress. Let's embrace the British spelling here. This is the half that actually separates Quartermaster from a typical media remote. Flip over and you get one screen that flags what needs attention: a stopped container, a degraded tunnel, downloads sitting blocked. Everything else stays quiet until it isn't. Portainer gets full container management, live stats plus start, stop and restart. Unraid gets array status, per disk temperature, and Docker and VM controls from the same screen. A disk running hot shows up here before you'd notice any other way, and the destructive actions sit behind a confirm step. Network gets its own section. AdGuard Home and Pi-hole both get blocking stats and a pause toggle, useful for the ten seconds a smart TV app refuses to load without ads turned back on. Cloudflare covers tunnels, security and analytics, which lines up with running cloudflared alongside Tailscale rather than instead of it. Tailscale itself isn't on the list yet. Glances covers general system stats: CPU, memory, disks, temperatures. Tdarr shows the transcode queue as a read only monitor, nothing more. Home Assistant is the one that surprised me. Lights, locks, climate and scenes sit in the same app you'd use to check whether Sonarr grabbed last night's episode. It might be more than some people want in one place, but it's a wild thing to see working. The part worth calling out is how notifications work. There's no company server sitting between your services and your phone. While the app is open, alerts fire the moment something finishes or fails. When the app is closed, the alert has to come from somewhere you already control: a self-hosted ntfy instance, or Pushover, Discord, Telegram. Your Sonarr instance pushes straight to your own ntfy server, and that pushes to your phone. Nothing about a finished download passes through infrastructure Quartermaster runs. Background checks for slower things, like storage filling up, run when iOS allows it. The app is upfront that this isn't a guarantee, which is a fair way to describe how background execution actually behaves on iOS rather than promising something the platform can't reliably deliver. Here's the full list of thirty connected services, grouped the way the app groups them: The app's website puts it plainly in the footer: a native iOS controller for your whole self-hosted stack, thirty services across two worlds, from your media to the homelab that runs it. Pure client. Credentials never leave your device. Built in the UK, made by one developer. What that means in practice is you do the work. You go collect your own API keys from each service and enter them into the app yourself. Nothing is scraped or auto-discovered. If a key changes on your end, fixing it is on you, not the app. Quartermaster also supports local-only access, which matters if you've got something like an iPad that never leaves the house. Point it at a plain 10.0.0.x or 192.168.0.x address and you're done. If you want more, Tailscale, Cloudflare, and header-based auth bypasses are all fair game too. But it also supports remote as well with Tailscale, Cloudflare, and header-based auth all ready to go to make a secure connection regardless of what ways you've set things up for yourself. The app isn't opinionated about which of those you pick. It doesn't push you toward a specific setup, it just works with whatever you already have. And there's no server owned by the developer sitting in the middle. It's your device talking directly to your services. Free covers one service, fully. Pro unlocks everything on the features page for 14.99 once, or 3.99 a month if you'd rather pay as you go. With this many integrations on offer, that's not a hard number to justify once more than one or two things on the list are part of your actual stack. If your homelab is just a media server and a couple of *arr containers, Quartermaster is more app than you need. It earns its place once the stack grows sideways: Unraid or Proxmox underneath, AdGuard or Pi-hole for DNS, Home Assistant tying the house into the same network. My stack leans harder into Jellyfin than Plex these days, and Quartermaster treats both as first class citizens, sessions and continue watching either way. Readarr and Tailscale aren't on the list yet, so those stay outside the app for me. Streamystats support would close the gap Tautulli leaves behind for Jellyfin users like me. I have a few services that are exposed to the internet via Cloudflare Tunnel but all of my services are behind Tailscale. Personally anything I want to access outside of Cloudflare I'd use Tailscale for. It's my vpn of choice for this sort of thing. Unraid exposes all of my docker services that I use as hostname nodes in my tailnet so I'm just using those for my remote access. Local access I'm using my local hostnames and IP addresses and Quartermaster also detects what wifi you are on so it can switch gears with out you stressing. There is a lot to be said about this app and how far it as come in the last 2 weeks of development. I've been using the test flight of the app and so many new features, services and integrations have been added to the app. There are also a bunch of great features coming down the pipeline in the next few updates that are going to make this app even better. (Search for missing episodes, poster states when items are being acquired, missing or not aired yet). This is one of those apps the dev actually uses himself, and it shows. Unraid support has been rock solid, being able to see everything in one app has been great. I've been using an app called Unraid Deck and wow it almost rivals that and this is just some feature added to a big app. The Lewis has added so much support for various services including request support in Seerr , and approvals of those requests. Shelfmark support has been added. It's wild that they have added Home Assistant support in the app which isn't something I'd have much use for since I use the official app but its a nice touch. Beszel support allows for the app the see whats going on with your servers that are running Beszel agent, it's great! Being able to see the health of your Cloudflare tunnel has been handy. I'd imagine that Tailscale support will be added as well since, why not. Using Needledrop for my music requests has been great. And my number one request for Streamystats to be added was and works smashingly well with much more coming with it such as Overview, Activity, Content lots of Deep Stats with watch hours and new playing row. I'm stoked about this one. I'll circle back around in a few weeks and update you all on how this app is evolving. I wrote a recent blog post about Proxmox VE and mentioned that Quartermaster workes really well with it. If you havent tried it yet its quite nice, between that and Unraid it looks like its supporting some great Homelab friendly services. Their website: The iOS App: Movies, TV & Music: Radarr, Sonarr, Lidarr Downloads: SABnzbd, NZBGet, qBittorrent, Transmission, Deluge Requests: Jellyseerr / Overseerr / Seerr, MusicSeerr Streaming: Jellyfin, Emby, Plex Books & Audiobooks: Komga, Kavita, Audiobookshelf Photos: Immich Smart Home: Home Assistant Activity & Stats: Tautulli Indexers & Subtitles: Prowlarr, Bazarr, NZBHydra2 Network: AdGuard Home, Pi-hole Servers & Docker: Glances, Portainer, Unraid, Proxmox VE Media Processing: Tdarr More: Cloudflare

0 views
Jason Tucker 2 months ago

Callsheet: The Companion App for Jellyfin (and Plex, If You Must)

I saw a post from Casey Liss about Callsheet a couple days ago and asked him a simple question on Mastodon. Does it support Jellyfin yet? I already knew it worked with Plex and have been using it since it came out many years back. Casey wrote back almost immediately. It's already there. What I didn't know is that Jellyfin support had shipped back in May. It looks like Casey is also moving way from Plex, that benefits us all as it relates to this app. I was already using the Plex feature in it and didn't realize that Jellyfin was added. If you haven't run into Callsheet before, it's a cast and crew lookup app for iOS, iPadOS, macOS, and visionOS, built by Casey. You probably know him from the Accidental Tech Podcast or his other show, Analog(ue) , with Mike Hurley. He built Callsheet as his own answer to a question a lot of us have had while watching something. Who is that actor, and what else have they been in? IMDb technically answers that question. It also makes you fight through ads and clutter to get there. Callsheet skips all of that and pulls its data from TMDB instead. That alone would make it a nice utility app. The part I care about is what Callsheet does once you're actually watching something, and which server it's watching along with you. None of that is new on its own. Trakt has done some version of "what am I currently watching" for years, mostly built around scrobbling from various clients. What's different here is that Callsheet ties that playback position directly to its cast and crew database. You're not just seeing a progress bar. You can jump straight from "I'm 20 minutes into this episode" to "who plays that character" without leaving the app. The per-title progress meter is newer than I realized. Casey added it in version 2026.5, released June 20, 2026, along with the "Ends At" popover that breaks down runtime versus what you've already watched. Before that, Now Playing showed what you were watching, just not how far into it you were. How the Jellyfin integration with Now Playing in Callsheet. Callsheet has supported Plex since version 2023.4, back in September 2023, alongside a similar integration for the Channels app. For years there was only one way to connect, and Casey has been pretty open that it was never great. He's described that original mechanism as unreliable to the point that he regretted including it at all. That original approach is now called Passive mode. It talks directly to Plex clients over the local network, no login required, and it can even show what's playing on someone else's Plex server if you're on the same network as their client. The tradeoff is that it barely works with iOS and iPadOS clients, and it only works at all if you're on the same network as the playback. In 2026.5 , Casey added a second option called Active mode. It requires logging into Plex, similar to Jellyfin's Quick Connect, but in exchange it works reliably, works with iOS and iPadOS clients, and works even when you're away from home. The catch is it only shows media from your own server, not anyone else's. Casey was candid about why he built this. He's been migrating his own media setup toward Jellyfin because Plex keeps getting worse, and once he'd already broken his own no-login rule to support Jellyfin in 2026.4, adding a login option for Plex was the obvious next step. Version 2026.4 , released in May 2026, added proper Jellyfin support to the Now Playing view. Setting it up takes a few steps: Once that's done, the connection sticks around. You don't have to repeat the dance every time you open the app. It's a little embarrassing that I went two months without noticing this had shipped. But now that I've had it running for even a few days, it's held up without a single hiccup. No missed progress updates, no stale sessions, nothing to troubleshoot. There's a detail buried in the release notes that's worth calling out on its own. If you point Callsheet at a Tailscale address for your Jellyfin server, whether that's your 100.x address or a home-domain.ts.net hostname, the Jellyfin integration is the only one of the three (Plex, Channels, and Jellyfin) that actually works remotely. Plex and Channels both need Callsheet to be on the same network as whatever's doing the playback. Jellyfin doesn't have that restriction. That matches how I use Tailscale everywhere else in my setup. If I'm out of the house watching something on my phone through my Jellyfin client, Callsheet can still show me progress and cast information without me being on my home network. Authorizing the Jellyfin Integration Not entirely, and I don't think that's really the goal. Trakt tracks watch history over time, builds stats, and has a whole social layer around what you and your friends are watching. Callsheet doesn't do any of that. What it does is answer the question you have right now, mid-episode, about who's on screen and how much time is left before you need to decide whether to keep going. For me, that's actually the more useful tool most of the time. I don't check Trakt while I'm watching something. I check it after, if at all. Callsheet is the app I reach for in the moment, and now that moment includes Jellyfin instead of just Plex. I've been slowly replacing pieces of my Plex-dependent workflow for a while now, and this is one more piece that no longer needs Plex to function. Casey mentioned in that same release he's been running Jellyfin alongside Plex on his own Mac mini, I'm curious how many other developers in this space are quietly making the same switch behind the scenes and what new Jellyfin introductions are out there for apps I'm already using. Time to dig more into what I'm using already to see what I can find. In Callsheet, go to Settings, then Persnickety Preferences, then Integrations. Find the Jellyfin section and enter your server URL. Callsheet generates a Quick Connect code. Open your Jellyfin server's admin dashboard and approve that code.

0 views
Jason Tucker 2 months ago

From Plex to Jellyfin Part 7: Watching Habits—Tautulli to Streamystats

When I was running Plex, Tautulli sat quietly in the background doing one thing I actually cared about: telling me what I'd watched and how much time I'd spent on it. I had no use for its notification system or custom script execution. I just wanted the numbers. The graphs. The "oh, I watched 40 hours of this show last month" kind of accountability. Tautulli was good at that. You'd log in, see your homepage stats, filter by time range, check your user page, and walk away knowing exactly which shows were eating your free time. Then you migrate to Jellyfin. Tautulli stops working because it only speaks Plex. Jellyfin doesn't have a built-in equivalent. You're suddenly flying blind. Enter Streamystats . Tautulli and Streamystats both answer the same basic question: what have I been watching? But they arrive at the answer through completely different lenses. Tautulli is a Python-based monitoring tool that tracks what was watched, who watched it, when and where, with detailed statistics about each library and user. The interface is functional, organized by discrete views (Users, Libraries, History, Graphs). Everything is there. Nothing is flashy. Streamystats is built with modern frameworks and includes a dashboard with overview statistics, live sessions, recommendations, and an interactive AI chat interface. The data architecture is newer. The UI feels more intentional. You're not clicking through separate tabs to find what you want; you're asking the dashboard what you want to know. Here's the practical difference: Tautulli made you navigate. Streamystats makes you browse. The rows that matter depend on what you actually use. I skipped notifications and scripting entirely. The loss is immaterial to me. I gained better graphs, a nicer interface, and a completely new capability I didn't have before: AI-driven insights. Tautulli's homepage gave you four key boxes: your top items this week, your top users, recent activity, your watch time by duration. Clean. Minimal. You look, you go. Streamystats' dashboard is denser. It defaults to showing you live sessions (who's watching right now), recent activity, trending content, and recommendations. Scroll down and you get genre breakdowns, watch time graphs, and completion rates. It's a different philosophy. Instead of answering "what was I doing," it's showing you "what's happening now and what should you notice." The real shift comes in the user pages. Your own user page in Streamystats shows your personal watch history with filters (date range, library, media type), time spent watching by date, watch frequency, top items, and now, heatmaps showing when you watch and how much. You can drill into any show and see your episode progression. You can see which users share your watching taste. That last one didn't exist in Tautulli. Not in any meaningful way. Streamystats added four things that fundamentally change how you interact with your library. Watchlists. You can create custom lists in the interface. Mark items as watched or unwatched. Set them public or private. Flag them as "promoted" so they show up as sections in your Jellyfin clients. This wasn't Tautulli's job. This was Radarr/Sonarr territory. Streamystats treats it as a first-class feature. This isnt something that I use since I have other tools doing this but its interesting. Semantic search and AI chat. If you enable OpenAI-compatible API access (OpenAl,Together Al, Fireworks Al, Voyage Al, OpenRouter, Ollama (Local), LM Studio (Local), LocalAl (Local), Custom) Streamystats can embed your library and answer questions about it. "Show me all the sci-fi I've started but haven't finished." "What should I watch based on what I liked last month?" The backend uses vectorchord to store embeddings and performs fuzzy similarity search. It's genuinely useful if you have a large library and a fuzzy memory of what you own. I've messed with this a bit. Lots of graphs and charts. See visually when you and your household watch. Monday night at 8pm? There's the spike. Thursday afternoon when the kids are home? Another pattern. Saturday morning movie binges? It's all there in a grid. Year in Review. Streamystats generates an annual recap of your watching habits. Total hours watched, top shows, top movies, most-watched genres, viewing trends across the year. It's Spotify Wrapped for your media server. You get a moment at the end of the year where you actually see what you spent your time on, quantified and visualized. Tautulli never bothered with this. It was data for data's sake. Streamystats understands that sometimes you want to feel something about your numbers. These weren't problems Tautulli solved. They're problems you didn't know you had until the tool offered an answer. Streamystats runs via Docker with a setup wizard that walks you through connecting your Jellyfin server, and the first load can take a while depending on library size. I used the Unraid template " Streamystats—AIO, " which bundles the frontend and backend together. One template, one configuration, done. The Docker Compose setup is straightforward. Change your SESSION_SECRET (generate one with openssl rand -hex 64). Point it at your Jellyfin server. Wait for the initial scan to complete. That's it. The first load took about fifteen minutes with my library size. After that, it runs quiet. The database grows as you watch, but there's no plugin to manage on the Jellyfin side. Streamystats relies solely on the Jellyfin API for statistics, so the playback reporting plugin is no longer needed. Part 6 covered how Moonfin 's native collections support and Moonbase's row reordering let you build a curated home screen without relying on the Home Screen Sections plugin. Streamystats extends that toolkit, but differently depending on which client you use. If you're on Moonfin, Streamystats isn't directly part of your home screen setup. Instead, you use the Seerr integration, which lives in Moonfin's Integrations menu. From there, you enable and reorder Discovery Rows: Recent Requests, Recently Added, Trending, Popular Movies. These rows pull from your Seerr instance and surface directly on your home screen alongside your automated collections. No Streamystats watchlists involved. This is how you surface curated request activity without a separate collection build job. If you're on Streamyfin , promoted watchlists work differently. When you create a watchlist in Streamystats and flag it as promoted, those watchlists become available as sections in Streamyfin. You're not just tracking what you've watched; you're creating rows that show up on your phone or tablet. You curate them directly in Streamystats. Mark items as watched or unwatched. Reorder them. Share them across your household. The promoted ones automatically surface alongside your automated collections. Both approaches serve the same purpose: surfacing specific curated or request-driven content on your home screen without relying on collection build automation. Your Year in Review data stays in Streamystats, but your best-of-the-year watchlist (if you're on Streamyfin) can live as a row on your home screen all year long. For Moonfin users, Seerr's Recent Requests row does something similar, showing what your household has been requesting to watch. Tautulli was observability. It told you what happened. Streamystats is observability with personality. It tells you what happened, suggests what matters, answers the question you were about to ask, and occasionally reminds you of the year you just had. For me, that's the difference. Tautulli was a tool I opened occasionally to satisfy curiosity. Streamystats is something I check regularly because it keeps showing me things I didn't know I wanted to know. The Year in Review sits somewhere in between. It's not essential infrastructure. It's just fun for end of the year reflection. Your household watching patterns are more interesting than they seem at first glance. Streamystats makes those patterns visible and occasionally celebratory in a way Tautulli never quite managed. That's not revolutionary. It's just better. What do you think? Make a free account here and leave a comment below!

0 views
Jason Tucker 2 months ago

From Plex to Jellyfin, Part 6: Rebuilding the Home Screen with Posterizarr, Maintainerr, and Moonfin

I told you in Part 3 that Moonfin and its companion server plugin Moonbase hit all the major criteria I was looking for in a client. What I didn't get into yet is the bigger reason any of this mattered to me in the first place: replicating what my Plex home screen actually did, not just what it looked like. For a long time, that home screen looked the way it did because of one tool: Aggregarr . It built collections from curated lists, generated custom artwork for those collections, and arranged the results into rows on my home screen. One tool, three jobs, done. Jellyfin doesn't have an Aggregarr. It has roughly six tools that each do a third of the job, and which six you actually need depends on which app your household watches through. If you're on stock Jellyfin Web instead of Moonfin, some of this still applies, some of it doesn't, and I'll call out which is which as we go. Most Jellyfin plugin roundups quietly assume you're using jellyfin-web in a browser. That's a reasonable assumption most of the time, except it means half the advice out there doesn't transfer if you're watching through a native client like Moonfin, Swiftfin, or the official Android TV app. Here's the architecture split that actually matters, and it took me longer to untangle than I expected: Plugins that inject into jellyfin-web. Home Screen Sections (HSS) and its companion plugins, Editor's Choice and Media Bar , fall into this bucket. I went and found the developer's own introduction post for HSS , and he says it plainly in the preamble: it only works on web-based clients, meaning a browser, the official iOS app, and the official Android app, since those three are built as wrappers around the web client. Native apps are explicitly excluded, and he names Swiftfin specifically as an example of what doesn't work. Moonfin, being a separate native Flutter build rather than a web wrapper, falls into that same excluded category. Features built natively into a client. Moonfin has its own collections support baked directly into the app now, reading straight from Jellyfin's API rather than depending on any plugin. It goes further than that: Moonfin markets itself as one of the first native Jellyfin clients with built-in Seerr support, which means Trending, Popular, Upcoming, and genre/studio/network rows can come straight from your Seerr instance with zero plugins involved. The Moonbase server plugin proxies that traffic directly, so Seerr doesn't even need to be reachable from the client itself. This is a recent addition, and it changes the calculus on a few things I'll get to below. Aggregarr's first job was building collections automatically from curated sources rather than making you do it by hand. Jellyfin has two real tools here, and they solve slightly different problems, but check what you already have before installing either one. If you're running Seerr alongside Moonbase, Moonfin's native Discovery Rows already pull Trending, Popular, Upcoming, and genre/studio/network groupings straight from Seerr, no plugin, no extra container, nothing to configure beyond having Seerr itself set up. That covers a real chunk of what Aggregarr's curated lists were doing on Plex. What it doesn't cover is anything from sources Seerr doesn't surface, Letterboxd, MDBList, AniList, and anything built from rules against your own existing library rather than discovery data. That's where the two tools below actually earn their place. jellyfin-plugin-auto-collections groups content you already own using rules: genre, decade, studio, that kind of thing. It's a native Jellyfin plugin, no separate container to run, and it has real maturity behind it: 226+ stars, steady releases going back to at least May 2025, with version bumps tracking each new Jellyfin server release. There's even a community repo of shared configuration templates if you don't want to write rules from scratch. JFC (jellyfin-collection) picks up where native Seerr discovery rows leave off: pulling curated lists from sources like Letterboxd, MDBList, and AniList, and optionally requesting whatever's missing through Sonarr or Radarr automatically. It reads Kometa-style YAML configuration, so if you happen to have old Plex Meta Manager files lying around, it'll reuse them. The catch is that it's a much younger project with no comparable track record yet. Back before I use Aggregarr I was using but Kometa itself does not support Jellyfin . The official Kometa-Team GitHub repo still describes itself as working with Plex, and the Jellyfin/Emby feature request on their tracker was closed back in 2021 without being merged. They changed request trackers and it shows as "planned" with no real movement that I can see. JFC is a separate, newer tool that happens to read Kometa's YAML format. That's a meaningful difference if you're trying to figure out which project to actually go install. My approach, and what I'd suggest if you're starting from scratch: confirm Discovery Rows are pulling real content first, since that's zero-install validation. Install auto-collections next, purely to confirm a plugin-built collection actually renders correctly as a Moonfin home row. Once that's confirmed, JFC is the one to reach for, but only for the specific curated-list and auto-request behavior nothing else in this stack already covers. Not everything from the Plex side of my stack needed a Jellyfin equivalent. Maintainerr is the one tool I carried over wholesale, because it already supports Jellyfin natively, not as an afterthought bolted on later, but as a first-class target alongside Plex and Emby. If you haven't run across it, Maintainerr is the inverse of a request tool like Seerr: instead of helping people add things to your library, it watches for content that's gone stale, unwatched for X days, never requested, whatever rule you define, and groups it into a collection before eventually deleting it, unmonitoring it in Radarr or Sonarr, and clearing the original request out of Seerr. It's the tool responsible for the "leaving soon" shelf a lot of self-hosted libraries have, and it's how I keep my library from quietly filling up with things nobody's touched since 2023. It's worth a mention here specifically because of how it ties into everything above. Aggregarr's own changelog even references it directly, tagging items that fall out of an Aggregarr-managed collection with an "agregarr-stale" label specifically to play nicely with Maintainerr picking them up. The two tools were already working together on the Plex side of my setup, and that relationship carries over untouched now that the library they're both pointed at is Jellyfin instead. My migration brought over the bulk of my existing rules directly, with a small handful that depended on watchlist data needing a rebuild against Streamystats instead, since that's the Jellyfin-side equivalent Maintainerr expects for that kind of rule. If anything, Maintainerr is the proof that not every part of this migration is a teardown and rebuild. Some of it is just pointing the same tool at a different server and moving on. One trick I use Maintainerr for is when I forget to watch something that is on the chopping block is I make a rule that builds a collection called "🪵 🪓On the chopping block" and then include that collection on the home screen so I can remember to watch something before it gets auto deleted by maintainerr. Aggregarr's second job was generating custom artwork for everything it built. The closest equivalent here is Posterizarr , and it's a genuinely capable tool: pulls textless artwork from Fanart.tv, TMDB, TVDB, and IMDb, applies your own overlay styles, and writes everything into a Kometa-compatible folder structure so it'll travel with you if you ever change tools again. It makes your library look fancy, take a minute and set it up and it will do some cool stuff for you. This is the section that's actually specific to running Moonfin, and it's the most useful thing I found in this whole process. Since HSS-family plugins are web-client-only by design, per the developer's own statement, a Moonfin household gets nothing from installing Editor's Choice, Media Bar, or the standalone Collection Sections plugin. None of them render outside jellyfin-web. Here's where it gets interesting, and it came from two separate places, not one. I opened a GitHub issue asking specifically about Collection Sections support , and a contributor's answer was direct: "We already have Collection home row support. You need to turn on the Collections toggle." Not "we integrate with that plugin," but "we built our own version of what that plugin does." Separately, in Moonfin's Discord, another contributor confirmed the bigger picture: as more native implementations land inside Moonfin, HSS support is likely to get removed entirely, because supporting an external plugin's output long-term is hard to maintain. The goal, in their words, is to keep HSS support around until native versions exist for whatever features people are actually using, not to rip it out on a fixed deadline. The practical upshot for anyone running Moonfin: skip Collection Sections. Go straight to Home Row Toggles in Moonfin's settings and enable Collections there instead. It already shows your collections as home rows natively, reading from the server directly, with a future the plugin doesn't have. I tested this on Apple TV specifically too, since the tvOS app only recently moved onto Moonfin's shared codebase, and it's working there as of this writing. There's a second native row type worth setting up alongside Collections, and it has nothing to do with HSS at all: Discovery Rows, fed straight from Seerr through the Moonbase plugin. Moonfin's own marketing leans on this hard, calling itself one of the first native Jellyfin clients with built-in Seerr support, and the row list reads like an Aggregarr feature checklist: Trending, Popular, Upcoming, My Requests, with filtering by genre, studio, and network on top. This is Moonbase talking to your Seerr instance directly, no jellyfin-web involved anywhere in the chain. It's funny, I'll most likely never use the Jellyfin-web client other than doing config changes. I mainly watch content on my Apple TV or my iPhone. S0 it's worth making sure that everything works on the client side since I'll never really use the web client side. One more thing worth correcting from a conversation I had a few weeks back about making home rows feel less static: a recent Moonbase release added genuine drag-and-drop reordering for home screen rows right in the admin settings, hide or reorder Continue Watching, Next Up, Latest Media, and everything else. I'd been assuming this needed scripting against an undocumented settings API to pull off. It doesn't. It's already sitting in the UI. One thing I went looking into separately: is there a way to make home rows more dynamic, a holiday section that shows up automatically and then disappears, without manually toggling things every year? KefinTweaks already does this, today, with zero scripting required. Its feature list specifically includes seasonally-themed sections, Halloween, Christmas, and others, with a configurable date window set right in its own admin page inside the Jellyfin dashboard. The catch, and it's the same shape of catch as Collection Sections: KefinTweaks builds this feature on top of the HSS plugin. It's riding the exact bridge that's on a path toward replacement once Moonfin builds native equivalents. It works right now. I wouldn't build a permanent workflow around it without expecting to revisit this once Moonfin's roadmap catches up. I did mention this in the Moonfin Discord and and few of the contributors said it was a good idea and I should submit a feature request. Pair that with the native reordering feature above and you've got most of what I was originally trying to script my way into: a section that appears on schedule and can be manually bumped to the top when it does, with no Settings API reverse-engineering required. This post covers the proven, lower-risk half of replicating what Aggregarr used to do: validating native collections and discovery rows, pairing Maintainerr and Streamystats to keep the library itself feeling alive rather than static, getting artwork generation working, and figuring out which home-row approach actually has a future if you're running Moonfin. Part two picks up with the harder, more custom half: JFC for the specific curated lists that Seerr's own discovery feeds don't reach, and a closer look at how far Moonfin's native row reordering can actually take a holiday-aware home screen before something genuinely custom is needed. Maybe I'll build something to do this or just submit that as a feature request so it can be baked into a Moonfin future release. I do want to make a massive shout out to Axl the developer of Moonfin, they have made a great product that is feature rich and works really well. With so much customization under the hood it really allows for a great experience for the end user. If you like his work, buy him a coffee. or stop by their Discord and give them a shout.

0 views
Jason Tucker 2 months ago

From Plex to Jellyfin, Part 5: Migrating Audiobook Progress to Audiobookshelf (Since Nobody Else Had)

I told you in Part 1 that Prologue and my audiobook collection were going to suck the most. I wasn't wrong, just early. Part 4 solved this exact problem for video. WatchState exists, it's mature, and it does the job. For audiobooks, nothing like that exists. I had to build it. This is the story of that script, and it's also a decent stand-in for the whole migration so far. Plex wasn't just a media server. It was the thing every other piece of my setup quietly assumed would always be there. Splitting one monolith into several focused tools is the right call. I said as much in Part 2 , when Navidrome took over music duty instead of asking Jellyfin to do a worse job of something Plex already did fine. Jellyfin does support audiobooks, technically. But going from Plex to Jellyfin for audiobooks would have meant running straight back into the same problem music almost had: a video-first platform bolting on a feature it was never built around. Audiobookshelf exists because it was built for exactly one job. Same logic that sent music to Navidrome sends audiobooks to Audiobookshelf. That part of the decision was easy. The part nobody warns you about is what happens to everything that was quietly wired into Plex along the way. Watch history. Listening position. Per-user state. None of that lives in a neutral, portable format. It lives inside Plex's database, shaped exactly the way Plex needed it shaped, and getting it out means understanding a schema nobody documented for this purpose. Decoupling a monolith doesn't just mean swapping the server. It means rebuilding every quiet assumption that monolith was making on your behalf. I want to tell you up front about this, I vibe coded this entire migration script. I spent time doing the research, working out how things work and in the end but Claude Code and I cracked out this script together and tested it as much as I could outside of my own library. With that ethical disclaimer out of the way, let's get started with attempting to migrate your audiobooks. My assumption going in was that audiobook progress would live at the album level. An audiobook is one item in your library, so the position you're at should live on that item. It doesn't. Plex stores listening position at the individual track level , the same as it would for a song on an album. A multi-file audiobook isn't one progress value. It's a pile of per-file offsets that happen to belong to the same parent. It could be one A4b or 100 wav, mp3 etc files. To get a usable position, you have to find the most recently played file in the book, then add up the full duration of every file that comes before it, then add that file's own offset. Only then do you have a single number that represents where the listener actually is. Plex also doesn't separate audiobook progress by account the way you'd hope. Two people in your household listening to the same book produce two completely separate sets of rows, keyed by Plex account ID. Nothing anywhere indicates they're the same physical file unless you go looking for it. And then there's duplication. Re-scans and re-imports over the years left several of my books with multiple album-level entries, all sharing the same external ID, each with its own slightly different progress. Plex doesn't clean this up on its own. Before writing anything, I looked for prior art. People migrate from Plex to Audiobookshelf often enough that I assumed someone had solved this. Nobody had, at least not publicly. The tooling that exists for syncing watch state, the same WatchState project that handled Part 4 for me, is built around video. It has no concept of an audiobook library, let alone the track-to-book aggregation problem above. It would be cool to have Audiobookshelf be added as a backend to WatchState but I doubt the author wants to deal with this mess. Audiobookshelf's own API supports writing progress directly, which meant the building blocks were there. The bridge between the two wasn't. So I vibe coded one. The full thing is on GitHub , but here's the shape of it. It reads directly from Plex's SQLite database, no Plex API involved, because the API doesn't expose this data in a form that's useful for bulk migration. For each audiobook, it walks every track belonging to that book, finds the one with the most recent last_viewed_at timestamp, and reconstructs an absolute position from there. Duplicate album entries get collapsed by matching on the book's external ID (its Audnex identifier, in my case), keeping whichever duplicate was most recently played and discarding the rest. Per-user separation runs through Plex's account_id . Each person you want to migrate maps to one Audiobookshelf API token in a config file, and the script processes one person at a time. Running it against the wrong account is the difference between migrating your progress and migrating your spouse's. Title matching turned out to be the part that bit me hardest. Plex and Audiobookshelf agree on almost nothing about how a title should be formatted. Heck, Plex isnt event made for audiobooks, it was hacked in. Plex pulls in subtitle and series information that Audiobookshelf strips entirely: My first pass handled this with a loose fallback: if an exact match failed, check whether either title contains the other as a substring. That felt reasonable for about ten minutes. Then I ran it against my own account and watched The Alchemist get matched to a Stephen King book called UR . The string "ur" is sitting right there inside "alchemist" once you normalize case and punctuation, and my matcher was happy to call that a hit. A few entries down, We Are Legion (We Are Bob) got matched to a Russian-language Jane Austen collection, for the same reason. Short, loosely-matched fragments turn into noise once your library has a few hundred books in it. The fix was a matching cascade instead of one clever rule. Try an exact match first. If that fails, strip everything after a colon and try again. If that fails, strip parenthetical asides. If that fails, strip a trailing series suffix like ", Book 3." Only after all of that fails does it fall back to substring matching, and only for titles long enough that a false match is actually unlikely. That last gate matters more than it looks. Short titles need to fail outright and get flagged for a manual look, not get force-fit into whatever string happens to contain them. This is a narrow problem. Plex-to-Audiobookshelf migrations aren't common enough that there's a thriving ecosystem of tools fighting over the use case. But narrow doesn't mean nobody else needs it. It means the few people who do need it have nowhere to look. The script is modular on purpose. A config file holds your Plex database path, your Audiobookshelf URL, and a token per person you're migrating. The matching logic, the Plex reading logic, and the Audiobookshelf API client are each separated into their own file instead of one script doing all three jobs at once. If your matching needs differ from mine, or your account structure is different, you can change one piece without touching the rest. It's on GitHub now, dry-run by default, so you can see exactly what it intends to write before it writes anything. Look, it isnt perfect so you could have your own LLM 5 years from now read it and then make a more up-to-date version of it if you want. Thats my goal here, vibe code it out and then let someone else vibe code their own version of it. Every part of this series so far has been some version of the same discovery: Plex wasn't just a media server, it was a set of assumptions baked into everything around it. Music assumed Plex. Watch history assumed Plex. Audiobook progress assumed Plex in a way that turned out to be more tangled than any of the others, because nobody had bothered to untangle it before. Decoupling each piece has been real work, not because any single piece is hard, but because each one hides its own version of "wait, how does this actually store data?" The upside is that what comes out the other side isn't locked to anything. Navidrome doesn't care what happens to Jellyfin. Audiobookshelf doesn't care what happens to either of them. These are all open source projects I'm trying to use to replace a software that I bought a lifetime license for, plexpass. They are ruining it for others with their crazy pricing model for lifetime licenses and a bunch of people are building various rafts to abandonship. In all of this I'm trying to build a framework for migrating from Plex to all the other apps in the opensource ecosystem that I had Plex doing. Movies, TV shows, music, audiobooks, pictures. If I ever want to move off one of these tools again, I'm not starting from zero. I've already done the hard part of learning where the data actually lives, now you do as well. That's the trade I'm making across this whole series. More pieces, more moving parts, and every single one of them replaceable on its own terms. At least when Jellyfin dies we can go and listen to music instead of what I had to do when Plex died, go outside and touch grass and sneeze because Im alergic. Read the rest of the series: Plex to Jellyfin Migrated anything similarly stubborn off Plex? Drop it in the comments.

0 views
Jason Tucker 2 months ago

From Plex to Jellyfin, Part 4: How WatchState Syncs Watch History Between Plex and Jellyfin

WatchState is a self-hosted tool that keeps watch history and play state synced across media servers. If you run more than one backend at once, say Plex and Jellyfin side by side during a migration, it's the piece that makes sure marking something watched in one place actually means something everywhere else. I run it on Unraid in Docker, syncing between Plex and Jellyfin while I work through my own move away from Plex. Here's how it actually works, what it can do, and a few things I got wrong setting it up that are worth knowing before you start. WatchState connects to each backend you give it: Plex, Jellyfin, Emby, and a handful of others. For each one, it can import existing play state into its own local database, and export play state back out to any backend you choose. Import and export are separate, deliberate operations, not one automatic two-way mirror. That distinction matters more than it sounds like it should. WatchState doesn't guess which backend is correct when two disagree. You decide that by choosing which backend to import from first and which ones to export to afterward. Matching the same episode or movie across two different servers is the actual hard problem here, not the syncing itself. WatchState does it primarily through external GUIDs, IMDB, TMDB, and TVDB IDs that both Plex and Jellyfin typically expose for properly matched content. When those IDs are missing or inconsistent, there's a fallback called path matching. It hashes the trailing two or three segments of a file's path, the season and episode folder structure, and uses that as a secondary identifier. If Plex and Jellyfin are pointed at the same files on disk, even with different mount roots, the file paths still line up underneath. Both Plex and Jellyfin can push webhook events to WatchState the moment something gets played, paused, or finished. That's what makes the sync feel close to real time instead of waiting on a scheduled job. A webhook event only updates WatchState's own local database, though. Getting that change pushed out to another backend is a separate step, handled by a scheduled Export task, plus a faster relay mode for small changesets called Push. Both need to actually be enabled, and both need each backend's Export setting turned on, or the webhook arrives, updates locally, and goes nowhere else. If more than one person uses your media server, WatchState handles that through what it calls identities. Point it at two backends and it tries to match each person's account by username automatically. Mismatched usernames, or managed and protected accounts on Plex, need a little manual attention, but the matching itself works cleanly once everyone's accounts line up. Each identity rides on the same webhook setup. There's no need for a separate webhook URL per person, which used to be a requirement in older versions and is one of the more useful quiet improvements in this project. I installed WatchState, pointed it at both backends, set up webhooks, and assumed I was done. I wasn't. Playing something in Plex never showed up as watched in Jellyfin, and my first instinct was to assume the tool itself was broken. It wasn't. The actual order of operations matters a lot here, and I'd skipped past it. You're supposed to pick one backend as your source of truth, import from it first, and only then bring in a second backend and push that history outward. I'd added both backends at once without thinking about which direction the data should flow. So I wiped the container, deleted its config folder, and started over reading the setup guide properly this time instead of clicking through it. That single change, doing the import and export in the right order, fixed most of what I thought was broken. One real bug was still hiding underneath, and it had nothing to do with WatchState. A specific episode kept failing to sync with a clear error: no metadata found for that item on the Jellyfin side. Jellyfin genuinely didn't know the file existed yet, because Sonarr and Radarr were only configured to notify Plex when new episodes finished importing, not Jellyfin. WatchState was reporting the problem correctly the entire time. I just hadn't pointed the blame at the right service. Adding a Jellyfin connection in Sonarr and Radarr's Connect settings, alongside the Plex one that already existed, closed that gap for good. New episodes now hit Jellyfin the moment they're imported instead of waiting on a library scan. If you run Streamystats alongside Jellyfin, the watch history WatchState backfills isn't wasted on it. Streamystats can infer playback sessions directly from the same watched-status field WatchState writes to, turning synced history into real entries in your stats dashboard. You lose device and client details for anything that originated on a different server, but the watch counts and dates carry over. The whole exercise was a good reminder that starting clean beats patching around a half-understood setup. Reading the actual order of operations before touching a second backend would have saved me an afternoon, but tearing it down and rebuilding it properly is what actually taught me how the tool wants to be used. Did you take the plunge moving away from Plex to something else like Jellyfin or Emby? Have you tried this or something similar to it? Let me know in the comments.

0 views
Jason Tucker 2 months ago

From Plex to Jellyfin, Part 3: Clients - Moonfin on Apple TV and the Moonbase Plugin

Honestly, the one thing that has really stopped me from moving over to Jellyfin is amount of basic apps for Jellyfin and none of them provide any additional features that would make it easier for me to find content on the server. I’ve put a lot of effort into building out my Plex installation, using a whole bunch of different doctor containers and tools to customize the database provide additional meta-data for the content in the way that it’s organized and coming up with collections and other rule based ways of displaying Rose and the interface making it fun and easy to find content on the server. The one thing that I’ve noticed with the clients is that they all lack this customization, but I’m finding that during my process of looking at different clients for the that it looks like Moonfin hits all of the major criteria that I’m looking for. I've cycled through a handful of Jellyfin clients on the Apple TV over the past year. The official app works, Infuse is the gold standard for 4K HDR playback, and Swiftfin covers the basics in a clean native shell. Moonfin is the newest name in that lineup, and it's the first one that made me rethink how much a TV client should be doing on its own. Moonfin started as a separate Apple TV app and has since folded into the larger Moonfin Core codebase, the same shared engine that powers its Android TV, mobile, and desktop clients. On tvOS specifically, it's built natively in SwiftUI with an MPVKit playback core under the hood, which is what lets it decode HEVC, AV1, DTS, and TrueHD without leaning on Jellyfin's server to transcode everything down to something safer. The playback overlay is where Moonfin earns its keep. Trickplay scrubbing, track and quality pickers, cast and crew info, a next-up card, and a pause overlay with speed and zoom controls are all built into the native player. Subtitle styling goes deeper than most clients bother with on a TV screen. Native MPVKit Playback - Hardware-accelerated playback with Dolby Vision Profile 7 decode and Atmos passthrough, advertised only when the connected display actually supports it. Direct play only show up as available when your actual display reports support for them. That sounds like a small detail, but it matters. A lot of clients will offer a quality toggle that looks correct on paper and then quietly falls back to a worse output, and you only find out by squinting at your TV's input info screen. Round that out with SyncPlay for watching together across devices, Live TV with DVR support, and skip intro and skip credits buttons that actually show up reliably, and you have a player that covers most of what Infuse does for free. tvOS 16 or later is the only real requirement. None of this is a finished product yet. Some Apple TV HD units see playback stuttering that's tracked as an open issue on GitHub. The Siri Remote's back button closes the app outright instead of stepping back a menu, which catches people off guard the first few times. If your library leans heavily on 4K remux files with Dolby Vision, Infuse is still more dependable for now. The Apple TV app on its own is a solid client. Install the companion server plugin, Moonbase, and it becomes part of something bigger. Moonbase is the backbone of the whole Moonfin ecosystem: it runs on your Jellyfin server and handles settings sync, runtime configuration, Seerr proxying, custom themes, and a hosted web version of Moonfin you can reach right from your Jellyfin instance. The settings model is the part that sold me. Preferences live in a profiled structure: one global baseline plus optional overrides for desktop, mobile, and TV. Set your media bar, navbar, and ratings preferences once at the global level, and every device inherits them automatically. Only flip a setting at the device level if the Apple TV genuinely needs something different from your phone. Seerr integration runs through the plugin as a proxy rather than a direct connection from the TV. The Apple TV app talks to Jellyfin, and Jellyfin forwards the request to Seerr with the session already authenticated. That means Seerr never has to be reachable from the TV itself, which is convenient if you're routing things through a Cloudflare tunnel and don't want to expose another hostname just so the living room can request a show. Admins can also set server-wide defaults for nearly every user-facing setting. New profiles inherit those defaults instead of starting blank, which is the kind of thing that actually matters once you have more than one person watching off the same server. Moonbase installs like any other Jellyfin plugin, through the repository system rather than a manual drop-in. Jellyfin Dashboard -> Administration -> Plugins -> Repositories -> Add Repository Name: Moonfin URL: https://raw.githubusercontent.com/Moonfin-Client/Plugin/refs/heads/master/manifest.json Jellyfin Dashboard -> Administration -> Plugins -> Catalog -> Moonfin -> Install Restart Jellyfin The web UI layer depends on the File Transformation plugin to inject itself into Jellyfin Web, so add that repository too if you want the Moonfin icon next to the SyncPlay button. Repository URL: https://www.iamparadox.dev/jellyfin/plugins/manifest.json Install: File Transformation Restart Jellyfin Force refresh your browser (Ctrl+Shift+R) If you're running Jellyfin in Docker on Unraid, the plugin lands under whatever path you've mapped to the container's plugin directory, typically something like /mnt/user/appdata/jellyfin/plugins/Moonfin/ . If you sit Jellyfin behind a reverse proxy or a Cloudflare tunnel, double check that every /Moonfin/ path gets forwarded, or the Seerr panel and settings sync will fail silently. Moonfin's Apple TV client is still young compared to clients that have had years to sand down their rough edges. The development pace has been fast enough that the stuttering and navigation quirks feel more like a punch list than a dead end. I'm leaving it installed on the living room Apple TV for now and keeping an eye on how it holds up over the next few updates. Interesting thing about this is how you can look at the Seerr requests you have made and see what one have completed. You can then go to Search and a new row a the bottom appears if you don't have the content you can request it. One weird issue that my wife and I had last night when we were watching a movie is that if you don’t have the built in screensaver enabled in the app then the system screensaver will kick in after X number of minutes right in the middle of the show. I need to submit that as a bug, but it’s really weird that something like that is in there. Do not have screensaver enabled just for in the middle of the movie Snoopy popped up and when I tried to exit out all the colors on the screen went wonky so I had to quit the app and start over again. Luckily, it did save my place. There is a lot more to explore and I'll update this post as I learn more. What are you using as a client on your Apple TV?

0 views
Jason Tucker 2 months ago

Nautiline vs. NaviBeat: Two iOS Apps worth considering for your Navidrome Library

In Part 2 of this series I mentioned Nautiline as my iOS client for Navidrome and kept moving. That wasn't the whole story. Since then I've been testing a bunch of mac, AppleTV and other apps to see whatelse is out there and a second notable app entered the picture: NaviBeat. It's newer, it covers more Apple hardware, and just like Nautiline it costs money up front. Both apps connect to Navidrome via the OpenSubsonic API. That's where the obvious overlap ends. Here's how they actually compare. Nautiline is an iOS-only OpenSubsonic client. It's $9.99 on the App Store and does exactly what it says: streams music from your Navidrome (or any OpenSubsonic-compatible server) with a clean, native-feeling interface. The feature list covers the things that matter day-to-day. Home and Away modes let you point the app at your local server address when you're on the same network and a public URL when you're not. Offline downloads work. Gapless playback works, even on transcoded streams, which is not something you can take for granted across clients. CarPlay and Siri are both supported. You can add custom headers, which is the feature that makes Cloudflare Access tunnels or Tailscale viable without jumping through authentication hoops every time. Transcoding support deserves a specific mention. Nautiline handles seekable transcoded playback, meaning you can jump around in a transcoded file the same way you would a locally cached one. A lot of clients struggle with this and quietly degrade the experience on mobile connections. Nautiline doesn't. It's iOS-only. No Mac app, no Apple TV, no iPad-native layout (it runs on iPad, but it's the phone layout scaled up). If your listening happens entirely on iPhone, that's fine. If you want something that spans devices, you're already at the edge of what Nautiline does. NaviBeat is a $5.99 Universal Purchase that covers iPhone, iPad, Mac, Apple TV, and Apple Watch. One purchase, five platforms, no subscription. Built by one developer in Belgrade using native SwiftUI throughout. The platform breadth is the headline, but the detail that actually matters more to me is that each version is purpose-built for its screen. The Apple TV app is focus-driven, designed for a Siri Remote. The Mac app has a real Mini Player that pins on top of other windows. It's not an Electron wrapper or a Catalyst port doing its best. Each app is native to the platform it runs on. Time-synced lyrics are supported across all platforms. NaviBeat reads LRC files straight from your Navidrome library and scrolls them in sync with playback. Tapping any line seeks to that point in the track. It's a small thing that becomes surprisingly useful once you have it. Offline pinning works for albums, playlists, and whole artists with an LRU cache size you set yourself. Last.fm integration is opt-in and requires no third-party relay. CarPlay is included. The Apple Watch app handles offline pinned playback independently, so you can leave your phone at home on a run and keep the music going from your wrist. NaviBeat is also a v1.0 release. It's solid but it's new. Rough edges exist. The developer is responsive and the roadmap is shaped by what beta testers actually ask for, which is a reasonable operating model for a one-person app. The creator of NaviBeat left a comment below indicating that much like Nautline it too has header login support for cloudflare and the like. If you need to login with Authelia and such you can do so using the add headers function. NaviBeat has this great feature that checks the server health for Artist Images, Lyrics and Album Covers and will event provide you with instructions on how to fix this for you. Price comparison first: Nautiline is $9.99 for iPhone. NaviBeat is $5.99 for iPhone, iPad, Mac, Apple TV, and Apple Watch. If you own more than one Apple device and want a consistent experience across all of them, NaviBeat is the more cost-effective option by a significant margin. Nautiline has been in development longer and feels more settled. The transcoding experience in particular is polished. If iPhone is your only Apple device and you want something that's been road-tested, Nautiline is the safer choice. NaviBeat's multi-platform approach is genuinely useful if you move between devices. The same library, the same queue, the same playback state, whether you're on your phone, your Mac, or your couch with the Apple TV remote its' very comparable to Spotify in this regard. That continuity across screens is something Nautiline can't offer. Both apps handle the core listening loop well: connect to your server, browse by artist and album, play music, manage a queue. Neither is missing the fundamentals. Both suppose login headers for cloudflare and such. Both, depending on what I'm doing. Nautiline stays on my phone for commuting and casual listening. It's been reliable long enough that I don't think about it, which is the correct end state for a music app. NaviBeat landed on my Mac and Apple TV because nothing else was filling those slots. Having Navidrome on the TV with a native tvOS interface is something I didn't know I wanted until I had it. I've been playing with a few apps for Mac and Windows to see what would works best for me. There are some great apps out there I'll be covering here but its nice to have a single app across all of the devices. At work I listen to music on an older iPad that sits next to my desk and is connected to my officer speakers, it works well running NaviBeat. If I were starting fresh today with zero Nautiline history, I'd probably go NaviBeat first. The $5.99 Universal Purchase covers the entire Apple ecosystem in a way that makes it hard to argue against. But Nautiline earned its place the hard way, by working correctly over time, and that counts for something. The good news for anyone just getting into Navidrome: you have MANY real options now and these two only scratch the surface of whats ou there. The iOS client situation for self-hosted music has gotten noticeably better, and both of these apps are part of why. As always I welcome your thoughts in the comments. What are you using to listen to your locally stored music?

0 views
Jason Tucker 2 months ago

Streaming Navidrome to Yamaha MusicCast with upmpdcli (No Shared Filesystem Required)

Navidrome is a great self-hosted music server. Fast, clean UI, solid Subsonic/OpenSubsonic API support. But Yamaha's MusicCast ecosystem doesn't speak that language. MusicCast uses DLNA/UPnP, which Navidrome doesn't serve natively. The obvious workaround is to run a separate DLNA server like Gerbera or MiniDLNA pointed at your music folder. That works, but it means both containers need access to the same filesystem path, you're maintaining a second index of your library, and you lose any metadata or playlist structure Navidrome manages. There's a cleaner approach: upmpdcli . upmpdcli is a UPnP media server that uses a Subsonic-compatible backend to serve content. You point it at your Navidrome instance via the Subsonic API, and it advertises itself on the network as a DLNA media server. MusicCast discovers it, browses it, and streams directly from Navidrome's library. No shared volume mounts. No second database. No filesystem access required at all. upmpdcli acts as a protocol translator between the Subsonic API and UPnP. That's the part that makes this setup worth writing about. The host networking requirement is non-negotiable. UPnP relies on multicast packets that don't survive Docker's default bridge networking. If your MusicCast gear sits on a separate IoT VLAN, you'll need a multicast proxy. More on that at the end. This is the whole thing. It's refreshingly short. Replace the IP and port with your actual Navidrome address. The is what shows up in the MusicCast app when you browse for media servers, so name it something you'll recognize. There are no volume mounts here. upmpdcli doesn't need access to your music files directly. It fetches everything through the Navidrome API and streams audio on demand. On Unraid, the easiest path is to drop this into a new compose stack via the Docker Compose manager (or the Community Apps compose UI if you're using that). Set the network mode to and paste in your environment variables. Because upmpdcli uses host networking, it won't appear in Unraid's Docker network view the same way bridge-networked containers do. That's expected. Check that it's running with: You should see output indicating it connected to your Navidrome instance and started the UPnP advertisement. If it throws a connection error, double-check the URL. Even with host networking, won't resolve to another container. Use the actual LAN IP of the machine running Navidrome. Open the MusicCast Controller app. Navigate to the room with your device, tap the input source, and look for Server or Media Server . "Navidrome DLNA" (or whatever you set as the friendly name) should appear automatically within a minute or two of the container starting. Browse in and you'll see your Navidrome library organized by Artist, Album, Genre, and Playlist. Select something and play it. Audio streams directly from Navidrome to the MusicCast device. The playlists part is worth calling out specifically. Because upmpdcli pulls from the Subsonic API rather than just reading a filesystem, it can expose playlists you've built in Navidrome. A basic DLNA file server can't do that. UPnP discovery breaks across VLANs because multicast doesn't route between network segments. If you've got your IoT devices isolated (good practice with UniFi), you need to reflect those multicast announcements between VLANs. Enabling IGMP snooping helps within a VLAN but won't bridge between them. For cross-VLAN UPnP, you need a multicast proxy. Running with on a host with interfaces on both VLANs handles this. It can also run as a Docker container with host networking: In your avahi-daemon.conf, set enable-reflector=yes under the [reflector] section. Once that's running, MusicCast should find upmpdcli even across VLAN boundaries. MusicCast's DLNA client is functional but not fancy. No queue management beyond what the device itself supports, no scrobbling, no smart playlists generated on the fly. You're browsing a tree, not using a purpose-built music app. For day-to-day listening I still reach for Symfonium on my phone connected to Navidrome directly. But for whole-home audio through MusicCast receivers and speakers, upmpdcli just works. It's local, fast, and the only thing that can break it is Navidrome going down, which would break everything else anyway. One container, a handful of environment variables, and your Navidrome library shows up on hardware that has no idea what a Subsonic API is. Navidrome already running and accessible on your network (Docker or otherwise) A Navidrome user account with API access (any account works) Your MusicCast device on the same VLAN as the machine running upmpdcli Host networking available for the upmpdcli container (required for UPnP/mDNS discovery)

0 views
Jason Tucker 2 months ago

From Plex to Jellyfin, Part 2: The Case for Navidrome (and Why Jellyfin Doesn't Get My Music)

I told you in Part 1 that PlexAmp was my biggest concern going into this migration. Spent some time sitting with that. Found an answer. That answer is Navidrome. And it means Jellyfin never has to touch my music library at all. Jellyfin is a video-first platform. Everything about how it's built and where developer attention goes reflects that. Music got added because a media server without music support feels incomplete, not because anyone set out to build the best music server on the internet. That's not a criticism. It's just the honest framing. Once you accept it, the weaknesses stop feeling like bugs and start feeling like predictable results of prioritization. Here's what those weaknesses look like in practice. The Subsonic API is an afterthought. The Subsonic API has been the standard protocol for self-hosted music clients for over fifteen years. Navidrome implements it natively; it is the protocol. Jellyfin's Subsonic support is a community plugin. It's partial, it has compatibility issues with certain clients, and you have to maintain it yourself outside of Jellyfin's core update cycle. If the plugin breaks on an update, your music clients break. The client ecosystem is thin. Because Jellyfin's Subsonic implementation is limited, the 20+ mature clients in that ecosystem don't fully work with it. You end up funneled toward Finamp. Finamp is a good app, but one client versus an ecosystem of purpose-built music apps is a real constraint. On iOS especially, the music client story hasn't kept pace with the video story. Scrobbling requires a plugin. Last.fm scrobbling in Jellyfin needs the Last.fm plugin installed and maintained separately. When Jellyfin updates, you hope the plugin ships a compatible version at the same time. It usually does. "Usually" is not a comfortable operating condition for something that should just work. Smart playlists don't exist. Jellyfin lets you build manual playlists. That's the whole story. No rules-based playlists, no "give me everything rated 4 stars or higher released before 1990 that I haven't played in 60 days." If you have a large library and care about how you surface music in it, this gap will bother you. ReplayGain support is inconsistent. If you've tagged your files with ReplayGain values, Jellyfin's handling of that metadata varies by client. Some honor it, some ignore it, and Jellyfin itself doesn't have a clear answer. That inconsistency matters if you've put in the time to get your listening levels right across albums. Performance degrades with large libraries. Jellyfin indexes video metadata, music metadata, and everything else in one database. Adding a large music library on top of an already-loaded video library produces real slowdown. A server handling 500 movies and 30 TV shows without complaint can start dragging when you throw 20,000 music tracks into the mix. Jellyfin's music feature exists. For casual listening, a playlist here, an album there, it's probably fine. For anyone with a real library who thinks about how they listen, it's a workaround looking for a better answer. Navidrome is a music server and nothing else. Written in Go, fast, and running in a container that idles around 50MB of RAM. It implements the full OpenSubsonic API natively, which means every client in that ecosystem connects without workarounds or plugins. It indexes your library, handles on-the-fly transcoding via FFmpeg when clients need it, scrobbles to Last.fm and ListenBrainz at the server level, and builds smart playlists from tag-based rules. No GPU required. No resource competition with Jellyfin. No plugins to maintain across updates. One container, one job. What it doesn't do: video, anything the *arr stack needs to talk to, or anything outside of audio. Music in, music out. That narrowness is the point. If you care about lossless audio, and if you're self-hosting your music library in 2026 you probably do, this part matters. Navidrome doesn't transcode by default. It streams files in their original format. FLAC, ALAC, WAV, your 24-bit/96kHz files arrive at the client exactly as they left your drive. FFmpeg only kicks in when a client requests a lower bitrate, say on a slow mobile connection, or when you've explicitly configured a transcode profile. Jellyfin also passes through lossless files, so on a spec sheet the difference looks small. The gap shows up in the client layer. Nautiline on iOS handles lossless passthrough properly and respects the ReplayGain tags already embedded in your files. Jellyfin's music clients are less consistent on both counts. Navidrome also recently added sonicSimilarity support through its plugin system. Audio-based recommendations, finding tracks that actually sound like the one you're playing, are now possible when a plugin provides the capability. It's early, but it's the kind of music-specific feature development that simply won't happen on the Jellyfin side. Jellyfin has video to worry about. PlexAmp works because it's a music-first iOS app built specifically around a music server. A purpose-built server paired with a purpose-built client is what makes it feel like a real product instead of a bolted-on feature. The equivalent on the Navidrome side is Nautiline . It's a $10 iOS app built specifically for OpenSubsonic-compatible servers. The interface feels native, not like a port of an Android app or a web wrapper in a trench coat. Album art renders correctly. Now Playing is clean. Offline downloads work. ReplayGain is respected. CarPlay works. The queue is persistent. Reviews from people switching to Navidrome tell the same story: try the available iOS clients, land on Nautiline, stop looking. That's where I landed too. The reason PlexAmp felt irreplaceable was that it was the only mobile music client in the self-hosted space that felt finished. Nautiline is finished. That's the gap closing. iOS covered. Desktop is a different conversation, and the answer there is Psysonic . Psysonic is a native desktop client for Mac and Windows built primarily around Navidrome. It's not a web app in an Electron shell pretending to be a desktop app. It uses Tauri and React and actually feels like something installed on your machine. The library browser is fast, the Now Playing dashboard is customizable, and it supports smart playlists natively, which pairs well with Navidrome's tag-based playlist rules. It's under active development, so rough edges exist. But the trajectory is clear and the update cadence is solid. The feature that stands out is Orbit . Orbit is Psysonic's shared listening mode. You start a session, share an invite link, and anyone else on your Navidrome server can join and listen in sync. The host controls playback and the queue. Guests can suggest tracks from anywhere in the library (albums, playlists, search results, favorites), and the host either approves them manually or enables auto-approve to keep things moving. Everyone's client mirrors the same track and position in real time. The thing that makes this practical rather than gimmicky: it runs entirely over your existing Navidrome server. No external relay, no third-party sync service, no extra account anywhere. Your server, your users, your playlists as the transport layer. Guests do need their own Navidrome login and access to the same server, so for anyone outside your network that means having Navidrome reachable publicly, which in my setup means pointing them at home-domain.com through Cloudflare rather than the Tailscale hostname. It's a genuinely fun feature for the "what are you listening to right now" use case, without turning your self-hosted setup into a social platform. Here's the decision this unlocks. If Jellyfin has to handle music well, I'm comparing the entire Jellyfin experience to the entire Plex experience. Plex wins that comparison on music. Every time. That's a hard migration to justify. But if Navidrome handles music and Jellyfin only has to handle video, the comparison becomes video versus video. That's a much more favorable fight for Jellyfin, which is genuinely good at video and improving. Navidrome is already running, pointed at the same directory that Lidarr writes to, doing its job in the background. That piece is done. Navidrome doesn't care what I decide about Jellyfin. The remaining concerns from Part 1, Tautulli, family app quality, the Prologue audiobook situation, are all video-adjacent problems. I can work through each of them without music muddying the comparison. Jellyfin doesn't have a music problem it's actively trying to solve. Navidrome is a music server that does nothing else and does it well. Those two facts point at the same answer: don't ask Jellyfin to handle music. Split the stack. One container, one job, a $10 iOS app. Music is handled. On to the harder problems. Next up: Part 3 — Setting Up Jellyfin on Unraid and Connecting the *arr Stack Running Navidrome? Using a different iOS client? Drop it in the comments.

0 views
Jason Tucker 2 months ago

Ghost + Newsletter for $15/Month: Why I Left PikaPods for MagicPages

A while back I wrote about stepping away from WPwatercooler and the WordPress ecosystem after more than twelve years . That post ended with a one-liner about writing my blog posts in Ghost going forward. This is the follow-up to that sentence. Ghost is faster and cleaner than WordPress from a maintenance perspective, and I don't spend half my time managing plugins that are one update away from breaking something. I landed on PikaPods as my host at $5/month: runs Ghost in a container, done. For a blog I write mostly for myself, that was perfect. Then I started thinking about a newsletter. Ghost has newsletter functionality built in. You can let people subscribe, send posts to them by email, all of that. What it doesn't include on a self-managed or PikaPods setup is an outbound email provider. Ghost needs something to actually send the mail. The recommended one from Ghost is Mailgun, which Ghost integrates with natively. The problem is Mailgun's retail pricing starts at $15/month for up to 10,000 emails. I have 15 subscribers. I'm not writing 10,000 emails worth of content a month. Spending $15/month on email infrastructure on top of $5/month on hosting to reach a handful of people who are probably also me on different devices felt like the wrong move. So I started looking at managed Ghost hosts that bundle email sending into the plan. I went with MagicPages at $15/month. The deciding factor wasn't just the email newsletter. MagicPages gives you a lot more on the backend than PikaPods does. PikaPods is great for what it is: a no-fuss container host. But it's not a Ghost-specific platform, so you don't get things like up to the minute managed updates, built-in email infrastructure, or any of the Ghost-adjacent tooling that a dedicated host provides. MagicPages handles all of that, including outbound email via Mailgun on their end. I don't see a Mailgun bill. I don't configure API keys. It just works. We went from retail Mailgun pricing to a fully-managed solution of Ghost + Mailgun for $15/month. Works for me. One thing I wasn't expecting was the in-dashboard customer portal. It's not a flashy feature but it's one of those small details that signals a product is well thought out. Support-wise, the contrast with my Midnight experience was immediate. The migration itself from PikaPods to MagicPages was straightforward, and Jannis, the owner, handled most of it himself. I pointed him at my PikaPods SFTP credentials and a database URL, and about 30 mins later everything was imported and live at my new URL. Members, posts, all of it. I didn't touch a JSON export file. He just did it. It also helps that MagicPages is clearly not standing still. Jannis recently posted about where the platform is headed, and it reads like someone who's been running this thing seriously for three years and is now building it like a real product. They've moved from Kubernetes to Docker Swarm, migrated to Cloudflare, simplified to a single plan with no artificial limits, and grown from a one-person side project to a small team. The infrastructure migration post in particular was the kind of transparency you rarely get from a hosting provider. They told you what they changed, why, and what it means for your site. That's not marketing, that's just being straight with your customers. The single plan thing is worth calling out specifically. The old Starter tier didn't include CDN or custom themes, and Jannis was direct about why it's gone: managed hosting done right can't be done confidently at that price point. I respect that more than a pricing page full of feature columns designed to make the cheap tier look almost good enough. Back to the migration, that kind of hands-on migration support isn't something you expect from a $15/month hosting service. It's the kind of thing you'd pay a consultant for. I've done this work with WordPress sites in the past and you can do it manually or you can pay for infrastructure to do it for you. MagicPages only does Ghost hosting and I'd imagine other people have moved from places like PikaPods if the solution they were offering wasn't fitting for that customer, much like I did. Getting the newsletter set up after the migration took about two minutes following their own documentation inside the customer portal. That's not me being generous with the timeline, that's genuinely how long it took. The Mailgun integration is pre-configured on their end, so there's no API key hunting, no DNS records to decipher on your own. You just turn it on. There was one small snag. The UI for setting up a custom sending domain had a bug where a dialog wasn't opening correctly, which blocked the email forwarding setup. I flagged it to Jannis, he sent me a direct link to work around it, handled the backend wiring himself once I'd done the DNS side, and that was the end of it. Bug encountered, bug worked around, issue closed, all within a few hours. That's a pretty good support experience even when something goes wrong. If I had to guess, it was something with the Cloudflare integration but who knows. The newsletter feature itself is exactly what I wanted. Nothing complicated. Readers can subscribe on the blog, posts can be sent to subscribers automatically or manually, and Ghost handles the subscription management. No third-party list tool, no separate platform to log into, no Zapier glue holding it together. It's all inside the Ghost admin I'm already using. If you're running Ghost on PikaPods and wondering how to add email without paying Mailgun retail prices, a managed Ghost host is the cleanest answer. You're going to spend more per month, but you're buying a complete solution instead of assembling one from parts. Maybe you have a zillion subscribers and you're sending 10,000 emails a month then $15 is nothing to sneeze at but maybe a managed solution like Midnight or MagicPages is the way to go for you. Disclaimer: This post isn't sponsored at all by either of these companies its just me doing what I do, sharing what I learned in the process and documenting the process for future me to read, or you to read in your inbox once you subscribe for free, or $5 if you want to pitch in for my new hosting bill. Lol. If you do want to join me in trying out MagicPages, use this affiliate link and I'll get a small commission. Bust honestly, waiting until 30 days when I report back on how my experience was with the service has gone. this is an affiliate link, there are many like it but this one is mine. Leave a comment below, I want to hear what you are using for your website, newsletter or any other content you are creating.

0 views
Jason Tucker 2 months ago

How to Run Backblaze Personal Backup on Unraid

When you're backing up homelab, your backup needs a backup. As I've been working my way through all the different cloud services I'm paying for, one thing I wanted to do was take a tiny bit of those savings and put them toward a cloud-based backup of my servers, not just the main Unraid box, but all the little machines scattered around my home office— the home lab, if you will. The plan is simple: everything backs up to the Unraid server, and then Unraid backs up to the cloud. Personal documents, photos, videos, audiobooks, ebooks, and anything else that would be a pain or impossible to replace. Backblaze Personal looked like the best option at $15 a month for unlimited data on one computer. The catch? It's a Windows and Mac product, and I'm running Linux. Backblaze Personal isn't designed for this. It's meant for your laptop or desktop, not a headless server with 35TB of media. But the pricing is hard to beat if you can make it work. The trick is running the Windows client inside a Wine container using the backblaze-personal-wine-container project by JonathanTreffler . You get a virtual display streamed to your browser through noVNC, and the Backblaze client thinks it's running on a normal Windows machine. No VM, no Windows license, just Wine doing its thing. It's janky in the best possible way. Create some windows drives and assign existing directories to them and you are now backing up your "windows" computer. The first real decision was figuring out what to back up. I've got over 35TB of stuff on the server, but most of it is replaceable— "Linux ISOs" and media files that I could re-download if I had to. What matters is the irreplaceable stuff: 2.3TB of photos in Immich, home videos, voice memos, archived websites, YouTube downloads, podcasts I produced over the years, music I ripped when I was in my teens, audiobooks, ebooks, and all the Docker configs and databases that make everything actually work. Backblaze Personal is priced for personal files, not a media vault, so I had to be selective. If I can get it "off the back of an Internet truck,” it doesn't need cloud backup; it can be replaced later. My kids’ first steps, wedding photos, and other irreplaceable memories— I need to keep them safe. Backing up live database files is a bad idea. You'll snapshot them mid-write and end up with corrupted backups. The fix was a nightly dump script that runs through Unraid's User Scripts plugin before Backblaze picks anything up. I'm dumping PostgreSQL databases for Authentik and Immich, plus MariaDB, into compressed SQL files. Finding the right connection method for each database was its own adventure. Authentik's PostgreSQL user isn't postgres; it's authentik. The MariaDB LSIO image uses Unix socket auth for root instead of password auth. You work through it, you document it, and you are good. I mounted the backup volumes as read-only because it seemed like a safe default. Then Backblaze silently failed to back anything up. The client tries to create a .bzvol metadata directory at the root of each drive, and it can't do that on a read-only mount. Dropping the :ro flags fixed it immediately. It's a good reminder that "safe defaults" aren't always compatible with how software actually behaves. After working through all the quirks, I've got 1.4 million files and about 2.5TB queued for backup. Database dumps run nightly and rotate automatically. The container throws a permission warning on startup, but that's just a Wine thing; the green checkmark in the Backblaze client is what matters. The whole setup is a reminder that on a homelab you are responsible for everything, until you put it in the cloud. Remember, "The Cloud is just someone else's computer.” If you're running a similar setup and want to know more about keeping your self-hosted stuff resilient, I've also written about fixing a corrupted Plex database and moving from Plex to Jellyfin and some solutions on documenting your homelab. What are you using in your homelab for offside backup? Join my site for free and leave a comment below.

0 views