feat: Add batch runner and priority list
This commit is contained in:
1 parent
568795e364
commit
ec23bf6d2d
3 files changed
+304
No files matched your search
@@ -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`.
|
||||||
@@ -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 ]]
|
||||||
@@ -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
|
||||||
Reference in new issue
Block a user