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.
76 lines
2.6 KiB
Bash
Executable file
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"
|