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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
28e0fdb631
commit
3df1cca91c
@@ -0,0 +1,95 @@
|
||||
# shellcheck shell=bash
|
||||
# Sourced helper for talking to the Plex Media Server HTTP API.
|
||||
# Not executable — source it: . "$COLLECTION_DIR/lib/plex-api.sh"
|
||||
#
|
||||
# Provides: plex_load_env, plex_require_deps, plex_get, plex_get_raw, plex_put,
|
||||
# plex_check_auth, plex_log, plex_warn, plex_die
|
||||
|
||||
plex_log() { printf '%s\n' "$*"; }
|
||||
plex_warn() { printf '%s\n' "$*" >&2; }
|
||||
plex_die() { plex_warn "error: $*"; exit 1; }
|
||||
|
||||
# Load the collection's .env into the environment. Silent if absent — the
|
||||
# caller's :? checks produce the actionable message.
|
||||
plex_load_env() {
|
||||
local env_file="${1:?plex_load_env needs a path}"
|
||||
[[ -f "$env_file" ]] || return 0
|
||||
set -a; . "$env_file"; set +a
|
||||
}
|
||||
|
||||
plex_require_deps() {
|
||||
local missing=()
|
||||
local dep
|
||||
for dep in curl jq; do
|
||||
command -v "$dep" >/dev/null 2>&1 || missing+=("$dep")
|
||||
done
|
||||
(( ${#missing[@]} == 0 )) || plex_die "missing required tools: ${missing[*]}"
|
||||
}
|
||||
|
||||
# Assemble the curl flags shared by every request. PLEX_INSECURE=1 adds -k;
|
||||
# plex.bergerhouse.net verifies cleanly from `dev`, so it should stay 0.
|
||||
_plex_curl_flags() {
|
||||
local -n out=$1
|
||||
out=(--silent --show-error --location-trusted --max-time "${PLEX_TIMEOUT:-30}")
|
||||
[[ "${PLEX_INSECURE:-0}" == "1" ]] && out+=(--insecure)
|
||||
return 0
|
||||
}
|
||||
|
||||
# plex_get <path> [query...] -> JSON on stdout
|
||||
# Path is relative to PLEX_URL and must start with '/'. Extra args are passed to
|
||||
# curl, so use --get --data-urlencode 'k=v' for query parameters.
|
||||
plex_get() {
|
||||
local path="${1:?plex_get needs a path}"; shift
|
||||
local flags; _plex_curl_flags flags
|
||||
curl "${flags[@]}" \
|
||||
-H 'Accept: application/json' \
|
||||
-H "X-Plex-Token: ${PLEX_TOKEN}" \
|
||||
"$@" \
|
||||
"${PLEX_URL%/}${path}"
|
||||
}
|
||||
|
||||
# Same as plex_get but returns the server's default XML — a few endpoints
|
||||
# (notably /library/sections/<id>/all with includeGuids) are richer in XML.
|
||||
plex_get_raw() {
|
||||
local path="${1:?plex_get_raw needs a path}"; shift
|
||||
local flags; _plex_curl_flags flags
|
||||
curl "${flags[@]}" \
|
||||
-H "X-Plex-Token: ${PLEX_TOKEN}" \
|
||||
"$@" \
|
||||
"${PLEX_URL%/}${path}"
|
||||
}
|
||||
|
||||
# plex_put <path> [curl args...] — mutating request. Callers are responsible for
|
||||
# honouring --dry-run before calling this.
|
||||
plex_put() {
|
||||
local path="${1:?plex_put needs a path}"; shift
|
||||
local flags; _plex_curl_flags flags
|
||||
curl "${flags[@]}" -X PUT \
|
||||
-H 'Accept: application/json' \
|
||||
-H "X-Plex-Token: ${PLEX_TOKEN}" \
|
||||
"$@" \
|
||||
"${PLEX_URL%/}${path}"
|
||||
}
|
||||
|
||||
# Fail fast with a useful message rather than letting every later call return
|
||||
# an empty body. Plex answers 401 with an HTML body for a bad/absent token.
|
||||
plex_check_auth() {
|
||||
: "${PLEX_URL:?PLEX_URL is not set — copy .env.example to .env}"
|
||||
: "${PLEX_TOKEN:?PLEX_TOKEN is not set — copy .env.example to .env}"
|
||||
[[ "$PLEX_TOKEN" != "replace-me" ]] || plex_die "PLEX_TOKEN is still the placeholder value"
|
||||
|
||||
local flags; _plex_curl_flags flags
|
||||
local code
|
||||
code="$(curl "${flags[@]}" -o /dev/null -w '%{http_code}' \
|
||||
-H "X-Plex-Token: ${PLEX_TOKEN}" "${PLEX_URL%/}/identity")" \
|
||||
|| plex_die "cannot reach ${PLEX_URL} (Tailscale down?)"
|
||||
[[ "$code" == "200" ]] || plex_die "${PLEX_URL}/identity returned HTTP ${code}"
|
||||
|
||||
code="$(curl "${flags[@]}" -o /dev/null -w '%{http_code}' \
|
||||
-H "X-Plex-Token: ${PLEX_TOKEN}" "${PLEX_URL%/}/library/sections")"
|
||||
case "$code" in
|
||||
200) return 0 ;;
|
||||
401) plex_die "PLEX_TOKEN rejected (HTTP 401) — the token is wrong or expired" ;;
|
||||
*) plex_die "/library/sections returned HTTP ${code}" ;;
|
||||
esac
|
||||
}
|
||||
Reference in New Issue
Block a user