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>
110 lines
3.5 KiB
Bash
Executable File
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 "$@"
|