autoNAS / standalone-media-importer.sh
Newer Older
5b5a565 3 months ago History
650 lines | 20.71kb
Bogdan Timofte authored 3 months ago
1
#!/bin/bash
2

            
3
# Standalone Media Importer
4
# Version: 1.0
5
# A comprehensive media file organizer that sorts photos and videos by date
6
# with various organization patterns and timezone handling
7

            
8
VERSION="1.0"
9
SCRIPT_NAME="Standalone Media Importer"
10

            
11
# Default values
12
ORGANIZATION="y"  # year/month-day_hour-minute_second.ext
13
SOURCE_PATTERNS=()
14
DESTINATION=""
15
KEEP_ORIGINALS=0
16
DRY_RUN=0
17
VERBOSE=0
18

            
19
# Counters and statistics
20
TOTAL_FILES=0
21
PROCESSED_FILES=0
22
SKIPPED_FILES=0
23
ERROR_FILES=0
24
TOTAL_SIZE=0
25
PROCESSED_SIZE=0
26
START_TIME=$(date +%s)
27

            
28
# Colors for output
29
RED='\033[0;31m'
30
GREEN='\033[0;32m'
31
YELLOW='\033[1;33m'
32
BLUE='\033[0;34m'
33
NC='\033[0m' # No Color
34

            
35
# Function to print colored output
36
print_color() {
37
    local color="$1"
38
    local message="$2"
39
    echo -e "${color}${message}${NC}"
40
}
41

            
42
# Function to log messages with timestamp
43
log_message() {
44
    local message="$1"
45
    local level="${2:-INFO}"
46
    local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
47

            
48
    case "$level" in
49
        "ERROR")
50
            print_color "$RED" "[$timestamp] ERROR: $message" >&2
51
            ;;
52
        "WARNING")
53
            print_color "$YELLOW" "[$timestamp] WARNING: $message"
54
            ;;
55
        "SUCCESS")
56
            print_color "$GREEN" "[$timestamp] SUCCESS: $message"
57
            ;;
58
        "INFO")
59
            if [[ $VERBOSE -eq 1 ]]; then
60
                print_color "$BLUE" "[$timestamp] INFO: $message"
61
            fi
62
            ;;
63
        *)
64
            echo "[$timestamp] $message"
65
            ;;
66
    esac
67
}
68

            
69
# Function to display help
70
show_help() {
71
    cat << EOF
72
$SCRIPT_NAME v$VERSION
73

            
74
USAGE:
75
    $0 [OPTIONS]
76

            
77
DESCRIPTION:
78
    Organizes media files (photos and videos) by date with various naming patterns.
79
    Handles timezone conversion for QuickTime files and preserves original timestamps.
80

            
81
OPTIONS:
82
    -o, --organization PATTERN
83
        Organization pattern (default: y):
84
        y -> target/yyyy/mm-dd_hh-mm-ss.orig_ext
85
        m -> target/yyyy/mm/dd_hh-mm-ss.orig_ext
86
        d -> target/yyyy/mm/dd/mm-dd_hh-mm-ss.orig_ext
87
        h -> target/yyyy/mm/dd/hh/mm-ss.orig_ext
88

            
89
    -s, --source PATTERN
90
        Source folder pattern(s) with simple regex support (*^$)
91
        Can be specified multiple times
92
        Examples:
93
            -s "/DCIM/*Video$"
94
            -s "/path/to/photos"
95
            -s "*.jpg"
96
        Default: all subfolders in current directory except destination
97

            
98
    -d, --destination PATH
99
        Destination folder (default: ./sorted)
100

            
101
    -k, --keep-originals
102
        Keep original files (copy instead of move)
103

            
104
    --dry-run
105
        Show what would be done without actually doing it
106

            
107
    -v, --verbose
108
        Enable verbose output
109

            
110
    -h, --help
111
        Show this help message
112

            
113
    --version
114
        Show version information
115

            
116
EXAMPLES:
117
    # Basic usage - organize all media in current directory
118
    $0
119

            
120
    # Organize with monthly folders, keep originals
121
    $0 -o m -k
122

            
123
    # Process specific folders with hourly organization
124
    $0 -o h -s "/DCIM/Camera" -s "/DCIM/Video" -d "/media/sorted"
125

            
126
    # Dry run with verbose output
127
    $0 --dry-run -v -s "*.mov" -d "/tmp/test"
128

            
129
DEPENDENCIES:
130
    Required: exiftool
131
    Optional: mediainfo, file (for enhanced metadata detection)
132

            
133
EOF
134
}
135

            
136
# Function to show version
137
show_version() {
138
    echo "$SCRIPT_NAME v$VERSION"
139
    echo "A comprehensive media file organizer with timezone support"
140
}
141

            
142
# Function to check dependencies
143
check_dependencies() {
144
    local missing_deps=()
145

            
146
    # Check for required dependencies
147
    if ! command -v exiftool &> /dev/null; then
148
        missing_deps+=("exiftool")
149
    fi
150

            
151
    # Check for optional dependencies
152
    local optional_missing=()
153
    if ! command -v mediainfo &> /dev/null; then
154
        optional_missing+=("mediainfo")
155
    fi
156

            
157
    if ! command -v file &> /dev/null; then
158
        optional_missing+=("file")
159
    fi
160

            
161
    if [[ ${#missing_deps[@]} -gt 0 ]]; then
162
        print_color "$RED" "ERROR: Missing required dependencies:"
163
        for dep in "${missing_deps[@]}"; do
164
            echo "  - $dep"
165
        done
166
        echo ""
167
        echo "Installation instructions:"
168
        echo "  macOS: brew install exiftool"
169
        echo "  Ubuntu/Debian: sudo apt-get install libimage-exiftool-perl"
170
        echo "  CentOS/RHEL: sudo yum install perl-Image-ExifTool"
171
        echo "  Arch: sudo pacman -S perl-image-exiftool"
172
        exit 1
173
    fi
174

            
175
    if [[ ${#optional_missing[@]} -gt 0 && $VERBOSE -eq 1 ]]; then
176
        log_message "Optional dependencies not found (functionality may be limited): ${optional_missing[*]}" "WARNING"
177
    fi
178

            
179
    log_message "All required dependencies found" "SUCCESS"
180
}
181

            
182
# Function to get file size in bytes
183
get_file_size() {
184
    local file="$1"
185
    if [[ -f "$file" ]]; then
186
        if command -v stat &> /dev/null; then
187
            # Try GNU stat first (Linux)
188
            stat -c%s "$file" 2>/dev/null || stat -f%z "$file" 2>/dev/null
189
        else
190
            # Fallback to ls
191
            ls -l "$file" | awk '{print $5}'
192
        fi
193
    else
194
        echo "0"
195
    fi
196
}
197

            
198
# Function to format file size
199
format_size() {
200
    local size=$1
201
    if (( size < 1024 )); then
202
        echo "${size}B"
203
    elif (( size < 1048576 )); then
204
        echo "$(( size / 1024 ))KB"
205
    elif (( size < 1073741824 )); then
206
        echo "$(( size / 1048576 ))MB"
207
    else
208
        echo "$(( size / 1073741824 ))GB"
209
    fi
210
}
211

            
212
# Function to extract date from file
213
extract_file_date() {
214
    local file="$1"
215
    local create_date=""
216
    local date_source=""
217

            
218
    # Try to get creation date from EXIF data
219
    local exif_output=$(exiftool -G1 -s -CreateDate -DateTimeOriginal -DateTime "$file" 2>/dev/null)
220

            
221
    if [[ -n "$exif_output" ]]; then
222
        # Parse the exiftool output to find the best date
223
        while IFS= read -r line; do
224
            if [[ "$line" =~ ^\[([^\]]+)\][[:space:]]*([^:]+):[[:space:]]*(.+)$ ]]; then
225
                local group="${BASH_REMATCH[1]}"
226
                local tag="${BASH_REMATCH[2]}"
227
                local value="${BASH_REMATCH[3]}"
228

            
229
                # Prefer DateTimeOriginal, then CreateDate, then DateTime
230
                if [[ "$tag" == "DateTimeOriginal" && -z "$create_date" ]]; then
231
                    create_date="$value"
232
                    date_source="$group:$tag"
233
                elif [[ "$tag" == "CreateDate" && "$date_source" != *"DateTimeOriginal"* ]]; then
234
                    create_date="$value"
235
                    date_source="$group:$tag"
236
                elif [[ "$tag" == "DateTime" && -z "$create_date" ]]; then
237
                    create_date="$value"
238
                    date_source="$group:$tag"
239
                fi
240
            fi
241
        done <<< "$exif_output"
242
    fi
243

            
244
    # If no EXIF date found, try mediainfo for video files
245
    if [[ -z "$create_date" ]] && command -v mediainfo &> /dev/null; then
246
        local media_date=$(mediainfo --Inform="General;%Recorded_Date%" "$file" 2>/dev/null)
247
        if [[ -n "$media_date" && "$media_date" != "0000-00-00 00:00:00" ]]; then
248
            create_date="$media_date"
249
            date_source="MediaInfo:Recorded_Date"
250
        fi
251
    fi
252

            
253
    # Fallback to file modification time
254
    if [[ -z "$create_date" ]]; then
255
        if [[ "$OSTYPE" == "darwin"* ]]; then
256
            # macOS
257
            create_date=$(stat -f "%Sm" -t "%Y:%m:%d %H:%M:%S" "$file" 2>/dev/null)
258
        else
259
            # Linux
260
            create_date=$(date -r "$file" "+%Y:%m:%d %H:%M:%S" 2>/dev/null)
261
        fi
262
        date_source="FileSystem:ModificationTime"
263
    fi
264

            
265
    if [[ -z "$create_date" ]]; then
266
        return 1
267
    fi
268

            
269
    # Convert EXIF format (YYYY:MM:DD HH:MM:SS) to standard format
270
    create_date=$(echo "$create_date" | sed 's/^\([0-9]\{4\}\):\([0-9]\{2\}\):\([0-9]\{2\}\)/\1-\2-\3/')
271

            
272
    # Handle QuickTime UTC conversion
273
    if [[ "$date_source" == *"QuickTime"* ]]; then
274
        local utc_timestamp=$(date -d "$create_date UTC" "+%s" 2>/dev/null)
275
        if [[ -n "$utc_timestamp" ]]; then
276
            create_date=$(date -d "@$utc_timestamp" "+%Y-%m-%d %H:%M:%S" 2>/dev/null)
277
            date_source="$date_source (converted from UTC)"
278
        fi
279
    fi
280

            
281
    echo "$create_date|$date_source"
282
    return 0
283
}
284

            
285
# Function to generate destination path based on organization pattern
286
generate_destination_path() {
287
    local date_str="$1"
288
    local original_filename="$2"
289
    local base_destination="$3"
290

            
291
    # Extract date components
292
    local year=$(date -d "$date_str" "+%Y" 2>/dev/null)
293
    local month=$(date -d "$date_str" "+%m" 2>/dev/null)
294
    local day=$(date -d "$date_str" "+%d" 2>/dev/null)
295
    local hour=$(date -d "$date_str" "+%H" 2>/dev/null)
296
    local minute=$(date -d "$date_str" "+%M" 2>/dev/null)
297
    local second=$(date -d "$date_str" "+%S" 2>/dev/null)
298

            
299
    if [[ -z "$year" || -z "$month" || -z "$day" ]]; then
300
        return 1
301
    fi
302

            
303
    # Get file extension
304
    local extension="${original_filename##*.}"
305
    local lowercase_ext="${extension,,}"
306

            
307
    # Generate path and filename based on organization pattern
308
    local dir_path=""
309
    local filename=""
310

            
311
    case "$ORGANIZATION" in
312
        "y")
313
            # target/yyyy/mm-dd_hh-mm-ss.orig_ext
314
            dir_path="$base_destination/$year"
315
            filename="${month}-${day}_${hour}-${minute}-${second}.${lowercase_ext}"
316
            ;;
317
        "m")
318
            # target/yyyy/mm/dd_hh-mm-ss.orig_ext
319
            dir_path="$base_destination/$year/$month"
320
            filename="${day}_${hour}-${minute}-${second}.${lowercase_ext}"
321
            ;;
322
        "d")
323
            # target/yyyy/mm/dd/mm-dd_hh-mm-ss.orig_ext
324
            dir_path="$base_destination/$year/$month/$day"
325
            filename="${month}-${day}_${hour}-${minute}-${second}.${lowercase_ext}"
326
            ;;
327
        "h")
328
            # target/yyyy/mm/dd/hh/mm-ss.orig_ext
329
            dir_path="$base_destination/$year/$month/$day/$hour"
330
            filename="${minute}-${second}.${lowercase_ext}"
331
            ;;
332
        *)
333
            log_message "Invalid organization pattern: $ORGANIZATION" "ERROR"
334
            return 1
335
            ;;
336
    esac
337

            
338
    echo "$dir_path/$filename"
339
    return 0
340
}
341

            
342
# Function to find files matching patterns
343
find_source_files() {
344
    local files=()
345

            
346
    if [[ ${#SOURCE_PATTERNS[@]} -eq 0 ]]; then
347
        # Default: find all media files in current directory and subdirectories
348
        # Exclude destination directory if it's a subdirectory of current directory
349
        local find_cmd="find . -type f"
350

            
351
        # Add exclusion for destination if it's relative to current directory
352
        if [[ "$DESTINATION" =~ ^\./.*$ ]] || [[ "$DESTINATION" =~ ^[^/].*$ ]]; then
353
            local abs_dest=$(cd "$DESTINATION" 2>/dev/null && pwd)
354
            local abs_current=$(pwd)
355
            if [[ "$abs_dest" == "$abs_current"* ]]; then
356
                find_cmd="$find_cmd ! -path \"$DESTINATION/*\""
357
            fi
358
        fi
359

            
360
        # Add media file extensions
361
        local extensions=("*.jpg" "*.jpeg" "*.png" "*.tiff" "*.tif" "*.cr2" "*.nef" "*.arw" "*.dng" "*.raw" "*.mp4" "*.mov" "*.avi" "*.mts" "*.m2ts" "*.mkv" "*.wmv" "*.3gp" "*.m4v")
362
        local ext_pattern=""
363
        for ext in "${extensions[@]}"; do
364
            if [[ -n "$ext_pattern" ]]; then
365
                ext_pattern="$ext_pattern -o"
366
            fi
367
            ext_pattern="$ext_pattern -iname $ext"
368
        done
369

            
370
        eval "$find_cmd \\( $ext_pattern \\)" | while IFS= read -r file; do
371
            echo "$file"
372
        done
373
    else
374
        # Use specified patterns
375
        for pattern in "${SOURCE_PATTERNS[@]}"; do
376
            # Handle different pattern types
377
            if [[ -d "$pattern" ]]; then
378
                # Directory pattern
379
                find "$pattern" -type f \( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.tiff" -o -iname "*.tif" -o -iname "*.cr2" -o -iname "*.nef" -o -iname "*.arw" -o -iname "*.dng" -o -iname "*.raw" -o -iname "*.mp4" -o -iname "*.mov" -o -iname "*.avi" -o -iname "*.mts" -o -iname "*.m2ts" -o -iname "*.mkv" -o -iname "*.wmv" -o -iname "*.3gp" -o -iname "*.m4v" \) 2>/dev/null
380
            elif [[ "$pattern" == *"*"* ]] || [[ "$pattern" == *"?"* ]]; then
381
                # Glob pattern
382
                for file in $pattern; do
383
                    if [[ -f "$file" ]]; then
384
                        echo "$file"
385
                    fi
386
                done
387
            else
388
                # Exact file or directory
389
                if [[ -f "$pattern" ]]; then
390
                    echo "$pattern"
391
                elif [[ -d "$pattern" ]]; then
392
                    find "$pattern" -type f \( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.tiff" -o -iname "*.tif" -o -iname "*.cr2" -o -iname "*.nef" -o -iname "*.arw" -o -iname "*.dng" -o -iname "*.raw" -o -iname "*.mp4" -o -iname "*.mov" -o -iname "*.avi" -o -iname "*.mts" -o -iname "*.m2ts" -o -iname "*.mkv" -o -iname "*.wmv" -o -iname "*.3gp" -o -iname "*.m4v" \) 2>/dev/null
393
                fi
394
            fi
395
        done
396
    fi
397
}
398

            
399
# Function to process a single file
400
process_file() {
401
    local file="$1"
402
    local file_size=$(get_file_size "$file")
403
    TOTAL_SIZE=$((TOTAL_SIZE + file_size))
404

            
405
    log_message "Processing: $file" "INFO"
406

            
407
    # Extract date information
408
    local date_info=$(extract_file_date "$file")
409
    if [[ $? -ne 0 ]] || [[ -z "$date_info" ]]; then
410
        log_message "Could not extract date from $file - skipping" "WARNING"
411
        SKIPPED_FILES=$((SKIPPED_FILES + 1))
412
        return 1
413
    fi
414

            
415
    local date_str="${date_info%|*}"
416
    local date_source="${date_info#*|}"
417

            
418
    log_message "Date: $date_str (from $date_source)" "INFO"
419

            
420
    # Generate destination path
421
    local dest_path=$(generate_destination_path "$date_str" "$(basename "$file")" "$DESTINATION")
422
    if [[ $? -ne 0 ]] || [[ -z "$dest_path" ]]; then
423
        log_message "Could not generate destination path for $file" "ERROR"
424
        ERROR_FILES=$((ERROR_FILES + 1))
425
        return 1
426
    fi
427

            
428
    # Handle filename conflicts
429
    local counter=1
430
    local original_dest_path="$dest_path"
431
    while [[ -f "$dest_path" ]]; do
432
        local dir_path=$(dirname "$original_dest_path")
433
        local filename=$(basename "$original_dest_path")
434
        local name_without_ext="${filename%.*}"
435
        local ext="${filename##*.}"
436
        dest_path="$dir_path/${name_without_ext}_${counter}.${ext}"
437
        counter=$((counter + 1))
438
    done
439

            
440
    local dest_dir=$(dirname "$dest_path")
441

            
442
    if [[ $DRY_RUN -eq 1 ]]; then
443
        if [[ $KEEP_ORIGINALS -eq 1 ]]; then
444
            print_color "$BLUE" "Would copy: $file -> $dest_path"
445
        else
446
            print_color "$BLUE" "Would move: $file -> $dest_path"
447
        fi
448
        PROCESSED_FILES=$((PROCESSED_FILES + 1))
449
        PROCESSED_SIZE=$((PROCESSED_SIZE + file_size))
450
        return 0
451
    fi
452

            
453
    # Create destination directory
454
    if ! mkdir -p "$dest_dir"; then
455
        log_message "Could not create directory: $dest_dir" "ERROR"
456
        ERROR_FILES=$((ERROR_FILES + 1))
457
        return 1
458
    fi
459

            
460
    # Copy or move file
461
    if [[ $KEEP_ORIGINALS -eq 1 ]]; then
462
        if cp "$file" "$dest_path"; then
463
            log_message "Copied: $file -> $dest_path" "SUCCESS"
464
            PROCESSED_FILES=$((PROCESSED_FILES + 1))
465
            PROCESSED_SIZE=$((PROCESSED_SIZE + file_size))
466
            return 0
467
        else
468
            log_message "Failed to copy: $file" "ERROR"
469
            ERROR_FILES=$((ERROR_FILES + 1))
470
            return 1
471
        fi
472
    else
473
        if mv "$file" "$dest_path"; then
474
            log_message "Moved: $file -> $dest_path" "SUCCESS"
475
            PROCESSED_FILES=$((PROCESSED_FILES + 1))
476
            PROCESSED_SIZE=$((PROCESSED_SIZE + file_size))
477
            return 0
478
        else
479
            log_message "Failed to move: $file" "ERROR"
480
            ERROR_FILES=$((ERROR_FILES + 1))
481
            return 1
482
        fi
483
    fi
484
}
485

            
486
# Function to display final report
487
show_report() {
488
    local end_time=$(date +%s)
489
    local elapsed_time=$((end_time - START_TIME))
490
    local hours=$((elapsed_time / 3600))
491
    local minutes=$(((elapsed_time % 3600) / 60))
492
    local seconds=$((elapsed_time % 60))
493

            
494
    echo ""
495
    print_color "$GREEN" "=========================================="
496
    print_color "$GREEN" "           PROCESSING REPORT"
497
    print_color "$GREEN" "=========================================="
498
    echo ""
499

            
500
    echo "Files Summary:"
501
    echo "  Total files found:     $TOTAL_FILES"
502
    echo "  Successfully processed: $PROCESSED_FILES"
503
    echo "  Skipped (no date):     $SKIPPED_FILES"
504
    echo "  Errors:                $ERROR_FILES"
505
    echo ""
506

            
507
    echo "Size Summary:"
508
    echo "  Total size found:      $(format_size $TOTAL_SIZE)"
509
    echo "  Successfully processed: $(format_size $PROCESSED_SIZE)"
510
    echo ""
511

            
512
    echo "Time Summary:"
513
    printf "  Time elapsed:          %02d:%02d:%02d\n" $hours $minutes $seconds
514

            
515
    if [[ $elapsed_time -gt 0 && $PROCESSED_FILES -gt 0 ]]; then
516
        local files_per_second=$((PROCESSED_FILES / elapsed_time))
517
        local mb_per_second=$((PROCESSED_SIZE / elapsed_time / 1048576))
518
        echo "  Speed:                 $files_per_second files/sec, ${mb_per_second}MB/sec"
519
    fi
520

            
521
    echo ""
522

            
523
    if [[ $DRY_RUN -eq 1 ]]; then
524
        print_color "$YELLOW" "DRY RUN MODE - No files were actually moved/copied"
525
    elif [[ $KEEP_ORIGINALS -eq 1 ]]; then
526
        print_color "$BLUE" "COPY MODE - Original files were preserved"
527
    else
528
        print_color "$GREEN" "MOVE MODE - Files were moved to destination"
529
    fi
530

            
531
    echo ""
532
    print_color "$GREEN" "=========================================="
533
}
534

            
535
# Parse command line arguments
536
while [[ $# -gt 0 ]]; do
537
    case $1 in
538
        -o|--organization)
539
            ORGANIZATION="$2"
540
            if [[ ! "$ORGANIZATION" =~ ^[ymdh]$ ]]; then
541
                print_color "$RED" "Error: Invalid organization pattern. Must be one of: y, m, d, h"
542
                exit 1
543
            fi
544
            shift 2
545
            ;;
546
        -s|--source)
547
            SOURCE_PATTERNS+=("$2")
548
            shift 2
549
            ;;
550
        -d|--destination)
551
            DESTINATION="$2"
552
            shift 2
553
            ;;
554
        -k|--keep-originals)
555
            KEEP_ORIGINALS=1
556
            shift
557
            ;;
558
        --dry-run)
559
            DRY_RUN=1
560
            shift
561
            ;;
562
        -v|--verbose)
563
            VERBOSE=1
564
            shift
565
            ;;
566
        -h|--help)
567
            show_help
568
            exit 0
569
            ;;
570
        --version)
571
            show_version
572
            exit 0
573
            ;;
574
        *)
575
            print_color "$RED" "Error: Unknown option: $1"
576
            echo "Use -h or --help for usage information."
577
            exit 1
578
            ;;
579
    esac
580
done
581

            
582
# Set default destination if not specified
583
if [[ -z "$DESTINATION" ]]; then
584
    DESTINATION="./sorted"
585
fi
586

            
587
# Convert destination to absolute path
588
DESTINATION=$(cd "$(dirname "$DESTINATION")" 2>/dev/null && pwd)/$(basename "$DESTINATION") || DESTINATION=$(realpath "$DESTINATION" 2>/dev/null) || DESTINATION="$DESTINATION"
589

            
590
# Display configuration
591
print_color "$GREEN" "$SCRIPT_NAME v$VERSION"
592
echo ""
593
echo "Configuration:"
594
echo "  Organization pattern: $ORGANIZATION"
595
echo "  Destination:         $DESTINATION"
596
echo "  Keep originals:      $([ $KEEP_ORIGINALS -eq 1 ] && echo "Yes" || echo "No")"
597
echo "  Dry run:             $([ $DRY_RUN -eq 1 ] && echo "Yes" || echo "No")"
598
echo "  Verbose:             $([ $VERBOSE -eq 1 ] && echo "Yes" || echo "No")"
599

            
600
if [[ ${#SOURCE_PATTERNS[@]} -gt 0 ]]; then
601
    echo "  Source patterns:"
602
    for pattern in "${SOURCE_PATTERNS[@]}"; do
603
        echo "    - $pattern"
604
    done
605
else
606
    echo "  Source patterns:     All media files in current directory"
607
fi
608

            
609
echo ""
610

            
611
# Check dependencies
612
check_dependencies
613

            
614
# Create destination directory if it doesn't exist (unless dry run)
615
if [[ $DRY_RUN -eq 0 ]]; then
616
    if ! mkdir -p "$DESTINATION"; then
617
        print_color "$RED" "Error: Cannot create destination directory: $DESTINATION"
618
        exit 1
619
    fi
620
fi
621

            
622
# Find all source files
623
print_color "$BLUE" "Scanning for media files..."
624
mapfile -t files < <(find_source_files)
625
TOTAL_FILES=${#files[@]}
626

            
627
if [[ $TOTAL_FILES -eq 0 ]]; then
628
    print_color "$YELLOW" "No media files found matching the specified patterns."
629
    exit 0
630
fi
631

            
632
print_color "$BLUE" "Found $TOTAL_FILES media files to process"
633
echo ""
634

            
635
# Process each file
636
for file in "${files[@]}"; do
637
    if [[ -f "$file" ]]; then
638
        process_file "$file"
639
    fi
640
done
641

            
642
# Show final report
643
show_report
644

            
645
# Exit with appropriate code
646
if [[ $ERROR_FILES -gt 0 ]]; then
647
    exit 1
648
else
649
    exit 0
650
fi