refactor(media): mount the view inside /var/lib/media-meta instead of under /mnt
The merged door was /mnt/media, which read like a third volume and split the host state across two trees. It is now /var/lib/media-meta/view, so the array layer, the SSD upper layer, the kernel workdir, the reap trash and the merged tree are one directory — and the mount table gains no new /mnt entry beside the array. systemd names a mount unit after its ESCAPED path, so the repo copy keeps the readable name media-meta-view.mount while `install` writes the host file as var-lib-media\x2dmeta-view.mount; `plan` prints both and the page records them, so nobody hunts for a unit that "does not exist". install also creates the mount point itself, because systemd will not. The seam now additionally pins that the unit's Where= is exactly the path both containers bind — guard and bind cannot drift apart. The Quadlet Lifecycle phantom-path example is corrected too: it named an invented /mnt/media/*, while the real phantom is the legacy /opt/jellyfin/* that dankserver/compose.yaml still carries for a service whose roots moved to /home/dank/Containers/jellyfin.
This commit is contained in:
1 parent
9706f5d2de
commit
3b29e173c2
13 files changed
+93
-67
No files matched your search
@@ -8,12 +8,17 @@ 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.
|
||||
`/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.
|
||||
|
||||
## Shape
|
||||
|
||||
@@ -22,20 +27,21 @@ SSD. One tree to both servers, one copy of the media.
|
||||
| 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 |
|
||||
| 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=/mnt/media`, and `scripts/deploy.sh
|
||||
dankserver --service jellyfin|plex` calls `media-view.sh install` + `verify`
|
||||
before it restarts either container.
|
||||
`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]] | `/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 |
|
||||
| [[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
|
||||
@@ -46,14 +52,14 @@ this change touches no library setting.
|
||||
```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 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; the defaults are the
|
||||
host values above.
|
||||
`--upper-base DIR` and `--lower DIR` relocate the layers for `plan` and `reap`;
|
||||
the defaults are the host values above.
|
||||
|
||||
## Laws
|
||||
|
||||
@@ -64,8 +70,9 @@ host values above.
|
||||
- **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
|
||||
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.<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
|
||||
@@ -87,19 +94,20 @@ host values above.
|
||||
|
||||
| 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.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: /mnt/media is not a mountpoint` | Someone made a plain directory there. That is the fail-closed guard working; remove it and install. |
|
||||
| `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. 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.
|
||||
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
|
||||
|
||||
|
||||
Reference in new issue
Block a user