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>
96 lines
4.1 KiB
Markdown
96 lines
4.1 KiB
Markdown
# TV Shows: 101 missing posters (investigated 2026-08-15)
|
|
|
|
What the symptom looked like, what it actually was, and what is still unknown.
|
|
|
|
## Symptom
|
|
|
|
Grey placeholder posters scattered through the **TV Shows** library in the Plex UI.
|
|
|
|
## Scope
|
|
|
|
| Library | Type | Affected |
|
|
| --- | --- | --- |
|
|
| TV Shows (id 2) | shows | **101 of 283** (36%) |
|
|
| TV Shows (id 2) | seasons | 0 of 954 |
|
|
| Movies (id 1) | movies | 0 (sampled 60 of 1059) |
|
|
| Movies 4k (id 3) | movies | 0 (sampled 40 of 79) |
|
|
|
|
Only show-level posters, only in one library. Seasons and episodes were untouched, which
|
|
is why the library still looked half-normal.
|
|
|
|
## The two failure modes
|
|
|
|
They are indistinguishable in the UI, and only one is visible to a metadata-only audit:
|
|
|
|
| Mode | Count | What it looks like in the API |
|
|
| --- | --- | --- |
|
|
| `MISSING` | 6 | the item has no `thumb` field at all |
|
|
| `BROKEN` | 95 | the item **has** a `thumb` field, but fetching it returns **404** |
|
|
|
|
The 95 BROKEN shows are the trap. `/library/sections/2/all` reports a perfectly
|
|
well-formed `thumb` URL for each one — e.g. `/library/metadata/13343/thumb/1785398727` —
|
|
and only an actual HTTP request reveals it serves an 85-byte HTML 404 page.
|
|
|
|
## Root cause: a lost selection pointer, not a lost file
|
|
|
|
For every affected show, `/library/metadata/<key>/posters` still returned a full
|
|
candidate list — 85 to 158 entries — **including the agent's own poster already
|
|
downloaded into the item's metadata bundle** as a `metadata://posters/...` entry.
|
|
|
|
What was gone was the selection:
|
|
|
|
```
|
|
affected shows with zero `selected: true` candidates: 101 / 101
|
|
control sample of working shows, selected count: 1 each (15 / 15)
|
|
affected shows with the poster still in the bundle: 100 / 101
|
|
```
|
|
|
|
So the images were never deleted. The database simply no longer marked *any* candidate as
|
|
selected for those items, and a show with nothing selected has no poster to serve.
|
|
|
|
Because the file was still local, the repair was a pure database operation — instant, no
|
|
internet round trip, no re-download:
|
|
|
|
```bash
|
|
PUT /library/metadata/<key>/poster?url=metadata://posters/tv.plex.agents.series_<hash>
|
|
```
|
|
|
|
Validated on one show (Severance, `13343`) before touching the rest: `thumb` went from
|
|
`404` to `200 image/jpeg`, 533 KB.
|
|
|
|
## Hypotheses tested and rejected
|
|
|
|
- **Unmatched media.** Rejected: all 283 shows had real `plex://show/...` GUIDs; none had
|
|
a `local://` GUID. The affected shows also had full season and episode counts.
|
|
- **A bad bulk metadata refresh.** A refresh clearly did run — 77 shows share an
|
|
`updatedAt` inside a 35-second window (~2026-07-30). But it does not correlate with the
|
|
damage: of the items in that window 54 are fine and 23 are broken, while 72 broken items
|
|
sit outside it entirely. Rejected as *the* cause.
|
|
- **A stale/expired thumb URL format.** Rejected: broken and working items had
|
|
structurally identical `thumb` URLs with timestamps from the same refresh.
|
|
|
|
## Still unknown
|
|
|
|
**Which event cleared the selection flags.** Establishing that needs the Plex server logs
|
|
on odin (`Plex Media Server.log`), which requires filesystem access to the container's
|
|
appdata — not available from `dev` at the time of writing (no SSH key is authorized on
|
|
odin). The `CleanOldBundles` Butler task is enabled on a 7-day interval and is the obvious
|
|
suspect to check first, but note that it does **not** fit the evidence cleanly: the poster
|
|
files survived in the bundles: only the database pointers were lost.
|
|
|
|
Worth watching: if posters rot again on roughly a 7-day cadence, that points at a Butler
|
|
task; if it correlates with a Plex version bump, it points at a migration.
|
|
|
|
## Repair record
|
|
|
|
| Step | Result |
|
|
| --- | --- |
|
|
| Validation on Severance | poster restored, verified `200 image/jpeg` |
|
|
| Bulk repair, local candidates | 100 shows restored |
|
|
| True Detective (no local candidate) | restored via `--allow-remote` from the agent |
|
|
| Final audit | **283 / 283 posters resolve** |
|
|
|
|
Tooling built from this: [../bin/poster-audit](../bin/poster-audit) and
|
|
[../bin/poster-repair](../bin/poster-repair). Re-run the audit if the symptom returns —
|
|
it is read-only and will say in a couple of minutes whether it is the same failure.
|