MediaImporter / media-importer.sh
Newer Older
85a3bb0 9 months ago History
729 lines | 24.7kb
Bogdan Timofte authored 9 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=""
13
FORCE_FULL_DATE=0
14
COLLECT_UNSORTABLE=0
15
SOURCE_PATTERNS=()
16
DESTINATION=""
17
KEEP_ORIGINALS=0
18
DRY_RUN=0
19
VERBOSE=0
20

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

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

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

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

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

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

            
76
USAGE:
77
    $0 [OPTIONS]
78

            
79
DESCRIPTION:
80
    Organizes media files (photos and videos) by date with various naming patterns.
81
    if [[ $exif_found -eq 0 ]]; then
82
        log_message "Warning: No EXIF date found for $file. Using filesystem modification time."
83
    -o, --organization PATTERN
84
        Organization pattern:
85
        y -> target/yyyy/mm-dd_hh-mm-ss.orig_ext
86
        m -> target/yyyy/mm/dd_hh-mm-ss.orig_ext
87
        d -> target/yyyy/mm/dd/mm-dd_hh-mm-ss.orig_ext
88
        h -> target/yyyy/mm/dd/hh/mm-ss.orig_ext
89

            
90
    --full-date
91
        Force all files to be named with full date (yyyy-mm-dd_hh-mm-ss.ext) in the destination folder, regardless of organization pattern.
92

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

            
102
    -d, --destination PATH
103
        Destination folder (default: ./sorted)
104

            
105
    -k, --keep-originals
106
        Keep original files (copy instead of move)
107

            
108
    --dry-run
109
        Show what would be done without actually doing it
110

            
111
    -v, --verbose
112
        Enable verbose output
113

            
114
    -h, --help
115
        Show this help message
116

            
117
    --version
118
        Show version information
119

            
120
EXAMPLES:
121
    # Basic usage - organize all media in current directory
122
    $0
123

            
124
    # Organize with monthly folders, keep originals
125
    $0 -o m -k
126

            
127
    # Process specific folders with hourly organization
128
    # Return date and source (no warnings or debug output)
129
    echo "$create_date|$date_source"
130
    # Dry run with verbose output
131
    $0 --dry-run -v -s "*.mov" -d "/tmp/test"
132

            
133
DEPENDENCIES:
134
    Required: exiftool
135
    Optional: mediainfo, file (for enhanced metadata detection)
136

            
137
EOF
138
}
139

            
140
# Function to show version
141
show_version() {
142
    echo "$SCRIPT_NAME v$VERSION"
143
    echo "A comprehensive media file organizer with timezone support"
144
}
145

            
146
# Function to check dependencies
147
check_dependencies() {
148
    local missing_deps=()
149

            
150
    # Check for required dependencies
151
    if ! command -v exiftool &> /dev/null; then
152
        missing_deps+=("exiftool")
153
    fi
154

            
155
    # Check for optional dependencies
156
    local optional_missing=()
157
    if ! command -v mediainfo &> /dev/null; then
158
        optional_missing+=("mediainfo")
159
    fi
160

            
161
    if ! command -v file &> /dev/null; then
162
        optional_missing+=("file")
163
    fi
164

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

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

            
183
    log_message "All required dependencies found" "SUCCESS"
184
}
185

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

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

            
216
# Function to extract date from file
217
extract_file_date() {
218
    local file="$1"
219
    local create_date=""
220
    local date_source=""
221
    local exif_found=0
222
    # Try to get creation date from EXIF data
223
    local exif_output=$(exiftool -G1 -s -CreateDate -DateTimeOriginal -DateTime "$file" 2>/dev/null)
224
    if [[ -n "$exif_output" ]]; then
225
        # Parse the exiftool output to find the best date
226
        while IFS= read -r line; do
227
            if [[ "$line" =~ ^\[([^\]]+)\][[:space:]]*([^:]+)[[:space:]]*:[[:space:]]*(.+)$ ]]; then
228
                local group="${BASH_REMATCH[1]}"
229
                local tag="${BASH_REMATCH[2]}"
230
                local value="${BASH_REMATCH[3]}"
231
                # Trim spaces from tag name
232
                tag=$(echo "$tag" | sed 's/[[:space:]]*$//')
233
                # Prefer DateTimeOriginal, then CreateDate, then DateTime
234
                if [[ "$tag" == "DateTimeOriginal" && -z "$create_date" ]]; then
235
                    create_date="$value"
236
                    date_source="$group:$tag"
237
                    exif_found=1
238
                elif [[ "$tag" == "CreateDate" && "$date_source" != *"DateTimeOriginal"* ]]; then
239
                    create_date="$value"
240
                    date_source="$group:$tag"
241
                    exif_found=1
242
                elif [[ "$tag" == "DateTime" && -z "$create_date" ]]; then
243
                    create_date="$value"
244
                    date_source="$group:$tag"
245
                    exif_found=1
246
                fi
247
            fi
248
        done <<< "$exif_output"
249
    fi
250
    # If no EXIF date found, try mediainfo for video files
251
    if [[ -z "$create_date" ]] && command -v mediainfo &> /dev/null; then
252
        local media_date=$(mediainfo --Inform="General;%Recorded_Date%" "$file" 2>/dev/null)
253
        if [[ -n "$media_date" && "$media_date" != "0000-00-00 00:00:00" ]]; then
254
            create_date="$media_date"
255
            date_source="MediaInfo:Recorded_Date"
256
        fi
257
    fi
258
    # If no EXIF or mediainfo date found, return failure
259
    if [[ -z "$create_date" ]]; then
260
        return 2  # No date metadata found
261
    fi
262

            
263
    # Convert EXIF format (YYYY:MM:DD HH:MM:SS) to standard format
264
    # Always output as yyyy-mm-dd hh:mm:ss (pad single digits)
265
    if [[ "$create_date" =~ ^([0-9]{4}):([0-9]{1,2}):([0-9]{1,2})[[:space:]]*([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2})$ ]]; then
266
        year="${BASH_REMATCH[1]}"
267
        month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))")
268
        day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))")
269
        hour=$(printf "%02d" "$((10#${BASH_REMATCH[4]}))")
270
        minute=$(printf "%02d" "$((10#${BASH_REMATCH[5]}))")
271
        second=$(printf "%02d" "$((10#${BASH_REMATCH[6]}))")
272
        create_date="$year-$month-$day $hour:$minute:$second"
273
    else
274
        # Try to convert yyyy-mm-dd hh:mm:ss (already correct)
275
        if [[ "$create_date" =~ ^([0-9]{4})-([0-9]{2})-([0-9]{2})[[:space:]]*([0-9]{2}):([0-9]{2}):([0-9]{2})$ ]]; then
276
            # Already correct
277
            :
278
        else
279
            print_color "$RED" "Error: Cannot parse date '$create_date'" >&2
280
            return 2
281
        fi
282
    fi
283

            
284
    # For QuickTime files, the CreateDate is in UTC and needs conversion to local time
285
    if [[ "$date_source" == *"QuickTime"* ]]; then
286
        # Convert UTC time to local time
287
        if [[ "$OSTYPE" == "darwin"* ]]; then
288
            # On macOS, use TZ=UTC to interpret the input time as UTC
289
            local utc_timestamp=$(TZ=UTC date -j -f "%Y-%m-%d %H:%M:%S" "$create_date" "+%s" 2>/dev/null)
290
            if [[ -n "$utc_timestamp" ]]; then
291
                create_date=$(date -j -r "$utc_timestamp" "+%Y-%m-%d %H:%M:%S" 2>/dev/null)
292
                date_source="$date_source (converted from UTC)"
293
            fi
294
        else
295
            local utc_timestamp=$(date -d "$create_date UTC" "+%s" 2>/dev/null)
296
            if [[ -n "$utc_timestamp" ]]; then
297
                create_date=$(date -d "@$utc_timestamp" "+%Y-%m-%d %H:%M:%S" 2>/dev/null)
298
                date_source="$date_source (converted from UTC)"
299
            fi
300
        fi
301
    fi
302

            
303
    echo "$create_date|$date_source"
304
    return 0
305
}
306

            
307
# Function to generate destination path based on organization pattern
308
generate_destination_path() {
309
    local date_str="$1"
310
    local original_filename="$2"
311
    local base_destination="$3"
312

            
313
    # Extract date components - handle both GNU and BSD date
314
    local year month day hour minute second
315
    if [[ "$OSTYPE" == "darwin"* ]]; then
316
        # macOS (BSD date) - use more robust regex (allow single-digit month/day, tolerate extra spaces)
317
        if [[ "$date_str" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2})[[:space:]]*([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2})$ ]]; then
318
            year="${BASH_REMATCH[1]}"
319
            month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))")
320
            day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))")
321
            hour=$(printf "%02d" "$((10#${BASH_REMATCH[4]}))")
322
            minute=$(printf "%02d" "$((10#${BASH_REMATCH[5]}))")
323
            second=$(printf "%02d" "$((10#${BASH_REMATCH[6]}))")
324
        else
325
            return 1
326
        fi
327
    else
328
        # Linux (GNU date)
329
        year=$(date -d "$date_str" "+%Y" 2>/dev/null)
330
        month=$(date -d "$date_str" "+%m" 2>/dev/null)
331
        day=$(date -d "$date_str" "+%d" 2>/dev/null)
332
        hour=$(date -d "$date_str" "+%H" 2>/dev/null)
333
        minute=$(date -d "$date_str" "+%M" 2>/dev/null)
334
        second=$(date -d "$date_str" "+%S" 2>/dev/null)
335
    fi
336

            
337
    if [[ -z "$year" || -z "$month" || -z "$day" ]]; then
338
        return 1
339
    fi
340

            
341
    # Get file extension
342
    local extension="${original_filename##*.}"
343
    local lowercase_ext=$(echo "$extension" | tr '[:upper:]' '[:lower:]')
344

            
345
    # Generate path and filename based on organization pattern
346
    local dir_path=""
347
    local filename=""
348
    if [[ $FORCE_FULL_DATE -eq 1 ]]; then
349
        dir_path="$base_destination"
350
        filename="${year}-${month}-${day}_${hour}-${minute}-${second}.${lowercase_ext}"
351
    else
352
        case "$ORGANIZATION" in
353
            "y")
354
                dir_path="$base_destination/$year"
355
                filename="${month}-${day}_${hour}-${minute}-${second}.${lowercase_ext}"
356
                ;;
357
            "m")
358
                dir_path="$base_destination/$year/$month"
359
                filename="${day}_${hour}-${minute}-${second}.${lowercase_ext}"
360
                ;;
361
            "d")
362
                dir_path="$base_destination/$year/$month/$day"
363
                filename="${month}-${day}_${hour}-${minute}-${second}.${lowercase_ext}"
364
                ;;
365
            "h")
366
                dir_path="$base_destination/$year/$month/$day/$hour"
367
                filename="${minute}-${second}.${lowercase_ext}"
368
                ;;
369
            *)
370
                log_message "Invalid organization pattern: $ORGANIZATION" "ERROR"
371
                return 1
372
                ;;
373
        esac
374
    fi
375
    echo "$dir_path/$filename"
376
    return 0
377
}
378

            
379
# Function to find files matching patterns
380
find_source_files() {
381
    local files=()
382

            
383
    if [[ ${#SOURCE_PATTERNS[@]} -eq 0 ]]; then
384
        # Default: find all media files in current directory and subdirectories
385
        # Exclude destination directory if it's a subdirectory of current directory
386
    local find_cmd="find -L . -type f"
387

            
388
        # Add exclusion for destination if it's relative to current directory
389
        if [[ "$DESTINATION" =~ ^\./.*$ ]] || [[ "$DESTINATION" =~ ^[^/].*$ ]]; then
390
            local abs_dest=$(cd "$DESTINATION" 2>/dev/null && pwd)
391
            local abs_current=$(pwd)
392
            if [[ "$abs_dest" == "$abs_current"* ]]; then
393
                find_cmd="$find_cmd ! -path \"$DESTINATION/*\""
394
            fi
395
        fi
396

            
397
        # Add media file extensions
398
        local extensions=("*.jpg" "*.jpeg" "*.png" "*.tiff" "*.tif" "*.cr2" "*.nef" "*.arw" "*.dng" "*.raw" "*.mp4" "*.mov" "*.avi" "*.mts" "*.m2ts" "*.mkv" "*.wmv" "*.3gp" "*.m4v")
399
        local ext_pattern=""
400
        for ext in "${extensions[@]}"; do
401
            if [[ -n "$ext_pattern" ]]; then
402
                ext_pattern="$ext_pattern -o"
403
            fi
404
            ext_pattern="$ext_pattern -iname $ext"
405
        done
406

            
407
    eval "$find_cmd \\( $ext_pattern \\)" | while IFS= read -r file; do
408
            echo "$file"
409
        done
410
    else
411
        # Use specified patterns
412
        for pattern in "${SOURCE_PATTERNS[@]}"; do
413
            # Handle different pattern types
414
            if [[ -d "$pattern" ]]; then
415
                # Directory pattern
416
                find -L "$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
417
            elif [[ "$pattern" == *"*"* ]] || [[ "$pattern" == *"?"* ]]; then
418
                # Glob pattern
419
                for file in $pattern; do
420
                    if [[ -f "$file" ]]; then
421
                        echo "$file"
422
                    fi
423
                done
424
            else
425
                # Exact file or directory
426
                if [[ -f "$pattern" ]]; then
427
                    echo "$pattern"
428
                elif [[ -d "$pattern" ]]; then
429
                    find -L "$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
430
                fi
431
            fi
432
        done
433
    fi
434
}
435

            
436
# Function to process a single file
437
process_file() {
438
    local file="$1"
439
    local file_size=$(get_file_size "$file")
440
    TOTAL_SIZE=$((TOTAL_SIZE + file_size))
441

            
442
    log_message "Processing: $file" "INFO"
443

            
444
    # Extract date information
445
    local date_info=$(extract_file_date "$file")
446
        local extract_status=$?
447
        if [[ $extract_status -eq 2 ]]; then
448
            if [[ $COLLECT_UNSORTABLE -eq 1 ]]; then
449
                local unsortable_dir="$DESTINATION/unsortable"
450
                mkdir -p "$unsortable_dir"
451
                local unsortable_path="$unsortable_dir/$(basename "$file")"
452
                if [[ $DRY_RUN -eq 1 ]]; then
453
                    print_color "$BLUE" "Would move unsortable: $file -> $unsortable_path"
454
                else
455
                    if mv "$file" "$unsortable_path"; then
456
                        log_message "Unsortable: $file -> $unsortable_path" "SUCCESS"
457
                    else
458
                        log_message "Failed to move unsortable file: $file" "ERROR"
459
                    fi
460
                fi
461
                SKIPPED_FILES=$((SKIPPED_FILES + 1))
462
            else
463
                log_message "Could not extract date from $file - skipping" "WARNING"
464
                SKIPPED_FILES=$((SKIPPED_FILES + 1))
465
            fi
466
            return 1
467
        elif [[ $extract_status -ne 0 ]] || [[ -z "$date_info" ]]; then
468
            log_message "Could not extract date from $file - skipping" "WARNING"
469
            SKIPPED_FILES=$((SKIPPED_FILES + 1))
470
            return 1
471
        fi
472
        local date_str="${date_info%|*}"
473
        local date_source="${date_info#*|}"
474
        log_message "Date: $date_str (from $date_source)" "INFO"
475
        # Generate destination path
476
        local dest_path=$(generate_destination_path "$date_str" "$(basename "$file")" "$DESTINATION")
477
        if [[ $? -ne 0 ]] || [[ -z "$dest_path" ]]; then
478
            log_message "Could not generate destination path for $file" "ERROR"
479
            ERROR_FILES=$((ERROR_FILES + 1))
480
            FATAL_ERROR=1
481
            return 2
482
    fi
483

            
484
    # Handle filename conflicts
485
    local counter=1
486
    local original_dest_path="$dest_path"
487
    while [[ -f "$dest_path" ]]; do
488
        local dir_path=$(dirname "$original_dest_path")
489
        local filename=$(basename "$original_dest_path")
490
        local name_without_ext="${filename%.*}"
491
        local ext="${filename##*.}"
492
        dest_path="$dir_path/${name_without_ext}_${counter}.${ext}"
493
        counter=$((counter + 1))
494
    done
495

            
496
    local dest_dir=$(dirname "$dest_path")
497

            
498
    if [[ $DRY_RUN -eq 1 ]]; then
499
        if [[ $KEEP_ORIGINALS -eq 1 ]]; then
500
            print_color "$BLUE" "Would copy: $file -> $dest_path"
501
        else
502
            print_color "$BLUE" "Would move: $file -> $dest_path"
503
        fi
504
        PROCESSED_FILES=$((PROCESSED_FILES + 1))
505
        PROCESSED_SIZE=$((PROCESSED_SIZE + file_size))
506
        return 0
507
    fi
508

            
509
    # Create destination directory
510
    if ! mkdir -p "$dest_dir"; then
511
        log_message "Could not create directory: $dest_dir" "ERROR"
512
        ERROR_FILES=$((ERROR_FILES + 1))
513
        return 1
514
    fi
515

            
516
    # Copy or move file
517
    if [[ $KEEP_ORIGINALS -eq 1 ]]; then
518
        if cp "$file" "$dest_path"; then
519
            log_message "Copied: $file -> $dest_path" "SUCCESS"
520
            PROCESSED_FILES=$((PROCESSED_FILES + 1))
521
            PROCESSED_SIZE=$((PROCESSED_SIZE + file_size))
522
            return 0
523
        else
524
            log_message "Failed to copy: $file" "ERROR"
525
            ERROR_FILES=$((ERROR_FILES + 1))
526
            return 1
527
        fi
528
    else
529
        if mv "$file" "$dest_path"; then
530
            log_message "Moved: $file -> $dest_path" "SUCCESS"
531
            PROCESSED_FILES=$((PROCESSED_FILES + 1))
532
            PROCESSED_SIZE=$((PROCESSED_SIZE + file_size))
533
            return 0
534
        else
535
            log_message "Failed to move: $file" "ERROR"
536
            ERROR_FILES=$((ERROR_FILES + 1))
537
            return 1
538
        fi
539
    fi
540
}
541

            
542
# Function to display final report
543
show_report() {
544
    local end_time=$(date +%s)
545
    local elapsed_time=$((end_time - START_TIME))
546
    local hours=$((elapsed_time / 3600))
547
    local minutes=$(((elapsed_time % 3600) / 60))
548
    local seconds=$((elapsed_time % 60))
549

            
550
    echo ""
551
    print_color "$GREEN" "=========================================="
552
    print_color "$GREEN" "           PROCESSING REPORT"
553
    print_color "$GREEN" "=========================================="
554
    echo ""
555

            
556
    echo "Files Summary:"
557
    echo "  Total files found:     $TOTAL_FILES"
558
    echo "  Successfully processed: $PROCESSED_FILES"
559
    echo "  Skipped (no date):     $SKIPPED_FILES"
560
    echo "  Errors:                $ERROR_FILES"
561
    echo ""
562

            
563
    echo "Size Summary:"
564
    echo "  Total size found:      $(format_size $TOTAL_SIZE)"
565
    echo "  Successfully processed: $(format_size $PROCESSED_SIZE)"
566
    echo ""
567

            
568
    echo "Time Summary:"
569
    printf "  Time elapsed:          %02d:%02d:%02d\n" $hours $minutes $seconds
570

            
571
    if [[ $elapsed_time -gt 0 && $PROCESSED_FILES -gt 0 ]]; then
572
        local files_per_second=$((PROCESSED_FILES / elapsed_time))
573
        local mb_per_second=$((PROCESSED_SIZE / elapsed_time / 1048576))
574
        echo "  Speed:                 $files_per_second files/sec, ${mb_per_second}MB/sec"
575
    fi
576

            
577
    echo ""
578

            
579
    if [[ $DRY_RUN -eq 1 ]]; then
580
        print_color "$YELLOW" "DRY RUN MODE - No files were actually moved/copied"
581
    elif [[ $KEEP_ORIGINALS -eq 1 ]]; then
582
        print_color "$BLUE" "COPY MODE - Original files were preserved"
583
    else
584
        print_color "$GREEN" "MOVE MODE - Files were moved to destination"
585
    fi
586

            
587
    echo ""
588
    print_color "$GREEN" "=========================================="
589
}
590

            
591
# Parse command line arguments
592
while [[ $# -gt 0 ]]; do
593
    case $1 in
594
        -o|--organization)
595
            ORGANIZATION="$2"
596
            if [[ ! "$ORGANIZATION" =~ ^[ymdh]$ ]]; then
597
                print_color "$RED" "Error: Invalid organization pattern. Must be one of: y, m, d, h"
598
                exit 1
599
            fi
600
            shift 2
601
            ;;
602
        --full-date)
603
            FORCE_FULL_DATE=1
604
            shift
605
            ;;
606
        --collect-unsortable)
607
            COLLECT_UNSORTABLE=1
608
            shift
609
            ;;
610
        -s|--source)
611
            SOURCE_PATTERNS+=("$2")
612
            shift 2
613
            ;;
614
        -d|--destination)
615
            DESTINATION="$2"
616
            shift 2
617
            ;;
618
        -k|--keep-originals)
619
            KEEP_ORIGINALS=1
620
            shift
621
            ;;
622
        --dry-run)
623
            DRY_RUN=1
624
            shift
625
            ;;
626
        -v|--verbose)
627
            VERBOSE=1
628
            shift
629
            ;;
630
        -h|--help)
631
            show_help
632
            exit 0
633
            ;;
634
        --version)
635
            show_version
636
            exit 0
637
            ;;
638
        *)
639
            print_color "$RED" "Error: Unknown option: $1"
640
            echo "Use -h or --help for usage information."
641
            exit 1
642
            ;;
643
    esac
644
done
645

            
646
# Set default destination if not specified
647
if [[ -z "$DESTINATION" ]]; then
648
    DESTINATION="./sorted"
649
fi
650

            
651
# If no organization is provided, default to flat full-date naming
652
if [[ -z "$ORGANIZATION" ]]; then
653
    FORCE_FULL_DATE=1
654
fi
655

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

            
659
# Display configuration
660
print_color "$GREEN" "$SCRIPT_NAME v$VERSION"
661
echo ""
662
echo "Configuration:"
663
echo "  Organization pattern: $ORGANIZATION"
664
echo "  Destination:         $DESTINATION"
665
echo "  Keep originals:      $([ $KEEP_ORIGINALS -eq 1 ] && echo "Yes" || echo "No")"
666
echo "  Dry run:             $([ $DRY_RUN -eq 1 ] && echo "Yes" || echo "No")"
667
echo "  Verbose:             $([ $VERBOSE -eq 1 ] && echo "Yes" || echo "No")"
668

            
669
if [[ ${#SOURCE_PATTERNS[@]} -gt 0 ]]; then
670
    echo "  Source patterns:"
671
    for pattern in "${SOURCE_PATTERNS[@]}"; do
672
        echo "    - $pattern"
673
    done
674
else
675
    echo "  Source patterns:     All media files in current directory"
676
fi
677

            
678
echo ""
679

            
680
# Check dependencies
681
check_dependencies
682

            
683
# Create destination directory if it doesn't exist (unless dry run)
684
if [[ $DRY_RUN -eq 0 ]]; then
685
    if ! mkdir -p "$DESTINATION"; then
686
        print_color "$RED" "Error: Cannot create destination directory: $DESTINATION"
687
        exit 1
688
    fi
689
fi
690

            
691
# Find all source files
692

            
693
print_color "$BLUE" "Scanning for media files..."
694
files=()
695
while IFS= read -r file; do
696
    files+=("$file")
697
done < <(find_source_files)
698
TOTAL_FILES=${#files[@]}
699

            
700
if [[ $TOTAL_FILES -eq 0 ]]; then
701
    print_color "$YELLOW" "No media files found matching the specified patterns."
702
    exit 0
703
fi
704

            
705
print_color "$BLUE" "Found $TOTAL_FILES media files to process"
706
echo ""
707

            
708
# Process each file
709

            
710
FATAL_ERROR=0
711
for file in "${files[@]}"; do
712
    if [[ -f "$file" ]]; then
713
        process_file "$file"
714
        if [[ $FATAL_ERROR -eq 1 ]]; then
715
            print_color "$RED" "Fatal error encountered. Stopping further processing."
716
            break
717
        fi
718
    fi
719
done
720

            
721
# Show final report
722
show_report
723

            
724
# Exit with appropriate code
725
if [[ $ERROR_FILES -gt 0 ]]; then
726
    exit 1
727
else
728
    exit 0
729
fi