diff --git a/plex/lib/posters.sh b/plex/lib/posters.sh index ca1c9ab..fcba0dd 100644 --- a/plex/lib/posters.sh +++ b/plex/lib/posters.sh @@ -57,19 +57,27 @@ posters_scan() { # 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" + # 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" \ | PLEX_URL="$PLEX_URL" PLEX_TOKEN="$PLEX_TOKEN" PLEX_INSECURE="${PLEX_INSECURE:-0}" \ - xargs -P "$jobs" -d '\n' -I{} bash -c ' - IFS=$'"'"'\t'"'"' read -r rk thumb title <<<"{}" + xargs -P "$jobs" -d '\n' -n 1 bash -c ' + IFS=" " read -r rk thumb title <<<"$1" flags=(--silent --show-error --max-time 30) [[ "${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}") - if [[ "$code" == "200" ]]; then + if [[ "$code" == "200" && "$ctype" == image/* ]]; then printf "%s\tOK\t%s\n" "$rk" "$title" else 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.