--- 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 `/var/lib/media-meta/view` 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 media bytes stay there and the *writes* land on SSD. One tree to both servers, one copy of the media. The mount point is not a volume and holds nothing while unmounted. It sits inside `/var/lib/media-meta` so that layers, kernel bookkeeping, the reap trash and the merged door are one directory — and so the mount table grows no new `/mnt` entry next to the array. **Not deployed on Dankserver — verified 2026-10-31.** `scripts/media-view.sh status` answers `view: /var/lib/media-meta/view NOT MOUNTED`, `/var/lib/media-meta` does not exist, and the installed quadlets under `~/.config/containers/systemd/` still carry the pre-view binds — `/mnt/entropy:/data:ro` for [[Jellyfin]], `/mnt/entropy:/mnt/entropy:ro` for [[Plex]]. Everything below is the shape git describes, not the shape the servers run. The cutover is an ordinary deploy: `deploy.sh dankserver --service jellyfin|plex` runs `ensure_media_view` (`install` then `verify`) before it restarts the container, so it must not be fired through the middle of a library scan. Until that deploy lands there is no upper layer, so no relayout can leave ghost metadata and `reap` has nothing to do — `media-apply.py` prints that follow-up only when the view really is a mountpoint. ## 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 | `/var/lib/media-meta/view` — the merged tree both servers bind | | Unit (repo) | `dankserver/host/systemd/media-meta-view.mount` — the source of truth | | Unit (host) | `var-lib-media\x2dmeta-view.mount` — systemd names a mount unit after its escaped path (`-` becomes `\x2d`); `media-view.sh install` writes the repo file under that name | | 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=/var/lib/media-meta/view`, 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]] | `/var/lib/media-meta/view:/data` | rw | It is the metadata writer; its sidecars become SSD bytes | | [[Plex]] | `/var/lib/media-meta/view:/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 | Putting the sidecars inside Plex's reach is not the same as making Plex use them: a library parses `.nfo` only with its *Local media information* agent enabled, and this change touches no library setting. ## 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 the view 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 for `plan` and `reap`; 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. systemd does not create a mount point, so `install` creates `view` itself; without it the mount simply fails. - **Never delete through the view.** A delete writes an overlayfs whiteout (`.wh.`) 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 | The view 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: …/view is not a mountpoint` | A plain directory is sitting where the overlay belongs. That is the fail-closed guard working; `install` on the box. | | `systemctl status` finds no such unit | Look under the escaped name, `var-lib-media\x2dmeta-view.mount`, not the repo file name. | ## 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. Unmounting alone (`systemctl stop 'var-lib-media\x2dmeta-view.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.