Files
scripts/plex/bin/poster-audit
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

110 lines
3.5 KiB
Bash
Executable File

#!/usr/bin/env bash
# Report items whose poster is missing or does not resolve. Read-only.
set -euo pipefail
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly COLLECTION_DIR="$(dirname "$SCRIPT_DIR")"
# shellcheck source=../lib/plex-api.sh
. "$COLLECTION_DIR/lib/plex-api.sh"
# shellcheck source=../lib/posters.sh
. "$COLLECTION_DIR/lib/posters.sh"
SECTION="TV Shows"
TYPE=2
FORMAT=text
LIST_OK=0
# Global, not local to main(): an EXIT trap fires after main() returns, by which
# point a local would be out of scope and `set -u` would abort the cleanup.
WORKDIR=""
cleanup() { [[ -n "$WORKDIR" ]] && rm -rf "$WORKDIR"; return 0; }
trap cleanup EXIT
usage() {
cat <<'EOF'
Usage: poster-audit [--section <id|title>] [--type <n>] [--json] [--list-ok]
--section Library section, by id or exact title. Default: "TV Shows".
--type Plex type code: 1=movie 2=show 3=season 4=episode. Default: 2.
--json Emit JSON instead of a human report.
--list-ok Also list items whose poster is fine.
--jobs N Parallel HTTP checks (env POSTER_JOBS, default 8).
--help
Statuses:
MISSING no thumb field on the item at all
BROKEN thumb field present, but the URL returns non-200
Read-only: makes no changes. Use poster-repair to fix what this finds.
EOF
}
main() {
while (( $# )); do
case "$1" in
--section) SECTION="${2:?--section needs a value}"; shift ;;
--type) TYPE="${2:?--type needs a value}"; shift ;;
--json) FORMAT=json ;;
--list-ok) LIST_OK=1 ;;
--jobs) POSTER_JOBS="${2:?--jobs needs a value}"; export POSTER_JOBS; shift ;;
--help|-h) usage; exit 0 ;;
*) plex_die "unknown argument: $1 (try --help)" ;;
esac
shift
done
plex_require_deps
plex_load_env "$COLLECTION_DIR/.env"
plex_check_auth
local section_id
section_id="$(posters_resolve_section "$SECTION")"
WORKDIR="$(mktemp -d)"
posters_fetch_items "$section_id" "$TYPE" "$WORKDIR/items.json"
posters_scan "$WORKDIR/items.json" | sort -t$'\t' -k3 > "$WORKDIR/scan.tsv"
local total missing broken ok
total="$(wc -l < "$WORKDIR/scan.tsv" | tr -d ' ')"
missing="$(awk -F'\t' '$2=="MISSING"' "$WORKDIR/scan.tsv" | wc -l | tr -d ' ')"
broken="$(awk -F'\t' '$2=="BROKEN"' "$WORKDIR/scan.tsv" | wc -l | tr -d ' ')"
ok="$(awk -F'\t' '$2=="OK"' "$WORKDIR/scan.tsv" | wc -l | tr -d ' ')"
if [[ "$FORMAT" == json ]]; then
jq -Rn --arg section "$SECTION" --arg id "$section_id" --arg type "$TYPE" \
--argjson total "$total" --argjson missing "$missing" \
--argjson broken "$broken" --argjson ok "$ok" \
--rawfile scan "$WORKDIR/scan.tsv" \
'{section:$section, sectionId:$id, type:$type,
totals:{total:$total, ok:$ok, missing:$missing, broken:$broken},
items:[$scan|split("\n")[]|select(length>0)|split("\t")
|{ratingKey:.[0], status:.[1], title:.[2]}]
| map(select(.status != "OK"))}'
return 0
fi
plex_log "Poster audit — section '${SECTION}' (id ${section_id}, type ${TYPE})"
plex_log " total ${total} ok ${ok} broken ${broken} missing ${missing}"
if (( broken + missing > 0 )); then
plex_log ""
plex_log "Needs repair:"
awk -F'\t' '$2!="OK" {printf " %-8s %-8s %s\n", $1, $2, $3}' "$WORKDIR/scan.tsv"
plex_log ""
plex_log "Run: bin/poster-repair --section '${SECTION}' --dry-run"
else
plex_log ""
plex_log "All posters resolve."
fi
if (( LIST_OK )); then
plex_log ""
plex_log "OK:"
awk -F'\t' '$2=="OK" {printf " %-8s %s\n", $1, $3}' "$WORKDIR/scan.tsv"
fi
}
main "$@"