#!/usr/bin/env bash set -euo pipefail TOOL_NAME="cleanup_garmin_varia_media_folder.sh" DEFAULT_MEDIA_ROOT="." MAX_APPLEDOUBLE_BYTES=4096 DRY_RUN=false VERBOSE=false MEDIA_ROOT="$DEFAULT_MEDIA_ROOT" TMP_MP4_LIST="" TMP_ACTIONS="" TMP_NONSTANDARD="" TMP_ZERO="" TMP_BLOCKED="" TMP_APPLE="" APPLE_ARTIFACTS_REMOVED=0 ZERO_SIZE_REMOVED=0 NONSTANDARD_REMOVED=0 RENAMES_DONE=0 BLOCKED_GROUPS=0 BLOCKED_FILES=0 usage() { cat <<'EOF' Usage: cleanup_garmin_varia_media_folder.sh [options] [MEDIA_ROOT] Purpose: Clean common Apple artifacts and zero-size MP4 files, then normalize duplicate MP4 names produced during copy/import retries. Rules: - Delete AppleDouble sidecars matching ._* only when size <= 4096 bytes - Delete zero-size .mp4 files - For canonical timestamp names: YYYY-MM-DD_HH-MM-SS.mp4 YYYY-MM-DD_HH-MM-SS_.mp4 if exactly one suffixed duplicate exists and the base file is missing, rename duplicate to base name - If base exists, or multiple suffixed duplicates exist, keep files unchanged and report them as blocked Options: --dry-run Print actions without changing files --verbose Print per-file operations -h, --help Show this help Examples: ./cleanup_garmin_varia_media_folder.sh --dry-run ~/Autofs/xdev/autonas/ext01/@Camera/import ./cleanup_garmin_varia_media_folder.sh ~/Autofs/xdev/is-baobab/nvme0n1/@backup/Garmin EOF } log_msg() { local level="$1" shift printf '[%s] [%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$level" "$*" } vlog_msg() { if [[ "$VERBOSE" == true ]]; then log_msg "INFO" "$*" fi } die() { log_msg "ERROR" "$*" exit 2 } cleanup_tmp_files() { rm -f -- "$TMP_MP4_LIST" "$TMP_ACTIONS" "$TMP_NONSTANDARD" "$TMP_ZERO" "$TMP_BLOCKED" "$TMP_APPLE" "$TMP_ACTIONS.tmp" 2>/dev/null || true } parse_args() { while [[ $# -gt 0 ]]; do case "$1" in --dry-run) DRY_RUN=true shift ;; --verbose) VERBOSE=true shift ;; -h|--help) usage exit 0 ;; -*) die "Unknown option: $1" ;; *) MEDIA_ROOT="$1" shift ;; esac done } init_tmp_files() { TMP_MP4_LIST="$(mktemp)" TMP_ACTIONS="$(mktemp)" TMP_NONSTANDARD="$(mktemp)" TMP_ZERO="$(mktemp)" TMP_BLOCKED="$(mktemp)" TMP_APPLE="$(mktemp)" trap cleanup_tmp_files EXIT } validate_media_root() { [[ -d "$MEDIA_ROOT" ]] || die "Media root not found: $MEDIA_ROOT" } rescan_mp4_files() { find "$MEDIA_ROOT" -type f -name '*.mp4' | sort > "$TMP_MP4_LIST" } safe_remove_file() { local path="$1" if [[ "$DRY_RUN" == true ]]; then return 0 fi rm -f -- "$path" } remove_apple_artifacts() { local file size while IFS= read -r file; do [[ -n "$file" ]] || continue size="$(wc -c < "$file" | tr -d ' ')" if [[ "$size" =~ ^[0-9]+$ ]] && [[ "$size" -le "$MAX_APPLEDOUBLE_BYTES" ]]; then APPLE_ARTIFACTS_REMOVED=$((APPLE_ARTIFACTS_REMOVED + 1)) printf '%s\n' "$file" >> "$TMP_APPLE" vlog_msg "Apple artifact: $file (${size} bytes)" safe_remove_file "$file" else vlog_msg "Skipped ._* larger than threshold: $file (${size} bytes)" fi done < <(find "$MEDIA_ROOT" -type f -name '._*' | sort) if [[ "$APPLE_ARTIFACTS_REMOVED" -gt 0 ]]; then if [[ "$DRY_RUN" == true ]]; then log_msg "INFO" "Would remove $APPLE_ARTIFACTS_REMOVED Apple artifact file(s)" else log_msg "INFO" "Removed $APPLE_ARTIFACTS_REMOVED Apple artifact file(s)" fi rescan_mp4_files fi } remove_zero_size_mp4() { local file while IFS= read -r file; do [[ -n "$file" ]] || continue if [[ ! -s "$file" ]]; then ZERO_SIZE_REMOVED=$((ZERO_SIZE_REMOVED + 1)) printf '%s\n' "$file" >> "$TMP_ZERO" vlog_msg "Zero-size MP4: $file" safe_remove_file "$file" fi done < "$TMP_MP4_LIST" if [[ "$ZERO_SIZE_REMOVED" -gt 0 ]]; then if [[ "$DRY_RUN" == true ]]; then log_msg "INFO" "Would remove $ZERO_SIZE_REMOVED zero-size MP4 file(s)" else log_msg "INFO" "Removed $ZERO_SIZE_REMOVED zero-size MP4 file(s)" fi rescan_mp4_files fi } collect_duplicate_actions() { local file base dir timestamp suffix : > "$TMP_ACTIONS" : > "$TMP_NONSTANDARD" while IFS= read -r file; do [[ -n "$file" ]] || continue base="$(basename "$file")" if [[ "$base" =~ ^([0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2})(_[0-9]+)?\.mp4$ ]]; then dir="$(dirname "$file")" timestamp="${BASH_REMATCH[1]}" suffix="${BASH_REMATCH[2]:-}" if [[ -n "$suffix" ]]; then suffix="${suffix#_}" else suffix=0 fi printf '%s\t%s\t%s\t%s\n' "$dir" "$timestamp" "$suffix" "$file" >> "$TMP_ACTIONS" elif [[ "$base" =~ _[0-9]+\.mp4$ ]]; then printf '%s\n' "$file" >> "$TMP_NONSTANDARD" NONSTANDARD_REMOVED=$((NONSTANDARD_REMOVED + 1)) vlog_msg "Non-standard duplicate-like file: $file" safe_remove_file "$file" fi done < "$TMP_MP4_LIST" } build_rename_plan() { awk -F'\t' ' function flush() { if (dup_count == 1 && orig_seen == 0) { print "RENAME\t" dup_files[1] "\t" dir "\t" timestamp } else if (dup_count > 0) { print "GROUP\t" dir "\t" timestamp "\t" dup_count for (i = 1; i <= dup_count; i++) { print "BLOCK\t" dup_files[i] } } } BEGIN { OFS = "\t" dir = "" timestamp = "" orig_seen = 0 dup_count = 0 } { if ($1 != dir || $2 != timestamp) { if (NR > 1) { flush() } dir = $1 timestamp = $2 orig_seen = 0 dup_count = 0 delete dup_files } if ($3 == 0) { orig_seen = 1 } else { dup_count++ dup_files[dup_count] = $4 } } END { if (NR > 0) { flush() } } ' "$TMP_ACTIONS" > "$TMP_ACTIONS.tmp" mv "$TMP_ACTIONS.tmp" "$TMP_ACTIONS" } apply_rename_plan() { local action src dir timestamp dup_count dst while IFS=$'\t' read -r action src dir timestamp dup_count; do [[ -n "$action" ]] || continue case "$action" in RENAME) dst="$dir/$timestamp.mp4" RENAMES_DONE=$((RENAMES_DONE + 1)) if [[ "$DRY_RUN" == true ]]; then log_msg "INFO" "DRY-RUN rename: $src -> $dst" else mv -- "$src" "$dst" vlog_msg "Renamed: $src -> $dst" fi ;; GROUP) BLOCKED_GROUPS=$((BLOCKED_GROUPS + 1)) ;; BLOCK) BLOCKED_FILES=$((BLOCKED_FILES + 1)) printf '%s\n' "$src" >> "$TMP_BLOCKED" ;; esac done < "$TMP_ACTIONS" } print_summary_lists() { if [[ -s "$TMP_APPLE" ]]; then if [[ "$DRY_RUN" == true ]]; then log_msg "INFO" "Apple artifacts that would be removed:" else log_msg "INFO" "Apple artifacts removed:" fi cat "$TMP_APPLE" fi if [[ -s "$TMP_ZERO" ]]; then if [[ "$DRY_RUN" == true ]]; then log_msg "INFO" "Zero-size MP4 files that would be removed:" else log_msg "INFO" "Zero-size MP4 files removed:" fi cat "$TMP_ZERO" fi if [[ -s "$TMP_NONSTANDARD" ]]; then if [[ "$DRY_RUN" == true ]]; then log_msg "INFO" "Non-standard duplicate-like MP4 files that would be removed:" else log_msg "INFO" "Non-standard duplicate-like MP4 files removed:" fi cat "$TMP_NONSTANDARD" fi if [[ -s "$TMP_BLOCKED" ]]; then log_msg "WARN" "Blocked duplicate files (manual review needed):" cat "$TMP_BLOCKED" fi } print_summary() { local mode_text mode_text="apply" if [[ "$DRY_RUN" == true ]]; then mode_text="dry-run" fi 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" } main() { parse_args "$@" init_tmp_files validate_media_root log_msg "INFO" "Starting cleanup for media root: $MEDIA_ROOT" if [[ "$DRY_RUN" == true ]]; then log_msg "INFO" "Dry-run enabled; no files will be changed" fi rescan_mp4_files remove_apple_artifacts remove_zero_size_mp4 collect_duplicate_actions build_rename_plan apply_rename_plan print_summary_lists print_summary if [[ "$BLOCKED_GROUPS" -gt 0 ]]; then log_msg "WARN" "Cleanup completed with blocked duplicate groups" exit 1 fi log_msg "INFO" "Cleanup completed successfully" } main "$@"