#!/usr/bin/env bash # Re-select a poster for items whose poster is missing or does not resolve. 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 DRY_RUN=0 LIMIT=0 export POSTER_ALLOW_REMOTE=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-repair [--dry-run] [--section ] [--type ] [--limit N] [--allow-remote] --dry-run Show what would be selected; change nothing. --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. --limit N Repair at most N items (0 = no limit). --allow-remote Also repair items with no locally-cached poster by pulling one from the metadata agent. Off by default: it re-downloads from the internet and may pick a different image than the one that was originally chosen. --jobs N Parallel HTTP checks during the scan (default 8). --help What it does: for each broken item it selects the poster already sitting in that item's metadata bundle. The image is not re-downloaded and no other metadata is touched, so this does not overwrite manually-chosen artwork — items that still have a working poster are skipped entirely. Idempotent: re-running repairs nothing once every poster resolves. EOF } main() { while (( $# )); do case "$1" in --dry-run) DRY_RUN=1 ;; --section) SECTION="${2:?--section needs a value}"; shift ;; --type) TYPE="${2:?--type needs a value}"; shift ;; --limit) LIMIT="${2:?--limit needs a value}"; shift ;; --allow-remote) POSTER_ALLOW_REMOTE=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)" plex_log "Scanning section '${SECTION}' (id ${section_id}, type ${TYPE})..." posters_fetch_items "$section_id" "$TYPE" "$WORKDIR/items.json" posters_scan "$WORKDIR/items.json" | awk -F'\t' '$2!="OK"' | sort -t$'\t' -k3 > "$WORKDIR/broken.tsv" local count count="$(wc -l < "$WORKDIR/broken.tsv" | tr -d ' ')" if (( count == 0 )); then plex_log "Nothing to repair — every poster resolves." return 0 fi plex_log "Found ${count} item(s) needing repair." (( DRY_RUN )) && plex_log "[dry-run] no changes will be made." plex_log "" local repaired=0 skipped=0 failed=0 processed=0 local rk status title candidate while IFS=$'\t' read -r rk status title; do if (( LIMIT > 0 && processed >= LIMIT )); then plex_log "Reached --limit ${LIMIT}; stopping." break fi processed=$((processed + 1)) candidate="$(posters_candidate "$rk")" || candidate="" if [[ -z "$candidate" ]]; then plex_warn "SKIP ${title} (${rk}, ${status}) — no locally-cached poster; retry with --allow-remote" skipped=$((skipped + 1)) continue fi if (( DRY_RUN )); then plex_log "[dry-run] would select for ${title} (${rk}): ${candidate:0:72}" repaired=$((repaired + 1)) continue fi if ! posters_select "$rk" "$candidate"; then plex_warn "FAIL ${title} (${rk}) — poster selection rejected" failed=$((failed + 1)) continue fi if posters_verify "$rk"; then plex_log "OK ${title} (${rk})" repaired=$((repaired + 1)) else plex_warn "FAIL ${title} (${rk}) — selected, but thumb still does not resolve" failed=$((failed + 1)) fi done < "$WORKDIR/broken.tsv" plex_log "" if (( DRY_RUN )); then plex_log "Summary (dry-run): ${repaired} would be repaired, ${skipped} skipped." else plex_log "Summary: ${repaired} repaired, ${skipped} skipped, ${failed} failed." fi (( failed == 0 )) } main "$@"