From ec23bf6d2d3153cfeb383bcac79ee7145f12cfa4 Mon Sep 17 00:00:00 2001 From: Keith Solomon Date: Sat, 19 Sep 2026 09:41:26 -0500 Subject: [PATCH] feat: Add batch runner and priority list --- README-batch.md | 96 ++++++++++++++++++++++++++ batch.sh | 174 ++++++++++++++++++++++++++++++++++++++++++++++++ priotity.txt | 34 ++++++++++ 3 files changed, 304 insertions(+) create mode 100644 README-batch.md create mode 100644 batch.sh create mode 100644 priotity.txt diff --git a/README-batch.md b/README-batch.md new file mode 100644 index 0000000..6c0cb9c --- /dev/null +++ b/README-batch.md @@ -0,0 +1,96 @@ +# Batch runner for Generic Plex Salvage + +Keep `batch.sh` in the same directory as: + +```text +scan.sh +recover.sh +salvage.py +``` + +The batch runner reads one folder per line and calls the existing generic +scanner or recovery script for each item. + +## Absolute paths + +Example `priority.txt`: + +```text +# Family priority recovery list +/Media/TV/SEAL Team +/Media/TV/Doctor Who +/Media/Movies/Alien +/Media/Movies/Aliens +``` + +Run: + +```bash +./batch.sh scan priority.txt +./batch.sh recover priority.txt +``` + +## Relative paths with a common base + +Example `priority.txt`: + +```text +TV/SEAL Team +TV/Doctor Who +Movies/Alien +Movies/Aliens +``` + +Run: + +```bash +./batch.sh scan priority.txt --base '/Media' +./batch.sh recover priority.txt --base '/Media' +``` + +## Behaviour + +- blank lines are ignored; +- lines beginning with `#` are comments; +- Windows CRLF text files are accepted; +- leading/trailing whitespace is removed; +- duplicate resolved paths are skipped; +- spaces in folder names are handled; +- folders are processed sequentially; +- one failed or metadata-dead folder does not stop later items; +- the batch exits nonzero after finishing if any requested folder failed. + +The underlying `scan.sh` / `recover.sh` safety rules are unchanged. Recovery +still processes only files classified `SAFE`. + +## Logging + +When `/mnt/recovery` is writable, batch logs and summaries are written there: + +```text +batch-scan-priority-YYYYMMDD-HHMMSS.log +batch-scan-priority-YYYYMMDD-HHMMSS-summary.txt +batch-recover-priority-YYYYMMDD-HHMMSS.log +batch-recover-priority-YYYYMMDD-HHMMSS-summary.txt +``` + +Each source folder still gets its own normal manifest, scan summary, recovery +TSV, and recovery summary from the generic salvage scripts. + +## Recommended workflow + +Scan the whole priority list first: + +```bash +./batch.sh scan priority.txt --base '/Media' +``` + +Review the resulting per-folder summaries and available recovery-disk space, +then recover the same list: + +```bash +./batch.sh recover priority.txt --base '/Media' +``` + +Recovery is restartable. Existing destination files whose sizes already match +their source inode are skipped by `recover.sh`. diff --git a/batch.sh b/batch.sh new file mode 100644 index 0000000..d1ef116 --- /dev/null +++ b/batch.sh @@ -0,0 +1,174 @@ +#!/usr/bin/env bash +set -uo pipefail + +usage() { + cat <<'EOF' +Usage: + ./batch.sh scan LIST.txt [--base '/source/base'] + ./batch.sh recover LIST.txt [--base '/source/base'] + +Examples: + ./batch.sh scan priority.txt + ./batch.sh recover priority.txt + ./batch.sh scan priority.txt --base '/Media' + ./batch.sh recover priority.txt --base '/Media' +EOF +} + +if [[ $# -lt 2 || $# -gt 4 ]]; then + usage >&2 + exit 2 +fi + +MODE="$1" +LIST_FILE="$2" +shift 2 + +BASE="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --base) + [[ $# -ge 2 ]] || { echo "ERROR: --base requires a path." >&2; exit 2; } + BASE="$2" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "ERROR: Unknown argument: $1" >&2 + usage >&2 + exit 2 + ;; + esac +done + +case "$MODE" in + scan|recover) ;; + *) echo "ERROR: mode must be 'scan' or 'recover'." >&2; exit 2 ;; +esac + +[[ -f "$LIST_FILE" ]] || { echo "ERROR: list file not found: $LIST_FILE" >&2; exit 1; } + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +WORKER="$SCRIPT_DIR/${MODE}.sh" + +[[ -x "$WORKER" ]] || { + echo "ERROR: expected executable worker beside batch.sh: $WORKER" >&2 + exit 1 +} + +if [[ -n "$BASE" ]]; then + [[ "$BASE" == /* ]] || BASE="/$BASE" + BASE="${BASE%/}" +fi + +if [[ -d /mnt/recovery && -w /mnt/recovery ]]; then + LOG_DIR="/mnt/recovery" +else + LOG_DIR="$(cd -- "$(dirname -- "$LIST_FILE")" && pwd)" +fi + +STAMP="$(date '+%Y%m%d-%H%M%S')" +LIST_BASENAME="$(basename -- "$LIST_FILE")" +LIST_STEM="${LIST_BASENAME%.*}" +LOG_FILE="$LOG_DIR/batch-${MODE}-${LIST_STEM}-${STAMP}.log" +SUMMARY_FILE="$LOG_DIR/batch-${MODE}-${LIST_STEM}-${STAMP}-summary.txt" + +declare -A SEEN=() + +total=0 +ok=0 +failed=0 +ignored=0 + +{ + echo "Generic Plex Salvage Batch" + echo "==========================" + echo "Mode: $MODE" + echo "List: $LIST_FILE" + echo "Base: ${BASE:-}" + echo "Started: $(date -Is)" + echo +} | tee -a "$LOG_FILE" + +while IFS= read -r raw || [[ -n "$raw" ]]; do + line="${raw%$'\r'}" + line="${line#"${line%%[![:space:]]*}"}" + line="${line%"${line##*[![:space:]]}"}" + + if [[ -z "$line" || "$line" == \#* ]]; then + ((ignored+=1)) + continue + fi + + if [[ "$line" == /* ]]; then + source_path="$line" + else + if [[ -z "$BASE" ]]; then + ((total+=1)) + ((failed+=1)) + echo "FAILED: relative entry requires --base: $line" | tee -a "$LOG_FILE" + continue + fi + source_path="$BASE/$line" + fi + + while [[ "$source_path" == *"//"* ]]; do + source_path="${source_path//\/\//\/}" + done + + if [[ -n "${SEEN[$source_path]+x}" ]]; then + ((ignored+=1)) + echo "SKIP duplicate: $source_path" | tee -a "$LOG_FILE" + continue + fi + SEEN["$source_path"]=1 + + ((total+=1)) + + { + echo "----------------------------------------------------------------" + echo "[$total] ${MODE^^}: $source_path" + echo "Started: $(date -Is)" + } | tee -a "$LOG_FILE" + + set +e + "$WORKER" "$source_path" 2>&1 | tee -a "$LOG_FILE" + rc=${PIPESTATUS[0]} + set -e + + if [[ $rc -eq 0 ]]; then + ((ok+=1)) + echo "RESULT: OK ($source_path)" | tee -a "$LOG_FILE" + else + ((failed+=1)) + echo "RESULT: FAILED rc=$rc ($source_path)" | tee -a "$LOG_FILE" + fi + + echo "Finished: $(date -Is)" | tee -a "$LOG_FILE" + echo | tee -a "$LOG_FILE" +done < "$LIST_FILE" + +{ + echo "GENERIC PLEX SALVAGE BATCH SUMMARY" + echo "==================================" + echo "Mode: $MODE" + echo "List: $LIST_FILE" + echo "Base: ${BASE:-}" + echo "Completed: $(date -Is)" + echo + echo "Processed: $total" + echo "Succeeded: $ok" + echo "Failed: $failed" + echo "Ignored: $ignored" + echo + echo "Full log: $LOG_FILE" +} | tee "$SUMMARY_FILE" + +echo +echo "Summary: $SUMMARY_FILE" + +[[ $failed -eq 0 ]] diff --git a/priotity.txt b/priotity.txt new file mode 100644 index 0000000..af8f7f2 --- /dev/null +++ b/priotity.txt @@ -0,0 +1,34 @@ +/Media/Videos/Television/Bob's Burgers +/Media/Videos/Television/Daredevil +/Media/Videos/Television/Fallout +/Media/Videos/Television/Landman +/Media/Videos/Television/Lioness +/Media/Videos/Television/Marshalls +/Media/Videos/Television/Reacher +/Media/Videos/Television/Spider Noir +/Media/Videos/Television/Sullivan’s Crossing +/Media/Videos/Television/Ted Lasso +/Media/Videos/Television/The Bear +/Media/Videos/Television/The Dutton Ranch +/Media/Videos/Television/The Hunting Wives +/Media/Videos/Television/The Morning Show +/Media/Videos/Television/The Vampire Diaries +/Media/Videos/Television/Tulsa King +/Media/Videos/Movies/Buffy the Vampire Slayer +/Media/Videos/Movies/Harry Potter and the Chamber of Secrets +/Media/Videos/Movies/Harry Potter and the Deathly Hallows Part 1 +/Media/Videos/Movies/Harry Potter and the Deathly Hallows Part 2 +/Media/Videos/Movies/Harry Potter and the Goblet of Fire +/Media/Videos/Movies/Harry Potter and the Half-Blood Prince +/Media/Videos/Movies/Harry Potter and the Order of the Phoenix +/Media/Videos/Movies/Harry Potter and the Philosopher's Stone +/Media/Videos/Movies/Harry Potter and the Prisoner of Azkaban +/Media/Videos/Movies/Pitch Perfect +/Media/Videos/Movies/Pitch Perfect 2 +/Media/Videos/Movies/Pitch Perfect 3 +/Media/Videos/Movies/Twilight +/Media/Videos/Movies/The Twilight Saga Breaking Dawn - Part 1 +/Media/Videos/Movies/The Twilight Saga Breaking Dawn - Part 2 +/Media/Videos/Movies/The Twilight Saga Eclipse +/Media/Videos/Movies/The Twilight Saga New Moon +/Media/Videos/Movies/Wait With Me