# plex Operational scripts for the Plex Media Server running as a container on **odin**. Reached at `https://plex.bergerhouse.net` (DNS resolves to odin's LAN address `192.168.90.103`, routed over the tailnet — see [../docs/odin-access.md](../docs/odin-access.md)). | Script | What it does | Mutates? | | --- | --- | --- | | [bin/plex-health](bin/plex-health) | Read-only health report: server, updates, libraries, unmatched media, Butler tasks, active streams | no | | [bin/poster-audit](bin/poster-audit) | Finds items whose poster is missing or does not resolve | no | | [bin/poster-repair](bin/poster-repair) | Re-selects the poster for items the audit flags | **yes** | ## Prerequisites - **Where it runs:** this workstation (`dev`), targeting odin over Tailscale. Nothing needs to be installed on odin itself — everything goes through the Plex HTTP API. - **Interpreters:** bash 4+ - **External tools:** `curl`, `jq` - **Access needed:** an admin `X-Plex-Token`. No SSH to odin is required. Unlike odin's `myunraid.net` vhost, `plex.bergerhouse.net` presents a TLS chain that verifies cleanly from `dev` — `PLEX_INSECURE` stays `0` and no `-k` is needed. ## Configuration Copy `.env.example` to `.env` and fill it in. `.env` is gitignored repo-wide. | Variable | Required | Description | | --- | --- | --- | | `PLEX_URL` | yes | Base URL of the server. Use `https://` — plain HTTP 302-redirects. | | `PLEX_TOKEN` | yes | Admin `X-Plex-Token`. | | `PLEX_INSECURE` | no | `1` to skip TLS verification. Not needed for `PLEX_URL` above. | | `PLEX_TIMEOUT` | no | Per-request timeout in seconds (default 30). | | `POSTER_JOBS` | no | Parallel HTTP checks during a poster scan (default 8). | To get a token: Plex Web → any library item → **⋯ → Get Info → View XML**, then copy the `X-Plex-Token` query parameter out of the URL that opens. The remaining `ODIN_*` and `UNRAID_*` variables in `.env.example` are placeholders for future scripts that need filesystem or host-level access. Nothing here uses them yet. ## Usage ```bash bin/plex-health # health report (exit 0 ok / 1 warn / 2 fail) bin/plex-health --posters # also check every poster (slow: 1 request per item) bin/poster-audit # what is broken in TV Shows bin/poster-audit --section Movies # any library, by title or id bin/poster-audit --json # machine-readable bin/poster-repair --dry-run # preview, changes nothing bin/poster-repair # apply bin/poster-repair --limit 5 # apply to the first 5 only ``` A full TV Shows scan makes ~283 HTTP requests and takes a couple of minutes. The scan dominates the runtime, not the repair. ## Behaviour notes - **Idempotent:** yes. `poster-repair` only touches items whose poster does not currently resolve, so a second run repairs nothing. `plex-health` never writes. - **Destructive operations:** none. `poster-repair` selects a poster that is *already in the item's metadata bundle* — no image is downloaded, deleted, or overwritten, and no other metadata field is touched. Items with a working poster are skipped, so a manually-chosen poster is never replaced. - **`--allow-remote` is the one exception:** for an item with no locally-cached poster it pulls one from the metadata agent over the internet, which may select a different image than the one originally chosen. Off by default. - **Scheduling:** manual for now. `plex-health` is cron-safe and exit-code driven if you want it on a timer. ## Gotchas **A missing poster has two distinct causes, and the Plex UI shows them identically.** Checking metadata alone will tell you the library is fine when it is not: - `MISSING` — the item has no `thumb` field at all. - `BROKEN` — the item *has* a `thumb` field, but fetching that URL returns **404**. This was 94 of the 100 affected shows. Any audit that only looks for absent fields misses almost all of them. **The failure is a lost database pointer, not a lost file.** For a broken item, `/library/metadata//posters` still lists all the candidates — often 85+ of them — with the agent's own poster already downloaded into the bundle as a `metadata://` entry. What is gone is the `selected: true` flag: every candidate reads `selected: false`, so the `thumb` URL resolves to nothing. A working item has exactly one candidate selected. Re-selecting the local candidate is therefore instant and needs no internet access. **Diagnose with the `selected` count, not the poster list length.** An affected show has plenty of poster candidates; the count of *selected* ones is the signal. **`PUT /library/metadata//poster?url=` needs the candidate's `ratingKey`,** which for a local candidate is the `metadata://posters/...` string, not the `/library/...` path from the same object's `key` field. **Season and episode posters were not affected** — only show-level ones, and only in the TV Shows library. The movie libraries sampled clean. Worth re-checking rather than assuming if this recurs. **The 2-minute tool timeout will kill a full repair run.** Run it with `nohup ... &` and tail the log, or use `--limit`.