Apps / Media / :8096

Jellyfin

Your movies, shows and music, streamed to any device.

No MiniControl yet? Download MiniControl, then install Jellyfin from its catalog.

Jellyfin turns a folder of movies, shows and music into your own streaming service. It fetches posters and descriptions, remembers where you stopped, and has apps for phones, TVs and browsers. It's free and open source, with no account and nothing sent to anyone else.

Why on a Mac: Apple silicon's media engine converts video for slower devices in hardware, quietly and at a few watts.

After the install: Open it to create your admin account and add your media folders.

Free, like the whole catalog.

What MiniControl installs

Packages
brew install --cask jellyfin
Web page
Port 8096
Keeps running
As LaunchAgent app.minicontrol.jellyfin, also after a restart
Its data
~/services/jellyfin~/Library/Caches/jellyfin
Nightly backup
Its settings and library, to iCloud Drive
Memory
300–500 MB, more while it converts video
Licence
GPL-2.0-only
The install script

What MiniControl runs on your server, word for word. The app shows the same script before it installs.

#!/bin/bash
# Generated by MiniControl for Jellyfin. Runs on the machine.
set -euo pipefail
export HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_ENV_HINTS=1 NONINTERACTIVE=1
export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:$PATH"
step() { echo "STEP|$1|$2"; }
fail() { echo "ERR|$1"; exit 1; }
U=$(id -u)
BREW=$(command -v brew || true)
[ -n "$BREW" ] || fail "Homebrew isn't installed. Server Setup → Homebrew installs it."
BP=$("$BREW" --prefix)
step 1 "Checking the machine"
for f in "$HOME"/Library/LaunchAgents/*.plist; do
  if grep -qsF -- 'Jellyfin.app/Contents/MacOS/jellyfin' "$f"; then fail "Jellyfin is already set up here, as $(basename "$f" .plist)."; fi
done
if lsof -nP -iTCP:8096 -sTCP:LISTEN >/dev/null 2>&1; then
  fail "Something else already uses port 8096: $(lsof -nP -iTCP:8096 -sTCP:LISTEN | awk 'NR == 2 {print $1}')."
fi
step 2 "Installing with Homebrew"
"$BREW" list --cask jellyfin >/dev/null 2>&1 || "$BREW" install --cask jellyfin
step 3 "Setting it up"
mkdir -p "$HOME/services/jellyfin/log" "$HOME/Library/LaunchAgents" "$HOME/services/jellyfin/data" "$HOME/services/jellyfin/config"
PL="$HOME/Library/LaunchAgents/app.minicontrol.jellyfin.plist"
cat > "$PL" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key><string>app.minicontrol.jellyfin</string>
  <key>ProgramArguments</key>
  <array>
    <string>/Applications/Jellyfin.app/Contents/MacOS/jellyfin</string>
    <string>--service</string>
    <string>--datadir</string>
    <string>$HOME/services/jellyfin/data</string>
    <string>--configdir</string>
    <string>$HOME/services/jellyfin/config</string>
    <string>--logdir</string>
    <string>$HOME/services/jellyfin/log</string>
    <string>--cachedir</string>
    <string>$HOME/Library/Caches/jellyfin</string>
    <string>--webdir</string>
    <string>/Applications/Jellyfin.app/Contents/Resources/jellyfin-web</string>
    <string>--ffmpeg</string>
    <string>/Applications/Jellyfin.app/Contents/MacOS/ffmpeg</string>
  </array>
  <key>RunAtLoad</key><true/>
  <key>KeepAlive</key><true/>
  <key>ThrottleInterval</key><integer>10</integer>
  <key>ProcessType</key><string>Background</string>
  <key>StandardOutPath</key><string>$HOME/services/jellyfin/log/launchd.out.log</string>
  <key>StandardErrorPath</key><string>$HOME/services/jellyfin/log/launchd.err.log</string>
</dict>
</plist>
EOF
plutil -lint "$PL" >/dev/null
[ -x "/Applications/Jellyfin.app/Contents/MacOS/jellyfin" ] || fail "/Applications/Jellyfin.app/Contents/MacOS/jellyfin isn't there after the install."
step 4 "Starting Jellyfin"
# Services run in the logged-in user's session (gui/<uid>), like every service in the panel.
launchctl bootstrap "gui/$U" "$PL" || fail "launchd didn't start it. Services run while you're logged in on the machine; Server Setup's \"Log in automatically after a restart\" fixes that."
if /usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate 2>/dev/null | grep -q enabled; then echo "FW|/Applications/Jellyfin.app/Contents/MacOS/jellyfin"; fi
step 5 "Waiting for it to answer"
# Localhost, and the Tailscale IP for apps that bind only to it.
# `|| true`: with set -e, a failed substitution in an assignment would end the script.
HOSTS="127.0.0.1 $(for t in "$BP/bin/tailscale" /usr/local/bin/tailscale; do [ -x "$t" ] && "$t" ip -4 2>/dev/null && break; done | head -1 || true)"
ok=0
for i in $(seq 1 45); do
  for h in $HOSTS; do
    if curl -fsS -m 3 -o /dev/null "http://$h:8096/health" 2>/dev/null; then ok=1; break 2; fi
  done
  sleep 2
done
[ "$ok" = 1 ] || echo "WARN|It started, but isn't answering on port 8096 yet. Big apps can take a minute on first start."
echo "DONE"

MiniControl isn’t affiliated with Jellyfin. Questions about Jellyfin itself go to its makers; about installing it, to hello@minicontrol.app.