Checking only shows and movies was hiding the larger problem. Battlestar Galactica has a real plex:// GUID, a poster, and passes a show-level audit, while all 74 of its episodes and all 5 of its seasons carry local:// GUIDs and display as "Episode 1", "Episode 2" with no titles or artwork. Surveying every level found 97 unmatched episodes of 11531 and 7 unmatched seasons of 954, across four shows — none of it visible before. Also adds known-issues.conf, an accepted-findings list. The unmatched movie in the Movies library is the first entry: the 2003 BSG miniseries is catalogued as television, so the movie agent has no record to match it against and the item cannot be fixed in place. Listed entries are still printed, as [info] rather than [warn], so they stop counting as unresolved faults without ever being silently suppressed. The episode list is a ~30MB response, so it is written to a temp file before parsing rather than piped into jq. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
117 lines
5.9 KiB
Markdown
117 lines
5.9 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** |
|
|
|
|
[known-issues.conf](known-issues.conf) lists findings that have been reviewed and
|
|
accepted. `plex-health` still reports them, but as `[info]` rather than `[warn]`, so a
|
|
known-and-accepted item stops making the whole run read as failing. Nothing is ever
|
|
suppressed silently.
|
|
|
|
## 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. Every other library was later checked in full (not sampled) and came
|
|
back 100% clean. Worth re-checking rather than assuming if this recurs.
|
|
|
|
**A matched show can be full of unmatched episodes.** Checking only the top level of a
|
|
library is not enough: Battlestar Galactica has a real `plex://show/...` GUID and looks
|
|
healthy in a show-level audit, while all 74 of its episodes carry `local://` GUIDs and
|
|
display as "Episode 1", "Episode 2"… with no titles or artwork. `plex-health` therefore
|
|
checks show, season, *and* episode level. The episode list for a library this size is a
|
|
~30 MB response — it is written to a temp file before parsing, never piped into `jq`.
|
|
|
|
**The 2-minute tool timeout will kill a full repair run.** Run it with `nohup ... &` and
|
|
tail the log, or use `--limit`.
|