Ryan Malesevich

amateur runner, technology enthusiast, and friend to all dogs

Homelab Chronicles: Chapter 4

22 December 2024

Since I experienced so many issues with the configuration of my new media system, this chapter of the Homelab Chronicles will be entirely focused on Jellyin. Like most tech related issues, they were entirely self-inflicted. Now that I have the metadata and plugins working as intended, there have been zero issues and I absolutely love it.

If this is your first time here, please familiarize yourself with my journey of building up my homelab through Chapters one, two, and three.

Why switch from Plex to Jellyfin?

Plex got me started on this Homelab adventure back in 2022. It’s been running on my Synology DS920+. Locally, the setup worked great. The underpowered Synology could easily serve the files throughout my internal network. When I traveled, I had the Plex Lifetime Pass so I could download the files locally to my iPad. There were two painpoints though:

  1. I allowed some friends and family to access the Plex library. Either my bandwidth or their bandwidth couldn’t serve the files appropriately. Try explaining all the various intricacies of networks, file sizes, bitrates, etc. to non-technical people. All I would get was “your Plex isn’t working.”
  2. Plex, as a company, is moving towards their own streaming platform. Our values are diverging.

The addition of the Mac mini meant I could run the server software on a beefier machine. If an encode needed to be done, it should be able to be done on the fly to keep playback as smooth as possible. I was going to install Plex, but had been hearing so much about Jellyfin I thought this would be the time to explore. Jellyfin is licensed under GNU GPL and it’s built by a community of people who share my values.

Problem 1 - ensuring the NAS is mounted to the Mac mini

Running Plex on the Synology meant that it would never have issues connecting to the file system. Separating the server from the file system isn’t a bad idea, but it just means care is needed to ensure that it would stay mounted all the time.

First, I thought adding it directly to the Login items in System settings would be enough. When logging in, it would mount the drive. Unfortunately, that is not enough. If there is ever a disconnection, I’d need to re-mount the drive or logout and log in.

This seemed like something a script could be written to handle. Let the script run regularly. Check if the drive is mounted. If it is, great! If it’s not, then try to re-connect it. I chose to write a shell script. The first version utilized osascript because it had a way to “tell” the system to connect. The script was scheduled to run every 5 minutes. Things worked well until they didn’t. If the NAS was down for an extended period of time, say when I purchased the Ubiquiti mini rack and needed to install the Gateway, a dialog box would open. The script would not run again until it was dismissed. This was not ideal. The script went through a second iteration.

The script needs my credentials to the file system. I didn’t want to hardcode it into the script, so I added a generic password to the key vault through this command:

security add-generic-password -a your_username -s your_server_name_or_ip -w your_password -U

The other requirement is that I couldn’t mount the NAS to the /Volumes drive through a Shell script. I learned that in macOS, you can mount drives anywhere on your system. 20 years I’ve been working with macOS, and this is the first time I learned that. I created a folder in my home drive called Mounts.

Here is the Shell script that I wrote:

#!/bin/bash

# SMB details
SERVER_NAME=""
USER_NAME=""
SHARE_NAME=""
MOUNT_POINT="$HOME/Mounts/$SHARE_NAME"

# Retrieve credentials from Keychain
PASSWORD=$(security find-generic-password -a $USER_NAME -s $SERVER_NAME -w 2>/dev/null)

# Check if credentials are retrieved
if [ -z "$PASSWORD" ]; then
    echo "$(date): Failed to retrieve credentials from Keychain. Exiting." >> ~/smb_reconnect.log
    exit 1
fi

# Check if the mount_point exists and is already connected
if [ ! -d "$MOUNT_POINT" ] || ! mount | grep -q "//$USER_NAME@$SERVER_NAME/$SHARE_NAME on $MOUNT_POINT"; then
    echo "$(date): Share is not mounted. Attempting to reconnect..." >> ~/smb_reconnect.log

    # Ensure the mount point directory exists
    if [ ! -d "$MOUNT_POINT" ]; then
        mkdir -p "$MOUNT_POINT"
    fi

    # Attempt to mount the SMB share
    mount_smbfs "//${USER_NAME}:${PASSWORD//@/%40}@${SERVER_NAME}/${SHARE_NAME}" "$MOUNT_POINT" 2>> ~/smb_reconnect.log
    
    if [ $? -eq 0 ]; then
        echo "$(date): Successfully reconnected to SMB share." >> ~/smb_reconnect.log
    else
        echo "$(date): Failed to reconnect to SMB share. Check if the SMB server is online." >> ~/smb_reconnect.log
    fi
else
    echo "$(date): Share is already mounted." >> ~/smb_reconnect.log
fi

The SERVER_NAME and USER_NAME should match what was added into the key vault. SHARE_NAME is the name of the drive on the NAS. I added logging into the smb_reconnect.log file for each step of the way.

After the Shell script was saved, set it to be executable and we’re ready:

 chmod +x checksmb.sh

Scheduling the script to run could be done through the LauchAgent or cron. I had never used the LaunchAgent, so I gave it a try. The steps were quite simple. Step 1 is to create a file in ~/Library/LaunchAgents/com.user.checksmb.plist with the following content:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.user.checksol</string>
    <key>ProgramArguments</key>
    <array>
        <string>/bin/bash</string>
        <string>/full/path/to/shell/script.sh</string>
    </array>
    <key>StartInterval</key>
    <integer>300</integer> <!-- Runs every 5 minutes -->
    <key>StandardOutPath</key>
    <string>/full/path/to/standard/out/path.log</string>
    <key>StandardErrorPath</key>
    <string>/full/path/to/standard/error/path.log</string>
</dict>
</plist>

Then add the PLIST to the launchd job:

launchctl load ~/Library/LaunchAgents/com.user.checksmb.plist

Since the update o the script, I’ve had no issues. I simulated a disconnect from the NAS while Jellyfin was playing. Jellyfin will buffer the video so long as the script re-connects the NAS before the local buffer is expired, there will be no disruptions.

Installing Jellyfin and connecting my libraries

Jellyfin can be installed locally or through Docker. I wasn’t sure how encoding would work if I used Docker as a middle layer, so I chose to install Jellyfin with the native macOS installer.

Once installed, I added a record to my Caddyfile so I could access the server through my reverse proxy. I found that accessing it through the web browser worked with authelia, but the Jellyfin iOS application did not like that. For now, I’ve disabled authelia on that endpoint and added additional protections.

I pointed Jellyfin to two folders on my NAS: one for Movies and one for Shows. All media is named consistently through a program called FileBot. FileBot connects to various metadata spots and names them appropriately. I use two presets, one for movies and one for TV shows. Both are using TheMovieDB as the source.

Movies:
{collection.colon('- ') + '/'} {ny.colon('- ')} /{n.colon('- ')} ({y}){subt} [{vf}]

Shows:
{ny.colon('- ')}/{'Season '+s}/{ny.colon('- ')} - {s00e00} - {t.colon('- ')}

I’m not thrilled with the Movies because I don’t like the additional collections folder, but my friend liked it for some reason so I chose not to fight too hard. Remember, that we use Syncthing to keep our consolidated libraries in multiple locations.

Jellyfin scanned the libraries and everything was there.

Syncing my watch history

I use Trakt religiously. Every TV show and movie I watch gets recorded there. I had manually set the items on Plex as watched. I scrobbled to Trakt from the Infuse app that was connected to my Plex library. I was hoping to avoid manual work with Jellyfin, but that was not to be.

Jellyfin has a direct plugin for Trakt integration. It meant that if I connected my Jellyfin account to my Trakt account, anything I watched with any app would be recorded on Trakt. This is what I wanted since I could use different video players with Jellyfin. The plugin has a two way sync that would mark anything that I had watched on Trakt as watched if it found a match in my Jellyfin library. I didn’t read through all the options, but then scheduled the jobs to run. Things happened. I first looked at my Jellyfin library and saw that it matched a lot of my movies and shows as watched. Success rate was not 100%. Then I went to Trakt, and that’s when my heart sank. Almost all my movie watches were deleted. TV episodes were mostly intact. About 70 random episodes were deleted from Trakt.

What happened? I’m still not sure. It was distressing. Thankfully, my movies were double tracked in Letterboxd so I could recover everything. The TV shows were more challenging because it was so random. I disabled everything on the Trakt integration other than the live scrobbling. Then I started a long time of manually fixing things.

  1. I first went through every movie and TV show in Jellyfin to make sure they were recorded correctly.
  2. While going through thousands of entries, I found that some movies didn’t get their metadata in Jellyfin identified successfully. I tried to fix this, but it happened at the same time there was a problem with TheMovieDB API that caused all Jellyfin users to experience the issue. It resolved itself after several hours.
  3. Manually setting the episodes that were deleted on Trakt to a representative day.
  4. Sync Trakt from my Letterboxd account.

Steps 1 through 3 were a pain, but step 4 proved to be the biggest pain. Trakt had a Letterboxd importer. I used it and thought things were great until I realized they didn’t use the diary date, but the day I added things to Letterboxd. My Letterboxd history was loaded from my previous IMDb history, so the day Letterboxd had was the same for almost 2000 movies. Trakt had the same and I did not like that. The import was done, so I painfully went through and manually deleted them from Trakt. After I deleted everything, I found that the import function had an undo. Ugh. 🤦‍♂️

Ultimately, I found a script on GitHub that worked much better. After running through it with over 3000 diary entries, there were only a handful of movies that couldn’t be matched. I manually added them.

Finally, my data was correct on Jellyfin and Trakt. The deletion happened on a Saturday. I wasn’t able to sleep into Sunday, so I woke up shortly after midnight and went to work. I don’t like leaving things in a bad state. It made for a very long day, but at the end the data was correct.

Sitaution today

I’m happy to report that since the initial configuration, I’ve had no issues. I’d recommend being very careful with the Jellyfin trakt plugin. The scrobbling works out well, so if that’s all you’re looking for then it’s great. Everything else is unknown to me.

I’m still using Infuse as my primary interface for playing the videos. There is a pro add-on that I’m happily paying for, since I use it on my Mac, iPhone, iPad, and two Apple TVs. I’ve found that Infuse syncs with Jellyfin a lot faster than it ever did with my Plex library.

Plex is still running on the Synology and that is how my friends connect. Over the next few weeks I’ll be migrating users over to Jellyfin and eventually decommission my Plex server.

Next time

There is only one more tale to catch us up to real-time. Getting jail2ban setup and working was a pain, but it wasn’t nearly as big of an issue than I had with Jellyfin. Spoiler alert for Chapter 5, I run into the limits of ChatGPT with all this. See you next time space cowboy.

homelab technology