legacy-arrflix/processes/subtitles/lib/sub-fetch.sh
s8n fedf3388b8 processes: subtitle acquisition v1 + AD S01 run
Adds processes/ umbrella for repeatable acquisition workflows. First child
is subtitles/, with recipe README (executable by Claude Code), CHANGELOG,
per-show run logs, and a tested helper at lib/sub-fetch.sh.

Run on American Dad: S01 (7 eps) passed, S02-S04 (51 eps) broke. Library
uses Hulu/DSP season ordering; OpenSubtitles indexes by Fox airing order;
plugin queries by (parent_imdb_id, season, episode) so library S02E01
returns 0 hits. v2 design = direct OpenSubtitles REST with per-episode
imdb_id lookup; pending API-key registration.
2026-05-09 22:56:31 +01:00

76 lines
2.6 KiB
Bash
Executable file

#!/usr/bin/env bash
# Subtitle fetch helper — recipe v1 Step 4.
#
# Single-episode loop body. Runs against a Jellyfin instance reachable from
# nullstone via `docker exec jellyfin curl ...`. Driver loops should source or
# call this per episode.
#
# Picker: highest DownloadCount among results that are NOT
# (HearingImpaired|MachineTranslated|AiTranslated|Forced); 23.976fps preferred.
# Falls back to all results if every candidate is HI/MT/AI/Forced.
#
# Side effects:
# - POSTs RemoteSearch download (consumes 1 of 20 daily free-tier slots)
# - docker cp's the resulting metadata-cache srt to MEDIA_DIR
#
# Caller env:
# TOK Jellyfin admin X-Emby-Token
# EP Jellyfin episode item id
# MEDIA_DIR destination dir on nullstone, e.g.
# '/home/user/media/tv/American Dad! (2005)/Season 01'
# MEDIA_BASE filename without extension, must match the .mkv basename
#
# Exits non-zero on no-subs (1) or download HTTP != 204 (2).
# Output to stdout: "OK <ep-id> -> <dest path>".
# Output to stderr: chosen sub release name + fps + DownloadCount, or error.
set -euo pipefail
: "${TOK:?TOK required}"
: "${EP:?EP required}"
: "${MEDIA_DIR:?MEDIA_DIR required}"
: "${MEDIA_BASE:?MEDIA_BASE required}"
NULLSTONE="${NULLSTONE:-user@192.168.0.100}"
RAW=$(ssh "$NULLSTONE" "docker exec jellyfin curl -s -H 'X-Emby-Token: $TOK' \
'http://localhost:8096/Items/$EP/RemoteSearch/Subtitles/eng'")
SUBID=$(printf '%s' "$RAW" | python3 -c "
import json, sys
subs = json.load(sys.stdin)
clean = [s for s in subs
if not (s.get('HearingImpaired') or s.get('MachineTranslated')
or s.get('AiTranslated') or s.get('Forced'))]
if not clean:
clean = subs
fps2398 = [s for s in clean if abs(s.get('FrameRate', 0) - 23.976) < 0.01]
pool = fps2398 if fps2398 else clean
pool.sort(key=lambda s: -s.get('DownloadCount', 0))
if pool:
print(pool[0]['Id'])
print(pool[0]['Name'], pool[0].get('FrameRate'),
pool[0].get('DownloadCount'), file=sys.stderr)
")
if [[ -z "$SUBID" ]]; then
echo "NO-SUBS for $EP" >&2
exit 1
fi
HTTP=$(ssh "$NULLSTONE" "docker exec jellyfin curl -s -o /dev/null -X POST \
-H 'X-Emby-Token: $TOK' \
'http://localhost:8096/Items/$EP/RemoteSearch/Subtitles/$SUBID' \
-w '%{http_code}'")
if [[ "$HTTP" != "204" ]]; then
echo "DL-FAIL HTTP=$HTTP for $EP $SUBID" >&2
exit 2
fi
SHARD="${EP:0:2}"
SRC_IN_CONTAINER="/config/metadata/library/$SHARD/$EP/$MEDIA_BASE.eng.srt"
DEST="$MEDIA_DIR/$MEDIA_BASE.eng.srt"
ssh "$NULLSTONE" "docker cp \"jellyfin:$SRC_IN_CONTAINER\" \"$DEST\"" >/dev/null
echo "OK $EP -> $DEST"