|
|
|
@@ -79,41 +79,12 @@ An apply also leaves the release folders it emptied standing where they were —
|
|
|
|
|
hundreds of them at the library root — which is what `sweep` removes, dry-run
|
|
|
|
|
first, from the same journal.
|
|
|
|
|
|
|
|
|
|
After it lands, both servers need a scan or the change is invisible.
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
# Run this from the host shell through `podman exec`, never from a neighbour
|
|
|
|
|
# container: Plex's HTTPD closes without a single byte on any request that
|
|
|
|
|
# arrives from another container's netns (10.89.x.x reads as "empty reply", not
|
|
|
|
|
# as a 401). The token rides stdin, never argv, where any process on the host
|
|
|
|
|
# could read it out of /proc/<pid>/cmdline.
|
|
|
|
|
umask 077
|
|
|
|
|
podman exec plex sh -c "sed -n 's/.*PlexOnlineToken=\"\\([^\"]*\\)\".*/\\1/p' \
|
|
|
|
|
'/config/Library/Application Support/Plex Media Server/Preferences.xml'" >/tmp/px.tok
|
|
|
|
|
podman exec -i plex sh -c 'read -r T
|
|
|
|
|
for k in $(curl -s -H "X-Plex-Token: $T" -H "Accept: application/json" \
|
|
|
|
|
http://127.0.0.1:32400/library/sections | grep -o "\"key\":\"[0-9]*\"" | cut -d\" -f4); do
|
|
|
|
|
curl -s -o /dev/null -w "plex section $k: %{http_code}\n" -X POST \
|
|
|
|
|
-H "X-Plex-Token: $T" "http://127.0.0.1:32400/library/sections/$k/refresh"
|
|
|
|
|
done' </tmp/px.tok
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
[[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, in the `MediaBrowser` form —
|
|
|
|
|
Jellyfin 12.4 refuses a bare `X-Emby-Token`, and the backslashes below matter:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
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; anything else queued no scan. A `401` from a key that answers `200` on a hand-typed request is this block's quoting, not the server — bash `printf` collapses `\"` to `"`, curl ends the quoted value at that quote, and Jellyfin receives `Authorization: MediaBrowser Token=` with nothing after it (`\\"` answers 204, `\"` answers 401, both run on this host). One accepted refresh is the whole procedure: a second one while a scan runs moves `Scan Media Library` to `Cancelling`, and once the runner holds `Cancelling` every later `204` is accepted and then dropped — the fix is `systemctl --user restart jellyfin.service` (never `deploy.sh`: that runs the metadata-view cutover), one POST after health answers, and then believe `ScheduledTasks` State `Running`, not the status code.
|
|
|
|
|
After it lands, both servers need a scan or the change is invisible. [[Plex]] and
|
|
|
|
|
[[Jellyfin]] own the exact requests; two laws hold at this step: `204` is
|
|
|
|
|
acceptance and anything else queued nothing — re-issue the identical request
|
|
|
|
|
rather than diagnosing — and ONE accepted refresh is the whole procedure, because
|
|
|
|
|
a second while a scan runs moves `Scan Media Library` to `Cancelling` and every
|
|
|
|
|
later `204` is accepted and then discarded.
|
|
|
|
|
|
|
|
|
|
### Proving the move landed
|
|
|
|
|
|
|
|
|
@@ -204,21 +175,30 @@ python3 scripts/media-census.py enumerate --root /mnt/entropy/TVShows \
|
|
|
|
|
--out /tmp/tv_enum.tsv
|
|
|
|
|
python3 scripts/media-census.py probe --root /mnt/entropy/TVShows \
|
|
|
|
|
--enum /tmp/tv_enum.tsv --jobs 6 --out /tmp/tv_probed.tsv
|
|
|
|
|
# The probe is the slow step: ffprobe reads both ends of every file.
|
|
|
|
|
# The probe is the slow step: ffprobe reads both ends of every file. So re-use
|
|
|
|
|
# the last one — a duration belongs to the file, not to its name: re-key the
|
|
|
|
|
# previous census through that pass's own journal (`src` -> `dst`) instead of
|
|
|
|
|
# seeking the array again. It is also the only column the film arm reads, and
|
|
|
|
|
# only for a file with no marker and no provider row (4,794 of 58,551 today,
|
|
|
|
|
# all of them covered by a re-key that costs seconds, not an hour).
|
|
|
|
|
# 3. The plan. --tmdb needs TMDB_API_KEY, --tvdb needs ~/.config/secrets/tvdb
|
|
|
|
|
# (`api_key=`, never argv); both write into the SAME cache file, so the plan is
|
|
|
|
|
# reproducible offline afterwards. Run it once to fill the cache, then again:
|
|
|
|
|
# the second run is the one whose numbers are quoted.
|
|
|
|
|
# Paths are only strings to a dry run, so this step can run on the workstation
|
|
|
|
|
# against scp'd TSVs — which is where the two provider secrets live.
|
|
|
|
|
python3 scripts/media-shows.py --root /mnt/entropy/TVShows --enum /tmp/tv_enum.tsv \
|
|
|
|
|
--titles /tmp/tv_titles.tsv --probed /tmp/tv_probed.tsv \
|
|
|
|
|
--tmdb-cache /mnt/entropy/media-tv-names.json --tmdb --tvdb \
|
|
|
|
|
--park /mnt/entropy/media-park/TVShows \
|
|
|
|
|
--out /mnt/entropy/media-relayout-shows-plan.tsv \
|
|
|
|
|
--exceptions /mnt/entropy/media-relayout-shows-exceptions.txt
|
|
|
|
|
--out /mnt/entropy/media-relayout-shows2-plan.tsv \
|
|
|
|
|
--exceptions /mnt/entropy/media-relayout-shows2-exceptions.txt
|
|
|
|
|
# 4. Read the exceptions file FIRST — it is the review, not a log. Same executor
|
|
|
|
|
# as above, but on this arm's OWN journal: a shared journal cannot be unwound.
|
|
|
|
|
scripts/media-apply.py apply --plan /mnt/entropy/media-relayout-shows-plan.tsv \
|
|
|
|
|
--root /mnt/entropy --journal /mnt/entropy/media-relayout-shows.jsonl --apply
|
|
|
|
|
# as above, but on this arm's OWN journal, NAMED PER PASS: the journal of an
|
|
|
|
|
# applied pass is its unwind record, so a second pass that reuses the name
|
|
|
|
|
# destroys the only map back.
|
|
|
|
|
scripts/media-apply.py apply --plan /mnt/entropy/media-relayout-shows2-plan.tsv \
|
|
|
|
|
--root /mnt/entropy --journal /mnt/entropy/media-relayout-shows2.jsonl --apply
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Evidence, strongest first — the order the planner believes things:
|
|
|
|
@@ -318,9 +298,8 @@ What it refuses:
|
|
|
|
|
rename to `.m4a` would claim it is; **audio no provider row names parks** too,
|
|
|
|
|
because unmatched audio is a tagging job, not a renaming job.
|
|
|
|
|
|
|
|
|
|
The plan applies on 2026-10-31 through `/mnt/entropy/media-relayout-music.jsonl`;
|
|
|
|
|
its rollback is that journal run through `media-apply.py rollback --apply`. ONE
|
|
|
|
|
writer at a time on this array: two rename passes race, and neither journal tells.
|
|
|
|
|
ONE writer at a time on this array: two rename passes race, and neither journal
|
|
|
|
|
tells — which is why each arm above carries its own journal name.
|
|
|
|
|
|
|
|
|
|
## The audiobook library's own relayout
|
|
|
|
|
|
|
|
|
@@ -376,9 +355,8 @@ no chapter row names, and a loose file in a folder holding two books all park un
|
|
|
|
|
that unrelated books share, or a chapter number mistagged as the artist (`0.4`).
|
|
|
|
|
`Various Artists` is not a placeholder — that is a multi-cast read.
|
|
|
|
|
|
|
|
|
|
The plan stands reviewed at `/mnt/entropy/media-relayout-audiobooks-plan.tsv`
|
|
|
|
|
(55,114 moves, 7,764 parked, executor preflight refused nothing); it applies on its
|
|
|
|
|
own journal, behind the music writer, and then refreshes the Plex section.
|
|
|
|
|
This arm applies behind the music writer — one writer at a time — on its own
|
|
|
|
|
journal, and then refreshes the Plex section.
|
|
|
|
|
|
|
|
|
|
## Laws
|
|
|
|
|
|
|
|
|
@@ -419,19 +397,6 @@ own journal, behind the music writer, and then refreshes the Plex section.
|
|
|
|
|
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.
|
|
|
|
@@ -460,8 +425,7 @@ own journal, behind the music writer, and then refreshes the Plex section.
|
|
|
|
|
recreates the folder, so the sweep is undone by the same command as the moves.
|
|
|
|
|
Proof carries downward: inside a folder the journal proves emptied, every
|
|
|
|
|
empty directory goes with it. A release's `Screens/` or `Sample/` is never a
|
|
|
|
|
move source itself, so 146 of them held their emptied parents on disk after
|
|
|
|
|
the first sweep.
|
|
|
|
|
move source itself, so it can hold an emptied parent until the next sweep.
|
|
|
|
|
A failed `rmdir` is not a removal: only a folder that is really gone leaves
|
|
|
|
|
scope, so the next sweep finishes what a permission problem stopped.
|
|
|
|
|
- **An episode-shaped name marks a SERIES, not a film — and the series is the
|
|
|
|
|