@@ -5,6 +5,21 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). |
||
| 5 | 5 |
|
| 6 | 6 |
--- |
| 7 | 7 |
|
| 8 |
+## [Unreleased] |
|
| 9 |
+ |
|
| 10 |
+### Changed |
|
| 11 |
+- `--move-source` renamed to `--delete-source` (old name kept as alias) |
|
| 12 |
+- `--continue-on-error` renamed to `--keep-going` (old name kept as alias) |
|
| 13 |
+### Added |
|
| 14 |
+- `--unattended` preset (`--delete-source` + `--keep-going`) for long unattended runs |
|
| 15 |
+ |
|
| 16 |
+- `cleanup_garmin_varia_media_folder.sh` helper for media-folder hygiene (import + transcoded output): |
|
| 17 |
+ removes AppleDouble artifacts (`._*`), removes zero-size MP4 files, |
|
| 18 |
+ and normalizes single-suffix duplicate timestamp names |
|
| 19 |
+ |
|
| 20 |
+### Behavior |
|
| 21 |
+- Import cleanup returns exit code `1` when duplicate groups are blocked and need manual review |
|
| 22 |
+ |
|
| 8 | 23 |
## [1.0.0] — 2026-05-04 |
| 9 | 24 |
|
| 10 | 25 |
Initial release. |
@@ -23,7 +23,13 @@ manifest placeholder for a future FIT sync pipeline. |
||
| 23 | 23 |
./garmin_varia_transcode.sh -s SampleFootage/Day/clip.mp4 -d Output --verbose |
| 24 | 24 |
|
| 25 | 25 |
# Transcode + delete originals after validation |
| 26 |
-./garmin_varia_transcode.sh -s SampleFootage -d Output --move-source |
|
| 26 |
+./garmin_varia_transcode.sh -s SampleFootage -d Output --delete-source |
|
| 27 |
+ |
|
| 28 |
+# Long unattended run preset (delete-source + keep-going) |
|
| 29 |
+./garmin_varia_transcode.sh -s SampleFootage -d Output --unattended |
|
| 30 |
+ |
|
| 31 |
+# Media cleanup helper (Apple artifacts, zero-byte MP4, duplicate suffix normalization) |
|
| 32 |
+./cleanup_garmin_varia_media_folder.sh --dry-run ~/Autofs/xdev/autonas/ext01/@Camera/import |
|
| 27 | 33 |
``` |
| 28 | 34 |
|
| 29 | 35 |
## Encoding Modes |
@@ -50,7 +56,11 @@ Options: |
||
| 50 | 56 |
--no-recursive Process only the top-level source directory |
| 51 | 57 |
--extensions LIST Comma-separated extensions (default: mp4,mov,avi,m4v) |
| 52 | 58 |
--verbose Full per-operation logs + ffmpeg/ffprobe output |
| 53 |
- --move-source Delete source after strict post-encode validation |
|
| 59 |
+ --delete-source Delete source after strict post-encode validation |
|
| 60 |
+ --keep-going Continue after source-file failures (default: stop) |
|
| 61 |
+ --unattended Preset for long runs: --delete-source + --keep-going |
|
| 62 |
+ --no-apple-repack-fallback Disable macOS avconvert fallback for unreadable MP4/MOV sources |
|
| 63 |
+ --apple-repack-fallback Enable macOS avconvert fallback (default) |
|
| 54 | 64 |
-h, --help Show full help with encoding mode details |
| 55 | 65 |
``` |
| 56 | 66 |
|
@@ -75,8 +85,46 @@ Options: |
||
| 75 | 85 |
## Safety |
| 76 | 86 |
|
| 77 | 87 |
- Destination inside source is rejected with an error |
| 78 |
-- `--move-source` only deletes source after codec + duration validation passes |
|
| 88 |
+- `--delete-source` only deletes source after codec + duration validation passes |
|
| 79 | 89 |
- `--dry-run` never writes files |
| 90 |
+- `Ctrl+C` behavior during long runs: |
|
| 91 |
+ first press requests stop after current file; second press force-stops current encode |
|
| 92 |
+- On macOS, unreadable MP4/MOV sources automatically try `avconvert --preset PresetPassthrough` |
|
| 93 |
+ as fallback before being marked as unreadable |
|
| 94 |
+ |
|
| 95 |
+Backward-compatible aliases: |
|
| 96 |
+- `--move-source` maps to `--delete-source` |
|
| 97 |
+- `--continue-on-error` maps to `--keep-going` |
|
| 98 |
+ |
|
| 99 |
+## Media Cleanup Utility |
|
| 100 |
+ |
|
| 101 |
+`cleanup_garmin_varia_media_folder.sh` is a companion utility for import and transcoded media folders. |
|
| 102 |
+ |
|
| 103 |
+What it does: |
|
| 104 |
+- Removes AppleDouble artifacts (`._*`) up to 4096 bytes |
|
| 105 |
+- Removes zero-size `.mp4` files |
|
| 106 |
+- Normalizes duplicate timestamp files: |
|
| 107 |
+ if `YYYY-MM-DD_HH-MM-SS.mp4` is missing and exactly one |
|
| 108 |
+ `YYYY-MM-DD_HH-MM-SS_<n>.mp4` exists, it renames it to base name |
|
| 109 |
+- Reports blocked duplicate groups when base exists or multiple suffixed duplicates exist |
|
| 110 |
+ |
|
| 111 |
+Usage: |
|
| 112 |
+ |
|
| 113 |
+```bash |
|
| 114 |
+# Preview only |
|
| 115 |
+./cleanup_garmin_varia_media_folder.sh --dry-run ~/Autofs/xdev/autonas/ext01/@Camera/import |
|
| 116 |
+ |
|
| 117 |
+# Apply changes |
|
| 118 |
+./cleanup_garmin_varia_media_folder.sh ~/Autofs/xdev/autonas/ext01/@Camera/import |
|
| 119 |
+ |
|
| 120 |
+# Example on transcoded output folder |
|
| 121 |
+./cleanup_garmin_varia_media_folder.sh --dry-run ~/Autofs/xdev/is-baobab/nvme0n1/@backup/Garmin |
|
| 122 |
+``` |
|
| 123 |
+ |
|
| 124 |
+Exit codes: |
|
| 125 |
+- `0`: cleanup completed with no blocked duplicate groups |
|
| 126 |
+- `1`: cleanup completed but blocked duplicate groups require manual review |
|
| 127 |
+- `2`: invalid invocation or runtime error |
|
| 80 | 128 |
|
| 81 | 129 |
## Source Files |
| 82 | 130 |
|
@@ -84,6 +132,7 @@ Options: |
||
| 84 | 132 |
SampleFootage/ Original clips (gitignored) |
| 85 | 133 |
Output/ Transcoded output (gitignored) |
| 86 | 134 |
garmin_varia_transcode.sh Main script |
| 135 |
+cleanup_garmin_varia_media_folder.sh Media cleanup utility (import + transcoded folders) |
|
| 87 | 136 |
CHANGELOG.md Version history |
| 88 | 137 |
DEVLOG.md Development decisions and rationale |
| 89 | 138 |
``` |
@@ -0,0 +1,8 @@ |
||
| 1 |
+{
|
|
| 2 |
+ "folders": [ |
|
| 3 |
+ {
|
|
| 4 |
+ "path": "." |
|
| 5 |
+ } |
|
| 6 |
+ ], |
|
| 7 |
+ "settings": {}
|
|
| 8 |
+} |
|
@@ -0,0 +1,356 @@ |
||
| 1 |
+#!/usr/bin/env bash |
|
| 2 |
+set -euo pipefail |
|
| 3 |
+ |
|
| 4 |
+TOOL_NAME="cleanup_garmin_varia_media_folder.sh" |
|
| 5 |
+DEFAULT_MEDIA_ROOT="." |
|
| 6 |
+MAX_APPLEDOUBLE_BYTES=4096 |
|
| 7 |
+ |
|
| 8 |
+DRY_RUN=false |
|
| 9 |
+VERBOSE=false |
|
| 10 |
+MEDIA_ROOT="$DEFAULT_MEDIA_ROOT" |
|
| 11 |
+ |
|
| 12 |
+TMP_MP4_LIST="" |
|
| 13 |
+TMP_ACTIONS="" |
|
| 14 |
+TMP_NONSTANDARD="" |
|
| 15 |
+TMP_ZERO="" |
|
| 16 |
+TMP_BLOCKED="" |
|
| 17 |
+TMP_APPLE="" |
|
| 18 |
+ |
|
| 19 |
+APPLE_ARTIFACTS_REMOVED=0 |
|
| 20 |
+ZERO_SIZE_REMOVED=0 |
|
| 21 |
+NONSTANDARD_REMOVED=0 |
|
| 22 |
+RENAMES_DONE=0 |
|
| 23 |
+BLOCKED_GROUPS=0 |
|
| 24 |
+BLOCKED_FILES=0 |
|
| 25 |
+ |
|
| 26 |
+usage() {
|
|
| 27 |
+ cat <<'EOF' |
|
| 28 |
+Usage: |
|
| 29 |
+ cleanup_garmin_varia_media_folder.sh [options] [MEDIA_ROOT] |
|
| 30 |
+ |
|
| 31 |
+Purpose: |
|
| 32 |
+ Clean common Apple artifacts and zero-size MP4 files, then normalize |
|
| 33 |
+ duplicate MP4 names produced during copy/import retries. |
|
| 34 |
+ |
|
| 35 |
+Rules: |
|
| 36 |
+ - Delete AppleDouble sidecars matching ._* only when size <= 4096 bytes |
|
| 37 |
+ - Delete zero-size .mp4 files |
|
| 38 |
+ - For canonical timestamp names: |
|
| 39 |
+ YYYY-MM-DD_HH-MM-SS.mp4 |
|
| 40 |
+ YYYY-MM-DD_HH-MM-SS_<n>.mp4 |
|
| 41 |
+ if exactly one suffixed duplicate exists and the base file is missing, |
|
| 42 |
+ rename duplicate to base name |
|
| 43 |
+ - If base exists, or multiple suffixed duplicates exist, keep files unchanged |
|
| 44 |
+ and report them as blocked |
|
| 45 |
+ |
|
| 46 |
+Options: |
|
| 47 |
+ --dry-run Print actions without changing files |
|
| 48 |
+ --verbose Print per-file operations |
|
| 49 |
+ -h, --help Show this help |
|
| 50 |
+ |
|
| 51 |
+Examples: |
|
| 52 |
+ ./cleanup_garmin_varia_media_folder.sh --dry-run ~/Autofs/xdev/autonas/ext01/@Camera/import |
|
| 53 |
+ ./cleanup_garmin_varia_media_folder.sh ~/Autofs/xdev/is-baobab/nvme0n1/@backup/Garmin |
|
| 54 |
+EOF |
|
| 55 |
+} |
|
| 56 |
+ |
|
| 57 |
+log_msg() {
|
|
| 58 |
+ local level="$1" |
|
| 59 |
+ shift |
|
| 60 |
+ printf '[%s] [%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$level" "$*" |
|
| 61 |
+} |
|
| 62 |
+ |
|
| 63 |
+vlog_msg() {
|
|
| 64 |
+ if [[ "$VERBOSE" == true ]]; then |
|
| 65 |
+ log_msg "INFO" "$*" |
|
| 66 |
+ fi |
|
| 67 |
+} |
|
| 68 |
+ |
|
| 69 |
+die() {
|
|
| 70 |
+ log_msg "ERROR" "$*" |
|
| 71 |
+ exit 2 |
|
| 72 |
+} |
|
| 73 |
+ |
|
| 74 |
+cleanup_tmp_files() {
|
|
| 75 |
+ rm -f -- "$TMP_MP4_LIST" "$TMP_ACTIONS" "$TMP_NONSTANDARD" "$TMP_ZERO" "$TMP_BLOCKED" "$TMP_APPLE" "$TMP_ACTIONS.tmp" 2>/dev/null || true |
|
| 76 |
+} |
|
| 77 |
+ |
|
| 78 |
+parse_args() {
|
|
| 79 |
+ while [[ $# -gt 0 ]]; do |
|
| 80 |
+ case "$1" in |
|
| 81 |
+ --dry-run) |
|
| 82 |
+ DRY_RUN=true |
|
| 83 |
+ shift |
|
| 84 |
+ ;; |
|
| 85 |
+ --verbose) |
|
| 86 |
+ VERBOSE=true |
|
| 87 |
+ shift |
|
| 88 |
+ ;; |
|
| 89 |
+ -h|--help) |
|
| 90 |
+ usage |
|
| 91 |
+ exit 0 |
|
| 92 |
+ ;; |
|
| 93 |
+ -*) |
|
| 94 |
+ die "Unknown option: $1" |
|
| 95 |
+ ;; |
|
| 96 |
+ *) |
|
| 97 |
+ MEDIA_ROOT="$1" |
|
| 98 |
+ shift |
|
| 99 |
+ ;; |
|
| 100 |
+ esac |
|
| 101 |
+ done |
|
| 102 |
+} |
|
| 103 |
+ |
|
| 104 |
+init_tmp_files() {
|
|
| 105 |
+ TMP_MP4_LIST="$(mktemp)" |
|
| 106 |
+ TMP_ACTIONS="$(mktemp)" |
|
| 107 |
+ TMP_NONSTANDARD="$(mktemp)" |
|
| 108 |
+ TMP_ZERO="$(mktemp)" |
|
| 109 |
+ TMP_BLOCKED="$(mktemp)" |
|
| 110 |
+ TMP_APPLE="$(mktemp)" |
|
| 111 |
+ trap cleanup_tmp_files EXIT |
|
| 112 |
+} |
|
| 113 |
+ |
|
| 114 |
+validate_media_root() {
|
|
| 115 |
+ [[ -d "$MEDIA_ROOT" ]] || die "Media root not found: $MEDIA_ROOT" |
|
| 116 |
+} |
|
| 117 |
+ |
|
| 118 |
+rescan_mp4_files() {
|
|
| 119 |
+ find "$MEDIA_ROOT" -type f -name '*.mp4' | sort > "$TMP_MP4_LIST" |
|
| 120 |
+} |
|
| 121 |
+ |
|
| 122 |
+safe_remove_file() {
|
|
| 123 |
+ local path="$1" |
|
| 124 |
+ if [[ "$DRY_RUN" == true ]]; then |
|
| 125 |
+ return 0 |
|
| 126 |
+ fi |
|
| 127 |
+ rm -f -- "$path" |
|
| 128 |
+} |
|
| 129 |
+ |
|
| 130 |
+remove_apple_artifacts() {
|
|
| 131 |
+ local file size |
|
| 132 |
+ while IFS= read -r file; do |
|
| 133 |
+ [[ -n "$file" ]] || continue |
|
| 134 |
+ size="$(wc -c < "$file" | tr -d ' ')" |
|
| 135 |
+ |
|
| 136 |
+ if [[ "$size" =~ ^[0-9]+$ ]] && [[ "$size" -le "$MAX_APPLEDOUBLE_BYTES" ]]; then |
|
| 137 |
+ APPLE_ARTIFACTS_REMOVED=$((APPLE_ARTIFACTS_REMOVED + 1)) |
|
| 138 |
+ printf '%s\n' "$file" >> "$TMP_APPLE" |
|
| 139 |
+ vlog_msg "Apple artifact: $file (${size} bytes)"
|
|
| 140 |
+ safe_remove_file "$file" |
|
| 141 |
+ else |
|
| 142 |
+ vlog_msg "Skipped ._* larger than threshold: $file (${size} bytes)"
|
|
| 143 |
+ fi |
|
| 144 |
+ done < <(find "$MEDIA_ROOT" -type f -name '._*' | sort) |
|
| 145 |
+ |
|
| 146 |
+ if [[ "$APPLE_ARTIFACTS_REMOVED" -gt 0 ]]; then |
|
| 147 |
+ if [[ "$DRY_RUN" == true ]]; then |
|
| 148 |
+ log_msg "INFO" "Would remove $APPLE_ARTIFACTS_REMOVED Apple artifact file(s)" |
|
| 149 |
+ else |
|
| 150 |
+ log_msg "INFO" "Removed $APPLE_ARTIFACTS_REMOVED Apple artifact file(s)" |
|
| 151 |
+ fi |
|
| 152 |
+ rescan_mp4_files |
|
| 153 |
+ fi |
|
| 154 |
+} |
|
| 155 |
+ |
|
| 156 |
+remove_zero_size_mp4() {
|
|
| 157 |
+ local file |
|
| 158 |
+ while IFS= read -r file; do |
|
| 159 |
+ [[ -n "$file" ]] || continue |
|
| 160 |
+ if [[ ! -s "$file" ]]; then |
|
| 161 |
+ ZERO_SIZE_REMOVED=$((ZERO_SIZE_REMOVED + 1)) |
|
| 162 |
+ printf '%s\n' "$file" >> "$TMP_ZERO" |
|
| 163 |
+ vlog_msg "Zero-size MP4: $file" |
|
| 164 |
+ safe_remove_file "$file" |
|
| 165 |
+ fi |
|
| 166 |
+ done < "$TMP_MP4_LIST" |
|
| 167 |
+ |
|
| 168 |
+ if [[ "$ZERO_SIZE_REMOVED" -gt 0 ]]; then |
|
| 169 |
+ if [[ "$DRY_RUN" == true ]]; then |
|
| 170 |
+ log_msg "INFO" "Would remove $ZERO_SIZE_REMOVED zero-size MP4 file(s)" |
|
| 171 |
+ else |
|
| 172 |
+ log_msg "INFO" "Removed $ZERO_SIZE_REMOVED zero-size MP4 file(s)" |
|
| 173 |
+ fi |
|
| 174 |
+ rescan_mp4_files |
|
| 175 |
+ fi |
|
| 176 |
+} |
|
| 177 |
+ |
|
| 178 |
+collect_duplicate_actions() {
|
|
| 179 |
+ local file base dir timestamp suffix |
|
| 180 |
+ |
|
| 181 |
+ : > "$TMP_ACTIONS" |
|
| 182 |
+ : > "$TMP_NONSTANDARD" |
|
| 183 |
+ |
|
| 184 |
+ while IFS= read -r file; do |
|
| 185 |
+ [[ -n "$file" ]] || continue |
|
| 186 |
+ base="$(basename "$file")" |
|
| 187 |
+ |
|
| 188 |
+ if [[ "$base" =~ ^([0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2})(_[0-9]+)?\.mp4$ ]]; then
|
|
| 189 |
+ dir="$(dirname "$file")" |
|
| 190 |
+ timestamp="${BASH_REMATCH[1]}"
|
|
| 191 |
+ suffix="${BASH_REMATCH[2]:-}"
|
|
| 192 |
+ if [[ -n "$suffix" ]]; then |
|
| 193 |
+ suffix="${suffix#_}"
|
|
| 194 |
+ else |
|
| 195 |
+ suffix=0 |
|
| 196 |
+ fi |
|
| 197 |
+ printf '%s\t%s\t%s\t%s\n' "$dir" "$timestamp" "$suffix" "$file" >> "$TMP_ACTIONS" |
|
| 198 |
+ elif [[ "$base" =~ _[0-9]+\.mp4$ ]]; then |
|
| 199 |
+ printf '%s\n' "$file" >> "$TMP_NONSTANDARD" |
|
| 200 |
+ NONSTANDARD_REMOVED=$((NONSTANDARD_REMOVED + 1)) |
|
| 201 |
+ vlog_msg "Non-standard duplicate-like file: $file" |
|
| 202 |
+ safe_remove_file "$file" |
|
| 203 |
+ fi |
|
| 204 |
+ done < "$TMP_MP4_LIST" |
|
| 205 |
+} |
|
| 206 |
+ |
|
| 207 |
+build_rename_plan() {
|
|
| 208 |
+ awk -F'\t' ' |
|
| 209 |
+function flush() {
|
|
| 210 |
+ if (dup_count == 1 && orig_seen == 0) {
|
|
| 211 |
+ print "RENAME\t" dup_files[1] "\t" dir "\t" timestamp |
|
| 212 |
+ } else if (dup_count > 0) {
|
|
| 213 |
+ print "GROUP\t" dir "\t" timestamp "\t" dup_count |
|
| 214 |
+ for (i = 1; i <= dup_count; i++) {
|
|
| 215 |
+ print "BLOCK\t" dup_files[i] |
|
| 216 |
+ } |
|
| 217 |
+ } |
|
| 218 |
+} |
|
| 219 |
+ |
|
| 220 |
+BEGIN {
|
|
| 221 |
+ OFS = "\t" |
|
| 222 |
+ dir = "" |
|
| 223 |
+ timestamp = "" |
|
| 224 |
+ orig_seen = 0 |
|
| 225 |
+ dup_count = 0 |
|
| 226 |
+} |
|
| 227 |
+ |
|
| 228 |
+{
|
|
| 229 |
+ if ($1 != dir || $2 != timestamp) {
|
|
| 230 |
+ if (NR > 1) {
|
|
| 231 |
+ flush() |
|
| 232 |
+ } |
|
| 233 |
+ dir = $1 |
|
| 234 |
+ timestamp = $2 |
|
| 235 |
+ orig_seen = 0 |
|
| 236 |
+ dup_count = 0 |
|
| 237 |
+ delete dup_files |
|
| 238 |
+ } |
|
| 239 |
+ |
|
| 240 |
+ if ($3 == 0) {
|
|
| 241 |
+ orig_seen = 1 |
|
| 242 |
+ } else {
|
|
| 243 |
+ dup_count++ |
|
| 244 |
+ dup_files[dup_count] = $4 |
|
| 245 |
+ } |
|
| 246 |
+} |
|
| 247 |
+ |
|
| 248 |
+END {
|
|
| 249 |
+ if (NR > 0) {
|
|
| 250 |
+ flush() |
|
| 251 |
+ } |
|
| 252 |
+} |
|
| 253 |
+' "$TMP_ACTIONS" > "$TMP_ACTIONS.tmp" |
|
| 254 |
+ |
|
| 255 |
+ mv "$TMP_ACTIONS.tmp" "$TMP_ACTIONS" |
|
| 256 |
+} |
|
| 257 |
+ |
|
| 258 |
+apply_rename_plan() {
|
|
| 259 |
+ local action src dir timestamp dup_count dst |
|
| 260 |
+ |
|
| 261 |
+ while IFS=$'\t' read -r action src dir timestamp dup_count; do |
|
| 262 |
+ [[ -n "$action" ]] || continue |
|
| 263 |
+ case "$action" in |
|
| 264 |
+ RENAME) |
|
| 265 |
+ dst="$dir/$timestamp.mp4" |
|
| 266 |
+ RENAMES_DONE=$((RENAMES_DONE + 1)) |
|
| 267 |
+ if [[ "$DRY_RUN" == true ]]; then |
|
| 268 |
+ log_msg "INFO" "DRY-RUN rename: $src -> $dst" |
|
| 269 |
+ else |
|
| 270 |
+ mv -- "$src" "$dst" |
|
| 271 |
+ vlog_msg "Renamed: $src -> $dst" |
|
| 272 |
+ fi |
|
| 273 |
+ ;; |
|
| 274 |
+ GROUP) |
|
| 275 |
+ BLOCKED_GROUPS=$((BLOCKED_GROUPS + 1)) |
|
| 276 |
+ ;; |
|
| 277 |
+ BLOCK) |
|
| 278 |
+ BLOCKED_FILES=$((BLOCKED_FILES + 1)) |
|
| 279 |
+ printf '%s\n' "$src" >> "$TMP_BLOCKED" |
|
| 280 |
+ ;; |
|
| 281 |
+ esac |
|
| 282 |
+ done < "$TMP_ACTIONS" |
|
| 283 |
+} |
|
| 284 |
+ |
|
| 285 |
+print_summary_lists() {
|
|
| 286 |
+ if [[ -s "$TMP_APPLE" ]]; then |
|
| 287 |
+ if [[ "$DRY_RUN" == true ]]; then |
|
| 288 |
+ log_msg "INFO" "Apple artifacts that would be removed:" |
|
| 289 |
+ else |
|
| 290 |
+ log_msg "INFO" "Apple artifacts removed:" |
|
| 291 |
+ fi |
|
| 292 |
+ cat "$TMP_APPLE" |
|
| 293 |
+ fi |
|
| 294 |
+ |
|
| 295 |
+ if [[ -s "$TMP_ZERO" ]]; then |
|
| 296 |
+ if [[ "$DRY_RUN" == true ]]; then |
|
| 297 |
+ log_msg "INFO" "Zero-size MP4 files that would be removed:" |
|
| 298 |
+ else |
|
| 299 |
+ log_msg "INFO" "Zero-size MP4 files removed:" |
|
| 300 |
+ fi |
|
| 301 |
+ cat "$TMP_ZERO" |
|
| 302 |
+ fi |
|
| 303 |
+ |
|
| 304 |
+ if [[ -s "$TMP_NONSTANDARD" ]]; then |
|
| 305 |
+ if [[ "$DRY_RUN" == true ]]; then |
|
| 306 |
+ log_msg "INFO" "Non-standard duplicate-like MP4 files that would be removed:" |
|
| 307 |
+ else |
|
| 308 |
+ log_msg "INFO" "Non-standard duplicate-like MP4 files removed:" |
|
| 309 |
+ fi |
|
| 310 |
+ cat "$TMP_NONSTANDARD" |
|
| 311 |
+ fi |
|
| 312 |
+ |
|
| 313 |
+ if [[ -s "$TMP_BLOCKED" ]]; then |
|
| 314 |
+ log_msg "WARN" "Blocked duplicate files (manual review needed):" |
|
| 315 |
+ cat "$TMP_BLOCKED" |
|
| 316 |
+ fi |
|
| 317 |
+} |
|
| 318 |
+ |
|
| 319 |
+print_summary() {
|
|
| 320 |
+ local mode_text |
|
| 321 |
+ mode_text="apply" |
|
| 322 |
+ if [[ "$DRY_RUN" == true ]]; then |
|
| 323 |
+ mode_text="dry-run" |
|
| 324 |
+ fi |
|
| 325 |
+ |
|
| 326 |
+ log_msg "INFO" "Summary ($mode_text): apple_removed=$APPLE_ARTIFACTS_REMOVED zero_removed=$ZERO_SIZE_REMOVED nonstandard_removed=$NONSTANDARD_REMOVED renamed=$RENAMES_DONE blocked_groups=$BLOCKED_GROUPS blocked_files=$BLOCKED_FILES" |
|
| 327 |
+} |
|
| 328 |
+ |
|
| 329 |
+main() {
|
|
| 330 |
+ parse_args "$@" |
|
| 331 |
+ init_tmp_files |
|
| 332 |
+ validate_media_root |
|
| 333 |
+ |
|
| 334 |
+ log_msg "INFO" "Starting cleanup for media root: $MEDIA_ROOT" |
|
| 335 |
+ if [[ "$DRY_RUN" == true ]]; then |
|
| 336 |
+ log_msg "INFO" "Dry-run enabled; no files will be changed" |
|
| 337 |
+ fi |
|
| 338 |
+ |
|
| 339 |
+ rescan_mp4_files |
|
| 340 |
+ remove_apple_artifacts |
|
| 341 |
+ remove_zero_size_mp4 |
|
| 342 |
+ collect_duplicate_actions |
|
| 343 |
+ build_rename_plan |
|
| 344 |
+ apply_rename_plan |
|
| 345 |
+ print_summary_lists |
|
| 346 |
+ print_summary |
|
| 347 |
+ |
|
| 348 |
+ if [[ "$BLOCKED_GROUPS" -gt 0 ]]; then |
|
| 349 |
+ log_msg "WARN" "Cleanup completed with blocked duplicate groups" |
|
| 350 |
+ exit 1 |
|
| 351 |
+ fi |
|
| 352 |
+ |
|
| 353 |
+ log_msg "INFO" "Cleanup completed successfully" |
|
| 354 |
+} |
|
| 355 |
+ |
|
| 356 |
+main "$@" |
|
@@ -39,26 +39,39 @@ VERBOSE=false |
||
| 39 | 39 |
MOVE_SOURCE=false |
| 40 | 40 |
SOURCE_PROVIDED=false |
| 41 | 41 |
DEST_PROVIDED=false |
| 42 |
+FAIL_FAST=true |
|
| 43 |
+SOURCE_READABLE_MODE="normal" |
|
| 44 |
+APPLE_REPACK_FALLBACK=true |
|
| 45 |
+REPACKED_SOURCE_PATH="" |
|
| 42 | 46 |
|
| 43 | 47 |
HAS_VIDEOTOOLBOX=false |
| 44 | 48 |
HAS_LIBX265=false |
| 45 | 49 |
HAS_LIBX264=false |
| 50 |
+HAS_AVCONVERT=false |
|
| 46 | 51 |
|
| 47 | 52 |
ENCODER_KIND="" |
| 48 | 53 |
VIDEO_CODEC="" |
| 49 | 54 |
VIDEO_CRF="" |
| 50 | 55 |
VIDEO_ARGS=() |
| 51 | 56 |
FIND_EXT_EXPR=() |
| 57 |
+EXT_LIST=() |
|
| 58 |
+VIDEO_FILES=() |
|
| 52 | 59 |
|
| 53 | 60 |
VIDEOS_PROCESSED=0 |
| 54 | 61 |
VIDEOS_SKIPPED=0 |
| 55 | 62 |
JSON_COPIED=0 |
| 56 | 63 |
JSON_SKIPPED=0 |
| 57 | 64 |
ERRORS=0 |
| 65 |
+INVALID_SOURCES_SKIPPED=0 |
|
| 66 |
+DESTINATION_FAILURES=0 |
|
| 58 | 67 |
TOTAL_VIDEO_TIME_SEC=0 |
| 59 | 68 |
TOTAL_FILE_REAL_TIME_SEC=0 |
| 60 | 69 |
|
| 61 | 70 |
DURATION_TOLERANCE_SEC=1.0 |
| 71 |
+STOP_AFTER_CURRENT=false |
|
| 72 |
+INTERRUPT_COUNT=0 |
|
| 73 |
+CURRENT_FFMPEG_PID="" |
|
| 74 |
+PROGRESS_LINE_OPEN=false |
|
| 62 | 75 |
|
| 63 | 76 |
usage() {
|
| 64 | 77 |
cat <<'EOF' |
@@ -75,7 +88,11 @@ Options: |
||
| 75 | 88 |
--no-recursive Process only the top-level source directory (default: recursive) |
| 76 | 89 |
--extensions LIST Comma-separated video extensions (default: mp4,mov,avi,m4v) |
| 77 | 90 |
--verbose Log each operation with timestamp; show ffmpeg/ffprobe output |
| 78 |
- --move-source Remove source file only after strict post-encode validation |
|
| 91 |
+ --delete-source Delete source file only after strict post-encode validation |
|
| 92 |
+ --keep-going Continue after source-file failures (default: stop) |
|
| 93 |
+ --unattended Preset for long runs: --delete-source + --keep-going |
|
| 94 |
+ --no-apple-repack-fallback Disable macOS avconvert fallback for unreadable MP4/MOV sources |
|
| 95 |
+ --apple-repack-fallback Enable macOS avconvert fallback (default) |
|
| 79 | 96 |
-h, --help Show this help |
| 80 | 97 |
|
| 81 | 98 |
Encoding Modes: |
@@ -116,7 +133,8 @@ Examples: |
||
| 116 | 133 |
./garmin_varia_transcode.sh -s clip.mp4 -d encoded --dry-run --verbose |
| 117 | 134 |
./garmin_varia_transcode.sh -s SampleFootage -d encoded --mode quality --crf 18 |
| 118 | 135 |
./garmin_varia_transcode.sh -s SampleFootage -d encoded --mode compat |
| 119 |
- ./garmin_varia_transcode.sh -s SampleFootage -d encoded --move-source |
|
| 136 |
+ ./garmin_varia_transcode.sh -s SampleFootage -d encoded --delete-source |
|
| 137 |
+ ./garmin_varia_transcode.sh -s SampleFootage -d encoded --unattended |
|
| 120 | 138 |
EOF |
| 121 | 139 |
} |
| 122 | 140 |
|
@@ -134,21 +152,103 @@ vlog_msg() {
|
||
| 134 | 152 |
return 0 |
| 135 | 153 |
} |
| 136 | 154 |
|
| 137 |
-# Quiet-mode per-file progress line |
|
| 138 |
-log_progress() {
|
|
| 155 |
+# Quiet-mode per-file progress lines |
|
| 156 |
+log_progress_start() {
|
|
| 139 | 157 |
local input_file="$1" |
| 158 |
+ local ts |
|
| 159 |
+ ts="$(date '+%Y-%m-%d %H:%M:%S')" |
|
| 160 |
+ printf '%s : Transcoding %s ...' "$ts" "$input_file" |
|
| 161 |
+ PROGRESS_LINE_OPEN=true |
|
| 162 |
+} |
|
| 163 |
+ |
|
| 164 |
+log_progress_done() {
|
|
| 140 | 165 |
local real_elapsed_sec="$2" |
| 141 | 166 |
local encode_elapsed_sec="$3" |
| 167 |
+ if [[ "$PROGRESS_LINE_OPEN" == true ]]; then |
|
| 168 |
+ printf ' done in %ss (encode %ss)\n' "$real_elapsed_sec" "$encode_elapsed_sec" |
|
| 169 |
+ PROGRESS_LINE_OPEN=false |
|
| 170 |
+ return |
|
| 171 |
+ fi |
|
| 172 |
+ |
|
| 173 |
+ local input_file="$1" |
|
| 142 | 174 |
local ts |
| 143 | 175 |
ts="$(date '+%Y-%m-%d %H:%M:%S')" |
| 144 | 176 |
echo "$ts : Transcoding $input_file ... done in ${real_elapsed_sec}s (encode ${encode_elapsed_sec}s)"
|
| 145 | 177 |
} |
| 146 | 178 |
|
| 179 |
+log_progress_failed() {
|
|
| 180 |
+ local input_file="$1" |
|
| 181 |
+ local real_elapsed_sec="$2" |
|
| 182 |
+ local encode_elapsed_sec="$3" |
|
| 183 |
+ local ffmpeg_rc="$4" |
|
| 184 |
+ local ffmpeg_log="$5" |
|
| 185 |
+ |
|
| 186 |
+ if [[ "$PROGRESS_LINE_OPEN" == true ]]; then |
|
| 187 |
+ printf ' FAILED after %ss (encode %ss, rc=%s, log=%s)\n' "$real_elapsed_sec" "$encode_elapsed_sec" "$ffmpeg_rc" "${ffmpeg_log:-n/a}"
|
|
| 188 |
+ PROGRESS_LINE_OPEN=false |
|
| 189 |
+ return |
|
| 190 |
+ fi |
|
| 191 |
+ |
|
| 192 |
+ local ts |
|
| 193 |
+ ts="$(date '+%Y-%m-%d %H:%M:%S')" |
|
| 194 |
+ echo "$ts : Transcoding $input_file ... FAILED after ${real_elapsed_sec}s (encode ${encode_elapsed_sec}s, rc=$ffmpeg_rc, log=${ffmpeg_log:-n/a})"
|
|
| 195 |
+} |
|
| 196 |
+ |
|
| 197 |
+log_progress_skipped_unreadable() {
|
|
| 198 |
+ local input_file="$1" |
|
| 199 |
+ |
|
| 200 |
+ if [[ "$PROGRESS_LINE_OPEN" == true ]]; then |
|
| 201 |
+ printf ' SKIPPED (unreadable/corrupted source)\n' |
|
| 202 |
+ PROGRESS_LINE_OPEN=false |
|
| 203 |
+ return |
|
| 204 |
+ fi |
|
| 205 |
+ |
|
| 206 |
+ local ts |
|
| 207 |
+ ts="$(date '+%Y-%m-%d %H:%M:%S')" |
|
| 208 |
+ echo "$ts : Transcoding $input_file ... SKIPPED (unreadable/corrupted source)" |
|
| 209 |
+} |
|
| 210 |
+ |
|
| 147 | 211 |
die() {
|
| 148 | 212 |
log_msg "ERROR" "$*" |
| 149 | 213 |
exit 1 |
| 150 | 214 |
} |
| 151 | 215 |
|
| 216 |
+handle_interrupt() {
|
|
| 217 |
+ INTERRUPT_COUNT=$((INTERRUPT_COUNT + 1)) |
|
| 218 |
+ STOP_AFTER_CURRENT=true |
|
| 219 |
+ |
|
| 220 |
+ if [[ "$INTERRUPT_COUNT" -eq 1 ]]; then |
|
| 221 |
+ if [[ -n "$CURRENT_FFMPEG_PID" ]]; then |
|
| 222 |
+ log_msg "WARN" "Stop requested. Will stop after current file. Press Ctrl+C again to abort current encode immediately." |
|
| 223 |
+ else |
|
| 224 |
+ log_msg "WARN" "Stop requested. Exiting before next file." |
|
| 225 |
+ fi |
|
| 226 |
+ return |
|
| 227 |
+ fi |
|
| 228 |
+ |
|
| 229 |
+ if [[ -n "$CURRENT_FFMPEG_PID" ]]; then |
|
| 230 |
+ log_msg "WARN" "Force-stopping current encode (pid=$CURRENT_FFMPEG_PID)." |
|
| 231 |
+ kill -INT "$CURRENT_FFMPEG_PID" 2>/dev/null || true |
|
| 232 |
+ fi |
|
| 233 |
+} |
|
| 234 |
+ |
|
| 235 |
+run_ffmpeg_with_signal_guard() {
|
|
| 236 |
+ ( |
|
| 237 |
+ trap '' INT TERM |
|
| 238 |
+ exec "$@" |
|
| 239 |
+ ) & |
|
| 240 |
+ |
|
| 241 |
+ CURRENT_FFMPEG_PID=$! |
|
| 242 |
+ if wait "$CURRENT_FFMPEG_PID"; then |
|
| 243 |
+ CURRENT_FFMPEG_PID="" |
|
| 244 |
+ return 0 |
|
| 245 |
+ fi |
|
| 246 |
+ |
|
| 247 |
+ local rc=$? |
|
| 248 |
+ CURRENT_FFMPEG_PID="" |
|
| 249 |
+ return "$rc" |
|
| 250 |
+} |
|
| 251 |
+ |
|
| 152 | 252 |
format_seconds() {
|
| 153 | 253 |
local sec="$1" |
| 154 | 254 |
local h m s |
@@ -171,6 +271,56 @@ make_temp_log_file() {
|
||
| 171 | 271 |
printf '%s\n' "$temp_path" |
| 172 | 272 |
} |
| 173 | 273 |
|
| 274 |
+make_temp_output_file() {
|
|
| 275 |
+ local target_file="$1" |
|
| 276 |
+ local target_dir target_base target_noext temp_path seed_path |
|
| 277 |
+ |
|
| 278 |
+ target_dir="$(dirname "$target_file")" |
|
| 279 |
+ target_base="$(basename "$target_file")" |
|
| 280 |
+ target_noext="${target_base%.*}"
|
|
| 281 |
+ |
|
| 282 |
+ seed_path="$(mktemp "$target_dir/.varia_tmp.${target_noext}.XXXXXX" 2>/dev/null || true)"
|
|
| 283 |
+ if [[ -n "$seed_path" ]]; then |
|
| 284 |
+ rm -f -- "$seed_path" 2>/dev/null || true |
|
| 285 |
+ temp_path="${seed_path}.mp4"
|
|
| 286 |
+ else |
|
| 287 |
+ temp_path="$target_dir/.varia_tmp.${target_noext}.$$.$RANDOM.mp4"
|
|
| 288 |
+ if ! touch "$temp_path" >/dev/null 2>&1; then |
|
| 289 |
+ temp_path="" |
|
| 290 |
+ else |
|
| 291 |
+ rm -f -- "$temp_path" 2>/dev/null || true |
|
| 292 |
+ fi |
|
| 293 |
+ fi |
|
| 294 |
+ |
|
| 295 |
+ printf '%s\n' "$temp_path" |
|
| 296 |
+} |
|
| 297 |
+ |
|
| 298 |
+cleanup_transcode_artifacts() {
|
|
| 299 |
+ local temp_output="$1" |
|
| 300 |
+ local final_output="$2" |
|
| 301 |
+ |
|
| 302 |
+ rm -f -- "$temp_output" 2>/dev/null || true |
|
| 303 |
+ |
|
| 304 |
+ # Defensive cleanup for previously failed non-atomic runs. |
|
| 305 |
+ if [[ -f "$final_output" && ! -s "$final_output" ]]; then |
|
| 306 |
+ rm -f -- "$final_output" 2>/dev/null || true |
|
| 307 |
+ fi |
|
| 308 |
+} |
|
| 309 |
+ |
|
| 310 |
+is_apple_noise_file() {
|
|
| 311 |
+ local file_path="$1" |
|
| 312 |
+ local base |
|
| 313 |
+ base="$(basename "$file_path")" |
|
| 314 |
+ |
|
| 315 |
+ case "$base" in |
|
| 316 |
+ ._*|.DS_Store|.AppleDouble|._.DS_Store) |
|
| 317 |
+ return 0 |
|
| 318 |
+ ;; |
|
| 319 |
+ esac |
|
| 320 |
+ |
|
| 321 |
+ return 1 |
|
| 322 |
+} |
|
| 323 |
+ |
|
| 174 | 324 |
require_value() {
|
| 175 | 325 |
local flag="$1" |
| 176 | 326 |
local value="${2:-}"
|
@@ -262,10 +412,35 @@ parse_args() {
|
||
| 262 | 412 |
VERBOSE=true |
| 263 | 413 |
shift |
| 264 | 414 |
;; |
| 415 |
+ --delete-source) |
|
| 416 |
+ MOVE_SOURCE=true |
|
| 417 |
+ shift |
|
| 418 |
+ ;; |
|
| 419 |
+ --keep-going) |
|
| 420 |
+ FAIL_FAST=false |
|
| 421 |
+ shift |
|
| 422 |
+ ;; |
|
| 423 |
+ --unattended) |
|
| 424 |
+ MOVE_SOURCE=true |
|
| 425 |
+ FAIL_FAST=false |
|
| 426 |
+ shift |
|
| 427 |
+ ;; |
|
| 428 |
+ --no-apple-repack-fallback) |
|
| 429 |
+ APPLE_REPACK_FALLBACK=false |
|
| 430 |
+ shift |
|
| 431 |
+ ;; |
|
| 432 |
+ --apple-repack-fallback) |
|
| 433 |
+ APPLE_REPACK_FALLBACK=true |
|
| 434 |
+ shift |
|
| 435 |
+ ;; |
|
| 265 | 436 |
--move-source) |
| 266 | 437 |
MOVE_SOURCE=true |
| 267 | 438 |
shift |
| 268 | 439 |
;; |
| 440 |
+ --continue-on-error) |
|
| 441 |
+ FAIL_FAST=false |
|
| 442 |
+ shift |
|
| 443 |
+ ;; |
|
| 269 | 444 |
-h|--help) |
| 270 | 445 |
usage |
| 271 | 446 |
exit 0 |
@@ -294,6 +469,72 @@ check_tools() {
|
||
| 294 | 469 |
command -v ffmpeg >/dev/null 2>&1 || die "ffmpeg not found in PATH" |
| 295 | 470 |
command -v ffprobe >/dev/null 2>&1 || die "ffprobe not found in PATH" |
| 296 | 471 |
command -v exiftool >/dev/null 2>&1 || die "exiftool not found in PATH" |
| 472 |
+ |
|
| 473 |
+ HAS_AVCONVERT=false |
|
| 474 |
+ if [[ "$(uname -s)" == "Darwin" ]] && command -v avconvert >/dev/null 2>&1; then |
|
| 475 |
+ HAS_AVCONVERT=true |
|
| 476 |
+ fi |
|
| 477 |
+} |
|
| 478 |
+ |
|
| 479 |
+make_temp_repack_file() {
|
|
| 480 |
+ local base_tmp="${TMPDIR:-/tmp}"
|
|
| 481 |
+ local seed_path temp_path |
|
| 482 |
+ |
|
| 483 |
+ base_tmp="${base_tmp%/}"
|
|
| 484 |
+ seed_path="$(mktemp "$base_tmp/varia_repack.XXXXXX" 2>/dev/null || true)" |
|
| 485 |
+ if [[ -n "$seed_path" ]]; then |
|
| 486 |
+ rm -f -- "$seed_path" 2>/dev/null || true |
|
| 487 |
+ temp_path="${seed_path}.mp4"
|
|
| 488 |
+ printf '%s\n' "$temp_path" |
|
| 489 |
+ return |
|
| 490 |
+ fi |
|
| 491 |
+ |
|
| 492 |
+ temp_path="$base_tmp/varia_repack.$$.$RANDOM.mp4" |
|
| 493 |
+ printf '%s\n' "$temp_path" |
|
| 494 |
+} |
|
| 495 |
+ |
|
| 496 |
+cleanup_repacked_input() {
|
|
| 497 |
+ local repacked_file="$1" |
|
| 498 |
+ if [[ -n "$repacked_file" ]]; then |
|
| 499 |
+ rm -f -- "$repacked_file" 2>/dev/null || true |
|
| 500 |
+ fi |
|
| 501 |
+} |
|
| 502 |
+ |
|
| 503 |
+try_apple_repack_for_unreadable() {
|
|
| 504 |
+ local source_file="$1" |
|
| 505 |
+ local repacked_file="" |
|
| 506 |
+ |
|
| 507 |
+ REPACKED_SOURCE_PATH="" |
|
| 508 |
+ |
|
| 509 |
+ if [[ "$APPLE_REPACK_FALLBACK" != true || "$HAS_AVCONVERT" != true || "$(uname -s)" != "Darwin" ]]; then |
|
| 510 |
+ return 1 |
|
| 511 |
+ fi |
|
| 512 |
+ |
|
| 513 |
+ repacked_file="$(make_temp_repack_file)" |
|
| 514 |
+ if [[ -z "$repacked_file" ]]; then |
|
| 515 |
+ return 1 |
|
| 516 |
+ fi |
|
| 517 |
+ |
|
| 518 |
+ if [[ "$VERBOSE" == true ]]; then |
|
| 519 |
+ log_msg "WARN" "Trying avconvert passthrough repack fallback: $source_file" |
|
| 520 |
+ if ! avconvert --source "$source_file" --preset PresetPassthrough --output "$repacked_file" --replace; then |
|
| 521 |
+ cleanup_repacked_input "$repacked_file" |
|
| 522 |
+ return 1 |
|
| 523 |
+ fi |
|
| 524 |
+ else |
|
| 525 |
+ if ! avconvert --source "$source_file" --preset PresetPassthrough --output "$repacked_file" --replace >/dev/null 2>&1; then |
|
| 526 |
+ cleanup_repacked_input "$repacked_file" |
|
| 527 |
+ return 1 |
|
| 528 |
+ fi |
|
| 529 |
+ fi |
|
| 530 |
+ |
|
| 531 |
+ if ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$repacked_file" >/dev/null 2>&1; then |
|
| 532 |
+ REPACKED_SOURCE_PATH="$repacked_file" |
|
| 533 |
+ return 0 |
|
| 534 |
+ fi |
|
| 535 |
+ |
|
| 536 |
+ cleanup_repacked_input "$repacked_file" |
|
| 537 |
+ return 1 |
|
| 297 | 538 |
} |
| 298 | 539 |
|
| 299 | 540 |
restore_metadata_with_exiftool() {
|
@@ -493,6 +734,72 @@ ffprobe_video_codec_or_empty() {
|
||
| 493 | 734 |
ffprobe -v error -select_streams v:0 -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 "$file_path" 2>/dev/null | head -n1 |
| 494 | 735 |
} |
| 495 | 736 |
|
| 737 |
+source_video_is_readable() {
|
|
| 738 |
+ local file_path="$1" |
|
| 739 |
+ REPACKED_SOURCE_PATH="" |
|
| 740 |
+ |
|
| 741 |
+ if ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$file_path" >/dev/null 2>&1; then |
|
| 742 |
+ SOURCE_READABLE_MODE="normal" |
|
| 743 |
+ return 0 |
|
| 744 |
+ fi |
|
| 745 |
+ |
|
| 746 |
+ if [[ "$VERBOSE" == true ]]; then |
|
| 747 |
+ log_msg "WARN" "Source probe failed, trying tolerant mode: $file_path" |
|
| 748 |
+ fi |
|
| 749 |
+ |
|
| 750 |
+ if ffprobe -v error -fflags +genpts -err_detect ignore_err -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$file_path" >/dev/null 2>&1; then |
|
| 751 |
+ SOURCE_READABLE_MODE="tolerant" |
|
| 752 |
+ vlog_msg "INFO" "Source readable in tolerant mode: $file_path" |
|
| 753 |
+ return 0 |
|
| 754 |
+ fi |
|
| 755 |
+ |
|
| 756 |
+ if try_apple_repack_for_unreadable "$file_path"; then |
|
| 757 |
+ SOURCE_READABLE_MODE="repacked" |
|
| 758 |
+ log_msg "WARN" "Using avconvert repack fallback for unreadable source: $file_path" |
|
| 759 |
+ return 0 |
|
| 760 |
+ fi |
|
| 761 |
+ |
|
| 762 |
+ SOURCE_READABLE_MODE="normal" |
|
| 763 |
+ return 1 |
|
| 764 |
+} |
|
| 765 |
+ |
|
| 766 |
+source_error_from_ffmpeg_log() {
|
|
| 767 |
+ local ffmpeg_log="$1" |
|
| 768 |
+ [[ -n "$ffmpeg_log" && -f "$ffmpeg_log" ]] || return 1 |
|
| 769 |
+ grep -Eiq 'invalid data found when processing input|error reading header|moov atom not found|corrupt|truncated|end of file|contradictionary STSC and STCO' "$ffmpeg_log" |
|
| 770 |
+} |
|
| 771 |
+ |
|
| 772 |
+destination_cannot_accept_file() {
|
|
| 773 |
+ local input_file="$1" |
|
| 774 |
+ local out_dir="$2" |
|
| 775 |
+ local ffmpeg_log="$3" |
|
| 776 |
+ local probe_path="" |
|
| 777 |
+ local avail_kb="" |
|
| 778 |
+ local avail_bytes=0 |
|
| 779 |
+ |
|
| 780 |
+ if [[ -n "$ffmpeg_log" && -f "$ffmpeg_log" ]]; then |
|
| 781 |
+ if grep -Eiq 'no space left on device|disk quota exceeded|read-only file system|permission denied|file too large|input/output error|stale file handle' "$ffmpeg_log"; then |
|
| 782 |
+ return 0 |
|
| 783 |
+ fi |
|
| 784 |
+ fi |
|
| 785 |
+ |
|
| 786 |
+ probe_path="$(mktemp "$out_dir/.varia_write_probe.XXXXXX" 2>/dev/null || true)" |
|
| 787 |
+ if [[ -z "$probe_path" ]]; then |
|
| 788 |
+ return 0 |
|
| 789 |
+ fi |
|
| 790 |
+ rm -f -- "$probe_path" 2>/dev/null || true |
|
| 791 |
+ |
|
| 792 |
+ avail_kb="$(df -Pk "$out_dir" 2>/dev/null | awk 'NR==2 {print $4}' || true)"
|
|
| 793 |
+ if [[ "$avail_kb" =~ ^[0-9]+$ ]]; then |
|
| 794 |
+ avail_bytes=$((avail_kb * 1024)) |
|
| 795 |
+ if [[ "$avail_bytes" -lt 67108864 ]]; then |
|
| 796 |
+ return 0 |
|
| 797 |
+ fi |
|
| 798 |
+ fi |
|
| 799 |
+ |
|
| 800 |
+ return 1 |
|
| 801 |
+} |
|
| 802 |
+ |
|
| 496 | 803 |
validate_transcoded_output() {
|
| 497 | 804 |
local input_file="$1" |
| 498 | 805 |
local output_file="$2" |
@@ -619,6 +926,10 @@ collect_video_files() {
|
||
| 619 | 926 |
build_find_expr_for_extensions |
| 620 | 927 |
|
| 621 | 928 |
while IFS= read -r -d '' file; do |
| 929 |
+ if is_apple_noise_file "$file"; then |
|
| 930 |
+ vlog_msg "SKIP" "Ignoring Apple artifact: $file" |
|
| 931 |
+ continue |
|
| 932 |
+ fi |
|
| 622 | 933 |
VIDEO_FILES+=("$file")
|
| 623 | 934 |
done < <( |
| 624 | 935 |
if [[ "$RECURSIVE" == true ]]; then |
@@ -634,7 +945,10 @@ collect_video_files() {
|
||
| 634 | 945 |
|
| 635 | 946 |
process_video_file() {
|
| 636 | 947 |
local input_file="$1" |
| 637 |
- local rel_path output_file out_dir |
|
| 948 |
+ local rel_path output_file temp_output_file out_dir |
|
| 949 |
+ local display_path |
|
| 950 |
+ local encode_input_file |
|
| 951 |
+ local repacked_input_file="" |
|
| 638 | 952 |
local file_started_at file_ended_at file_real_elapsed_sec |
| 639 | 953 |
local encode_started_at encode_ended_at encode_elapsed_sec=0 |
| 640 | 954 |
local post_elapsed_sec |
@@ -645,17 +959,38 @@ process_video_file() {
|
||
| 645 | 959 |
rel_path="$(rel_path_from_source "$input_file")" |
| 646 | 960 |
output_file="$DEST_DIR/${rel_path%.*}.mp4"
|
| 647 | 961 |
out_dir="$(dirname "$output_file")" |
| 962 |
+ display_path="${input_file#$PWD/}"
|
|
| 963 |
+ encode_input_file="$input_file" |
|
| 648 | 964 |
|
| 649 | 965 |
mkdir -p "$out_dir" |
| 650 | 966 |
|
| 967 |
+ if ! source_video_is_readable "$input_file"; then |
|
| 968 |
+ ERRORS=$((ERRORS + 1)) |
|
| 969 |
+ INVALID_SOURCES_SKIPPED=$((INVALID_SOURCES_SKIPPED + 1)) |
|
| 970 |
+ if [[ "$VERBOSE" == true ]]; then |
|
| 971 |
+ log_msg "ERROR" "Skipping unreadable/corrupted source video: $input_file" |
|
| 972 |
+ else |
|
| 973 |
+ log_progress_start "$display_path" |
|
| 974 |
+ log_progress_skipped_unreadable "$display_path" |
|
| 975 |
+ fi |
|
| 976 |
+ return 2 |
|
| 977 |
+ fi |
|
| 978 |
+ |
|
| 979 |
+ if [[ "$SOURCE_READABLE_MODE" == "repacked" && -n "$REPACKED_SOURCE_PATH" ]]; then |
|
| 980 |
+ repacked_input_file="$REPACKED_SOURCE_PATH" |
|
| 981 |
+ encode_input_file="$REPACKED_SOURCE_PATH" |
|
| 982 |
+ vlog_msg "INFO" "Encoding from avconvert repacked source: $repacked_input_file" |
|
| 983 |
+ fi |
|
| 984 |
+ |
|
| 651 | 985 |
if [[ -f "$output_file" && "$OVERWRITE" != true ]]; then |
| 652 | 986 |
vlog_msg "SKIP" "Video exists: $output_file" |
| 653 | 987 |
VIDEOS_SKIPPED=$((VIDEOS_SKIPPED + 1)) |
| 988 |
+ cleanup_repacked_input "$repacked_input_file" |
|
| 654 | 989 |
return |
| 655 | 990 |
fi |
| 656 | 991 |
|
| 657 | 992 |
local has_audio=false |
| 658 |
- if probe_has_audio "$input_file"; then |
|
| 993 |
+ if probe_has_audio "$encode_input_file"; then |
|
| 659 | 994 |
has_audio=true |
| 660 | 995 |
fi |
| 661 | 996 |
|
@@ -667,20 +1002,31 @@ process_video_file() {
|
||
| 667 | 1002 |
|
| 668 | 1003 |
build_video_args |
| 669 | 1004 |
|
| 1005 |
+ temp_output_file="$(make_temp_output_file "$output_file")" |
|
| 1006 |
+ if [[ -z "$temp_output_file" ]]; then |
|
| 1007 |
+ ERRORS=$((ERRORS + 1)) |
|
| 1008 |
+ DESTINATION_FAILURES=$((DESTINATION_FAILURES + 1)) |
|
| 1009 |
+ log_msg "ERROR" "Could not create temporary output file: $output_file" |
|
| 1010 |
+ return 3 |
|
| 1011 |
+ fi |
|
| 1012 |
+ |
|
| 670 | 1013 |
local cmd=(ffmpeg -hide_banner) |
| 1014 |
+ if [[ "$SOURCE_READABLE_MODE" == "tolerant" ]]; then |
|
| 1015 |
+ cmd+=( -fflags +genpts -err_detect ignore_err ) |
|
| 1016 |
+ fi |
|
| 671 | 1017 |
if [[ "$OVERWRITE" == true ]]; then |
| 672 | 1018 |
cmd+=( -y ) |
| 673 | 1019 |
else |
| 674 | 1020 |
cmd+=( -n ) |
| 675 | 1021 |
fi |
| 676 | 1022 |
|
| 677 |
- cmd+=( -i "$input_file" -map 0:v:0 ) |
|
| 1023 |
+ cmd+=( -i "$encode_input_file" -map 0:v:0 ) |
|
| 678 | 1024 |
|
| 679 | 1025 |
if [[ "$has_audio" == true ]]; then |
| 680 | 1026 |
cmd+=( -map 0:a? -c:a aac -b:a 128k ) |
| 681 | 1027 |
fi |
| 682 | 1028 |
|
| 683 |
- cmd+=( "${VIDEO_ARGS[@]}" -map_metadata 0 -movflags +faststart "$output_file" )
|
|
| 1029 |
+ cmd+=( "${VIDEO_ARGS[@]}" -map_metadata 0 -movflags +faststart "$temp_output_file" )
|
|
| 684 | 1030 |
|
| 685 | 1031 |
if [[ "$VERBOSE" == true ]]; then |
| 686 | 1032 |
log_msg "INFO" "Command: $(join_cmd_for_log "${cmd[@]}")"
|
@@ -691,28 +1037,33 @@ process_video_file() {
|
||
| 691 | 1037 |
return 0 |
| 692 | 1038 |
fi |
| 693 | 1039 |
|
| 694 |
- vlog_msg "INFO" "Encoding: $input_file -> $output_file" |
|
| 1040 |
+ if [[ "$VERBOSE" != true ]]; then |
|
| 1041 |
+ log_progress_start "$display_path" |
|
| 1042 |
+ fi |
|
| 1043 |
+ |
|
| 1044 |
+ vlog_msg "INFO" "Encoding: $input_file -> $output_file (temp: $temp_output_file)" |
|
| 695 | 1045 |
encode_started_at="$(date +%s)" |
| 696 | 1046 |
vlog_msg "CHECKPOINT" "encode_start: $input_file" |
| 697 | 1047 |
|
| 698 | 1048 |
local ffmpeg_rc=0 |
| 1049 |
+ local ffmpeg_log="" |
|
| 699 | 1050 |
if [[ "$VERBOSE" == true ]]; then |
| 700 | 1051 |
# Verbose: show ffmpeg output directly |
| 701 |
- if "${cmd[@]}"; then
|
|
| 1052 |
+ if run_ffmpeg_with_signal_guard "${cmd[@]}"; then
|
|
| 702 | 1053 |
: |
| 703 | 1054 |
else |
| 704 | 1055 |
ffmpeg_rc=$? |
| 705 | 1056 |
fi |
| 706 | 1057 |
else |
| 707 | 1058 |
# Quiet (default): redirect ffmpeg output; keep log on failure |
| 708 |
- local ffmpeg_log |
|
| 709 | 1059 |
ffmpeg_log="$(make_temp_log_file)" |
| 710 | 1060 |
if [[ -z "$ffmpeg_log" ]]; then |
| 711 | 1061 |
ERRORS=$((ERRORS + 1)) |
| 1062 |
+ DESTINATION_FAILURES=$((DESTINATION_FAILURES + 1)) |
|
| 712 | 1063 |
log_msg "ERROR" "Could not create temporary ffmpeg log file" |
| 713 |
- return 1 |
|
| 1064 |
+ return 3 |
|
| 714 | 1065 |
fi |
| 715 |
- if "${cmd[@]}" >"$ffmpeg_log" 2>&1; then
|
|
| 1066 |
+ if run_ffmpeg_with_signal_guard "${cmd[@]}" >"$ffmpeg_log" 2>&1; then
|
|
| 716 | 1067 |
rm -f "$ffmpeg_log" |
| 717 | 1068 |
else |
| 718 | 1069 |
ffmpeg_rc=$? |
@@ -724,6 +1075,14 @@ process_video_file() {
|
||
| 724 | 1075 |
vlog_msg "CHECKPOINT" "encode_end: $input_file (encode_elapsed=${encode_elapsed_sec}s)"
|
| 725 | 1076 |
|
| 726 | 1077 |
if [[ "$ffmpeg_rc" -ne 0 ]]; then |
| 1078 |
+ local failure_rc=1 |
|
| 1079 |
+ if destination_cannot_accept_file "$input_file" "$out_dir" "$ffmpeg_log"; then |
|
| 1080 |
+ failure_rc=3 |
|
| 1081 |
+ elif source_error_from_ffmpeg_log "$ffmpeg_log"; then |
|
| 1082 |
+ failure_rc=2 |
|
| 1083 |
+ fi |
|
| 1084 |
+ |
|
| 1085 |
+ cleanup_transcode_artifacts "$temp_output_file" "$output_file" |
|
| 727 | 1086 |
file_ended_at="$(date +%s)" |
| 728 | 1087 |
file_real_elapsed_sec=$((file_ended_at - file_started_at)) |
| 729 | 1088 |
local encode_elapsed_fmt |
@@ -732,42 +1091,96 @@ process_video_file() {
|
||
| 732 | 1091 |
real_elapsed_fmt="$(format_seconds "$file_real_elapsed_sec")" |
| 733 | 1092 |
|
| 734 | 1093 |
ERRORS=$((ERRORS + 1)) |
| 1094 |
+ if [[ "$failure_rc" -eq 2 ]]; then |
|
| 1095 |
+ INVALID_SOURCES_SKIPPED=$((INVALID_SOURCES_SKIPPED + 1)) |
|
| 1096 |
+ elif [[ "$failure_rc" -eq 3 ]]; then |
|
| 1097 |
+ DESTINATION_FAILURES=$((DESTINATION_FAILURES + 1)) |
|
| 1098 |
+ fi |
|
| 735 | 1099 |
if [[ "$VERBOSE" == true ]]; then |
| 736 | 1100 |
log_msg "ERROR" "ffmpeg failed: $input_file (real=${file_real_elapsed_sec}s / $real_elapsed_fmt, encode=${encode_elapsed_sec}s / $encode_elapsed_fmt, rc=$ffmpeg_rc)"
|
| 737 | 1101 |
else |
| 738 |
- local ts |
|
| 739 |
- ts="$(date '+%Y-%m-%d %H:%M:%S')" |
|
| 740 |
- echo "$ts : Transcoding $input_file ... FAILED after ${file_real_elapsed_sec}s (encode ${encode_elapsed_sec}s, rc=$ffmpeg_rc, log=${ffmpeg_log:-n/a})"
|
|
| 741 |
- rm -f "${ffmpeg_log:-}"
|
|
| 1102 |
+ log_progress_failed "$display_path" "$file_real_elapsed_sec" "$encode_elapsed_sec" "$ffmpeg_rc" "$ffmpeg_log" |
|
| 742 | 1103 |
fi |
| 743 |
- return 1 |
|
| 1104 |
+ if [[ "$failure_rc" -eq 3 ]]; then |
|
| 1105 |
+ log_msg "ERROR" "Destination cannot accept more output data: $out_dir" |
|
| 1106 |
+ fi |
|
| 1107 |
+ cleanup_repacked_input "$repacked_input_file" |
|
| 1108 |
+ return "$failure_rc" |
|
| 744 | 1109 |
fi |
| 745 | 1110 |
|
| 746 | 1111 |
TOTAL_VIDEO_TIME_SEC=$((TOTAL_VIDEO_TIME_SEC + encode_elapsed_sec)) |
| 747 | 1112 |
|
| 748 |
- if ! restore_metadata_with_exiftool "$input_file" "$output_file"; then |
|
| 1113 |
+ if ! restore_metadata_with_exiftool "$input_file" "$temp_output_file"; then |
|
| 1114 |
+ cleanup_transcode_artifacts "$temp_output_file" "$output_file" |
|
| 1115 |
+ if [[ "$STOP_AFTER_CURRENT" == true && "$INTERRUPT_COUNT" -gt 0 ]]; then |
|
| 1116 |
+ cleanup_repacked_input "$repacked_input_file" |
|
| 1117 |
+ return 4 |
|
| 1118 |
+ fi |
|
| 749 | 1119 |
ERRORS=$((ERRORS + 1)) |
| 750 |
- return 1 |
|
| 1120 |
+ DESTINATION_FAILURES=$((DESTINATION_FAILURES + 1)) |
|
| 1121 |
+ cleanup_repacked_input "$repacked_input_file" |
|
| 1122 |
+ return 3 |
|
| 751 | 1123 |
fi |
| 752 | 1124 |
|
| 753 |
- if ! map_garmin_model_to_standard_tags "$input_file" "$output_file"; then |
|
| 1125 |
+ if ! map_garmin_model_to_standard_tags "$input_file" "$temp_output_file"; then |
|
| 1126 |
+ cleanup_transcode_artifacts "$temp_output_file" "$output_file" |
|
| 1127 |
+ if [[ "$STOP_AFTER_CURRENT" == true && "$INTERRUPT_COUNT" -gt 0 ]]; then |
|
| 1128 |
+ cleanup_repacked_input "$repacked_input_file" |
|
| 1129 |
+ return 4 |
|
| 1130 |
+ fi |
|
| 754 | 1131 |
ERRORS=$((ERRORS + 1)) |
| 755 |
- return 1 |
|
| 1132 |
+ DESTINATION_FAILURES=$((DESTINATION_FAILURES + 1)) |
|
| 1133 |
+ cleanup_repacked_input "$repacked_input_file" |
|
| 1134 |
+ return 3 |
|
| 756 | 1135 |
fi |
| 757 | 1136 |
|
| 758 |
- if ! write_transcode_encoder_metadata "$output_file"; then |
|
| 1137 |
+ if ! write_transcode_encoder_metadata "$temp_output_file"; then |
|
| 1138 |
+ cleanup_transcode_artifacts "$temp_output_file" "$output_file" |
|
| 1139 |
+ if [[ "$STOP_AFTER_CURRENT" == true && "$INTERRUPT_COUNT" -gt 0 ]]; then |
|
| 1140 |
+ cleanup_repacked_input "$repacked_input_file" |
|
| 1141 |
+ return 4 |
|
| 1142 |
+ fi |
|
| 759 | 1143 |
ERRORS=$((ERRORS + 1)) |
| 760 |
- return 1 |
|
| 1144 |
+ DESTINATION_FAILURES=$((DESTINATION_FAILURES + 1)) |
|
| 1145 |
+ cleanup_repacked_input "$repacked_input_file" |
|
| 1146 |
+ return 3 |
|
| 761 | 1147 |
fi |
| 762 | 1148 |
|
| 763 | 1149 |
if [[ "$MOVE_SOURCE" == true ]]; then |
| 764 |
- if ! validate_transcoded_output "$input_file" "$output_file"; then |
|
| 1150 |
+ if ! validate_transcoded_output "$encode_input_file" "$temp_output_file"; then |
|
| 1151 |
+ cleanup_transcode_artifacts "$temp_output_file" "$output_file" |
|
| 1152 |
+ if [[ "$STOP_AFTER_CURRENT" == true && "$INTERRUPT_COUNT" -gt 0 ]]; then |
|
| 1153 |
+ cleanup_repacked_input "$repacked_input_file" |
|
| 1154 |
+ return 4 |
|
| 1155 |
+ fi |
|
| 765 | 1156 |
ERRORS=$((ERRORS + 1)) |
| 766 |
- return 1 |
|
| 1157 |
+ DESTINATION_FAILURES=$((DESTINATION_FAILURES + 1)) |
|
| 1158 |
+ cleanup_repacked_input "$repacked_input_file" |
|
| 1159 |
+ return 3 |
|
| 767 | 1160 |
fi |
| 768 | 1161 |
fi |
| 769 | 1162 |
|
| 770 |
- touch -r "$input_file" "$output_file" || true |
|
| 1163 |
+ touch -r "$input_file" "$temp_output_file" || true |
|
| 1164 |
+ |
|
| 1165 |
+ if [[ "$OVERWRITE" == true ]]; then |
|
| 1166 |
+ if ! mv -f "$temp_output_file" "$output_file"; then |
|
| 1167 |
+ cleanup_transcode_artifacts "$temp_output_file" "$output_file" |
|
| 1168 |
+ ERRORS=$((ERRORS + 1)) |
|
| 1169 |
+ DESTINATION_FAILURES=$((DESTINATION_FAILURES + 1)) |
|
| 1170 |
+ log_msg "ERROR" "Failed to move completed output into place: $output_file" |
|
| 1171 |
+ cleanup_repacked_input "$repacked_input_file" |
|
| 1172 |
+ return 3 |
|
| 1173 |
+ fi |
|
| 1174 |
+ else |
|
| 1175 |
+ if ! mv -n "$temp_output_file" "$output_file"; then |
|
| 1176 |
+ cleanup_transcode_artifacts "$temp_output_file" "$output_file" |
|
| 1177 |
+ ERRORS=$((ERRORS + 1)) |
|
| 1178 |
+ DESTINATION_FAILURES=$((DESTINATION_FAILURES + 1)) |
|
| 1179 |
+ log_msg "ERROR" "Failed to move completed output into place: $output_file" |
|
| 1180 |
+ cleanup_repacked_input "$repacked_input_file" |
|
| 1181 |
+ return 3 |
|
| 1182 |
+ fi |
|
| 1183 |
+ fi |
|
| 771 | 1184 |
|
| 772 | 1185 |
if [[ "$MOVE_SOURCE" == true ]]; then |
| 773 | 1186 |
if rm -f "$input_file"; then |
@@ -775,6 +1188,7 @@ process_video_file() {
|
||
| 775 | 1188 |
else |
| 776 | 1189 |
ERRORS=$((ERRORS + 1)) |
| 777 | 1190 |
log_msg "ERROR" "Failed to remove source after validation: $input_file" |
| 1191 |
+ cleanup_repacked_input "$repacked_input_file" |
|
| 778 | 1192 |
return 1 |
| 779 | 1193 |
fi |
| 780 | 1194 |
fi |
@@ -793,10 +1207,11 @@ process_video_file() {
|
||
| 793 | 1207 |
if [[ "$VERBOSE" == true ]]; then |
| 794 | 1208 |
log_msg "INFO" "Transcoded: $output_file (real=${file_real_elapsed_sec}s / $(format_seconds "$file_real_elapsed_sec"), encode=${encode_elapsed_sec}s / $(format_seconds "$encode_elapsed_sec"), post=${post_elapsed_sec}s / $(format_seconds "$post_elapsed_sec"))"
|
| 795 | 1209 |
else |
| 796 |
- local display_path="${input_file#$PWD/}"
|
|
| 797 |
- log_progress "$display_path" "$file_real_elapsed_sec" "$encode_elapsed_sec" |
|
| 1210 |
+ log_progress_done "$display_path" "$file_real_elapsed_sec" "$encode_elapsed_sec" |
|
| 798 | 1211 |
fi |
| 799 | 1212 |
|
| 1213 |
+ cleanup_repacked_input "$repacked_input_file" |
|
| 1214 |
+ |
|
| 800 | 1215 |
return 0 |
| 801 | 1216 |
} |
| 802 | 1217 |
|
@@ -862,6 +1277,10 @@ copy_sidecars_json() {
|
||
| 862 | 1277 |
|
| 863 | 1278 |
local jf |
| 864 | 1279 |
for jf in "${json_files[@]}"; do
|
| 1280 |
+ if is_apple_noise_file "$jf"; then |
|
| 1281 |
+ vlog_msg "SKIP" "Ignoring Apple artifact JSON: $jf" |
|
| 1282 |
+ continue |
|
| 1283 |
+ fi |
|
| 865 | 1284 |
copy_one_json "$jf" |
| 866 | 1285 |
done |
| 867 | 1286 |
} |
@@ -907,6 +1326,8 @@ main() {
|
||
| 907 | 1326 |
local total_started_at total_ended_at total_elapsed_sec total_elapsed_fmt |
| 908 | 1327 |
total_started_at="$(date +%s)" |
| 909 | 1328 |
|
| 1329 |
+ trap 'handle_interrupt' INT TERM |
|
| 1330 |
+ |
|
| 910 | 1331 |
parse_args "$@" |
| 911 | 1332 |
check_tools |
| 912 | 1333 |
|
@@ -928,19 +1349,51 @@ main() {
|
||
| 928 | 1349 |
resolve_encoder |
| 929 | 1350 |
|
| 930 | 1351 |
collect_video_files |
| 931 |
- if [[ ${#VIDEO_FILES[@]} -eq 0 ]]; then
|
|
| 1352 |
+ if [[ -z "${VIDEO_FILES[*]-}" ]]; then
|
|
| 932 | 1353 |
log_msg "INFO" "No video files found for extensions: $EXTENSIONS_CSV" |
| 933 | 1354 |
fi |
| 934 | 1355 |
|
| 935 | 1356 |
local f |
| 936 |
- for f in "${VIDEO_FILES[@]}"; do
|
|
| 937 |
- if ! process_video_file "$f"; then |
|
| 938 |
- log_msg "ERROR" "Stopping encoding chain due to ffmpeg failure/interruption" |
|
| 1357 |
+ for f in ${VIDEO_FILES+"${VIDEO_FILES[@]}"}; do
|
|
| 1358 |
+ if [[ "$STOP_AFTER_CURRENT" == true ]]; then |
|
| 1359 |
+ log_msg "INFO" "Stop requested; ending before next file" |
|
| 939 | 1360 |
break |
| 940 | 1361 |
fi |
| 1362 |
+ |
|
| 1363 |
+ local process_rc=0 |
|
| 1364 |
+ if process_video_file "$f"; then |
|
| 1365 |
+ continue |
|
| 1366 |
+ else |
|
| 1367 |
+ process_rc=$? |
|
| 1368 |
+ fi |
|
| 1369 |
+ |
|
| 1370 |
+ case "$process_rc" in |
|
| 1371 |
+ 2) |
|
| 1372 |
+ log_msg "INFO" "Continuing after unreadable/corrupted source file" |
|
| 1373 |
+ continue |
|
| 1374 |
+ ;; |
|
| 1375 |
+ 3) |
|
| 1376 |
+ log_msg "ERROR" "Stopping encoding chain because destination is not writable or is out of space" |
|
| 1377 |
+ break |
|
| 1378 |
+ ;; |
|
| 1379 |
+ 4) |
|
| 1380 |
+ log_msg "INFO" "Stopped by user after current file" |
|
| 1381 |
+ break |
|
| 1382 |
+ ;; |
|
| 1383 |
+ *) |
|
| 1384 |
+ if [[ "$FAIL_FAST" == true ]]; then |
|
| 1385 |
+ log_msg "ERROR" "Stopping encoding chain due to ffmpeg failure/interruption" |
|
| 1386 |
+ break |
|
| 1387 |
+ fi |
|
| 1388 |
+ log_msg "ERROR" "Continuing after ffmpeg failure because --keep-going is enabled" |
|
| 1389 |
+ continue |
|
| 1390 |
+ ;; |
|
| 1391 |
+ esac |
|
| 941 | 1392 |
done |
| 942 | 1393 |
|
| 943 |
- if [[ "$ERRORS" -eq 0 ]]; then |
|
| 1394 |
+ if [[ "$STOP_AFTER_CURRENT" == true ]]; then |
|
| 1395 |
+ log_msg "INFO" "Skipping sidecar copy and manifest because run was stopped by user" |
|
| 1396 |
+ elif [[ "$ERRORS" -eq 0 ]]; then |
|
| 944 | 1397 |
copy_sidecars_json |
| 945 | 1398 |
write_manifest |
| 946 | 1399 |
else |
@@ -975,7 +1428,7 @@ main() {
|
||
| 975 | 1428 |
fi |
| 976 | 1429 |
run_non_file_overhead_fmt="$(format_seconds "$run_non_file_overhead_sec")" |
| 977 | 1430 |
|
| 978 |
- log_msg "INFO" "Summary: videos_processed=$VIDEOS_PROCESSED videos_skipped=$VIDEOS_SKIPPED json_copied=$JSON_COPIED json_skipped=$JSON_SKIPPED errors=$ERRORS" |
|
| 1431 |
+ log_msg "INFO" "Summary: videos_processed=$VIDEOS_PROCESSED videos_skipped=$VIDEOS_SKIPPED invalid_sources_skipped=$INVALID_SOURCES_SKIPPED destination_failures=$DESTINATION_FAILURES json_copied=$JSON_COPIED json_skipped=$JSON_SKIPPED errors=$ERRORS" |
|
| 979 | 1432 |
log_msg "INFO" "Timing: run_real=${total_elapsed_sec}s / $total_elapsed_fmt, files_real_total=${TOTAL_FILE_REAL_TIME_SEC}s / $(format_seconds "$TOTAL_FILE_REAL_TIME_SEC"), files_encode_total=${TOTAL_VIDEO_TIME_SEC}s / $(format_seconds "$TOTAL_VIDEO_TIME_SEC"), files_post_total=${file_post_total_sec}s / $file_post_total_fmt, run_non_file_overhead=${run_non_file_overhead_sec}s / $run_non_file_overhead_fmt"
|
| 980 | 1433 |
log_msg "INFO" "Timing(avg): file_real_avg=${avg_file_real_time_sec}s / $avg_file_real_time_fmt, file_encode_avg=${avg_video_time_sec}s / $avg_video_time_fmt"
|
| 981 | 1434 |
|