diff --git a/serverAppConfigs-docs/Dankserver/Jellyfin.md b/serverAppConfigs-docs/Dankserver/Jellyfin.md index b2b6af3..d511ffd 100644 --- a/serverAppConfigs-docs/Dankserver/Jellyfin.md +++ b/serverAppConfigs-docs/Dankserver/Jellyfin.md @@ -182,6 +182,48 @@ scripts/jellyfin-providers.sh verify # plugin + key LENGTHS, never key bytes 2. Set metadata/language preferences (`LANG=en_US.UTF-8` pinned in the unit). 3. Configure clients to use `http://192.168.0.235:8096` or the guarded vhost. +## Triggering a scan + +The 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: 12.4 authenticates +only `Authorization: MediaBrowser Token="…"` and refuses a bare `X-Emby-Token` +for every key, valid or not. 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`. 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, so re-issue the identical +request instead of diagnosing. 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 discarded — the status code stops meaning +anything. The fix is `systemctl --user restart jellyfin.service` (never +`deploy.sh`: that runs the never-tested metadata-view cutover), one POST after +health answers, and then believe `ScheduledTasks` State at 130 % CPU, not the +response code. + +**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. + ## See Also - [[Plex]] — co-resident media server on the same volume diff --git a/serverAppConfigs-docs/Dankserver/Media Relayout.md b/serverAppConfigs-docs/Dankserver/Media Relayout.md index 1740802..7a056e1 100644 --- a/serverAppConfigs-docs/Dankserver/Media Relayout.md +++ b/serverAppConfigs-docs/Dankserver/Media Relayout.md @@ -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//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/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/-/` — 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 diff --git a/serverAppConfigs-docs/Dankserver/Plex.md b/serverAppConfigs-docs/Dankserver/Plex.md index 0e7b831..ab24a5e 100644 --- a/serverAppConfigs-docs/Dankserver/Plex.md +++ b/serverAppConfigs-docs/Dankserver/Plex.md @@ -87,3 +87,30 @@ root runs as the `dank` user, and the deployer adopts the data root with while the HTTP layer finishes loading; the first boot of a years-old library took ~10 minutes (2026-09 cutover). The deployer gate now allows 600 s before failing; later boots are much faster (warm page cache). + +## Triggering a scan + +A rename on disk is invisible until the section is rescanned, and a moved file is +a `Couldn't find the file to stream` 404 until that scan passes it. Run this from +the host shell through `podman exec`, never from a neighbour container: PMS'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//cmdline`; it lives in `Preferences.xml` inside the RW data bind. + +```bash +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'