diff --git a/qbt-relink-daily-batch.sh b/qbt-relink-daily-batch.sh new file mode 100755 index 0000000..0e083e9 --- /dev/null +++ b/qbt-relink-daily-batch.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +# Batch-drive qbt-relink-daily.sh over every single-file torrent matching a +# name pattern (e.g. all ~1268 individual Jeopardy episode torrents). +# +# Usage: +# ./qbt-relink-daily-batch.sh [concurrency] +# ./qbt-relink-daily-batch.sh jeopardy "/data/video/tv/Jeopardy! (1984)" 4 +# +# Runs up to qbt-relink-daily.sh invocations in parallel +# (different hashes only - the underlying script's own no-repeat-same-hash +# rule is unaffected since each torrent here is distinct). Each torrent's +# full log goes to /tmp/qbt-relink-daily-batch/.log. Failures are +# logged and skipped, never retried automatically within the same run - +# re-run the whole batch command afterward to pick up anything that failed +# or was skipped (both qbt-relink-daily.sh and this driver are idempotent: +# already-relinked torrents are detected and skipped instantly). + +set -euo pipefail + +ENV_FILE="$HOME/.claude-homelab/.env" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +LOG_DIR="/tmp/qbt-relink-daily-batch" +mkdir -p "$LOG_DIR" + +if [[ ! -f "$ENV_FILE" ]]; then + echo "Error: $ENV_FILE not found" >&2 + exit 1 +fi +# shellcheck source=/dev/null +source "$ENV_FILE" + +NAME_SUBSTRING="${1:-}" +SHOW_ROOT="${2:-}" +CONCURRENCY="${3:-4}" +if [[ -z "$NAME_SUBSTRING" || -z "$SHOW_ROOT" ]]; then + echo "Usage: $0 [concurrency]" >&2 + exit 1 +fi + +COOKIE_JAR=$(mktemp) +trap 'rm -f "$COOKIE_JAR"' EXIT +curl -s -c "$COOKIE_JAR" -X POST "$QBITTORRENT_URL/api/v2/auth/login" \ + --data-urlencode "username=$QBITTORRENT_USERNAME" \ + --data-urlencode "password=$QBITTORRENT_PASSWORD" > /dev/null + +hashes=$(curl -s -b "$COOKIE_JAR" "$QBITTORRENT_URL/api/v2/torrents/info" | python3 -c " +import json, sys +d = json.load(sys.stdin) +needle = sys.argv[1].lower() +for t in d: + if needle in t['name'].lower(): + print(t['hash']) +" "$NAME_SUBSTRING") + +total=$(echo "$hashes" | grep -c . || true) +echo "Found $total torrent(s) matching '$NAME_SUBSTRING'. Running with concurrency $CONCURRENCY." +echo "Per-torrent logs: $LOG_DIR/.log" +echo + +ok=0 +failed=0 +skipped_running=0 +declare -a fail_list=() + +run_one() { + local hash="$1" + "$SCRIPT_DIR/qbt-relink-daily.sh" "$hash" "$SHOW_ROOT" > "$LOG_DIR/$hash.log" 2>&1 +} + +i=0 +running=0 +declare -A pids=() +for hash in $hashes; do + i=$((i + 1)) + run_one "$hash" & + pids["$hash"]=$! + running=$((running + 1)) + if [[ "$running" -ge "$CONCURRENCY" ]]; then + wait -n + running=$((running - 1)) + fi +done + +echo "All $i job(s) launched, waiting for the rest to finish..." +wait + +echo +echo "=== Summary ===" +for hash in "${!pids[@]}"; do + if grep -q "No data loss detected" "$LOG_DIR/$hash.log" 2>/dev/null || grep -q "Nothing to do" "$LOG_DIR/$hash.log" 2>/dev/null; then + ok=$((ok + 1)) + else + failed=$((failed + 1)) + fail_list+=("$hash") + fi +done + +echo "OK: $ok / $total" +echo "Failed or incomplete: $failed" +if [[ "$failed" -gt 0 ]]; then + echo "Failed hashes (see $LOG_DIR/.log for detail):" + for h in "${fail_list[@]}"; do + echo " $h" + done +fi