New owner page Dankserver/Media Metadata View.md: the shape, the two binds and why Plex keeps the /mnt/entropy container path, the commands, the laws (SSD-only upper layer, never mount over an unmounted array, never delete through the view because a whiteout only hides the array's copy, reap after any relayout, inotify does not cross the merge), the failure signatures, and the rollback. Jellyfin.md: /data is the view, so Local metadata works; the "Read-only file system : '/data/…/movie.nfo'" line that used to be expected noise is now the signature of a missing view. Plex.md: same bind, one fact changed. Media Relayout.md: two new laws for the metadata that does not follow a rename. AGENTS.md: runbook-index row plus a summary row. Root index regenerated.
106 lines
5.8 KiB
Markdown
106 lines
5.8 KiB
Markdown
---
|
|
type: reference
|
|
title: Media Metadata View
|
|
description: "The SSD-backed overlay the two media servers read and write: shape, guards, commands, laws, rollback."
|
|
tags: [dankserver, media-server, jellyfin, plex, storage]
|
|
timestamp: 2026-10-30T00:00:00Z
|
|
---
|
|
|
|
# Media Metadata View
|
|
|
|
`/mnt/media` is an OverlayFS merge of the media library: the RAID6 array below,
|
|
a directory on the NVMe above. It exists so [[Jellyfin]] can write Kodi-format
|
|
sidecars (`movie.nfo`, `tvshow.nfo`, `episode.nfo`, artwork) that [[Plex]] can
|
|
read. Sidecar metadata is only useful sitting next to the media file, and the
|
|
array is read-only and slow — so the bytes stay there and the *writes* land on
|
|
SSD. One tree to both servers, one copy of the media.
|
|
|
|
## Shape
|
|
|
|
| Item | Value |
|
|
|---|---|
|
|
| Lower layer | `/mnt/entropy` — the RAID6 array, source of truth, never written |
|
|
| Upper layer | `/var/lib/media-meta/upper` — NVMe; every write lands here |
|
|
| Work dir | `/var/lib/media-meta/work` — overlayfs internals, same filesystem |
|
|
| Mount point | `/mnt/media`, unit `mnt-media.mount` (`WantedBy=local-fs.target`) |
|
|
| Repo source | `dankserver/host/systemd/mnt-media.mount` — host copy is generated state |
|
|
| Installer | `scripts/media-view.sh` — `plan` / `install` / `verify` / `status` / `reap` |
|
|
| Written by | Jellyfin only (Plex binds read-only); nothing else in the fleet touches it |
|
|
|
|
Both consumers guard the view with
|
|
`UnitConfigItem=ConditionPathIsMountPoint=/mnt/media`, and `scripts/deploy.sh
|
|
dankserver --service jellyfin|plex` calls `media-view.sh install` + `verify`
|
|
before it restarts either container.
|
|
|
|
| Consumer | Bind | Mode | Why that path |
|
|
|---|---|---|---|
|
|
| [[Jellyfin]] | `/mnt/media:/data` | rw | It is the metadata writer; its sidecars become SSD bytes |
|
|
| [[Plex]] | `/mnt/media:/mnt/entropy` | ro | Every path in the PMS database was recorded as `/mnt/entropy/...`; only the HOST side of the bind moved, so its database still resolves |
|
|
|
|
## Commands
|
|
|
|
```bash
|
|
scripts/media-view.sh plan # print the resolved shape, touch nothing
|
|
sudo scripts/media-view.sh install # create dirs, install the unit, mount, enable
|
|
scripts/media-view.sh verify # is /mnt/media really the overlay? (deploy gate)
|
|
scripts/media-view.sh status # mount options + upper-layer size/file count
|
|
scripts/media-view.sh reap # list ghost metadata (media moved, sidecars left)
|
|
sudo scripts/media-view.sh reap --apply # move ghosts to the SSD trash, never delete
|
|
```
|
|
|
|
`--upper-base DIR` and `--lower DIR` relocate the layers; the defaults are the
|
|
host values above.
|
|
|
|
## Laws
|
|
|
|
- **The upper layer must sit on non-rotational storage.** `install` and `verify`
|
|
refuse via `lsblk -dno ROTA` if the upper base lives on the array: an overlay
|
|
whose upper is the spinners is the original problem with extra steps, and it
|
|
silently turns every metadata write back into a seek.
|
|
- **Never mount the view over an unmounted lower.** `RequiresMountsFor=/mnt/entropy`
|
|
plus the container `ConditionPathIsMountPoint` guards: an empty `lowerdir`
|
|
presents an *empty library* as truth, and a media scan would then clear the
|
|
database of everything.
|
|
- **Never delete through `/mnt/media`.** A delete writes an overlayfs whiteout
|
|
(`.wh.<name>`) in the upper layer, which hides the array's copy without
|
|
removing it — the file reappears the moment the view is unmounted, and the
|
|
upper layer now carries a landmine. Deletions belong on the raw path
|
|
(`/mnt/entropy`), which the relayout tool already treats as the only writer.
|
|
`reap` refuses to remove whiteouts and only warns about them.
|
|
- **A relayout must be followed by `reap`.** Renames happen on the raw path, so
|
|
sidecars the view already wrote stay at the old coordinates: a ghost folder
|
|
the servers browse and find nothing in. `reap` lists upper-layer directories
|
|
with no counterpart below and moves them (with their subtree) into a dated
|
|
`.AGENTTRASH/` under the SSD base. It is recursive and file-safe: a sidecar
|
|
that exists only above is normal, so only *directories* are candidates.
|
|
- **File-watcher events do not cross the overlay.** Plex's NFO agent and
|
|
Jellyfin's real-time monitor see inotify events for writes that land *in the
|
|
upper layer*, not for changes on the array underneath. After any relayout or
|
|
externally-written metadata, trigger a library refresh — see
|
|
[[Media Relayout]] for the refresh procedure.
|
|
|
|
## Failure signatures
|
|
|
|
| Symptom | Meaning |
|
|
|---|---|
|
|
| `jellyfin.service` / `plex.service` show `ConditionPathIsMountPoint=... was not met` and stay inactive | `/mnt/media` is not mounted. Do not start them anyway: `scripts/media-view.sh install`, then `verify`. |
|
|
| Jellyfin log: `Read-only file system : '/data/Movies/…/movie.nfo'` | The container is binding the raw array, not the view — the quadlet drifted back or the view is gone. `media-view.sh status`. |
|
|
| A folder appears in a media server with artwork/`.nfo` but no video | Ghost metadata from a relayout. `media-view.sh reap`. |
|
|
| `verify` prints `refused: /mnt/media is not a mountpoint` | Someone made a plain directory there. That is the fail-closed guard working; remove it and install. |
|
|
|
|
## Rollback
|
|
|
|
Revert the commit that pointed the two quadlets at the view and re-deploy both
|
|
services — they bind `/mnt/entropy` read-only again, which is the pre-view
|
|
shape. The NVMe upper layer can stay: nothing reads it once the binds are back,
|
|
and it holds no state either server needs. Removing the mount alone
|
|
(`systemctl stop mnt-media.mount`) is safe while no container binds it, and the
|
|
array is never modified by any of this.
|
|
|
|
## Links
|
|
|
|
- [[Jellyfin]] — the metadata writer; its data layout section.
|
|
- [[Plex]] — the read-only consumer; in-place bind-mount history.
|
|
- [[Media Relayout]] — the tool that moves media on the raw path and the reason `reap` exists.
|
|
- [[Entropy Volume]] — the array underneath.
|