processes/ -> playbooks/ (git mv preserves history; updated cross-refs in ROADMAP, README, subtitles playbook + scripts). playbooks/import-media/README.md v1.0 — 7-step import workflow: stage on onyx -> rsync to nullstone -> chmod -> verify scan -> Items/Counts bump -> optional subtitle pass -> run-log Cross-references docs/05/07/08, ADMIN-GUIDE, README. Mirrors the existing subtitles playbook structure (CHANGELOG + runs/_template). CHANGELOG v1.0 lists known gaps (bin/cleanup-import.sh and bin/normalize.py still doc-only, ROADMAP M6). First run logged: playbooks/import-media/runs/lilo-stitch-2002.md. Lilo & Stitch (2002) imported to /home/user/media/movies/, item c2f4aff133c1b9631500fadf293b0b2f, TMDb 11544, MovieCount 3 -> 4. LibraryMonitor didn't auto-fire — needed manual /Library/Refresh; playbook updated to make this an unconditional step. Source: 1080p BluRay HEVC 10-bit / EAC3 5.1 / 2x PGS embedded subs. Per quality bar (README.md:41) — passes.
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"
|