|
|
|
@@ -1,7 +1,7 @@
|
|
|
|
|
---
|
|
|
|
|
type: reference
|
|
|
|
|
title: Media Relayout
|
|
|
|
|
description: "Census, plan and apply the movie library into Jellyfin format: the gates to read, and the rollback."
|
|
|
|
|
description: "Census, plan and apply the movie library into Jellyfin format, and move TV out of it: the gates, refusals, rollback."
|
|
|
|
|
tags: [dankserver, media-server, jellyfin, plex, procedure]
|
|
|
|
|
timestamp: 2026-10-30T00:00:00Z
|
|
|
|
|
---
|
|
|
|
@@ -91,19 +91,23 @@ podman exec -i plex sh -c 'read -r T
|
|
|
|
|
done' </tmp/px.tok
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
[[Jellyfin]] has no credential on this box that can trigger a scan: the two keys
|
|
|
|
|
in its database are Seerr's, unused since 2026-09-01, and answer 401 to every
|
|
|
|
|
call. Mint one (*Dashboard → Advanced → API Keys*, Administrator) and store it in
|
|
|
|
|
`~/.config/secrets/jellyfin`, or press *Library → Scan All Libraries* in the web
|
|
|
|
|
UI. A scripted scan passes the key in a mode-600 curl config, never in a header
|
|
|
|
|
argument:
|
|
|
|
|
[[Jellyfin]]'s scan key lives in `~/.config/secrets/jellyfin` (mode 600; the
|
|
|
|
|
same value is `JELLYFIN_API_KEY` in the ignored `dankserver/.env`). It rides a
|
|
|
|
|
mode-600 curl config, never a header argument, and it must use the
|
|
|
|
|
`MediaBrowser` form — Jellyfin 12.4 refuses a bare `X-Emby-Token` for any key:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
umask 077; printf 'header = "X-Emby-Token: %s"\n' "$(cat ~/.config/secrets/jellyfin)" >/tmp/jf.hdr
|
|
|
|
|
curl -s -K /tmp/jf.hdr -o /dev/null -w "jellyfin scan: %{http_code}\n" \
|
|
|
|
|
-X POST http://127.0.0.1:8096/Library/Refresh
|
|
|
|
|
umask 077
|
|
|
|
|
{ printf 'header = "Authorization: MediaBrowser Token=\\"%s\\""\n' \
|
|
|
|
|
"$(cat ~/.config/secrets/jellyfin)"
|
|
|
|
|
echo 'url = "http://127.0.0.1:8096/Library/Refresh"'
|
|
|
|
|
echo 'request = "POST"'; } >/tmp/jf.hdr
|
|
|
|
|
curl -s -K /tmp/jf.hdr -o /dev/null -w "jellyfin scan: %{http_code}\n"
|
|
|
|
|
shred -u /tmp/jf.hdr
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
`204` is acceptance; the scan then runs on the server's own schedule.
|
|
|
|
|
|
|
|
|
|
## TV in the movie library
|
|
|
|
|
|
|
|
|
|
A box set that arrives in the movie share stays there until someone moves it:
|
|
|
|
@@ -133,13 +137,22 @@ from the box-set folder. What it refuses it prints and leaves alone:
|
|
|
|
|
- a destination two files both claim: the executor refuses the whole plan
|
|
|
|
|
before its first rename, and only Frank decides which copy is the keeper.
|
|
|
|
|
- an episode the destination library already holds at the same byte count. The
|
|
|
|
|
movie share carried a second copy of all 124 Monk episodes — the TV library
|
|
|
|
|
has had the same box set for years, every episode the same size, ~31 GiB of
|
|
|
|
|
it. The mover leaves such a series in place and says so; a duplicate is
|
|
|
|
|
deleted or kept by Frank, never copied into one library by a tool.
|
|
|
|
|
movie share carried a second copy of the whole Monk box set — 124 episodes
|
|
|
|
|
both sides, the same episode list, every byte count identical, 31.5 GiB each
|
|
|
|
|
way, and head+tail blake2b identical on a 6-episode sample. The mover leaves
|
|
|
|
|
such a series in place and says so; a duplicate is deleted or kept by Frank,
|
|
|
|
|
never copied into one library by a tool.
|
|
|
|
|
|
|
|
|
|
A sidecar that shares an episode's own name (`.srt`, `.nfo`) moves with it.
|
|
|
|
|
|
|
|
|
|
Below the threshold the mover prints and leaves, so a lone downloaded episode of
|
|
|
|
|
a real show stays in the movie library on purpose — six did on 2026-10-30.
|
|
|
|
|
Filing one by hand is the same executor on a hand-written plan and the same
|
|
|
|
|
journal; the trap is the series year. Read it from the file's own name or the
|
|
|
|
|
folder it owns, never from the title whose bonus folder it happens to be parked
|
|
|
|
|
in — `The Cleveland Show S03E07` inside `Die Hard (1988)/extras/` is not a 1988
|
|
|
|
|
show. A name whose marker is `Part 2` is a film and stays put.
|
|
|
|
|
|
|
|
|
|
## Laws
|
|
|
|
|
|
|
|
|
|
- **A moved file is a 404 in [[Plex]] until its section is rescanned.** The
|
|
|
|
@@ -166,6 +179,32 @@ A sidecar that shares an episode's own name (`.srt`, `.nfo`) moves with it.
|
|
|
|
|
folder holding it, and extras drifted into a neighbour. A bare number counts
|
|
|
|
|
only when it could be a release year, so `1080` and `2160` never pose as
|
|
|
|
|
agreement between two names.
|
|
|
|
|
- **Compare two media libraries with the series engine, never a path
|
|
|
|
|
substring.** Filtering paths for `monk` also returns *Monkey Dust*, and
|
|
|
|
|
letting whichever file a walk happens to reach first stand for an episode
|
|
|
|
|
paired two libraries into a fake duplicate — the same sloppiness pointed the
|
|
|
|
|
other way deletes a file that is unique. The comparison is `media-titles.ep_of`
|
|
|
|
|
on both sides: series, season, episode, bytes, and a hash before anything is
|
|
|
|
|
trashed.
|
|
|
|
|
- **A deletion is staged on the item's own filesystem.** The trash rule names
|
|
|
|
|
`~/.AGENTTRASH`, but `/home` and `/mnt/entropy` are different devices: a 31 GiB
|
|
|
|
|
stage there is a copy rather than a rename, it has to fit in the 187 GiB of
|
|
|
|
|
free space, and `rename(2)` refuses it anyway. Trash goes beside the item —
|
|
|
|
|
`/mnt/entropy/.AGENTTRASH/<date>-<topic>/` — so it costs one inode change and
|
|
|
|
|
rolls back exactly like the move that made it.
|
|
|
|
|
- **A Jellyfin 401 is the header, not the key.** 12.4 authenticates only
|
|
|
|
|
`Authorization: MediaBrowser Token="…"` and refuses a bare `X-Emby-Token` for
|
|
|
|
|
every key, valid or not. This page carried "no credential on this box can
|
|
|
|
|
trigger a scan, the two Seerr keys answer 401" for weeks because of that
|
|
|
|
|
header — the keys were innocent. Before believing a 401, prove the key against
|
|
|
|
|
the server's own table: `select count(*) from ApiKeys where AccessToken = ?`
|
|
|
|
|
on `jellyfin.db` opened `mode=ro&immutable=1`.
|
|
|
|
|
- **A scan that must see a new folder has to be recursive.** `POST
|
|
|
|
|
/Library/Refresh` is the incremental pass, and inotify does not cross a
|
|
|
|
|
rootless podman bind, so folders created on the host stay invisible to it. The
|
|
|
|
|
move is `POST /Items/{id}/Refresh?Recursive=true` (204) per library, the id
|
|
|
|
|
from `GET /Library/VirtualFolders` — its field is `ItemId`, not `Id` — and
|
|
|
|
|
that same record's `RefreshStatus` is the progress read.
|
|
|
|
|
- **A file whose name carries no words is bonus material, not a folder.**
|
|
|
|
|
`.Extra 1 ( 1H 27min multisub ).mkv` parses to `1`; a movie folder named `1`
|
|
|
|
|
is not a movie. It becomes a review line telling a human to name the file.
|
|
|
|
|