Expert knowledge for media applications (Jellyfin, Immich). Use when managing media storage, NFS mounts, or application-specific configurations.
| Operation | MCP Tool |
|---|---|
| Media services health + library stats | get_media_status |
| Fix missing/broken metadata | fix_jellyfin_metadata(name="TITLE") |
| Touch NFS path (trigger inotify) | touch_nas_path(path="/cluster/media/...") |
| Subtitle status (wanted/missing) | get_subtitle_status |
| Subtitle download history | get_subtitle_history |
| Trigger subtitle search | search_subtitles(type, id) |
All media is stored on the QNAP NAS (storage.lab.mtgibbs.dev β 192.168.1.61) and mounted via NFS. PV paths use /cluster/... (QNAP export root), not /volume1/cluster/... (old Synology path).
Migration history: Originally Synology DS420 (192.168.1.60). QNAP TS-435XeU brought up 2026-04-20. All PVs cutover to QNAP 2026-04-30. Synology retained read-only briefly; status TBD.
The QNAP Storage Pool view (and MCP list_storages / get_system_info) shows Pool 1 at ~95% used / ~1.1 TB free β this is reservation, not data. DataVol1 is thick-provisioned: it reserves its full 15 TB from the pool whether used or not, plus a 1.8 TB snapshot reserve (0 actually used) + 0.2 TB system. So the pool looks ~95% "full" while real data is only ~5.58 TB (β37%).
5.58 / 14.88 TB), NOT the pool free-space.pool_status:-1 / usable:false in the API are display quirks, not faults.)nfsvers pinned). Verified 2026-06-18 β jellyfin-video mounts as NFSv4.1 with this QNAP. Do NOT explicitly set nfsvers=4 β it broke immich mounts (remove if present).storage.lab.mtgibbs.dev (IP change = a Pi-hole DNS flip + pod restart). Exception β jellyfin-video-nfs hardcodes 192.168.1.61: it mounts on pi-k3s, which uses public DNS (not Pi-hole) and resolves the hostname only via the /etc/hosts override DaemonSet (a single point of failure). Hardcoding removes that hop. (Trade-off: a future QNAP IP change must edit this PV directly, not just DNS.)hard mount turns a brief NAS stall into a permanent freeze (see recovery runbook below). The right fix differs by access mode:jellyfin-video-nfs, kiwix-zim-nfs): soft,timeo=150,retrans=2,nconnect=4 β soft makes a stall error-and-recover instead of hang (safe β no writes to corrupt). timeo lowered 600β150 (15s, fail-fast β a stall becomes a blip not a multi-min freeze) 2026-06-18. Jellyfin soft-mount: 2026-06-15; kiwix brought to standard: 2026-06-18.media-downloads/library/music/books; also immich, calendar): keep hard (a soft write-timeout can corrupt) + add timeo=600,retrans=2,nconnect=4 for throughput + resilience. The 4 media PVs: 2026-06-16.server: 192.168.1.61) is ONLY on jellyfin-video-nfs β it mounts on pi-k3s (public DNS + /etc/hosts override SPOF). Worker-node PVs keep the hostname (Pi-hole resolves it; easy IP change).If pods are stuck in ContainerCreating:
showmount -e 192.168.1.61 from a worker node (confirms QNAP is exporting)storage.lab.mtgibbs.dev resolves correctly: dig storage.lab.mtgibbs.dev @192.168.1.55kubectl delete pod) β running containers keep old mountskubectl exec -- mount | grep nfs NOT df (df can show stale entries)Symptom: a pod (esp. Jellyfin, pinned to pi-k3s) hangs on every media read β ls /media
may still list (cached metadata) but dd/actual reads hang; the pod gets stuck Terminating.
dmesg on the node shows nfs: server storage.lab.mtgibbs.dev not responding, still trying.
Root cause (confirmed 2026-06-15): a brief NAS read-stall (spinning-RAID5 seek contention
under streaming load β see the disk-contention schedule below) on a hard mount β the kernel
retries forever instead of erroring, so a momentary hiccup becomes a permanent freeze. Everything
else was healthy β NAS up (worker-1 read at 89 MB/s), node Ready, conntrack 3% full, 0 NIC
errors, DNS fine. It is array-stall Γ hard-mount, not network/DNS/CPU. The
soft,nconnect mount options on the PV are the durable fix; kubectl rollout restart does NOT
clear it (a rolling restart never drops the wedged mount β you must fully release it).
Fast recovery (~30 s):
# 1. force-lazy-unmount the wedged NFS mount on the node it's pinned to (Jellyfin = pi-k3s / .55)
ssh mtgibbs@192.168.1.55 \
"mount | grep jellyfin-video-nfs | awk '{print \$3}' | xargs -r sudo umount -f -l"
# 2. recreate the pod so it mounts a FRESH connection (force β the old one won't terminate)
kubectl delete pod -n jellyfin --all --force --grace-period=0
# 3. verify a real read is fast again (expect ~100 MB/s)
kubectl exec -n jellyfin deploy/jellyfin -- sh -c \
'dd if="$(find /media/Movies -name "*.mkv" | head -1)" of=/dev/null bs=1M count=64'
nconnect=1 but ROOT CAUSE STILL OPEN; SD card is a SEPARATE chronic issueTWO problems were tangled together. One is mitigated-not-solved, one is understood:
1. THE STREAMING DROPS β REDUCED by nconnect=1, but NOT fixed. ROOT CAUSE STILL OPEN.
Theory: on nconnect=4/2, a parallel NFS TCP connection intermittently wedged (sent-but-unanswered RPCs
on connection #2 β NFS-Ganesha #1374; seen in live mountstats xprt 6/21), starving the stream. Lowering
to a single connection (commit 18de740, nconnect=4β2β1) clearly REDUCED the frequency β Harvey (4K
remux) sailed past its old 1:16:54 crash point, and many hours/movies played clean.
β οΈ But it did NOT eliminate it: "Hundreds of Beavers" crashed at 1:32:21 on nconnect=1 (2026-06-22,
same alive-but-I/O-blocked signature: /health "task was canceled", playback stopped, pod restartCount 0β1).
So nconnect=1 is an improvement, not a cure β keep it, but the residual crash mechanism is unidentified.
CRITICAL recorder caveat: the recorder triggered on procs_blocked>=4, which catches the many-process SD
stalls but MISSES the few-process NFS read stall that actually crashes streaming β so "classifier 0 NFS"
was a THRESHOLD ARTIFACT, not evidence. Recorder v2 re-tuned (procs_blocked>=2 OR a jellyfin/ffmpeg/dotnet
thread in D-state) to capture the NEXT streaming crash with stacks. Until then the streaming root cause is OPEN.
2. THE pi-k3s SD CARD β a real, chronic, SEPARATE issue that does NOT kill the stream. A client-side
recorder caught 7 hangs with kernel stacks (2026-06-22): every one was the local SD card
(jbd2/mmcblk0p2 journal + mmc_blk_rw_wait + mmc_sd_detect/mmc_rescan re-detecting the card) β the
card intermittently drops off the MMC bus and stalls all local writes (procs_blocked spikes 5β9, ext4
journal blocks, pihole/k3s pile up). Classifier across all events: jbd2/mmc = 37, NFS/rpc = 0. Crucially,
Harvey survived TWO of these stalls mid-stream (16:21 @ rx 8.94, 16:36 @ rx 7.08 MB/s) β so the SD stalls
are NOT the stream-killer; they were a confounding signal the recorder surfaced. Likely cause: healthy
SanDisk high-endurance card (β€6 mo) run at SDR104/200 MHz on a Pi 5 with stale EEPROM (Jun 2025) β
marginal high-speed signaling, NOT wear (no logged mmc errors; command hangs ~9 s then silently recovers).
Fix (for CLUSTER HEALTH, not streaming β the SD card backs the k3s datastore + pihole + /config):
EEPROM firmware update β cap SD speed / move to NVMe. All 3 Pi 5s are on the same stale bootloader + SD boot.
KEY LESSONS (hard-won):
iowait% and load are MISLEADING aggregates on a near-idle Pi β they inflate from transient D-state
churn. node_procs_blocked (D-state count: 0 baseline β 5β25 during a real hang) + kernel stacks are
ground truth. Months of chasing "iowait pinned = QNAP stall" was partly chasing an inflated metric./tmp/hang-recorder.sh,
8 s sampling, dumps procs_blocked+xprt+D-state stacks on >=4) is what finally cracked it.Detection / NEXT STEP: persistent recorder v2 runs on pi-k3s (/usr/local/bin/nfs-hang-recorder.sh,
systemd nfs-hang-recorder.service, logs /var/log/nfs-hang-recorder.log) β re-tuned 2026-06-22 to trigger
on procs_blocked>=2 OR a jellyfin/ffmpeg/dotnet D-state thread, so it catches the few-process NFS read
stall the v1 >=4 threshold missed. The open task: capture the next streaming crash (Beavers-type, ~1:30
into a movie) with stacks β identify the residual root cause (NFS read stall vs SD-stall-hitting-the-reader
vs Infuse/Jellyfin). Also live: MediaNFSReadHang + NodeIOWaitSustained alerts (29ab46b), media-nfs-health
dashboard (6f4e1af). Full arc: docs/recaps/2026-06-22-* (note: that recap also says "RESOLVED" β superseded by this).
The soft mount (2026-06-15) stopped the permanent freeze but not the stream drop β which kicked off
the multi-day hunt below. The QNAP rule-outs and instrumentation here are still accurate; the conclusions
(HDD-standby, "QNAP-side stall") were superseded by the nconnect + SD-card findings above.
Incident 2026-06-16 (~20:01 & ~20:32 EDT / 00:32Z 6/17): "Kill Bill: Vol. 2" via Infuse-Direct
(direct play, Apple TV) dropped early and repeatedly β playback stopped at 108507 ms (1:48) then
172278 ms (2:52). Recovered on its own (stream restartable; no pod restart).
Evidence captured:
get_media_status β healthy: true, restarts: 0; simultaneously
get_cluster_health β Liveness probe failed ... GET /health: context deadline exceeded at 00:32:59Z.
Alive but I/O-blocked, not crashed (matches the segment-scan tell documented below).02:00β06:00);
zero scan/task activity in the 19:xxβ20:xx pod-log window. The Master Schedule is being honored.Idle (0 items), Sonarr & Radarr queues empty.
(Caveat: snapshot was ~3 min post-recovery; a job finishing exactly at 20:32 can't be fully excluded.)Update 2026-06-16 ~22:17 EDT (cluster-ops verified live during a 3rd drop):
soft β the fix is live on the running pod (age 22h, 0 restarts, pinned pi-k3s):
192.168.1.61:/cluster/media/video ... nfs4 (ro,...,soft,proto=tcp,nconnect=4,timeo=600,retrans=2,addr=192.168.1.61).
The "still on old hard mount" sub-theory is DISPROVEN.dmesg on pi-k3s shows NO recent
"not responding, still trying" (only old pre-fix entries from Jun 15 22:05). Destructive recovery was
correctly NOT run β nothing to unwedge.1:48 (108507ms),
2:52 (172278ms), then a clean ~96-min session, then a deep stop at 1:36:40 (5799856ms). The
soft mount is surfacing each transient QNAP hiccup as a stream-death-then-self-recover.192.168.1.55:9100): an active stream idles at ~5%
iowait; at the 20:32 drop iowait SLAMMED to 90% for ~6 min (20:30β20:36) and at the 22:18 drop to
50%, CPU otherwise flat. The Pi's NFS reads were starved β the QNAP could not serve reads β not a
cluster / network / CPU / mount fault. A sustained 6-min 90% stall reads more like a heavy QNAP-side
job than a momentary disk hiccup. Identifying which QNAP process/disk spiked at 20:32 & 22:18 is the
open blocker β pull it via the qnap-ro MCP (confirmed healthy; see hypothesis 1 + RESUME below).Leading hypotheses (updated 2026-06-17):
qnap-ro and does not hold up:list_logs (warning+error) returns only 15 entries total,
newest 2026-04-30 β ZERO disk / RAID / SMART / bad-sector / read-error / command-timeout events,
and nothing at the 06-16 stall times. A drive in TLER deep-recovery or reallocating sectors would
raise QTS System-Event warnings; there are none.pool_status:-1 / usable:false are the documented display quirks, not faults);
volume 41% used (thick-provisioning confirmed again).qnap-ro exposes disk temperature +
status flags (get_system_info, list_storages) but NOT reallocated/pending/uncorrectable counts.
To read those, use smartctl -a /dev/sd[abc] over SSH or the QTS UI (Storage & Snapshots β Disk β
SMART). Given 7 weeks of clean logs + healthy temps + intact RAID, raw-SMART is belt-and-suspenders,
not urgent.qnap-ro did NOT auto-connect this session (Claude launched without mcp-auth
in its env β the claude() zsh guard only fires on a terminal launch, not GUI/IDE). Reached it
directly via JSON-RPC over HTTP with no Claude restart: op read "op://pi-cluster/QNAP NAS/ MCP_Token_ReadOnly" β POST http://qnap-mcp.lab.mtgibbs.dev:8442/mcp (initialize β grab
Mcp-Session-Id header β notifications/initialized β tools/list β tools/call). This is the reliable
fallback when the in-process MCP client isn't connected.soft,timeo=600(=60s),retrans=2
turned a single slow QNAP read into a multi-minute freeze (~60s Γ retries before EIO). Shortened
timeoβ150 (15s/attempt; commit 69181d4, verified live) so a hang errors ~4Γ faster β blip, not
freeze. Do NOT revert to bare hard (reintroduces the permanent wedge). retrans=2 still multiplies
before EIO β drop to 1 for a harder ceiling if 15s-class blips still drop Infuse.immich-server is replicas: 0 (scaled
to zero) and ML is off (machine-learning.enabled: false); only immich-postgresql (idle) + valkey
run. The server that does all photo I/O isn't running, so Immich cannot be driving the evening
iowait (the pg/valkey restarts are incidental). Config drift to note: the HelmRelease declares
server.enabled: true (wants 1 replica) but live is 0 β a manual scale-down not in Git, or Flux not
reconciling immich. Worth a look, but a separate issue from the stream drops.
[UPDATE 2026-06-26: that drift was later made durable by the park commit 53e5b34 (6-17); Immich
was then RESUMED via PR #33 β server.enabled+valkey.enabled true, postgres replicas: 1. The
replicas: 0 state described here no longer holds; see "Immich (Photos) β Status" below.]βΆ STATUS (2026-06-18) β LEADING HYPOTHESIS (not confirmed): QNAP HDD Standby spin-down. Fix applied.
Suspected cause is HDD spin-up latency (not a failing disk, not a "burst the array can't sustain").
QNAP Disk Standby was enabled with a 30-min timer (Disk StandBy Timeout = 30, confirmed via SSH).
Theory: Infuse front-loads a huge buffer then coasts 30+ min with zero reads (measured: QNAP idle
during the coast) β the 3 disks spin down β the next refill read hits cold disks β spin-up
(~15β30 s) hangs the NFS read β pi-k3s iowait pins ~90%, RX collapses to 0 β the old 60 s mount timeout
stretched the freeze to minutes β drop. Fits most datapoints (healthy SMART, idle CPU, no logged event,
0 TCP retransmits, intermittent). NOT CONFIRMED: we never caught the disks spinning up during a hang,
never reproduced it, and the one crash captured in detail (13:12) was preceded by ~22 min of steady reads
β which keep platters spun up, so it doesn't cleanly fit spin-down (see caveat in the subsection).
Fix applied 2026-06-18 (low-risk, worth doing regardless): (1) Disk Standby DISABLED; (2) timeo=600β150
on jellyfin-video-nfs (69181d4) backstop. Confirm by absence: a few long coast-prone movies, zero
drops + no NodeIOWaitStall = case closed; a recurrence = theory wrong, reopen.
Diagnostics status:
soft,nconnect=4,timeo=600,retrans=2 on
the running pod; reads 70β202 MB/s; no wedge in dmesg. "Still on hard mount" disproven.qnap-ro MCP triaged (2026-06-16): server healthy (HTTP 200); "offline" was mcp-auth not
loaded into the launch shell. Fixed ~/.zshrc (added MCP_AUTH_LOADED flag + claude() guard).qnap-ro (2026-06-17, direct HTTP): logs clean 7 wk (no disk/RAID/SMART
events), disks 46β49 Β°C no alerts, RAID5 intact, 41% used. Disk-failure hypothesis NOT supported.
Raw sector counts not MCP-exposed (SSH/UI only); QPKG footprint minimal (no indexer/AV scan job).query_top_processes returns empty even live β tool unusable here.06-15 14:30Z; Radarr last 06-16 02:58Z (=06-15
22:58 EDT), nothing on 06-16 evening; backups weekly-Sunday (last 06-14); SAB idle at recovery. A
download finishing "exactly at 20:32" would fire an *arr downloadFolderImported β none exists.
- Immich β server scaled to 0. QNAP β disk healthy, CPU idle, no indexer job.
- Plex β DEAD. plex-external endpoint 192.168.1.53:32400 (legacy "external Pi 3") does not
respond (no ping, conn timeout). Plex was replaced by Jellyfin Dec 2025; Pi 3 decommissioned May
2026. clusters/pi-k3s/external-services/plex.yaml is STALE config (Endpoint+Svc+Ingress+TLS for
nothing) β candidate for deletion.NodeIOWaitStall (>30% iowait 5m β
Discord) + Grafana dashboard media-nfs-health pairing iowait Γ NIC-RX (commit 6f4e1af, verified
rendering). Captured the real crash in Prometheus β mid-stream NFS read-hang (RX collapses + iowait
pins; NOT a "burst the array can't sustain").timeo=600β150. Both low-risk β applied even though causation is unproven.NodeIOWaitStall = confirmed
by absence. A recurrence = theory wrong β start from the steady-read inconsistency (subsection caveat).Leading hypothesis (NOT confirmed β see the caveat at the end): QNAP Disk Standby was enabled with
a 30-minute spin-down timer (Disk StandBy Timeout = 30, confirmed via SSH getcfg/uLinux.conf).
The proposed chain β steps 1β2 measured; steps 3β4 inferred, never observed directly:
QNAP β(NFSv4.1)β pi-k3s/Jellyfin β(HTTP)β Apple TV (no direct Apple TVβQNAP path β
only the 3 K3s nodes hold NFS/2049 connections). During an active read pi-k3s eth0 RX β TX (pure
pass-through, no transcode).eth2 ~2 pkt/s during a 45-min coast).soft,timeo=600(60s),retrans=2 mount amplified the spin-up wait into a
multi-minute freeze before EIO.Clean capture (Prometheus, "Bad Guys 2", 2026-06-17 13:12 EDT): steady stream ~6% iowait /
~11.7 MB/s RX, then at 13:12 RX collapsed (11.7β0.06) while iowait pinned ~92% for 8 min; SD card
stayed 3%. Jellyfin logged /health "task was canceled" (alive but I/O-blocked) then Playback stopped.
Why this matched every earlier dead end: healthy SMART (spin-up isn't an error), idle QNAP CPU (spin-up is mechanical), no logged event, 0 TCP retransmits / 0 NIC errors (TCP delivered the request β the QNAP just couldn't answer until the platters were up), and intermittent (only when a coast exceeds 30 min then needs a refill β so short / fully-buffered movies "work fine").
Two-layer fix (2026-06-18):
timeo=600β150 on jellyfin-video-nfs (commit 69181d4, live vers=4.1,...,timeo=150) β
any future read-stall errors in ~15 s, a blip not an 8-min freeze.What we PROVED vs INFERRED (the honest line): Proved β the symptom (read-hang: RXβ0 + iowait pin), the rule-outs (not network / CPU / disk-error / local-disk / competing-job), and that Disk Standby was enabled (30-min timer). Inferred β that a coast actually spun the disks down and the refill hung on spin-up. We never observed the disks in standby during a hang, and never reproduced it (the "replay from 1h15m" was a transient iowait blip while playing from buffer, not a real repro).
β οΈ The inconsistency that keeps this a HYPOTHESIS, not a confirmed cause: the only crash captured in
detail (13:12) was preceded by ~22 min of steady ~11.7 MB/s reads β continuous reads keep platters
spun up, so a spin-down shouldn't have been possible right before it. Either that crash was atypical, or
there's a gap at the 2-min sampling granularity, or spin-down is the wrong explanation for it.
Confirmation is still OPEN: (a) by absence β long coast-prone movies, zero drops + no NodeIOWaitStall;
or (b) the gold standard β catch a hdparm -C standby β spinning-up transition during a live hang.
Two instrumentation caveats (learned the hard way 2026-06-17): (1) pi-k3s iowait ALONE is noisy β
brief 40β54% blips self-recover in ~1 min and are often local /config (SD-card) writes, not a stream
stall. The real signature is a sustained (5-min+) iowait PIN with RX collapsing β exactly what the
deployed NodeIOWaitStall (>30% for 5 min) keys on. (2) During coast there are no live reads, so a
movie plays fine with everything idle β "it's playing" β "the read path is healthy."
POSTSCRIPT (2026-06-22): the HDD-standby hypothesis above was SUPERSEDED. The continuous recorder caught the hangs with kernel stacks β they're the local SD card (
jbd2/mmc), not QNAP spin-down, and they do not kill the stream (Harvey survived two mid-stream). The streaming drops were the NFSnconnectstuck-connection, fixed bynconnect=1. See the RESOLVED block at the top of this section.
53e5b34) to save Pi
resources while unused, then un-parked as the exact reverse of the park. Park/resume recipe: flip
helmrelease.yaml server.enabled + valkey.enabled and postgresql.yaml replicas together
(true/1 β false/0) β all three must move, or a Helm re-render undoes a partial change. PVCs are
prune: disabled, so the photo library + Postgres DB survive a park (no restore needed on resume).
Resumes on pinned v2.4.1 / chart 0.10.3; ML stays disabled. SDD spec: specs/immich-resume/.https://immich.lab.mtgibbs.devpv.yaml: Mounts /cluster/photos to /data.IMMICH_MEDIA_LOCATION=/datahttps://jellyfin.lab.mtgibbs.devpv.yaml: Mounts /cluster/media/video.readOnly: true)Jellyfin's LibraryMonitor uses Linux inotify to watch for new files. inotify does not work across NFS. Files written by Radarr/Sonarr to the NFS share are invisible to Jellyfin's file watcher.
The Sonarr/Radarr Emby/Jellyfin notifications ARE configured and working (host: jellyfin.jellyfin.svc.cluster.local, port: 8096, API keys valid, HTTP 200 on test). However, these send targeted updates (/emby/Library/Series/Updated, /emby/Library/Movies/Updated) which only refresh metadata for existing items in Jellyfin's database. They cannot discover new titles on NFS. Keep the notifications (they help with metadata refresh) but they do not solve discovery.
Jellyfin's "Scan Media Library" task (key RefreshLibrary, id 7738148ffcd07979c7ceb148e06b3aed) is the only way to discover new titles on NFS (inotify can't). Set the trigger via the Jellyfin API:
JF_KEY=$(op read 'op://pi-cluster/JellyFin/api-key')
# Daily 2:00 AM. TimeOfDayTicks = 7200s * 1e7. DO NOT use a short IntervalTrigger β see below.
curl -X POST -H "X-Emby-Token: $JF_KEY" -H "Content-Type: application/json" \
"https://jellyfin.lab.mtgibbs.dev/ScheduledTasks/7738148ffcd07979c7ceb148e06b3aed/Triggers" \
-d '[{"Type":"DailyTrigger","TimeOfDayTicks":72000000000}]'
History / why NOT a 15-minute interval:
IntervalTicks: 9000000000) so new downloads appeared fast (inotify is dead over NFS).Tradeoff: new media now appears after the 2 AM scan, not within 15 min. The proper fix to keep fast discovery without the choke is on-import scan triggers from Sonarr/Radarr (host jellyfin.jellyfin.svc.cluster.local:8096, /Library/Media/Updated with Updated type to force a path scan) β once wired, the periodic scan can be dropped. Also enable "Automatically refresh metadata from the internet" in Jellyfin library settings.
The Library scan is not the only seek-heavy scheduled task. The Media Segment Scan (Key: TaskExtractMediaSegments, Id: f861734dd71b37f9482b52a820e39013) β which analyzes files for intro/credit-skip markers β does the same random-read thrash across all media and triggers the same mid-stream drops on the RAID5.
IntervalTrigger (IntervalTicks: 432000000000), which roamed into prime-time evening and starved an active stream (Jellyfin's own /health endpoint timed out β [ERR] A task was canceled. URL GET /health β and the readiness/liveness probes failed while the 8m38s segment scan ran). Moved off the interval to a DailyTrigger; currently 04:00 as part of the 2026-06-10 contention re-stagger (see Master Schedule below for the full, deconflicted picture).JF_KEY=$(op read 'op://pi-cluster/JellyFin/api-key')
# Daily 4:00 AM. TimeOfDayTicks = 14400s * 1e7. NOT an IntervalTrigger β it roams into prime time.
curl -X POST -H "X-Emby-Token: $JF_KEY" -H "Content-Type: application/json" \
"https://jellyfin.lab.mtgibbs.dev/ScheduledTasks/f861734dd71b37f9482b52a820e39013/Triggers" \
-d '[{"Type":"DailyTrigger","TimeOfDayTicks":144000000000}]'
Diagnostic tell:
get_media_statusreportshealthy: true(pod Ready, 0 restarts) whileget_cluster_healthshowsReadiness/Liveness probe failed ... GET /health: context deadline exceeded. That contradiction = Jellyfin alive but I/O-starved, not crashed. Checkget_pod_logsfor a long-running... Scan Completed after N minute(s)overlapping the/healthcancellations. General rule: any scheduled task that does broad random reads must be pinned to an off-hoursDailyTrigger, never anIntervalTrigger.
The "pure seek latency" claim above is no longer folklore β it's measured. Replaying a 02:16β02:30 UTC segment-scan window in Prometheus (node pi-k3s = 192.168.1.55:9100), four signals side by side:
Time(UTC) iowait user+sys-CPU load1 jellyfin-mem
(0β1) (0β1) (4 cores) (limit 2560MB)
02:10 0.001 0.056 0.41 550 MB β idle baseline
02:16 0.221 0.054 10.7 550 MB β scan starts
02:22 0.223 0.051 36.1 574 MB
02:28 0.224 0.057 53.8 600 MB
02:30 0.227 0.056 59.6 707 MB β scan peak
02:40 0.001 0.055 1.12 660 MB β scan done
0.6% β 23% (~38Γ) while user+system CPU stays flat at ~5% β the CPU is waiting on the QNAP, not computing. Rules out "service slog / CPU-bound."0.7 β 59.6 (~85Γ) with CPU at 5% = the textbook I/O-bound fingerprint (load counts D-state threads blocked on NFS reads, ~59 of them queued behind the scan's random reads β the stream's sequential read is just one more in that line).707 MB vs the 2560 MB limit, 0 OOMKills, 0 restarts β memory starvation is not a factor; don't chase it.Replay query (Grafana proxies PromQL; creds at op://pi-cluster/grafana user admin, datasource uid prometheus):
PW=$(op read 'op://pi-cluster/grafana/password')
BASE="https://grafana.lab.mtgibbs.dev/api/datasources/proxy/uid/prometheus/api/v1"
# iowait fraction on pi-k3s (avg across cores = 0β1). Swap mode= for user|system to see CPU stays flat.
curl -s -u "admin:$PW" --data-urlencode \
'query=avg(rate(node_cpu_seconds_total{instance="192.168.1.55:9100",mode="iowait"}[2m]))' \
--data-urlencode start=<epoch> --data-urlencode end=<epoch> --data-urlencode step=120 \
"$BASE/query_range"
Open thread (diagnose later): a second iowait spike with the same flat-CPU/high-load signature appears at
01:38β01:44 UTCβ some other broad-random-read job lands there too; identify it so it doesn't ambush a stream. QNAP-side per-disk queue depth / NFS op latency needs theqnap-roMCP (was timing out 2026-06-10).
The QNAP is a 3-disk spinning RAID5 over NFS. Random-read jobs (library / subtitle / trickplay / chapter scans) thrash disk seeks and starve sequential streams β see the measured proof above. Governing rule: only one heavy NFS job starts at a time, never inside the Sunday backup window, never during streaming peak. Every scheduler below runs America/New_York, so all times are local (EDT).
backup-jobs CronJob cascade (pvcβpostgresβworker2βmediaβgit-mirrorβunifi) writing configs to /share/cluster/backups. Keep media scans out of it.| Time (EDT) | Job | Service | Task id / settings key |
|---|---|---|---|
| 02:00 daily | Scan Media Library | Jellyfin | 7738148ffcd07979c7ceb148e06b3aed |
| 04:00 daily | Media Segment Scan | Jellyfin | f861734dd71b37f9482b52a820e39013 |
| 04:30 daily | Audio Normalization | Jellyfin | ec2f221fd8e7706b3d3afd2c4591b4d7 |
| 05:00 daily | Extract Chapter Images | Jellyfin | 4e6637c832ed644d1af3370a2506e80a |
| 06:00 daily | Generate Trickplay Images | Jellyfin | 64f5f44cd30dc273cb9890205473bbcc |
| Tue 03:00 | Index All Movies Subtitles | Bazarr | radarr.full_update |
| Thu 03:00 | Index All Episodes Subtitles | Bazarr | sonarr.full_update |
Set any Jellyfin task time (TimeOfDayTicks = seconds-past-midnight Γ 1e7 β 02:00=72000000000, 04:00=144000000000, 04:30=162000000000, 05:00=180000000000, 06:00=216000000000):
JF_KEY=$(op read 'op://pi-cluster/JellyFin/api-key')
curl -X POST -H "X-Emby-Token: $JF_KEY" -H "Content-Type: application/json" \
"https://jellyfin.lab.mtgibbs.dev/ScheduledTasks/<TASK_ID>/Triggers" \
-d '[{"Type":"DailyTrigger","TimeOfDayTicks":<TICKS>}]'
Bazarr full-library subtitle indexes β radarr=movies, sonarr=episodes; dailyβweekly (_day: 0=Mon β¦ 6=Sun):
BZ=$(op read 'op://pi-cluster/mcp-homelab/bazarr-api-key')
curl -X POST -H "X-Api-Key: $BZ" "https://bazarr.lab.mtgibbs.dev/api/system/settings" \
--data-urlencode "settings-radarr-full_update=Weekly" --data-urlencode "settings-radarr-full_update_day=1" --data-urlencode "settings-radarr-full_update_hour=3" \
--data-urlencode "settings-sonarr-full_update=Weekly" --data-urlencode "settings-sonarr-full_update_day=3" --data-urlencode "settings-sonarr-full_update_hour=3"
Every24h cleanups (Clean Cache/Log/Transcode, Update Plugins, Refresh Guide, Download missing subs/lyrics) still anchor to pod-start (~22:15) β complete in ~0s, harmless. Pin to a DailyTrigger if they ever grow teeth./cluster/photos on the same spindles β upload-driven + a light nightly job, ML disabled. Low overnight load; verify its nightly time if the photo library grows.Every24h); (4) *arr midnight metadata cluster (app-fixed, left in place). Backups untouched.Root Cause A (new title): NFS inotify β see above. The daily 2 AM scan picks it up (or trigger a manual scan / on-import Sonarr-Radarr scan β see scan-schedule note above).
Root Cause B (title visible but wrong/missing metadata): Item exists in database with incomplete metadata (NULL DateLastRefreshed). Jellyfin won't display items with failed/interrupted metadata fetches.
Solution 0 - MCP (TRY FIRST):
Use fix_jellyfin_metadata(name="SHOW_NAME") β searches Jellyfin library and triggers a full metadata refresh via API. No kubectl or API keys needed.
Solution 1 - UI (if item is visible):
Solution 2 - API (if item is NOT visible):
# First, find the item ID in the database
kubectl -n jellyfin exec -it deploy/jellyfin -- sqlite3 /config/data/library.db \
"SELECT Id, Name FROM TypedBaseItems WHERE Name LIKE '%SHOW_NAME%' AND Type LIKE '%Series%';"
# Then trigger a full metadata refresh
JF_KEY=$(op read 'op://pi-cluster/JellyFin/api-key')
curl -X POST "https://jellyfin.lab.mtgibbs.dev/Items/ITEM_ID_HERE/Refresh?metadataRefreshMode=FullRefresh&imageRefreshMode=FullRefresh" \
-H "X-Emby-Token: $JF_KEY"
https://requests.lab.mtgibbs.devfallenbagel/jellyseerr:2.7.3 β the newest stable release (from Aug 2025).develop / preview-*. The badge stays amber regardless of what we pin to; the only way to "satisfy" it is to run an unstable build. Stay on stable.The project is mid-rebrand from Jellyseerr to Seerr β see the preview-seerr / preview-rename-tags tags, and the ~10-month gap with no new stable since 2.7.3. When the successor ships its first stable release β possibly under a new image name (e.g. seerr) β that is the trigger to do a real migration (new manifest/image name, check for a config/data migration step). Until then, 2.7.3 is correct. Treat the next stable release as the only real "update" β ignore the in-app nag in the meantime.
Found 2026-07-13: a whole TV show, fully downloaded and playable in Jellyfin, still showed as "requested"/still-processing in Jellyseerr. Root cause was not Sonarr/Radarr β the show, Sonarr, and Jellyfin all agreed the file was present. Jellyseerr's own Jellyfin service config (.jellyfin.libraries in its settings) only had Movies and Turbo Fire in the synced-library list; the real Shows library had been added to Jellyfin at some point but never enabled in Jellyseerr. Its jellyfin-recently-added-scan (5 min) and jellyfin-full-scan (daily) jobs ran fine on schedule but could never see anything in an unsynced library, so jellyfinMediaId never populates and status is permanently stuck at PROCESSING (3). This was systemic, not a one-off: all 17 TV requests were affected; movies were fine because Movies was in the sync list.
Diagnose:
KEY=$(kubectl exec -n media deploy/jellyseerr -- cat /app/config/settings.json | jq -r '.main.apiKey')
# current synced libraries
curl -sS "https://requests.lab.mtgibbs.dev/api/v1/settings/jellyfin" -H "X-Api-Key: $KEY" | jq '.libraries'
# ALL libraries Jellyfin actually has (ignores current enabled state)
curl -sS "https://requests.lab.mtgibbs.dev/api/v1/settings/jellyfin/library?sync=true" -H "X-Api-Key: $KEY" | jq
If a library is present in Jellyfin's VirtualFolders but missing (or enabled:false) from the first call, that's the bug.
Fix β enable via the dedicated library endpoint (GET, not POST/PUT β both return 405/400):
ENABLE_IDS="<comma-separated library ids to keep enabled, e.g. all of them>"
curl -sS "https://requests.lab.mtgibbs.dev/api/v1/settings/jellyfin/library?enable=${ENABLE_IDS}" -H "X-Api-Key: $KEY"
This is the only endpoint that actually persists library enable/disable β POST /api/v1/settings/jellyfin rejects libraries as read-only, and PUT isn't a supported method on that route at all. Then trigger a scan immediately rather than waiting for the cron:
curl -sS -X POST "https://requests.lab.mtgibbs.dev/api/v1/settings/jobs/jellyfin-full-scan/run" -H "X-Api-Key: $KEY"
Poll GET /api/v1/settings/jobs for running:false, then re-check GET /api/v1/media?take=100&filter=all β affected titles should flip from status:3 (PROCESSING, jellyfinMediaId:null) to status:5 (AVAILABLE) or 4 (PARTIALLY_AVAILABLE for legitimately still-airing shows).
Also note: Jellyseerr's own API key is generated in-app, not stored in 1Password β pull it from the running pod's /app/config/settings.json (main.apiKey) if you need to hit its API directly.
Prevention: whenever a new Jellyfin library is added (movies, shows, or otherwise), check Jellyseerr's synced-library list β it does not pick up new libraries automatically.
.nfo to NFS β cosmetic only. The media mount is readOnly: true; metadata is stored in the DB, not on disk.English - PGSSUB) in the menu but draws nothing when selected (long-standing tvOS limitation,
docs/recaps/2026-05-06-bazarr-subtitle-pipeline.md Β§8). If stuck with a misaligned external SRT and no
embedded track, there is no in-player workaround for Apple TV.Recurring bad advice, both from generic Jellyfin guidance and from Claude: "the Apple TV drops mid-movie and Infuse is the one constant β switch to the native Jellyfin tvOS app to A/B the client." That test is not available here. Infuse is not a preference; it is the fix for three independent tvOS-app defects, and every fallback path routes through a transcode the Pi 5 cannot perform (BCM2712 has HEVC decode only β no hardware encoder at all, so any burn-in falls back to software x264/x265 on the ARM cores).
| # | tvOS Jellyfin app defect | Infuse's answer | Cost of reverting |
|---|---|---|---|
| 1 | Cannot render embedded PGS bitmap subs (Blu-ray / foreign cinema) | native PGS direct-play renderer | server-side burn-in transcode, explicitly rejected 2026-05-06 |
| 2 | ASS/SSA softsubs on anime β styled/positioned tracks the app handles badly | renders ASS natively | burn-in transcode again, on a large library |
| 3 | No subtitle offset control; clumsy multi-track audio/sub selection (jpn/eng dual-audio anime) | per-track audio + subtitle selection with offset | no in-player workaround at all |
Row 2 is user-reported (the anime subtitle-sync complaints), captured here 2026-09-22 because it lived only in Matt's head and kept getting re-litigated. The mechanism (ASS β burn-in) is the likely-but-unverified explanation; rows 1 and 3 are documented in the 2026-05-06 recap. Verified either way, the conclusion holds: the native app is not a usable fallback.
Consequences for the open streaming-crash investigation
(docs/incidents/2026-06-22-streaming-crash-handoff.md β residual drop at 75-110 min, always Infuse-Direct,
zero server-side trace):
docs/recaps/2026-06-18-jellyfin-nfs-streaming-drops-rootcause-instrumentation.md is an Infuse setting.
Shrinking it turns "huge burst β long coast β cold-disk refill hang" into a steadier read β which is precisely
the mechanism the NFS spin-up theory blames. Free, reversible, not yet tried.Stale radarr.db-wal and radarr.db-shm files from an unclean shutdown persist on local-path storage and cause startup failures.
Fix:
# Scale to 0 β graceful shutdown checkpoints the WAL
kubectl -n media scale deployment radarr --replicas=0
# Wait for termination, then scale back up
kubectl -n media scale deployment radarr --replicas=1
Always back up the DB files before attempting recovery. WAL file disappears after a clean shutdown.
To connect to the database for debugging:
kubectl -n immich exec -it deploy/immich-postgresql -- psql -U immich -d immich