Fix poster scan losing items whose title contains a quote

posters_scan passed each line to the worker with `xargs -I{}`, which splices
the text straight into the shell command. Any title containing an apostrophe
or a quote became a shell syntax error and that item was dropped — silently,
since the error went to stderr while the audit counted only what came back.

No show title happened to trigger it, so this survived the show-level work.
The first episode-level scan hit it immediately: titles like "That's a shirt?"
and "WAIT A MINUTE, ..." produced `unexpected EOF while looking for matching
quote`, which means that episode run reported nothing trustworthy.

Lines are now passed as a positional argument, so the shell never parses their
content. Regression-tested with titles containing apostrophes, double quotes,
commas, backticks and $(...) — all handled, and the $(...) case confirmed not
to execute.

Also tightens the check itself: a poster is OK only if the response is 200
*and* an image content-type. Plex answers some paths with 200 and an XML body,
which the status-only check counted as a working poster.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lucas Berger
2026-08-15 21:40:06 -04:00
co-authored by Claude Opus 5
parent 29476235ab
commit eaf91b157b
+13 -5
View File
@@ -57,19 +57,27 @@ posters_scan() {
# Items with no thumb field at all need no HTTP request. # Items with no thumb field at all need no HTTP request.
jq -r '.MediaContainer.Metadata[]?|select(has("thumb")|not)|"\(.ratingKey)\tMISSING\t\(.title)"' "$items_json" jq -r '.MediaContainer.Metadata[]?|select(has("thumb")|not)|"\(.ratingKey)\tMISSING\t\(.title)"' "$items_json"
# Each line is passed to the worker as a positional ARGUMENT ($1), never
# substituted into the command text. `xargs -I{}` would splice the title
# straight into the shell string, so a title containing an apostrophe or a
# quote — "That's", 'MINUTE,' — becomes a syntax error and that item is
# silently lost. Real titles at episode scale are full of them.
jq -r '.MediaContainer.Metadata[]?|select(has("thumb"))|"\(.ratingKey)\t\(.thumb)\t\(.title)"' "$items_json" \ jq -r '.MediaContainer.Metadata[]?|select(has("thumb"))|"\(.ratingKey)\t\(.thumb)\t\(.title)"' "$items_json" \
| PLEX_URL="$PLEX_URL" PLEX_TOKEN="$PLEX_TOKEN" PLEX_INSECURE="${PLEX_INSECURE:-0}" \ | PLEX_URL="$PLEX_URL" PLEX_TOKEN="$PLEX_TOKEN" PLEX_INSECURE="${PLEX_INSECURE:-0}" \
xargs -P "$jobs" -d '\n' -I{} bash -c ' xargs -P "$jobs" -d '\n' -n 1 bash -c '
IFS=$'"'"'\t'"'"' read -r rk thumb title <<<"{}" IFS=" " read -r rk thumb title <<<"$1"
flags=(--silent --show-error --max-time 30) flags=(--silent --show-error --max-time 30)
[[ "${PLEX_INSECURE:-0}" == "1" ]] && flags+=(--insecure) [[ "${PLEX_INSECURE:-0}" == "1" ]] && flags+=(--insecure)
code=$(curl "${flags[@]}" -o /dev/null -w "%{http_code}" \ # Status alone is not proof of a poster: Plex answers some paths with
# HTTP 200 and an XML or HTML body. Require an image content-type.
read -r code ctype < <(curl "${flags[@]}" -o /dev/null \
-w "%{http_code} %{content_type}" \
-H "X-Plex-Token: ${PLEX_TOKEN}" "${PLEX_URL%/}${thumb}") -H "X-Plex-Token: ${PLEX_TOKEN}" "${PLEX_URL%/}${thumb}")
if [[ "$code" == "200" ]]; then if [[ "$code" == "200" && "$ctype" == image/* ]]; then
printf "%s\tOK\t%s\n" "$rk" "$title" printf "%s\tOK\t%s\n" "$rk" "$title"
else else
printf "%s\tBROKEN\t%s\n" "$rk" "$title" printf "%s\tBROKEN\t%s\n" "$rk" "$title"
fi' fi' _
} }
# Echo the best poster candidate URL for an item, or nothing if there is none. # Echo the best poster candidate URL for an item, or nothing if there is none.