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
Executable
+242
@@ -0,0 +1,242 @@
|
||||
#!/usr/bin/env bash
|
||||
# Read-only health report for the Plex Media Server.
|
||||
#
|
||||
# Exit status: 0 = all checks passed, 1 = at least one WARN, 2 = at least one FAIL.
|
||||
# Safe to run from cron; makes no changes.
|
||||
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"
|
||||
|
||||
WARNS=0
|
||||
FAILS=0
|
||||
CHECK_POSTERS=0
|
||||
|
||||
# Global, not local: an EXIT trap fires after the owning function has returned,
|
||||
# by which point a local would be out of scope and `set -u` would abort cleanup.
|
||||
WORKDIR=""
|
||||
cleanup() { [[ -n "$WORKDIR" ]] && rm -rf "$WORKDIR"; return 0; }
|
||||
trap cleanup EXIT
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: plex-health [--posters] [--help]
|
||||
|
||||
--posters Also run a full poster-integrity check on every show/movie
|
||||
library. Costs one HTTP request per item, so it is off by
|
||||
default; without it the report notes that it was skipped.
|
||||
--help
|
||||
|
||||
Exit status: 0 ok, 1 warnings, 2 failures.
|
||||
EOF
|
||||
}
|
||||
|
||||
ok() { printf ' [ ok ] %s\n' "$*"; }
|
||||
warn() { printf ' [warn] %s\n' "$*"; WARNS=$((WARNS + 1)); }
|
||||
fail() { printf ' [FAIL] %s\n' "$*"; FAILS=$((FAILS + 1)); }
|
||||
info() { printf ' [info] %s\n' "$*"; }
|
||||
section() { printf '\n%s\n' "$*"; }
|
||||
|
||||
# ── checks ───────────────────────────────────────────────────────────────────
|
||||
|
||||
check_server() {
|
||||
section "Server"
|
||||
local root
|
||||
root="$(plex_get /)" || { fail "cannot read server root"; return; }
|
||||
|
||||
local version platform pv user signin sub
|
||||
version="$(jq -r '.MediaContainer.version // "?"' <<<"$root")"
|
||||
platform="$(jq -r '.MediaContainer.platform // "?"' <<<"$root")"
|
||||
pv="$(jq -r '.MediaContainer.platformVersion // "?"' <<<"$root")"
|
||||
user="$(jq -r '.MediaContainer.myPlexUsername // "?"' <<<"$root")"
|
||||
signin="$(jq -r '.MediaContainer.myPlexSigninState // "?"' <<<"$root")"
|
||||
sub="$(jq -r '.MediaContainer.myPlexSubscription // false' <<<"$root")"
|
||||
|
||||
ok "Plex ${version} on ${platform} ${pv}"
|
||||
[[ "$signin" == "ok" ]] && ok "myPlex sign-in: ${user}" || fail "myPlex sign-in state: ${signin}"
|
||||
[[ "$sub" == "true" ]] && ok "Plex Pass subscription active" || info "no active Plex Pass subscription"
|
||||
}
|
||||
|
||||
check_updates() {
|
||||
section "Updates"
|
||||
local st
|
||||
st="$(plex_get /updater/status)" || { warn "updater status unavailable"; return; }
|
||||
local size can
|
||||
size="$(jq -r '.MediaContainer.size // 0' <<<"$st")"
|
||||
can="$(jq -r '.MediaContainer.canInstall // false' <<<"$st")"
|
||||
if [[ "$size" == "0" ]]; then
|
||||
ok "no server update pending"
|
||||
else
|
||||
info "update available (canInstall=${can}) — this server updates via its Docker image, not in-app"
|
||||
fi
|
||||
}
|
||||
|
||||
check_remote_access() {
|
||||
section "Remote access"
|
||||
local acct
|
||||
acct="$(plex_get /myplex/account)" || { warn "cannot read myPlex account"; return; }
|
||||
local mstate merr
|
||||
mstate="$(jq -r '.MyPlex.mappingState // "?"' <<<"$acct")"
|
||||
merr="$(jq -r '.MyPlex.mappingError // ""' <<<"$acct")"
|
||||
if [[ -z "$merr" ]]; then
|
||||
ok "port mapping: ${mstate}"
|
||||
else
|
||||
# odin sits behind a Pangolin/Newt tunnel with no open inbound ports, so an
|
||||
# "unreachable" mapping is the expected steady state, not a fault.
|
||||
info "port mapping ${mstate}/${merr} — expected: odin has no open inbound ports (tunnel ingress)"
|
||||
fi
|
||||
}
|
||||
|
||||
check_libraries() {
|
||||
section "Libraries"
|
||||
local sections
|
||||
sections="$(plex_get /library/sections)" || { fail "cannot list library sections"; return; }
|
||||
|
||||
local key title type refreshing code count
|
||||
while IFS=$'\t' read -r key title type refreshing; do
|
||||
# Top-level item count: movies for a movie library, shows for a show
|
||||
# library. Music sections are counted by artist.
|
||||
case "$type" in
|
||||
movie) code=1 ;;
|
||||
show) code=2 ;;
|
||||
artist) code=8 ;;
|
||||
*) code=1 ;;
|
||||
esac
|
||||
count="$(plex_get "/library/sections/${key}/all" --get --data-urlencode "type=${code}" \
|
||||
| jq -r '.MediaContainer.totalSize // .MediaContainer.size // "?"' 2>/dev/null || echo '?')"
|
||||
if [[ "$refreshing" == "true" ]]; then
|
||||
info "${title} (id ${key}, ${type}): ${count} items — currently refreshing"
|
||||
else
|
||||
ok "${title} (id ${key}, ${type}): ${count} items"
|
||||
fi
|
||||
done < <(jq -r '.MediaContainer.Directory[]|"\(.key)\t\(.title)\t\(.type)\t\(.refreshing)"' <<<"$sections")
|
||||
}
|
||||
|
||||
check_unmatched() {
|
||||
section "Unmatched media"
|
||||
local sections
|
||||
sections="$(plex_get /library/sections)"
|
||||
local key title type code items unmatched
|
||||
while IFS=$'\t' read -r key title type; do
|
||||
case "$type" in
|
||||
movie) code=1 ;;
|
||||
show) code=2 ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
items="$(plex_get "/library/sections/${key}/all" --get --data-urlencode "type=${code}")" || continue
|
||||
# An unmatched item keeps a local:// guid instead of a real agent guid.
|
||||
unmatched="$(jq -r '[.MediaContainer.Metadata[]?
|
||||
|select((.guid//"")|test("^(local://|com\\.plexapp\\.agents\\.none)"))]|length' <<<"$items")"
|
||||
if [[ "$unmatched" == "0" ]]; then
|
||||
ok "${title}: all items matched to an agent"
|
||||
else
|
||||
warn "${title}: ${unmatched} unmatched item(s) — they will never get metadata or artwork"
|
||||
fi
|
||||
done < <(jq -r '.MediaContainer.Directory[]|"\(.key)\t\(.title)\t\(.type)"' <<<"$sections")
|
||||
}
|
||||
|
||||
check_posters() {
|
||||
section "Poster integrity"
|
||||
if (( ! CHECK_POSTERS )); then
|
||||
info "skipped (one request per item) — re-run with --posters, or use bin/poster-audit"
|
||||
return
|
||||
fi
|
||||
local sections
|
||||
sections="$(plex_get /library/sections)"
|
||||
local key title type code bad total
|
||||
WORKDIR="$(mktemp -d)"
|
||||
while IFS=$'\t' read -r key title type; do
|
||||
case "$type" in
|
||||
movie) code=1 ;;
|
||||
show) code=2 ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
posters_fetch_items "$key" "$code" "$WORKDIR/items.json"
|
||||
posters_scan "$WORKDIR/items.json" > "$WORKDIR/scan.tsv"
|
||||
total="$(wc -l < "$WORKDIR/scan.tsv" | tr -d ' ')"
|
||||
bad="$(awk -F'\t' '$2!="OK"' "$WORKDIR/scan.tsv" | wc -l | tr -d ' ')"
|
||||
if [[ "$bad" == "0" ]]; then
|
||||
ok "${title}: ${total}/${total} posters resolve"
|
||||
else
|
||||
warn "${title}: ${bad}/${total} posters missing or broken — run bin/poster-repair --section '${title}'"
|
||||
fi
|
||||
done < <(jq -r '.MediaContainer.Directory[]|"\(.key)\t\(.title)\t\(.type)"' <<<"$sections")
|
||||
}
|
||||
|
||||
check_butler() {
|
||||
section "Scheduled maintenance (Butler)"
|
||||
local b
|
||||
b="$(plex_get /butler)" || { warn "cannot read butler tasks"; return; }
|
||||
|
||||
local backup optimize
|
||||
backup="$(jq -r '.ButlerTasks.ButlerTask[]|select(.name=="BackupDatabase")|.enabled' <<<"$b")"
|
||||
optimize="$(jq -r '.ButlerTasks.ButlerTask[]|select(.name=="OptimizeDatabase")|.enabled' <<<"$b")"
|
||||
|
||||
[[ "$backup" == "true" ]] && ok "database backup enabled" || warn "database backup DISABLED — no automatic recovery point"
|
||||
[[ "$optimize" == "true" ]] && ok "database optimize enabled" || warn "database optimize disabled"
|
||||
|
||||
local disabled
|
||||
disabled="$(jq -r '[.ButlerTasks.ButlerTask[]|select(.enabled==false)|.name]|join(", ")' <<<"$b")"
|
||||
[[ -n "$disabled" ]] && info "disabled tasks: ${disabled}"
|
||||
}
|
||||
|
||||
check_activity() {
|
||||
section "Current activity"
|
||||
local s a
|
||||
s="$(plex_get /status/sessions)" || { warn "cannot read sessions"; return; }
|
||||
local streams transcodes
|
||||
streams="$(jq -r '.MediaContainer.size // 0' <<<"$s")"
|
||||
transcodes="$(jq -r '[.MediaContainer.Metadata[]?|select((.TranscodeSession//empty)!=empty)]|length' <<<"$s")"
|
||||
ok "${streams} active stream(s), ${transcodes} transcoding"
|
||||
|
||||
a="$(plex_get /activities)" || return
|
||||
local acts
|
||||
acts="$(jq -r '.MediaContainer.size // 0' <<<"$a")"
|
||||
if [[ "$acts" == "0" ]]; then
|
||||
ok "no background activities running"
|
||||
else
|
||||
info "${acts} background activity/activities: $(jq -r '[.MediaContainer.Activity[]?|.title]|join(", ")' <<<"$a")"
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
while (( $# )); do
|
||||
case "$1" in
|
||||
--posters) CHECK_POSTERS=1 ;;
|
||||
--help|-h) usage; exit 0 ;;
|
||||
*) plex_die "unknown argument: $1 (try --help)" ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
plex_require_deps
|
||||
plex_load_env "$COLLECTION_DIR/.env"
|
||||
|
||||
printf 'Plex health check — %s\n' "${PLEX_URL:-unset}"
|
||||
plex_check_auth
|
||||
printf ' [ ok ] reachable and authenticated\n'
|
||||
|
||||
check_server
|
||||
check_updates
|
||||
check_remote_access
|
||||
check_libraries
|
||||
check_unmatched
|
||||
check_posters
|
||||
check_butler
|
||||
check_activity
|
||||
|
||||
section "Result"
|
||||
if (( FAILS > 0 )); then
|
||||
printf ' %d failure(s), %d warning(s)\n' "$FAILS" "$WARNS"; exit 2
|
||||
elif (( WARNS > 0 )); then
|
||||
printf ' %d warning(s)\n' "$WARNS"; exit 1
|
||||
fi
|
||||
printf ' all checks passed\n'
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable
+109
@@ -0,0 +1,109 @@
|
||||
#!/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 "$@"
|
||||
Executable
+134
@@ -0,0 +1,134 @@
|
||||
#!/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 <id|title>] [--type <n>]
|
||||
[--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 "$@"
|
||||
Reference in New Issue
Block a user