Files
doc/nuc/jellyfin-server.md
Julien LutranandClaude Opus 5 9468f30f92 jellyfin: scan on download completion — inotify cannot work over NFS
Finished downloads stopped appearing in Jellyfin after the 2026-08-30
storage move, and everything looked healthy: the file was on nas, visible
through NFS inside the container, and /media/downloads is a configured
library. The cause is that Jellyfin watches libraries with inotify, which
only reports changes made through the local mount — transmission now
writes on nas while jellyfin-server reads over NFS on nuc, so no event
ever reaches it. EnableRealtimeMonitor is true and SupportsLibraryMonitor
reports true, which is why it looks fine. Previously both shared one local
dataset on nuc and it worked.

transmission now calls Jellyfin's /Library/Refresh via script-torrent-done.
The hook always exits 0 and never blocks (transmission runs it
synchronously; a hanging hook stalls the daemon), and both the success and
missing-key paths are tested. nuc is usually powered off, so a failed
request is expected and logged rather than treated as an error — the
scheduled scan catches up.

Also records that nuc's mount is read-only, so reorganising downloads into
movies/tv-shows must now happen on nas, and the ordered checklist for
"my download is not in Jellyfin" — the first three checks all passed when
this was hit, which is what made it confusing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 20:21:36 +02:00

188 lines
8.2 KiB
Markdown

# jellyfin-server
Jellyfin media **server** in an unprivileged Incus container on `nuc`.
- Image: `images:ubuntu/24.04`, Jellyfin from the official repo (repo.jellyfin.org)
- IP: `192.168.0.5` (LAN bridge) — web UI/API on `http://192.168.0.5:8096`
- iGPU render node (`/dev/dri/renderD128`) passed for QSV/VAAPI hardware transcoding
- Media library: host `/srv/media` — since 2026-08-30 an **NFSv4 mount
from `nas`** (`192.168.0.4:/media`, dataset `tank/media`), mounted at
`/media` in the container with **`shift=false`** and `readonly=true`.
See [nas/nas-install.md](../nas/nas-install.md) §8.
- Port 8096 additionally proxied to the host address (`web` proxy device)
## Install script
Run as root on the Incus host: `MEDIA_DIR=/srv/media ./install.sh`
```bash
#!/usr/bin/env bash
# Adapted from:
# https://forgejo.benoit.jp.net/benoitjpnet/Laminar/src/branch/main/cfg/jobs/jellyfin.run
set -euxo pipefail
CNAME="${CNAME:-jellyfin-server}"
IMAGE="${IMAGE:-images:ubuntu/24.04}"
MEDIA_DIR="${MEDIA_DIR:-/srv/media}" # host directory containing your media library
[ -d "$MEDIA_DIR" ] || { echo "MEDIA_DIR=$MEDIA_DIR does not exist on the host"; exit 1; }
incus launch "$IMAGE" "$CNAME"
# Wait until the container has working DNS/network
for i in $(seq 1 30); do
incus exec "$CNAME" -- getent hosts repo.jellyfin.org >/dev/null 2>&1 && break
sleep 2
done
incus config set "$CNAME" environment.DEBIAN_FRONTEND=noninteractive
incus exec "$CNAME" -- timedatectl set-timezone Europe/Paris
incus config set "$CNAME" environment.DEBCONF_NONINTERACTIVE_SEEN=true
# --- Jellyfin from the official repository -----------------------------------
incus exec "$CNAME" -- apt-get update
incus exec "$CNAME" -- apt-get upgrade -y
incus exec "$CNAME" -- apt-get install -y --no-install-recommends curl gnupg ca-certificates
incus exec "$CNAME" -- mkdir -p /etc/apt/keyrings
incus exec "$CNAME" -- bash -c 'curl -fsSL https://repo.jellyfin.org/jellyfin_team.gpg.key | gpg --dearmor -o /etc/apt/keyrings/jellyfin.gpg'
incus exec "$CNAME" -- bash -c 'cat > /etc/apt/sources.list.d/jellyfin.sources <<EOF
Types: deb
URIs: https://repo.jellyfin.org/ubuntu
Suites: noble
Components: main
Architectures: amd64
Signed-By: /etc/apt/keyrings/jellyfin.gpg
EOF'
incus exec "$CNAME" -- apt-get update
incus exec "$CNAME" -- apt-get install -y jellyfin
# Optional: OpenCL runtime, only needed for HDR tone-mapping during transcodes
incus exec "$CNAME" -- apt-get install -y --no-install-recommends intel-opencl-icd || true
# --- iGPU render node for hardware transcoding (QSV/VAAPI) -------------------
RENDER_GID="$(incus exec "$CNAME" -- getent group render | cut -d: -f3)"
incus config device add "$CNAME" igpu gpu gid="$RENDER_GID"
incus exec "$CNAME" -- usermod -aG render,video jellyfin
# --- Media library (read-only is fine unless you let Jellyfin save NFO/artwork
# next to the files) --------------------------------------------------
# NOTE: shift=true requires idmapped-mount support on MEDIA_DIR's filesystem —
# fine on local fs (ext4/btrfs/xfs), NOT supported on CIFS/NFS mounts.
incus config device add "$CNAME" media disk source="$MEDIA_DIR" path=/media shift=true
# --- Expose the web UI/API on the host's LAN address -------------------------
incus config device add "$CNAME" web proxy listen=tcp:0.0.0.0:8096 connect=tcp:127.0.0.1:8096
incus config set "$CNAME" boot.autostart=true
incus restart "$CNAME"
echo "Done. Open http://<host-ip>:8096 to run the setup wizard."
```
## Media over NFS (2026-08-30)
The library moved to `nas` when the 4 TB left nuc's USB enclosure. The
container keeps the same path, so everything below still applies — only
the mount underneath `/srv/media` changed.
```sh
# nuc host: /etc/fstab
192.168.0.4:/media /srv/media nfs4 ro,_netdev,soft,timeo=100,retrans=3 0 0
```
`shift=true` **cannot** be used: idmapped mounts are not supported on
NFS (nor CIFS). Per the troubleshooting note below, dropping the shift is
enough for a read-only library — the export uses `all_squash` so files
carry synthetic world-readable ownership:
```sh
incus stop jellyfin-server # shift cannot be hot-applied
incus config device set jellyfin-server media shift=false
incus config device set jellyfin-server media readonly=true
incus start jellyfin-server
```
⚠️ Two traps:
- **Boot ordering.** Add `remote-fs.target` to nuc's
`/etc/systemd/system/incus.service.d/after-zfs.conf`, or the container
starts against an empty mountpoint and Jellyfin shows an empty library
(and may prune the library metadata).
- **`soft` is deliberate.** A hung nas should fail Jellyfin's reads, not
wedge nuc's processes in uninterruptible sleep the way the suspended
`usb4t` pool did ([usb4t-dropouts.md](usb4t-dropouts.md)).
`transmission-bt` is no longer on nuc — it moved to nas and writes to
the dataset locally ([nas/transmission-bt.md](../nas/transmission-bt.md)),
so nuc's mount is read-only and there is exactly one writer.
## ⚠️ Real-time monitoring does not work over NFS (2026-08-31)
Libraries have `EnableRealtimeMonitor=true` and Jellyfin reports
`SupportsLibraryMonitor: true`, but **new files never appear on their
own**. Jellyfin watches with inotify, which only reports changes made
through the local mount; transmission writes them on **nas**, so nuc's
NFS client sees nothing. Jellyfin looks healthy and silently misses
everything until a scan.
This is a regression from the 2026-08-30 storage move — before it,
transmission and jellyfin-server shared one local dataset on nuc and
inotify fired normally.
**Fix in place:** transmission calls Jellyfin's `/Library/Refresh` when a
download completes — see
[nas/transmission-bt.md](../nas/transmission-bt.md). Downloads appear
within seconds; if nuc is powered off the request fails harmlessly and
the scheduled scan catches up.
Manual scan (UI): Dashboard → Scheduled Tasks → **Scan Media Library**.
By API:
```sh
curl -X POST -H "X-Emby-Token: <key>" http://192.168.0.5:8096/Library/Refresh # expect 204
```
Diagnosing "my download is not in Jellyfin", in order — the first three
were all fine when this was hit, which is what made it confusing:
```sh
ls /export/media/downloads/ # on nas: file there?
incus exec jellyfin-server -- ls /media/downloads/ # visible through NFS?
incus exec jellyfin-server -- find /var/lib/jellyfin/root -name '*.mblink' -exec cat {} + # in a library path?
incus exec jellyfin-server -- cat /var/lib/jellyfin/data/ScheduledTasks/*.js | grep -o '"Name":"Scan Media Library".*' # when did it last scan?
```
⚠️ **nuc's mount is read-only.** Reorganising finished downloads into
`/media/movies` or `/media/tv-shows` can no longer be done from nuc — do
it on nas under `/export/media/`.
## First-run configuration
1. Run the setup wizard; add libraries pointing at `/media/...`.
2. Dashboard → Playback → Transcoding:
- Hardware acceleration: **Intel QuickSync (QSV)** (fallback: VA-API),
device `/dev/dri/renderD128`.
- Enable hardware decoding for the codecs you use; Alder Lake-N does
H.264/HEVC/VP9/AV1 decode and H.264/HEVC encode.
- Enable "Low-Power" encoders only if GuC/HuC is loaded (see below).
## Troubleshooting / notes
- **`Failed to setup device mount "media": idmapping abilities are required
but aren't supported on system`** — the media source is on a filesystem
without idmapped-mount support (CIFS/NFS). Either move the media to a
local fs, or drop the shift (`incus config device set jellyfin-server
media shift=false`; on CIFS files are world-readable synthetic ownership,
enough for a read-only library). Hot-applying `shift` on a running
container fails — stop it first.
- **QSV "low-power" encode fails** — Alder Lake-N needs GuC/HuC firmware
submission. On the **host**:
`echo 'options i915 enable_guc=3' > /etc/modprobe.d/i915.conf`,
`update-initramfs -u`, reboot; verify with `dmesg | grep -i 'guc\|huc'`.
Or just untick the low-power options.
- GPU check: `incus exec jellyfin-server -- ls -l /dev/dri`.
- Pin a version with `apt-get install jellyfin=<ver>+ubu2404` for
reproducibility; keep major versions in sync with the client's JMP.
- To bake a reusable image instead: run the script, then
`incus publish jellyfin-server --alias jellyfin-server-<ver>`.