feat: Add batch runner and priority list

This commit is contained in:
keith committed 2026-09-19 09:41:26 -05:00
1 parent 568795e364
commit ec23bf6d2d
3 files changed
+304

No files matched your search

+96
View File
@@ -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`.
+174
View File
@@ -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:-<none>}"
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:-<none>}"
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 ]]
+34
View File
@@ -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