VariaReEncoder / cleanup_garmin_varia_media_folder.sh
Newer Older
356 lines | 8.436kb
Bogdan Timofte authored a month ago
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 "$@"