@@ -10,11 +10,16 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). |
||
| 10 | 10 |
### Changed |
| 11 | 11 |
- `--move-source` renamed to `--delete-source` (old name kept as alias) |
| 12 | 12 |
- `--continue-on-error` renamed to `--keep-going` (old name kept as alias) |
| 13 |
+- `--unattended` automatically enables input staging when `--staging-dir` is set |
|
| 14 |
+- Source and destination preflight checks now fail before staging/scanning when |
|
| 15 |
+ source or destination NFS/autofs mounts are unreadable or unwritable |
|
| 13 | 16 |
### Added |
| 14 | 17 |
- `--unattended` preset (`--delete-source` + `--keep-going`) for long unattended runs |
| 15 | 18 |
- `--staging-dir DIR` to place intermediate transcoding temp files on a fast local path, |
| 16 | 19 |
with automatic fallback to destination temp files when staging is unavailable |
| 17 | 20 |
or lacks sufficient free space for the current file |
| 21 |
+- `--stage-input` to copy each source video to staging before encoding, which avoids |
|
| 22 |
+ long ffmpeg reads directly from flaky NAS/autofs mounts |
|
| 18 | 23 |
- automatic RAM disk creation on macOS when `--staging-dir` points to a missing |
| 19 | 24 |
`/Volumes/<Name>` path, configurable with `--staging-ramdisk-mb N` |
| 20 | 25 |
- `--debug-timing N` to stop after N video files and print timing statistics for quick profiling |
@@ -24,12 +24,15 @@ optimized for Apple Photos / QuickTime compatibility. |
||
| 24 | 24 |
# Transcode + delete originals after validation |
| 25 | 25 |
./garmin_varia_transcode.sh -s SampleFootage -d Output --delete-source |
| 26 | 26 |
|
| 27 |
-# Long unattended run preset (delete-source + keep-going) |
|
| 27 |
+# Long unattended run preset (delete-source + keep-going; also stages input when --staging-dir is set) |
|
| 28 | 28 |
./garmin_varia_transcode.sh -s SampleFootage -d Output --unattended |
| 29 | 29 |
|
| 30 | 30 |
# NFS destination with local staging for temp outputs |
| 31 | 31 |
./garmin_varia_transcode.sh -s SampleFootage -d ~/Autofs/xdev/is-baobab/nvme0n1/@backup/Garmin --staging-dir /tmp/varia_staging |
| 32 | 32 |
|
| 33 |
+# Flaky NAS/autofs source: copy each source clip to local staging before encode |
|
| 34 |
+./garmin_varia_transcode.sh -s ~/Autofs/xdev/autonas/ext01/@Camera/import -d ~/Autofs/xdev/autonas/ext00/@Garmin --unattended --staging-dir /Volumes/VARIA_RAM --staging-ramdisk-mb 512 --stage-input |
|
| 35 |
+ |
|
| 33 | 36 |
# Timing debug: process first 20 files then stop and print final statistics |
| 34 | 37 |
./garmin_varia_transcode.sh -s SampleFootage -d Output --debug-timing 20 |
| 35 | 38 |
|
@@ -57,6 +60,8 @@ Options: |
||
| 57 | 60 |
--staging-dir DIR Temporary staging directory for intermediate output files |
| 58 | 61 |
Falls back to destination temp files if staging is unavailable |
| 59 | 62 |
or does not have enough free space for current file |
| 63 |
+ --stage-input Copy each source video to staging before encoding; useful |
|
| 64 |
+ for flaky NAS/autofs source reads |
|
| 60 | 65 |
--staging-ramdisk-mb N RAM disk size in MB when auto-creating missing /Volumes staging |
| 61 | 66 |
directory on macOS (default: 8192) |
| 62 | 67 |
--debug-timing N Process at most N video files, then stop and print timing stats |
@@ -94,15 +99,16 @@ Options: |
||
| 94 | 99 |
## Safety |
| 95 | 100 |
|
| 96 | 101 |
- Destination inside source is rejected with an error |
| 102 |
+- Source readability and destination writability are checked before staging or |
|
| 103 |
+ scanning, so stale/inaccessible NFS/autofs mounts fail with an explicit error |
|
| 97 | 104 |
- `--delete-source` only deletes source after codec + duration validation passes |
| 98 | 105 |
- `--dry-run` never writes files |
| 99 | 106 |
- `Ctrl+C` behavior during long runs: |
| 100 | 107 |
first press requests stop after current file; second press force-stops current encode |
| 101 |
- - Graceful quit between files: |
|
| 102 |
- Press `q` (or `Q`) between files to request a graceful stop before the next |
|
| 103 |
- file begins. The program consumes the keypress (it is not echoed) so you can |
|
| 104 |
- press `q` while an encode is running and it will be honored once that encode |
|
| 105 |
- finishes. |
|
| 108 |
+- Graceful quit between files: |
|
| 109 |
+ press `q` (or `Q`) during interactive runs to request a graceful stop before |
|
| 110 |
+ the next file begins. `--unattended` disables `q`/`Q` terminal polling so |
|
| 111 |
+ buffered terminal input cannot stop long runs accidentally; use `Ctrl+C`. |
|
| 106 | 112 |
- On macOS, unreadable MP4/MOV sources automatically try `avconvert --preset PresetPassthrough` |
| 107 | 113 |
as fallback before being marked as unreadable |
| 108 | 114 |
|
@@ -1,5 +1,5 @@ |
||
| 1 | 1 |
#!/usr/bin/env bash |
| 2 |
-set -euo pipefail |
|
| 2 |
+set -Eeuo pipefail |
|
| 3 | 3 |
|
| 4 | 4 |
# Garmin Varia batch transcoder (macOS/Linux) |
| 5 | 5 |
# |
@@ -19,6 +19,7 @@ DEFAULT_EXTENSIONS="mp4,mov,avi,m4v" |
||
| 19 | 19 |
DEFAULT_CRF_HEVC=20 |
| 20 | 20 |
DEFAULT_CRF_H264=19 |
| 21 | 21 |
DEFAULT_STAGING_RAMDISK_MB=8192 |
| 22 |
+DEFAULT_PREFLIGHT_TIMEOUT_SEC=10 |
|
| 22 | 23 |
|
| 23 | 24 |
# Garmin publicly declares a 140-degree field of view. |
| 24 | 25 |
# We infer practical focal metadata from this single spec. |
@@ -39,14 +40,17 @@ RECURSIVE=true |
||
| 39 | 40 |
SINGLE_FILE="" |
| 40 | 41 |
VERBOSE=false |
| 41 | 42 |
MOVE_SOURCE=false |
| 43 |
+UNATTENDED=false |
|
| 42 | 44 |
SOURCE_PROVIDED=false |
| 43 | 45 |
DEST_PROVIDED=false |
| 44 | 46 |
STAGING_PROVIDED=false |
| 47 |
+STAGE_INPUT=false |
|
| 45 | 48 |
STAGING_RAMDISK_MB="$DEFAULT_STAGING_RAMDISK_MB" |
| 46 | 49 |
AUTO_CREATED_STAGING_RAMDISK=false |
| 47 | 50 |
AUTO_CREATED_STAGING_PATH="" |
| 48 | 51 |
AUTO_CREATED_STAGING_DEV="" |
| 49 | 52 |
STAGING_RAMDISK_CREATED_AT=0 |
| 53 |
+STAGING_RAMDISK_CREATE_ERROR="" |
|
| 50 | 54 |
AUTO_STAGING_CLEANED_UP=false |
| 51 | 55 |
RUN_STARTED_AT=0 |
| 52 | 56 |
FIRST_ENCODE_STARTED_AT=0 |
@@ -57,6 +61,8 @@ FAIL_FAST=true |
||
| 57 | 61 |
SOURCE_READABLE_MODE="normal" |
| 58 | 62 |
APPLE_REPACK_FALLBACK=true |
| 59 | 63 |
REPACKED_SOURCE_PATH="" |
| 64 |
+STAGED_INPUT_PATH="" |
|
| 65 |
+STAGED_INPUT_FAILURE_RC=0 |
|
| 60 | 66 |
|
| 61 | 67 |
HAS_VIDEOTOOLBOX=false |
| 62 | 68 |
HAS_LIBX265=false |
@@ -82,10 +88,13 @@ INPUT_BYTES_PROCESSED=0 |
||
| 82 | 88 |
OUTPUT_BYTES_PROCESSED=0 |
| 83 | 89 |
|
| 84 | 90 |
DURATION_TOLERANCE_SEC=1.0 |
| 91 |
+PREFLIGHT_TIMEOUT_SEC="$DEFAULT_PREFLIGHT_TIMEOUT_SEC" |
|
| 85 | 92 |
STOP_AFTER_CURRENT=false |
| 86 | 93 |
INTERRUPT_COUNT=0 |
| 87 | 94 |
CURRENT_FFMPEG_PID="" |
| 88 | 95 |
PROGRESS_LINE_OPEN=false |
| 96 |
+FINAL_REPORT_PRINTED=false |
|
| 97 |
+MAIN_FLOW_STARTED=false |
|
| 89 | 98 |
|
| 90 | 99 |
usage() {
|
| 91 | 100 |
cat <<'EOF' |
@@ -97,6 +106,8 @@ Options: |
||
| 97 | 106 |
-d, --destination, --output DIR Destination directory (default: current directory) |
| 98 | 107 |
--staging-dir DIR Temporary staging directory for intermediate output files |
| 99 | 108 |
(falls back to destination if staging cannot be used) |
| 109 |
+ --stage-input Copy each source video to the staging directory before |
|
| 110 |
+ encoding; useful for flaky NAS/autofs source reads |
|
| 100 | 111 |
--staging-ramdisk-mb N RAM disk size in MB when auto-creating missing /Volumes staging |
| 101 | 112 |
directory on macOS (default: 8192) |
| 102 | 113 |
--debug-timing N Process at most N video files, then stop and print timing stats |
@@ -114,6 +125,14 @@ Options: |
||
| 114 | 125 |
--apple-repack-fallback Enable macOS avconvert fallback (default) |
| 115 | 126 |
-h, --help Show this help |
| 116 | 127 |
|
| 128 |
+Run Controls: |
|
| 129 |
+ q / Q Request graceful stop before the next file (interactive runs only) |
|
| 130 |
+ Ctrl+C Request stop after the current encode finishes |
|
| 131 |
+ Ctrl+C x2 Force-stop the current encode |
|
| 132 |
+ |
|
| 133 |
+ Note: --unattended disables q/Q terminal polling so long runs cannot be |
|
| 134 |
+ stopped accidentally by buffered terminal input. Use Ctrl+C for unattended. |
|
| 135 |
+ |
|
| 117 | 136 |
Encoding Modes: |
| 118 | 137 |
hardware Uses hevc_videotoolbox (Apple Silicon / Intel Mac GPU). macOS only. |
| 119 | 138 |
~4-5s per 30s clip, ~35W (measured on Apple Silicon MacBook Pro). |
@@ -163,6 +182,24 @@ log_msg() {
|
||
| 163 | 182 |
echo "[$ts] [$level] $*" |
| 164 | 183 |
} |
| 165 | 184 |
|
| 185 |
+handle_unexpected_error() {
|
|
| 186 |
+ local rc=$? |
|
| 187 |
+ local line_no="${1:-unknown}"
|
|
| 188 |
+ local command_text="${2:-unknown}"
|
|
| 189 |
+ |
|
| 190 |
+ if [[ "$rc" -eq 0 ]]; then |
|
| 191 |
+ return 0 |
|
| 192 |
+ fi |
|
| 193 |
+ |
|
| 194 |
+ if [[ "$PROGRESS_LINE_OPEN" == true ]]; then |
|
| 195 |
+ printf '\n' |
|
| 196 |
+ PROGRESS_LINE_OPEN=false |
|
| 197 |
+ fi |
|
| 198 |
+ |
|
| 199 |
+ log_msg "ERROR" "Unexpected script failure at line $line_no (rc=$rc): $command_text" |
|
| 200 |
+ return "$rc" |
|
| 201 |
+} |
|
| 202 |
+ |
|
| 166 | 203 |
# Verbose-only log: suppressed in default quiet mode |
| 167 | 204 |
vlog_msg() {
|
| 168 | 205 |
[[ "$VERBOSE" == true ]] && log_msg "$@" |
@@ -258,22 +295,29 @@ handle_interrupt() {
|
||
| 258 | 295 |
# -t 1 is a safety net for platforms where select() on a VMIN=0 fd does not |
| 259 | 296 |
# report readable-when-empty; in that case we block at most 1s then exit. |
| 260 | 297 |
check_for_quit_key() {
|
| 298 |
+ [[ "$UNATTENDED" == true ]] && return 0 |
|
| 299 |
+ [[ -t 0 ]] || return 0 |
|
| 261 | 300 |
[[ -e /dev/tty ]] || return 0 |
| 262 |
- local key old_stty found=false |
|
| 263 |
- old_stty=$(stty -g </dev/tty 2>/dev/null) || return 0 |
|
| 264 |
- stty -echo -icanon min 0 time 0 </dev/tty 2>/dev/null \ |
|
| 265 |
- || { stty "$old_stty" </dev/tty 2>/dev/null || true; return 0; }
|
|
| 266 |
- while IFS= read -r -s -n 1 -t 1 key </dev/tty 2>/dev/null; do |
|
| 301 |
+ local key old_stty found=false tty_fd |
|
| 302 |
+ if ! exec {tty_fd}</dev/tty 2>/dev/null; then
|
|
| 303 |
+ return 0 |
|
| 304 |
+ fi |
|
| 305 |
+ old_stty=$(stty -g <&"$tty_fd" 2>/dev/null) || { exec {tty_fd}<&-; return 0; }
|
|
| 306 |
+ stty -echo -icanon min 0 time 0 <&"$tty_fd" 2>/dev/null \ |
|
| 307 |
+ || { stty "$old_stty" <&"$tty_fd" 2>/dev/null || true; exec {tty_fd}<&-; return 0; }
|
|
| 308 |
+ while IFS= read -r -s -n 1 -t 1 key <&"$tty_fd" 2>/dev/null; do |
|
| 267 | 309 |
if [[ "$key" == "q" || "$key" == "Q" ]]; then |
| 268 | 310 |
found=true |
| 269 | 311 |
break |
| 270 | 312 |
fi |
| 271 | 313 |
done |
| 272 |
- stty "$old_stty" </dev/tty 2>/dev/null || true |
|
| 314 |
+ stty "$old_stty" <&"$tty_fd" 2>/dev/null || true |
|
| 315 |
+ exec {tty_fd}<&-
|
|
| 273 | 316 |
if [[ "$found" == true ]]; then |
| 274 | 317 |
STOP_AFTER_CURRENT=true |
| 275 | 318 |
log_msg "INFO" "Quit key pressed; stopping before next file" |
| 276 | 319 |
fi |
| 320 |
+ return 0 |
|
| 277 | 321 |
} |
| 278 | 322 |
|
| 279 | 323 |
run_ffmpeg_with_signal_guard() {
|
@@ -432,6 +476,31 @@ make_temp_output_file() {
|
||
| 432 | 476 |
printf '%s\n' "$temp_path" |
| 433 | 477 |
} |
| 434 | 478 |
|
| 479 |
+make_temp_staged_input_file() {
|
|
| 480 |
+ local source_file="$1" |
|
| 481 |
+ local staging_dir="$2" |
|
| 482 |
+ local source_base source_noext source_ext seed_path temp_path |
|
| 483 |
+ |
|
| 484 |
+ source_base="$(basename "$source_file")" |
|
| 485 |
+ source_noext="${source_base%.*}"
|
|
| 486 |
+ source_ext="$(file_extension_or_tmp "$source_file")" |
|
| 487 |
+ |
|
| 488 |
+ seed_path="$(mktemp "$staging_dir/.varia_input.${source_noext}.XXXXXX" 2>/dev/null || true)"
|
|
| 489 |
+ if [[ -n "$seed_path" ]]; then |
|
| 490 |
+ rm -f -- "$seed_path" 2>/dev/null || true |
|
| 491 |
+ temp_path="${seed_path}.${source_ext}"
|
|
| 492 |
+ else |
|
| 493 |
+ temp_path="$staging_dir/.varia_input.${source_noext}.$$.$RANDOM.${source_ext}"
|
|
| 494 |
+ if ! touch "$temp_path" >/dev/null 2>&1; then |
|
| 495 |
+ temp_path="" |
|
| 496 |
+ else |
|
| 497 |
+ rm -f -- "$temp_path" 2>/dev/null || true |
|
| 498 |
+ fi |
|
| 499 |
+ fi |
|
| 500 |
+ |
|
| 501 |
+ printf '%s\n' "$temp_path" |
|
| 502 |
+} |
|
| 503 |
+ |
|
| 435 | 504 |
cleanup_transcode_artifacts() {
|
| 436 | 505 |
local temp_output="$1" |
| 437 | 506 |
local final_output="$2" |
@@ -447,6 +516,19 @@ cleanup_transcode_artifacts() {
|
||
| 447 | 516 |
fi |
| 448 | 517 |
} |
| 449 | 518 |
|
| 519 |
+cleanup_staged_input() {
|
|
| 520 |
+ local staged_input="$1" |
|
| 521 |
+ [[ -n "$staged_input" ]] || return 0 |
|
| 522 |
+ rm -f -- "$staged_input" 2>/dev/null || true |
|
| 523 |
+} |
|
| 524 |
+ |
|
| 525 |
+cleanup_process_inputs() {
|
|
| 526 |
+ local staged_input="$1" |
|
| 527 |
+ local repacked_input="$2" |
|
| 528 |
+ cleanup_staged_input "$staged_input" |
|
| 529 |
+ cleanup_repacked_input "$repacked_input" |
|
| 530 |
+} |
|
| 531 |
+ |
|
| 450 | 532 |
is_apple_noise_file() {
|
| 451 | 533 |
local file_path="$1" |
| 452 | 534 |
local base |
@@ -484,6 +566,77 @@ path_is_within() {
|
||
| 484 | 566 |
[[ "$child" == "$parent" || "$child" == "$parent"/* ]] |
| 485 | 567 |
} |
| 486 | 568 |
|
| 569 |
+run_with_timeout() {
|
|
| 570 |
+ local timeout_sec="$1" |
|
| 571 |
+ shift |
|
| 572 |
+ local pid elapsed=0 rc=0 |
|
| 573 |
+ |
|
| 574 |
+ "$@" & |
|
| 575 |
+ pid=$! |
|
| 576 |
+ |
|
| 577 |
+ while kill -0 "$pid" 2>/dev/null; do |
|
| 578 |
+ if [[ "$elapsed" -ge "$timeout_sec" ]]; then |
|
| 579 |
+ kill -TERM "$pid" 2>/dev/null || true |
|
| 580 |
+ sleep 1 |
|
| 581 |
+ kill -KILL "$pid" 2>/dev/null || true |
|
| 582 |
+ return 124 |
|
| 583 |
+ fi |
|
| 584 |
+ sleep 1 |
|
| 585 |
+ elapsed=$((elapsed + 1)) |
|
| 586 |
+ done |
|
| 587 |
+ |
|
| 588 |
+ wait "$pid" 2>/dev/null |
|
| 589 |
+ rc=$? |
|
| 590 |
+ return "$rc" |
|
| 591 |
+} |
|
| 592 |
+ |
|
| 593 |
+preflight_source_readable() {
|
|
| 594 |
+ local source_dir="$1" |
|
| 595 |
+ local rc=0 |
|
| 596 |
+ |
|
| 597 |
+ log_msg "INFO" "Preflight: checking source is readable: $source_dir" |
|
| 598 |
+ trap - ERR |
|
| 599 |
+ set +e |
|
| 600 |
+ run_with_timeout "$PREFLIGHT_TIMEOUT_SEC" bash -c ' |
|
| 601 |
+ source_dir="$1" |
|
| 602 |
+ [[ -d "$source_dir" && -r "$source_dir" && -x "$source_dir" ]] || exit 10 |
|
| 603 |
+ find "$source_dir" -maxdepth 1 -print -quit >/dev/null 2>&1 |
|
| 604 |
+ ' _ "$source_dir" |
|
| 605 |
+ rc=$? |
|
| 606 |
+ set -e |
|
| 607 |
+ trap 'handle_unexpected_error "$LINENO" "$BASH_COMMAND"' ERR |
|
| 608 |
+ if [[ "$rc" -ne 0 ]]; then |
|
| 609 |
+ if [[ "$rc" -eq 124 ]]; then |
|
| 610 |
+ die "Source is not readable within ${PREFLIGHT_TIMEOUT_SEC}s, likely stale/inaccessible NFS/autofs mount: $source_dir"
|
|
| 611 |
+ fi |
|
| 612 |
+ die "Source is not readable or searchable: $source_dir" |
|
| 613 |
+ fi |
|
| 614 |
+} |
|
| 615 |
+ |
|
| 616 |
+preflight_destination_writable() {
|
|
| 617 |
+ local dest_dir="$1" |
|
| 618 |
+ local rc=0 |
|
| 619 |
+ |
|
| 620 |
+ log_msg "INFO" "Preflight: checking destination is writable: $dest_dir" |
|
| 621 |
+ trap - ERR |
|
| 622 |
+ set +e |
|
| 623 |
+ run_with_timeout "$PREFLIGHT_TIMEOUT_SEC" bash -c ' |
|
| 624 |
+ dest_dir="$1" |
|
| 625 |
+ mkdir -p "$dest_dir" >/dev/null 2>&1 || exit 10 |
|
| 626 |
+ probe="$(mktemp "$dest_dir/.varia_write_probe.XXXXXX" 2>/dev/null)" || exit 11 |
|
| 627 |
+ rm -f -- "$probe" >/dev/null 2>&1 || exit 12 |
|
| 628 |
+ ' _ "$dest_dir" |
|
| 629 |
+ rc=$? |
|
| 630 |
+ set -e |
|
| 631 |
+ trap 'handle_unexpected_error "$LINENO" "$BASH_COMMAND"' ERR |
|
| 632 |
+ if [[ "$rc" -ne 0 ]]; then |
|
| 633 |
+ if [[ "$rc" -eq 124 ]]; then |
|
| 634 |
+ die "Destination is not writable within ${PREFLIGHT_TIMEOUT_SEC}s, likely stale/inaccessible NFS/autofs mount: $dest_dir"
|
|
| 635 |
+ fi |
|
| 636 |
+ die "Destination is not writable: $dest_dir" |
|
| 637 |
+ fi |
|
| 638 |
+} |
|
| 639 |
+ |
|
| 487 | 640 |
file_size_bytes_or_zero() {
|
| 488 | 641 |
local file_path="$1" |
| 489 | 642 |
local file_size="0" |
@@ -500,6 +653,18 @@ file_size_bytes_or_zero() {
|
||
| 500 | 653 |
fi |
| 501 | 654 |
} |
| 502 | 655 |
|
| 656 |
+file_extension_or_tmp() {
|
|
| 657 |
+ local file_path="$1" |
|
| 658 |
+ local base ext |
|
| 659 |
+ base="$(basename "$file_path")" |
|
| 660 |
+ ext="${base##*.}"
|
|
| 661 |
+ if [[ "$base" == "$ext" || -z "$ext" ]]; then |
|
| 662 |
+ printf 'tmp\n' |
|
| 663 |
+ else |
|
| 664 |
+ printf '%s\n' "$ext" |
|
| 665 |
+ fi |
|
| 666 |
+} |
|
| 667 |
+ |
|
| 503 | 668 |
dir_available_bytes_or_zero() {
|
| 504 | 669 |
local dir_path="$1" |
| 505 | 670 |
local avail_kb="" |
@@ -533,6 +698,28 @@ staging_has_space_for_input() {
|
||
| 533 | 698 |
[[ "$available_bytes" -ge "$needed_bytes" ]] |
| 534 | 699 |
} |
| 535 | 700 |
|
| 701 |
+staging_has_space_for_input_copy() {
|
|
| 702 |
+ local staging_dir="$1" |
|
| 703 |
+ local input_file="$2" |
|
| 704 |
+ local input_size_bytes=0 |
|
| 705 |
+ local needed_bytes=0 |
|
| 706 |
+ local available_bytes=0 |
|
| 707 |
+ |
|
| 708 |
+ [[ -n "$staging_dir" && -d "$staging_dir" && -w "$staging_dir" ]] || return 1 |
|
| 709 |
+ |
|
| 710 |
+ input_size_bytes="$(file_size_bytes_or_zero "$input_file")" |
|
| 711 |
+ if [[ "$input_size_bytes" -le 0 ]]; then |
|
| 712 |
+ return 1 |
|
| 713 |
+ fi |
|
| 714 |
+ |
|
| 715 |
+ # Input staging needs room for the source copy plus the output and metadata |
|
| 716 |
+ # rewrite temp files. Garmin 30s clips are usually fixed-size, so 3x is a |
|
| 717 |
+ # conservative and easy-to-explain guard. |
|
| 718 |
+ needed_bytes=$((input_size_bytes * 3)) |
|
| 719 |
+ available_bytes="$(dir_available_bytes_or_zero "$staging_dir")" |
|
| 720 |
+ [[ "$available_bytes" -ge "$needed_bytes" ]] |
|
| 721 |
+} |
|
| 722 |
+ |
|
| 536 | 723 |
join_cmd_for_log() {
|
| 537 | 724 |
local out="" |
| 538 | 725 |
local arg |
@@ -563,6 +750,10 @@ parse_args() {
|
||
| 563 | 750 |
STAGING_PROVIDED=true |
| 564 | 751 |
shift 2 |
| 565 | 752 |
;; |
| 753 |
+ --stage-input) |
|
| 754 |
+ STAGE_INPUT=true |
|
| 755 |
+ shift |
|
| 756 |
+ ;; |
|
| 566 | 757 |
--staging-ramdisk-mb) |
| 567 | 758 |
require_value "$1" "${2:-}"
|
| 568 | 759 |
STAGING_RAMDISK_MB="$2" |
@@ -626,6 +817,7 @@ parse_args() {
|
||
| 626 | 817 |
shift |
| 627 | 818 |
;; |
| 628 | 819 |
--unattended) |
| 820 |
+ UNATTENDED=true |
|
| 629 | 821 |
MOVE_SOURCE=true |
| 630 | 822 |
FAIL_FAST=false |
| 631 | 823 |
shift |
@@ -684,6 +876,10 @@ parse_args() {
|
||
| 684 | 876 |
if [[ "$STAGING_RAMDISK_MB" -le 0 ]]; then |
| 685 | 877 |
die "--staging-ramdisk-mb must be greater than 0" |
| 686 | 878 |
fi |
| 879 |
+ |
|
| 880 |
+ if [[ "$STAGE_INPUT" != true && "$STAGING_PROVIDED" == true && "$MOVE_SOURCE" == true && "$FAIL_FAST" == false ]]; then |
|
| 881 |
+ STAGE_INPUT=true |
|
| 882 |
+ fi |
|
| 687 | 883 |
} |
| 688 | 884 |
|
| 689 | 885 |
check_tools() {
|
@@ -989,6 +1185,83 @@ source_video_is_readable() {
|
||
| 989 | 1185 |
return 1 |
| 990 | 1186 |
} |
| 991 | 1187 |
|
| 1188 |
+stage_input_for_encode() {
|
|
| 1189 |
+ local source_file="$1" |
|
| 1190 |
+ local source_size staged_size |
|
| 1191 |
+ local staged_duration |
|
| 1192 |
+ local available_bytes=0 needed_bytes=0 |
|
| 1193 |
+ |
|
| 1194 |
+ STAGED_INPUT_PATH="" |
|
| 1195 |
+ STAGED_INPUT_FAILURE_RC=0 |
|
| 1196 |
+ |
|
| 1197 |
+ if [[ "$STAGE_INPUT" != true ]]; then |
|
| 1198 |
+ return 0 |
|
| 1199 |
+ fi |
|
| 1200 |
+ |
|
| 1201 |
+ if [[ "$DRY_RUN" == true ]]; then |
|
| 1202 |
+ vlog_msg "DRY-RUN" "Would copy source to staging before encode: $source_file" |
|
| 1203 |
+ return 0 |
|
| 1204 |
+ fi |
|
| 1205 |
+ |
|
| 1206 |
+ if [[ "$STAGING_PROVIDED" != true || -z "$STAGING_DIR" ]]; then |
|
| 1207 |
+ STAGED_INPUT_FAILURE_RC=3 |
|
| 1208 |
+ log_msg "ERROR" "--stage-input requires --staging-dir" |
|
| 1209 |
+ return 1 |
|
| 1210 |
+ fi |
|
| 1211 |
+ |
|
| 1212 |
+ if [[ ! -d "$STAGING_DIR" || ! -w "$STAGING_DIR" ]]; then |
|
| 1213 |
+ STAGED_INPUT_FAILURE_RC=3 |
|
| 1214 |
+ log_msg "ERROR" "Input staging directory is not writable: $STAGING_DIR" |
|
| 1215 |
+ return 1 |
|
| 1216 |
+ fi |
|
| 1217 |
+ |
|
| 1218 |
+ source_size="$(file_size_bytes_or_zero "$source_file")" |
|
| 1219 |
+ available_bytes="$(dir_available_bytes_or_zero "$STAGING_DIR")" |
|
| 1220 |
+ needed_bytes=$((source_size * 3)) |
|
| 1221 |
+ if [[ "$source_size" -le 0 || "$available_bytes" -lt "$needed_bytes" ]]; then |
|
| 1222 |
+ STAGED_INPUT_FAILURE_RC=3 |
|
| 1223 |
+ log_msg "ERROR" "Insufficient staging space for input copy and output temp files: $source_file (needed=${needed_bytes}B available=${available_bytes}B source=${source_size}B staging=$STAGING_DIR)"
|
|
| 1224 |
+ return 1 |
|
| 1225 |
+ fi |
|
| 1226 |
+ |
|
| 1227 |
+ STAGED_INPUT_PATH="$(make_temp_staged_input_file "$source_file" "$STAGING_DIR")" |
|
| 1228 |
+ if [[ -z "$STAGED_INPUT_PATH" ]]; then |
|
| 1229 |
+ STAGED_INPUT_FAILURE_RC=3 |
|
| 1230 |
+ log_msg "ERROR" "Could not create staged input temp file: $source_file" |
|
| 1231 |
+ return 1 |
|
| 1232 |
+ fi |
|
| 1233 |
+ |
|
| 1234 |
+ vlog_msg "INFO" "Copying source to staging before encode: $source_file -> $STAGED_INPUT_PATH" |
|
| 1235 |
+ if ! dd if="$source_file" of="$STAGED_INPUT_PATH" bs=4m status=none; then |
|
| 1236 |
+ cleanup_staged_input "$STAGED_INPUT_PATH" |
|
| 1237 |
+ STAGED_INPUT_PATH="" |
|
| 1238 |
+ STAGED_INPUT_FAILURE_RC=2 |
|
| 1239 |
+ log_msg "ERROR" "Failed to copy source to staging: $source_file" |
|
| 1240 |
+ return 1 |
|
| 1241 |
+ fi |
|
| 1242 |
+ |
|
| 1243 |
+ staged_size="$(file_size_bytes_or_zero "$STAGED_INPUT_PATH")" |
|
| 1244 |
+ if [[ "$source_size" -le 0 || "$source_size" != "$staged_size" ]]; then |
|
| 1245 |
+ cleanup_staged_input "$STAGED_INPUT_PATH" |
|
| 1246 |
+ STAGED_INPUT_PATH="" |
|
| 1247 |
+ STAGED_INPUT_FAILURE_RC=2 |
|
| 1248 |
+ log_msg "ERROR" "Staged source copy failed size validation: $source_file (source=${source_size}B staged=${staged_size}B)"
|
|
| 1249 |
+ return 1 |
|
| 1250 |
+ fi |
|
| 1251 |
+ |
|
| 1252 |
+ staged_duration="$(ffprobe_duration_or_empty "$STAGED_INPUT_PATH")" |
|
| 1253 |
+ if [[ -z "$staged_duration" ]]; then |
|
| 1254 |
+ cleanup_staged_input "$STAGED_INPUT_PATH" |
|
| 1255 |
+ STAGED_INPUT_PATH="" |
|
| 1256 |
+ STAGED_INPUT_FAILURE_RC=2 |
|
| 1257 |
+ log_msg "ERROR" "Staged source copy failed duration probe: $STAGED_INPUT_PATH" |
|
| 1258 |
+ return 1 |
|
| 1259 |
+ fi |
|
| 1260 |
+ |
|
| 1261 |
+ vlog_msg "INFO" "Staged input OK: $STAGED_INPUT_PATH" |
|
| 1262 |
+ return 0 |
|
| 1263 |
+} |
|
| 1264 |
+ |
|
| 992 | 1265 |
source_error_from_ffmpeg_log() {
|
| 993 | 1266 |
local ffmpeg_log="$1" |
| 994 | 1267 |
[[ -n "$ffmpeg_log" && -f "$ffmpeg_log" ]] || return 1 |
@@ -1093,6 +1366,7 @@ build_video_args() {
|
||
| 1093 | 1366 |
normalize_source_dir() {
|
| 1094 | 1367 |
SOURCE_DIR="$(to_abs_path "$SOURCE_DIR")" |
| 1095 | 1368 |
[[ -d "$SOURCE_DIR" ]] || die "Source directory not found: $SOURCE_DIR" |
| 1369 |
+ preflight_source_readable "$SOURCE_DIR" |
|
| 1096 | 1370 |
SOURCE_DIR="$(cd "$SOURCE_DIR" && pwd)" |
| 1097 | 1371 |
} |
| 1098 | 1372 |
|
@@ -1110,6 +1384,7 @@ create_missing_staging_ramdisk_if_needed() {
|
||
| 1110 | 1384 |
local dev="" |
| 1111 | 1385 |
local mount_point="" |
| 1112 | 1386 |
|
| 1387 |
+ STAGING_RAMDISK_CREATE_ERROR="" |
|
| 1113 | 1388 |
staging_path="${staging_path%/}"
|
| 1114 | 1389 |
|
| 1115 | 1390 |
if [[ -d "$staging_path" ]]; then |
@@ -1118,6 +1393,7 @@ create_missing_staging_ramdisk_if_needed() {
|
||
| 1118 | 1393 |
fi |
| 1119 | 1394 |
|
| 1120 | 1395 |
if [[ "$(uname -s)" != "Darwin" ]]; then |
| 1396 |
+ STAGING_RAMDISK_CREATE_ERROR="auto-create is only supported on macOS" |
|
| 1121 | 1397 |
return |
| 1122 | 1398 |
fi |
| 1123 | 1399 |
|
@@ -1125,17 +1401,20 @@ create_missing_staging_ramdisk_if_needed() {
|
||
| 1125 | 1401 |
/Volumes/*) |
| 1126 | 1402 |
;; |
| 1127 | 1403 |
*) |
| 1404 |
+ STAGING_RAMDISK_CREATE_ERROR="auto-create only supports top-level /Volumes/<Name> paths" |
|
| 1128 | 1405 |
return |
| 1129 | 1406 |
;; |
| 1130 | 1407 |
esac |
| 1131 | 1408 |
|
| 1132 | 1409 |
ramdisk_name="$(basename "$staging_path")" |
| 1133 | 1410 |
if [[ -z "$ramdisk_name" || "$ramdisk_name" == "." || "$ramdisk_name" == ".." ]]; then |
| 1411 |
+ STAGING_RAMDISK_CREATE_ERROR="invalid RAM disk volume name derived from staging path" |
|
| 1134 | 1412 |
return |
| 1135 | 1413 |
fi |
| 1136 | 1414 |
|
| 1137 | 1415 |
if [[ "$staging_path" != "/Volumes/$ramdisk_name" ]]; then |
| 1138 | 1416 |
# Only auto-create for top-level /Volumes/<Name>, not nested paths. |
| 1417 |
+ STAGING_RAMDISK_CREATE_ERROR="auto-create only supports top-level /Volumes/<Name> paths" |
|
| 1139 | 1418 |
return |
| 1140 | 1419 |
fi |
| 1141 | 1420 |
|
@@ -1146,10 +1425,12 @@ create_missing_staging_ramdisk_if_needed() {
|
||
| 1146 | 1425 |
vlog_msg "INFO" "Creating RAM disk for staging: /Volumes/$ramdisk_name (${STAGING_RAMDISK_MB}MB)"
|
| 1147 | 1426 |
dev="$(/usr/bin/hdiutil attach -nomount "ram://$sectors" 2>/dev/null | /usr/bin/awk 'NR==1 {print $1}' || true)"
|
| 1148 | 1427 |
if [[ -z "$dev" ]]; then |
| 1428 |
+ STAGING_RAMDISK_CREATE_ERROR="hdiutil attach failed for ${STAGING_RAMDISK_MB}MB RAM disk"
|
|
| 1149 | 1429 |
return |
| 1150 | 1430 |
fi |
| 1151 | 1431 |
|
| 1152 | 1432 |
if ! /usr/sbin/diskutil eraseVolume APFS "$ramdisk_name" "$dev" >/dev/null 2>&1; then |
| 1433 |
+ STAGING_RAMDISK_CREATE_ERROR="diskutil eraseVolume failed for RAM disk device $dev" |
|
| 1153 | 1434 |
/usr/bin/hdiutil detach "$dev" >/dev/null 2>&1 || true |
| 1154 | 1435 |
return |
| 1155 | 1436 |
fi |
@@ -1164,6 +1445,8 @@ create_missing_staging_ramdisk_if_needed() {
|
||
| 1164 | 1445 |
AUTO_CREATED_STAGING_PATH="$mount_point" |
| 1165 | 1446 |
AUTO_CREATED_STAGING_DEV="$dev" |
| 1166 | 1447 |
STAGING_RAMDISK_CREATED_AT="$(date +%s)" |
| 1448 |
+ else |
|
| 1449 |
+ STAGING_RAMDISK_CREATE_ERROR="RAM disk device $dev was created but mount point was not found (expected $mount_point)" |
|
| 1167 | 1450 |
fi |
| 1168 | 1451 |
} |
| 1169 | 1452 |
|
@@ -1196,6 +1479,14 @@ attempt_unmount_auto_staging_ramdisk() {
|
||
| 1196 | 1479 |
|
| 1197 | 1480 |
cleanup_on_exit() {
|
| 1198 | 1481 |
local rc=$? |
| 1482 |
+ trap - ERR |
|
| 1483 |
+ if [[ "$MAIN_FLOW_STARTED" == true && "$FINAL_REPORT_PRINTED" != true ]]; then |
|
| 1484 |
+ if [[ "$rc" -eq 0 ]]; then |
|
| 1485 |
+ log_msg "WARN" "Run exited before final report despite exit=0; this indicates an early-return path" |
|
| 1486 |
+ else |
|
| 1487 |
+ log_msg "ERROR" "Run exited before final report (exit=$rc)" |
|
| 1488 |
+ fi |
|
| 1489 |
+ fi |
|
| 1199 | 1490 |
attempt_unmount_auto_staging_ramdisk |
| 1200 | 1491 |
return "$rc" |
| 1201 | 1492 |
} |
@@ -1213,6 +1504,9 @@ normalize_staging_dir() {
|
||
| 1213 | 1504 |
STAGING_DIR="$AUTO_CREATED_STAGING_PATH" |
| 1214 | 1505 |
log_msg "INFO" "Created staging RAM disk: $STAGING_DIR" |
| 1215 | 1506 |
else |
| 1507 |
+ if [[ -n "$STAGING_RAMDISK_CREATE_ERROR" ]]; then |
|
| 1508 |
+ die "Staging directory not found and RAM disk auto-create failed: $STAGING_DIR ($STAGING_RAMDISK_CREATE_ERROR)" |
|
| 1509 |
+ fi |
|
| 1216 | 1510 |
die "Staging directory not found: $STAGING_DIR" |
| 1217 | 1511 |
fi |
| 1218 | 1512 |
fi |
@@ -1299,6 +1593,7 @@ process_video_file() {
|
||
| 1299 | 1593 |
local rel_path output_file temp_output_file out_dir |
| 1300 | 1594 |
local display_path |
| 1301 | 1595 |
local encode_input_file |
| 1596 |
+ local staged_input_file="" |
|
| 1302 | 1597 |
local preferred_temp_dir="" |
| 1303 | 1598 |
local repacked_input_file="" |
| 1304 | 1599 |
local input_size_bytes=0 output_size_bytes=0 |
@@ -1315,9 +1610,36 @@ process_video_file() {
|
||
| 1315 | 1610 |
display_path="${input_file#$PWD/}"
|
| 1316 | 1611 |
encode_input_file="$input_file" |
| 1317 | 1612 |
|
| 1318 |
- mkdir -p "$out_dir" |
|
| 1613 |
+ vlog_msg "INFO" "Preparing $rel_path" |
|
| 1614 |
+ |
|
| 1615 |
+ if [[ "$OVERWRITE" != true && -f "$output_file" ]]; then |
|
| 1616 |
+ vlog_msg "SKIP" "Video exists: $output_file" |
|
| 1617 |
+ VIDEOS_SKIPPED=$((VIDEOS_SKIPPED + 1)) |
|
| 1618 |
+ return |
|
| 1619 |
+ fi |
|
| 1319 | 1620 |
|
| 1320 |
- if ! source_video_is_readable "$input_file"; then |
|
| 1621 |
+ if ! stage_input_for_encode "$encode_input_file"; then |
|
| 1622 |
+ ERRORS=$((ERRORS + 1)) |
|
| 1623 |
+ cleanup_process_inputs "$STAGED_INPUT_PATH" "$repacked_input_file" |
|
| 1624 |
+ if [[ "$STAGED_INPUT_FAILURE_RC" -eq 3 ]]; then |
|
| 1625 |
+ DESTINATION_FAILURES=$((DESTINATION_FAILURES + 1)) |
|
| 1626 |
+ return 3 |
|
| 1627 |
+ fi |
|
| 1628 |
+ INVALID_SOURCES_SKIPPED=$((INVALID_SOURCES_SKIPPED + 1)) |
|
| 1629 |
+ return 2 |
|
| 1630 |
+ fi |
|
| 1631 |
+ staged_input_file="$STAGED_INPUT_PATH" |
|
| 1632 |
+ if [[ -n "$staged_input_file" ]]; then |
|
| 1633 |
+ encode_input_file="$staged_input_file" |
|
| 1634 |
+ fi |
|
| 1635 |
+ |
|
| 1636 |
+ if [[ "$DRY_RUN" == true ]]; then |
|
| 1637 |
+ log_msg "DRY-RUN" "Would transcode: $input_file -> $output_file" |
|
| 1638 |
+ cleanup_process_inputs "$staged_input_file" "$repacked_input_file" |
|
| 1639 |
+ return 0 |
|
| 1640 |
+ fi |
|
| 1641 |
+ |
|
| 1642 |
+ if ! source_video_is_readable "$encode_input_file"; then |
|
| 1321 | 1643 |
ERRORS=$((ERRORS + 1)) |
| 1322 | 1644 |
INVALID_SOURCES_SKIPPED=$((INVALID_SOURCES_SKIPPED + 1)) |
| 1323 | 1645 |
if [[ "$VERBOSE" == true ]]; then |
@@ -1326,6 +1648,7 @@ process_video_file() {
|
||
| 1326 | 1648 |
log_progress_start "$display_path" |
| 1327 | 1649 |
log_progress_skipped_unreadable "$display_path" |
| 1328 | 1650 |
fi |
| 1651 |
+ cleanup_process_inputs "$staged_input_file" "$repacked_input_file" |
|
| 1329 | 1652 |
return 2 |
| 1330 | 1653 |
fi |
| 1331 | 1654 |
|
@@ -1335,13 +1658,6 @@ process_video_file() {
|
||
| 1335 | 1658 |
vlog_msg "INFO" "Encoding from avconvert repacked source: $repacked_input_file" |
| 1336 | 1659 |
fi |
| 1337 | 1660 |
|
| 1338 |
- if [[ -f "$output_file" && "$OVERWRITE" != true ]]; then |
|
| 1339 |
- vlog_msg "SKIP" "Video exists: $output_file" |
|
| 1340 |
- VIDEOS_SKIPPED=$((VIDEOS_SKIPPED + 1)) |
|
| 1341 |
- cleanup_repacked_input "$repacked_input_file" |
|
| 1342 |
- return |
|
| 1343 |
- fi |
|
| 1344 |
- |
|
| 1345 | 1661 |
local has_audio=false |
| 1346 | 1662 |
if probe_has_audio "$encode_input_file"; then |
| 1347 | 1663 |
has_audio=true |
@@ -1368,6 +1684,7 @@ process_video_file() {
|
||
| 1368 | 1684 |
ERRORS=$((ERRORS + 1)) |
| 1369 | 1685 |
DESTINATION_FAILURES=$((DESTINATION_FAILURES + 1)) |
| 1370 | 1686 |
log_msg "ERROR" "Could not create temporary output file: $output_file" |
| 1687 |
+ cleanup_process_inputs "$staged_input_file" "$repacked_input_file" |
|
| 1371 | 1688 |
return 3 |
| 1372 | 1689 |
fi |
| 1373 | 1690 |
if [[ "$STAGING_PROVIDED" == true && -n "$STAGING_DIR" ]] && ! path_is_within "$temp_output_file" "$STAGING_DIR"; then |
@@ -1396,11 +1713,6 @@ process_video_file() {
|
||
| 1396 | 1713 |
log_msg "INFO" "Command: $(join_cmd_for_log "${cmd[@]}")"
|
| 1397 | 1714 |
fi |
| 1398 | 1715 |
|
| 1399 |
- if [[ "$DRY_RUN" == true ]]; then |
|
| 1400 |
- log_msg "DRY-RUN" "Would transcode: $input_file -> $output_file" |
|
| 1401 |
- return 0 |
|
| 1402 |
- fi |
|
| 1403 |
- |
|
| 1404 | 1716 |
if [[ "$VERBOSE" != true ]]; then |
| 1405 | 1717 |
log_progress_start "$display_path" |
| 1406 | 1718 |
fi |
@@ -1436,6 +1748,7 @@ process_video_file() {
|
||
| 1436 | 1748 |
ERRORS=$((ERRORS + 1)) |
| 1437 | 1749 |
DESTINATION_FAILURES=$((DESTINATION_FAILURES + 1)) |
| 1438 | 1750 |
log_msg "ERROR" "Could not create temporary ffmpeg log file" |
| 1751 |
+ cleanup_process_inputs "$staged_input_file" "$repacked_input_file" |
|
| 1439 | 1752 |
return 3 |
| 1440 | 1753 |
fi |
| 1441 | 1754 |
if run_ffmpeg_with_signal_guard "${cmd[@]}" >"$ffmpeg_log" 2>&1; then
|
@@ -1454,7 +1767,7 @@ process_video_file() {
|
||
| 1454 | 1767 |
# Treat any user-triggered abort as a clean stop — no error counters, no noise. |
| 1455 | 1768 |
if [[ "$STOP_AFTER_CURRENT" == true && "$INTERRUPT_COUNT" -gt 0 ]]; then |
| 1456 | 1769 |
cleanup_transcode_artifacts "$temp_output_file" "$output_file" |
| 1457 |
- cleanup_repacked_input "$repacked_input_file" |
|
| 1770 |
+ cleanup_process_inputs "$staged_input_file" "$repacked_input_file" |
|
| 1458 | 1771 |
return 4 |
| 1459 | 1772 |
fi |
| 1460 | 1773 |
|
@@ -1487,45 +1800,45 @@ process_video_file() {
|
||
| 1487 | 1800 |
if [[ "$failure_rc" -eq 3 ]]; then |
| 1488 | 1801 |
log_msg "ERROR" "Destination cannot accept more output data: $out_dir" |
| 1489 | 1802 |
fi |
| 1490 |
- cleanup_repacked_input "$repacked_input_file" |
|
| 1803 |
+ cleanup_process_inputs "$staged_input_file" "$repacked_input_file" |
|
| 1491 | 1804 |
return "$failure_rc" |
| 1492 | 1805 |
fi |
| 1493 | 1806 |
|
| 1494 | 1807 |
TOTAL_VIDEO_TIME_SEC=$((TOTAL_VIDEO_TIME_SEC + encode_elapsed_sec)) |
| 1495 | 1808 |
|
| 1496 |
- if ! restore_metadata_with_exiftool "$input_file" "$temp_output_file"; then |
|
| 1809 |
+ if ! restore_metadata_with_exiftool "$encode_input_file" "$temp_output_file"; then |
|
| 1497 | 1810 |
cleanup_transcode_artifacts "$temp_output_file" "$output_file" |
| 1498 | 1811 |
if [[ "$STOP_AFTER_CURRENT" == true && "$INTERRUPT_COUNT" -gt 0 ]]; then |
| 1499 |
- cleanup_repacked_input "$repacked_input_file" |
|
| 1812 |
+ cleanup_process_inputs "$staged_input_file" "$repacked_input_file" |
|
| 1500 | 1813 |
return 4 |
| 1501 | 1814 |
fi |
| 1502 | 1815 |
ERRORS=$((ERRORS + 1)) |
| 1503 | 1816 |
DESTINATION_FAILURES=$((DESTINATION_FAILURES + 1)) |
| 1504 |
- cleanup_repacked_input "$repacked_input_file" |
|
| 1817 |
+ cleanup_process_inputs "$staged_input_file" "$repacked_input_file" |
|
| 1505 | 1818 |
return 3 |
| 1506 | 1819 |
fi |
| 1507 | 1820 |
|
| 1508 |
- if ! map_garmin_model_to_standard_tags "$input_file" "$temp_output_file"; then |
|
| 1821 |
+ if ! map_garmin_model_to_standard_tags "$encode_input_file" "$temp_output_file"; then |
|
| 1509 | 1822 |
cleanup_transcode_artifacts "$temp_output_file" "$output_file" |
| 1510 | 1823 |
if [[ "$STOP_AFTER_CURRENT" == true && "$INTERRUPT_COUNT" -gt 0 ]]; then |
| 1511 |
- cleanup_repacked_input "$repacked_input_file" |
|
| 1824 |
+ cleanup_process_inputs "$staged_input_file" "$repacked_input_file" |
|
| 1512 | 1825 |
return 4 |
| 1513 | 1826 |
fi |
| 1514 | 1827 |
ERRORS=$((ERRORS + 1)) |
| 1515 | 1828 |
DESTINATION_FAILURES=$((DESTINATION_FAILURES + 1)) |
| 1516 |
- cleanup_repacked_input "$repacked_input_file" |
|
| 1829 |
+ cleanup_process_inputs "$staged_input_file" "$repacked_input_file" |
|
| 1517 | 1830 |
return 3 |
| 1518 | 1831 |
fi |
| 1519 | 1832 |
|
| 1520 | 1833 |
if ! write_transcode_encoder_metadata "$temp_output_file"; then |
| 1521 | 1834 |
cleanup_transcode_artifacts "$temp_output_file" "$output_file" |
| 1522 | 1835 |
if [[ "$STOP_AFTER_CURRENT" == true && "$INTERRUPT_COUNT" -gt 0 ]]; then |
| 1523 |
- cleanup_repacked_input "$repacked_input_file" |
|
| 1836 |
+ cleanup_process_inputs "$staged_input_file" "$repacked_input_file" |
|
| 1524 | 1837 |
return 4 |
| 1525 | 1838 |
fi |
| 1526 | 1839 |
ERRORS=$((ERRORS + 1)) |
| 1527 | 1840 |
DESTINATION_FAILURES=$((DESTINATION_FAILURES + 1)) |
| 1528 |
- cleanup_repacked_input "$repacked_input_file" |
|
| 1841 |
+ cleanup_process_inputs "$staged_input_file" "$repacked_input_file" |
|
| 1529 | 1842 |
return 3 |
| 1530 | 1843 |
fi |
| 1531 | 1844 |
|
@@ -1535,27 +1848,36 @@ process_video_file() {
|
||
| 1535 | 1848 |
ERRORS=$((ERRORS + 1)) |
| 1536 | 1849 |
if destination_cannot_accept_file "$input_file" "$out_dir" ""; then |
| 1537 | 1850 |
DESTINATION_FAILURES=$((DESTINATION_FAILURES + 1)) |
| 1538 |
- cleanup_repacked_input "$repacked_input_file" |
|
| 1851 |
+ cleanup_process_inputs "$staged_input_file" "$repacked_input_file" |
|
| 1539 | 1852 |
return 3 |
| 1540 | 1853 |
fi |
| 1541 | 1854 |
# Validation failed but destination is healthy: the source or encoder produced |
| 1542 | 1855 |
# a corrupt/truncated output. Treat as source error so --keep-going can skip it. |
| 1543 | 1856 |
INVALID_SOURCES_SKIPPED=$((INVALID_SOURCES_SKIPPED + 1)) |
| 1544 | 1857 |
log_msg "ERROR" "Encode produced invalid output (source may be corrupt): $input_file" |
| 1545 |
- cleanup_repacked_input "$repacked_input_file" |
|
| 1858 |
+ cleanup_process_inputs "$staged_input_file" "$repacked_input_file" |
|
| 1546 | 1859 |
return 2 |
| 1547 | 1860 |
fi |
| 1548 | 1861 |
fi |
| 1549 | 1862 |
|
| 1550 | 1863 |
touch -r "$input_file" "$temp_output_file" || true |
| 1551 | 1864 |
|
| 1865 |
+ if ! mkdir -p "$out_dir"; then |
|
| 1866 |
+ cleanup_transcode_artifacts "$temp_output_file" "$output_file" |
|
| 1867 |
+ ERRORS=$((ERRORS + 1)) |
|
| 1868 |
+ DESTINATION_FAILURES=$((DESTINATION_FAILURES + 1)) |
|
| 1869 |
+ log_msg "ERROR" "Failed to create destination directory: $out_dir" |
|
| 1870 |
+ cleanup_process_inputs "$staged_input_file" "$repacked_input_file" |
|
| 1871 |
+ return 3 |
|
| 1872 |
+ fi |
|
| 1873 |
+ |
|
| 1552 | 1874 |
if [[ "$OVERWRITE" == true ]]; then |
| 1553 | 1875 |
if ! mv -f "$temp_output_file" "$output_file"; then |
| 1554 | 1876 |
cleanup_transcode_artifacts "$temp_output_file" "$output_file" |
| 1555 | 1877 |
ERRORS=$((ERRORS + 1)) |
| 1556 | 1878 |
DESTINATION_FAILURES=$((DESTINATION_FAILURES + 1)) |
| 1557 | 1879 |
log_msg "ERROR" "Failed to move completed output into place: $output_file" |
| 1558 |
- cleanup_repacked_input "$repacked_input_file" |
|
| 1880 |
+ cleanup_process_inputs "$staged_input_file" "$repacked_input_file" |
|
| 1559 | 1881 |
return 3 |
| 1560 | 1882 |
fi |
| 1561 | 1883 |
else |
@@ -1564,7 +1886,7 @@ process_video_file() {
|
||
| 1564 | 1886 |
ERRORS=$((ERRORS + 1)) |
| 1565 | 1887 |
DESTINATION_FAILURES=$((DESTINATION_FAILURES + 1)) |
| 1566 | 1888 |
log_msg "ERROR" "Failed to move completed output into place: $output_file" |
| 1567 |
- cleanup_repacked_input "$repacked_input_file" |
|
| 1889 |
+ cleanup_process_inputs "$staged_input_file" "$repacked_input_file" |
|
| 1568 | 1890 |
return 3 |
| 1569 | 1891 |
fi |
| 1570 | 1892 |
fi |
@@ -1580,7 +1902,7 @@ process_video_file() {
|
||
| 1580 | 1902 |
else |
| 1581 | 1903 |
ERRORS=$((ERRORS + 1)) |
| 1582 | 1904 |
log_msg "ERROR" "Failed to remove source after validation: $input_file" |
| 1583 |
- cleanup_repacked_input "$repacked_input_file" |
|
| 1905 |
+ cleanup_process_inputs "$staged_input_file" "$repacked_input_file" |
|
| 1584 | 1906 |
return 1 |
| 1585 | 1907 |
fi |
| 1586 | 1908 |
fi |
@@ -1602,7 +1924,7 @@ process_video_file() {
|
||
| 1602 | 1924 |
log_progress_done "$display_path" "$file_real_elapsed_sec" "$encode_elapsed_sec" |
| 1603 | 1925 |
fi |
| 1604 | 1926 |
|
| 1605 |
- cleanup_repacked_input "$repacked_input_file" |
|
| 1927 |
+ cleanup_process_inputs "$staged_input_file" "$repacked_input_file" |
|
| 1606 | 1928 |
|
| 1607 | 1929 |
return 0 |
| 1608 | 1930 |
} |
@@ -1612,10 +1934,21 @@ main() {
|
||
| 1612 | 1934 |
total_started_at="$(date +%s)" |
| 1613 | 1935 |
RUN_STARTED_AT="$total_started_at" |
| 1614 | 1936 |
|
| 1937 |
+ if ! pwd >/dev/null 2>&1; then |
|
| 1938 |
+ log_msg "WARN" "Current working directory no longer exists or is inaccessible; switching to a stable directory before continuing." |
|
| 1939 |
+ if [[ -n "${HOME:-}" && -d "$HOME" ]]; then
|
|
| 1940 |
+ cd "$HOME" || cd / |
|
| 1941 |
+ else |
|
| 1942 |
+ cd / |
|
| 1943 |
+ fi |
|
| 1944 |
+ fi |
|
| 1945 |
+ |
|
| 1615 | 1946 |
trap 'handle_interrupt' INT TERM |
| 1947 |
+ trap 'handle_unexpected_error "$LINENO" "$BASH_COMMAND"' ERR |
|
| 1616 | 1948 |
trap 'cleanup_on_exit' EXIT |
| 1617 | 1949 |
|
| 1618 | 1950 |
parse_args "$@" |
| 1951 |
+ MAIN_FLOW_STARTED=true |
|
| 1619 | 1952 |
check_tools |
| 1620 | 1953 |
|
| 1621 | 1954 |
# Auto single-file detection: if --source points to a file, treat it as single-file mode |
@@ -1626,35 +1959,47 @@ main() {
|
||
| 1626 | 1959 |
|
| 1627 | 1960 |
normalize_source_dir |
| 1628 | 1961 |
normalize_dest_dir |
| 1629 |
- normalize_staging_dir |
|
| 1630 |
- collect_extensions |
|
| 1631 | 1962 |
|
| 1632 | 1963 |
if path_is_within "$DEST_DIR" "$SOURCE_DIR"; then |
| 1633 | 1964 |
die "Destination must not be inside source. Choose a destination outside source: source=$SOURCE_DIR destination=$DEST_DIR" |
| 1634 | 1965 |
fi |
| 1635 | 1966 |
|
| 1967 |
+ preflight_destination_writable "$DEST_DIR" |
|
| 1968 |
+ normalize_staging_dir |
|
| 1969 |
+ if [[ "$STAGE_INPUT" == true ]]; then |
|
| 1970 |
+ log_msg "INFO" "Input staging enabled; sources will be copied to staging before encoding" |
|
| 1971 |
+ fi |
|
| 1972 |
+ collect_extensions |
|
| 1973 |
+ |
|
| 1636 | 1974 |
detect_encoders |
| 1637 | 1975 |
resolve_encoder |
| 1638 | 1976 |
|
| 1977 |
+ log_msg "INFO" "Scanning source for videos: $SOURCE_DIR" |
|
| 1639 | 1978 |
collect_video_files |
| 1640 | 1979 |
if [[ -z "${VIDEO_FILES[*]-}" ]]; then
|
| 1641 | 1980 |
log_msg "INFO" "No video files found for extensions: $EXTENSIONS_CSV" |
| 1981 |
+ else |
|
| 1982 |
+ log_msg "INFO" "Found ${#VIDEO_FILES[@]} candidate video file(s)"
|
|
| 1642 | 1983 |
fi |
| 1643 | 1984 |
|
| 1644 | 1985 |
local f |
| 1986 |
+ vlog_msg "INFO" "Starting processing loop for ${#VIDEO_FILES[@]} candidate file(s)"
|
|
| 1645 | 1987 |
for f in ${VIDEO_FILES+"${VIDEO_FILES[@]}"}; do
|
| 1646 |
- check_for_quit_key |
|
| 1988 |
+ vlog_msg "CHECKPOINT" "loop_file: $f" |
|
| 1989 |
+ check_for_quit_key || true |
|
| 1647 | 1990 |
if [[ "$STOP_AFTER_CURRENT" == true ]]; then |
| 1648 | 1991 |
log_msg "INFO" "Stop requested; ending before next file" |
| 1649 | 1992 |
break |
| 1650 | 1993 |
fi |
| 1651 | 1994 |
|
| 1652 | 1995 |
local process_rc=0 |
| 1996 |
+ vlog_msg "INFO" "Dispatching $(rel_path_from_source "$f")" |
|
| 1653 | 1997 |
if process_video_file "$f"; then |
| 1654 | 1998 |
process_rc=0 |
| 1655 | 1999 |
else |
| 1656 | 2000 |
process_rc=$? |
| 1657 | 2001 |
fi |
| 2002 |
+ vlog_msg "CHECKPOINT" "process_rc=$process_rc file=$f" |
|
| 1658 | 2003 |
|
| 1659 | 2004 |
if [[ "$DEBUG_TIMING_LIMIT" -gt 0 ]]; then |
| 1660 | 2005 |
DEBUG_TIMING_FILES=$((DEBUG_TIMING_FILES + 1)) |
@@ -1763,6 +2108,7 @@ main() {
|
||
| 1763 | 2108 |
"$staging_warmup_fmt" \ |
| 1764 | 2109 |
"$INPUT_BYTES_PROCESSED" \ |
| 1765 | 2110 |
"$OUTPUT_BYTES_PROCESSED" |
| 2111 |
+ FINAL_REPORT_PRINTED=true |
|
| 1766 | 2112 |
|
| 1767 | 2113 |
if [[ "$ERRORS" -gt 0 ]]; then |
| 1768 | 2114 |
exit 1 |