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

5.1 KiB

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).

Script What it does Mutates?
bin/plex-health Read-only health report: server, updates, libraries, unmatched media, Butler tasks, active streams no
bin/poster-audit Finds items whose poster is missing or does not resolve no
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 devPLEX_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

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.