Files
scripts/plex/README.md
T
Lucas BergerandClaude Opus 5 3df1cca91c Add plex collection: health check and poster repair
Plex on odin had 101 of 283 TV shows showing grey placeholder posters. The
cause was not missing images: for every affected show the poster was still
downloaded in the item's metadata bundle, but no candidate was marked
`selected` in the database, so the thumb URL resolved to a 404. Working shows
had exactly one selected candidate; all 101 affected had zero.

Two details make this easy to misdiagnose, so both are captured in the docs:
only 6 items lacked a `thumb` field outright, while 95 advertised a
well-formed thumb URL that 404s on fetch — a metadata-only audit reports the
library as healthy. And a bulk metadata refresh, the obvious suspect, does not
correlate with the damage.

Adds three scripts against the Plex HTTP API (no SSH to odin needed):

  plex-health    read-only report — server, updates, libraries, unmatched
                 media, Butler tasks, active streams; exit 0/1/2
  poster-audit   read-only; fetches every thumb to find the 404s
  poster-repair  re-selects the locally-cached poster; --dry-run, idempotent,
                 skips items whose poster already works so manually-chosen
                 artwork is never replaced

All 283 TV Shows posters now resolve. The health check also surfaced one
unmatched movie, Battlestar Galactica the Mini Series.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 21:15:06 -04:00

105 lines
5.1 KiB
Markdown

# 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/<key>/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/<key>/poster?url=<candidate>` 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`.