MediaImporter / media-importer.sh
85a3bb0 9 months ago History
1 contributor
729 lines | 24.7kb
#!/bin/bash

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

VERSION="1.0"
SCRIPT_NAME="Standalone Media Importer"

# Default values
ORGANIZATION=""
FORCE_FULL_DATE=0
COLLECT_UNSORTABLE=0
SOURCE_PATTERNS=()
DESTINATION=""
KEEP_ORIGINALS=0
DRY_RUN=0
VERBOSE=0

# Counters and statistics
TOTAL_FILES=0
PROCESSED_FILES=0
SKIPPED_FILES=0
ERROR_FILES=0
TOTAL_SIZE=0
PROCESSED_SIZE=0
START_TIME=$(date +%s)

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

# Function to print colored output
print_color() {
    local color="$1"
    local message="$2"
    echo -e "${color}${message}${NC}"
}

# Function to log messages with timestamp
log_message() {
    local message="$1"
    local level="${2:-INFO}"
    local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    
    case "$level" in
        "ERROR")
            print_color "$RED" "[$timestamp] ERROR: $message" >&2
            ;;
        "WARNING")
            print_color "$YELLOW" "[$timestamp] WARNING: $message"
            ;;
        "SUCCESS")
            print_color "$GREEN" "[$timestamp] SUCCESS: $message"
            ;;
        "INFO")
            if [[ $VERBOSE -eq 1 ]]; then
                print_color "$BLUE" "[$timestamp] INFO: $message"
            fi
            ;;
        *)
            echo "[$timestamp] $message"
            ;;
    esac
}

# Function to display help
show_help() {
    cat << EOF
$SCRIPT_NAME v$VERSION

USAGE:
    $0 [OPTIONS]

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

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

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

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

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

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

    -v, --verbose
        Enable verbose output

    -h, --help
        Show this help message

    --version
        Show version information

EXAMPLES:
    # Basic usage - organize all media in current directory
    $0

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

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

DEPENDENCIES:
    Required: exiftool
    Optional: mediainfo, file (for enhanced metadata detection)

EOF
}

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

# Function to check dependencies
check_dependencies() {
    local missing_deps=()
    
    # Check for required dependencies
    if ! command -v exiftool &> /dev/null; then
        missing_deps+=("exiftool")
    fi
    
    # Check for optional dependencies
    local optional_missing=()
    if ! command -v mediainfo &> /dev/null; then
        optional_missing+=("mediainfo")
    fi
    
    if ! command -v file &> /dev/null; then
        optional_missing+=("file")
    fi
    
    if [[ ${#missing_deps[@]} -gt 0 ]]; then
        print_color "$RED" "ERROR: Missing required dependencies:"
        for dep in "${missing_deps[@]}"; do
            echo "  - $dep"
        done
        echo ""
        echo "Installation instructions:"
        echo "  macOS: brew install exiftool"
        echo "  Ubuntu/Debian: sudo apt-get install libimage-exiftool-perl"
        echo "  CentOS/RHEL: sudo yum install perl-Image-ExifTool"
        echo "  Arch: sudo pacman -S perl-image-exiftool"
        exit 1
    fi
    
    if [[ ${#optional_missing[@]} -gt 0 && $VERBOSE -eq 1 ]]; then
        log_message "Optional dependencies not found (functionality may be limited): ${optional_missing[*]}" "WARNING"
    fi
    
    log_message "All required dependencies found" "SUCCESS"
}

# Function to get file size in bytes
get_file_size() {
    local file="$1"
    if [[ -f "$file" ]]; then
        if command -v stat &> /dev/null; then
            # Try GNU stat first (Linux)
            stat -c%s "$file" 2>/dev/null || stat -f%z "$file" 2>/dev/null
        else
            # Fallback to ls
            ls -l "$file" | awk '{print $5}'
        fi
    else
        echo "0"
    fi
}

# Function to format file size
format_size() {
    local size=$1
    if (( size < 1024 )); then
        echo "${size}B"
    elif (( size < 1048576 )); then
        echo "$(( size / 1024 ))KB"
    elif (( size < 1073741824 )); then
        echo "$(( size / 1048576 ))MB"
    else
        echo "$(( size / 1073741824 ))GB"
    fi
}

# Function to extract date from file
extract_file_date() {
    local file="$1"
    local create_date=""
    local date_source=""
    local exif_found=0
    # Try to get creation date from EXIF data
    local exif_output=$(exiftool -G1 -s -CreateDate -DateTimeOriginal -DateTime "$file" 2>/dev/null)
    if [[ -n "$exif_output" ]]; then
        # Parse the exiftool output to find the best date
        while IFS= read -r line; do
            if [[ "$line" =~ ^\[([^\]]+)\][[:space:]]*([^:]+)[[:space:]]*:[[:space:]]*(.+)$ ]]; then
                local group="${BASH_REMATCH[1]}"
                local tag="${BASH_REMATCH[2]}"
                local value="${BASH_REMATCH[3]}"
                # Trim spaces from tag name
                tag=$(echo "$tag" | sed 's/[[:space:]]*$//')
                # Prefer DateTimeOriginal, then CreateDate, then DateTime
                if [[ "$tag" == "DateTimeOriginal" && -z "$create_date" ]]; then
                    create_date="$value"
                    date_source="$group:$tag"
                    exif_found=1
                elif [[ "$tag" == "CreateDate" && "$date_source" != *"DateTimeOriginal"* ]]; then
                    create_date="$value"
                    date_source="$group:$tag"
                    exif_found=1
                elif [[ "$tag" == "DateTime" && -z "$create_date" ]]; then
                    create_date="$value"
                    date_source="$group:$tag"
                    exif_found=1
                fi
            fi
        done <<< "$exif_output"
    fi
    # If no EXIF date found, try mediainfo for video files
    if [[ -z "$create_date" ]] && command -v mediainfo &> /dev/null; then
        local media_date=$(mediainfo --Inform="General;%Recorded_Date%" "$file" 2>/dev/null)
        if [[ -n "$media_date" && "$media_date" != "0000-00-00 00:00:00" ]]; then
            create_date="$media_date"
            date_source="MediaInfo:Recorded_Date"
        fi
    fi
    # If no EXIF or mediainfo date found, return failure
    if [[ -z "$create_date" ]]; then
        return 2  # No date metadata found
    fi
    
    # Convert EXIF format (YYYY:MM:DD HH:MM:SS) to standard format
    # Always output as yyyy-mm-dd hh:mm:ss (pad single digits)
    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
        year="${BASH_REMATCH[1]}"
        month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))")
        day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))")
        hour=$(printf "%02d" "$((10#${BASH_REMATCH[4]}))")
        minute=$(printf "%02d" "$((10#${BASH_REMATCH[5]}))")
        second=$(printf "%02d" "$((10#${BASH_REMATCH[6]}))")
        create_date="$year-$month-$day $hour:$minute:$second"
    else
        # Try to convert yyyy-mm-dd hh:mm:ss (already correct)
        if [[ "$create_date" =~ ^([0-9]{4})-([0-9]{2})-([0-9]{2})[[:space:]]*([0-9]{2}):([0-9]{2}):([0-9]{2})$ ]]; then
            # Already correct
            :
        else
            print_color "$RED" "Error: Cannot parse date '$create_date'" >&2
            return 2
        fi
    fi
    
    # For QuickTime files, the CreateDate is in UTC and needs conversion to local time
    if [[ "$date_source" == *"QuickTime"* ]]; then
        # Convert UTC time to local time
        if [[ "$OSTYPE" == "darwin"* ]]; then
            # On macOS, use TZ=UTC to interpret the input time as UTC
            local utc_timestamp=$(TZ=UTC date -j -f "%Y-%m-%d %H:%M:%S" "$create_date" "+%s" 2>/dev/null)
            if [[ -n "$utc_timestamp" ]]; then
                create_date=$(date -j -r "$utc_timestamp" "+%Y-%m-%d %H:%M:%S" 2>/dev/null)
                date_source="$date_source (converted from UTC)"
            fi
        else
            local utc_timestamp=$(date -d "$create_date UTC" "+%s" 2>/dev/null)
            if [[ -n "$utc_timestamp" ]]; then
                create_date=$(date -d "@$utc_timestamp" "+%Y-%m-%d %H:%M:%S" 2>/dev/null)
                date_source="$date_source (converted from UTC)"
            fi
        fi
    fi
    
    echo "$create_date|$date_source"
    return 0
}

# Function to generate destination path based on organization pattern
generate_destination_path() {
    local date_str="$1"
    local original_filename="$2"
    local base_destination="$3"
    
    # Extract date components - handle both GNU and BSD date
    local year month day hour minute second
    if [[ "$OSTYPE" == "darwin"* ]]; then
        # macOS (BSD date) - use more robust regex (allow single-digit month/day, tolerate extra spaces)
        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
            year="${BASH_REMATCH[1]}"
            month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))")
            day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))")
            hour=$(printf "%02d" "$((10#${BASH_REMATCH[4]}))")
            minute=$(printf "%02d" "$((10#${BASH_REMATCH[5]}))")
            second=$(printf "%02d" "$((10#${BASH_REMATCH[6]}))")
        else
            return 1
        fi
    else
        # Linux (GNU date)
        year=$(date -d "$date_str" "+%Y" 2>/dev/null)
        month=$(date -d "$date_str" "+%m" 2>/dev/null)
        day=$(date -d "$date_str" "+%d" 2>/dev/null)
        hour=$(date -d "$date_str" "+%H" 2>/dev/null)
        minute=$(date -d "$date_str" "+%M" 2>/dev/null)
        second=$(date -d "$date_str" "+%S" 2>/dev/null)
    fi
    
    if [[ -z "$year" || -z "$month" || -z "$day" ]]; then
        return 1
    fi
    
    # Get file extension
    local extension="${original_filename##*.}"
    local lowercase_ext=$(echo "$extension" | tr '[:upper:]' '[:lower:]')
    
    # Generate path and filename based on organization pattern
    local dir_path=""
    local filename=""
    if [[ $FORCE_FULL_DATE -eq 1 ]]; then
        dir_path="$base_destination"
        filename="${year}-${month}-${day}_${hour}-${minute}-${second}.${lowercase_ext}"
    else
        case "$ORGANIZATION" in
            "y")
                dir_path="$base_destination/$year"
                filename="${month}-${day}_${hour}-${minute}-${second}.${lowercase_ext}"
                ;;
            "m")
                dir_path="$base_destination/$year/$month"
                filename="${day}_${hour}-${minute}-${second}.${lowercase_ext}"
                ;;
            "d")
                dir_path="$base_destination/$year/$month/$day"
                filename="${month}-${day}_${hour}-${minute}-${second}.${lowercase_ext}"
                ;;
            "h")
                dir_path="$base_destination/$year/$month/$day/$hour"
                filename="${minute}-${second}.${lowercase_ext}"
                ;;
            *)
                log_message "Invalid organization pattern: $ORGANIZATION" "ERROR"
                return 1
                ;;
        esac
    fi
    echo "$dir_path/$filename"
    return 0
}

# Function to find files matching patterns
find_source_files() {
    local files=()
    
    if [[ ${#SOURCE_PATTERNS[@]} -eq 0 ]]; then
        # Default: find all media files in current directory and subdirectories
        # Exclude destination directory if it's a subdirectory of current directory
    local find_cmd="find -L . -type f"
        
        # Add exclusion for destination if it's relative to current directory
        if [[ "$DESTINATION" =~ ^\./.*$ ]] || [[ "$DESTINATION" =~ ^[^/].*$ ]]; then
            local abs_dest=$(cd "$DESTINATION" 2>/dev/null && pwd)
            local abs_current=$(pwd)
            if [[ "$abs_dest" == "$abs_current"* ]]; then
                find_cmd="$find_cmd ! -path \"$DESTINATION/*\""
            fi
        fi
        
        # Add media file extensions
        local extensions=("*.jpg" "*.jpeg" "*.png" "*.tiff" "*.tif" "*.cr2" "*.nef" "*.arw" "*.dng" "*.raw" "*.mp4" "*.mov" "*.avi" "*.mts" "*.m2ts" "*.mkv" "*.wmv" "*.3gp" "*.m4v")
        local ext_pattern=""
        for ext in "${extensions[@]}"; do
            if [[ -n "$ext_pattern" ]]; then
                ext_pattern="$ext_pattern -o"
            fi
            ext_pattern="$ext_pattern -iname $ext"
        done
        
    eval "$find_cmd \\( $ext_pattern \\)" | while IFS= read -r file; do
            echo "$file"
        done
    else
        # Use specified patterns
        for pattern in "${SOURCE_PATTERNS[@]}"; do
            # Handle different pattern types
            if [[ -d "$pattern" ]]; then
                # Directory pattern
                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
            elif [[ "$pattern" == *"*"* ]] || [[ "$pattern" == *"?"* ]]; then
                # Glob pattern
                for file in $pattern; do
                    if [[ -f "$file" ]]; then
                        echo "$file"
                    fi
                done
            else
                # Exact file or directory
                if [[ -f "$pattern" ]]; then
                    echo "$pattern"
                elif [[ -d "$pattern" ]]; then
                    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
                fi
            fi
        done
    fi
}

# Function to process a single file
process_file() {
    local file="$1"
    local file_size=$(get_file_size "$file")
    TOTAL_SIZE=$((TOTAL_SIZE + file_size))
    
    log_message "Processing: $file" "INFO"
    
    # Extract date information
    local date_info=$(extract_file_date "$file")
        local extract_status=$?
        if [[ $extract_status -eq 2 ]]; then
            if [[ $COLLECT_UNSORTABLE -eq 1 ]]; then
                local unsortable_dir="$DESTINATION/unsortable"
                mkdir -p "$unsortable_dir"
                local unsortable_path="$unsortable_dir/$(basename "$file")"
                if [[ $DRY_RUN -eq 1 ]]; then
                    print_color "$BLUE" "Would move unsortable: $file -> $unsortable_path"
                else
                    if mv "$file" "$unsortable_path"; then
                        log_message "Unsortable: $file -> $unsortable_path" "SUCCESS"
                    else
                        log_message "Failed to move unsortable file: $file" "ERROR"
                    fi
                fi
                SKIPPED_FILES=$((SKIPPED_FILES + 1))
            else
                log_message "Could not extract date from $file - skipping" "WARNING"
                SKIPPED_FILES=$((SKIPPED_FILES + 1))
            fi
            return 1
        elif [[ $extract_status -ne 0 ]] || [[ -z "$date_info" ]]; then
            log_message "Could not extract date from $file - skipping" "WARNING"
            SKIPPED_FILES=$((SKIPPED_FILES + 1))
            return 1
        fi
        local date_str="${date_info%|*}"
        local date_source="${date_info#*|}"
        log_message "Date: $date_str (from $date_source)" "INFO"
        # Generate destination path
        local dest_path=$(generate_destination_path "$date_str" "$(basename "$file")" "$DESTINATION")
        if [[ $? -ne 0 ]] || [[ -z "$dest_path" ]]; then
            log_message "Could not generate destination path for $file" "ERROR"
            ERROR_FILES=$((ERROR_FILES + 1))
            FATAL_ERROR=1
            return 2
    fi
    
    # Handle filename conflicts
    local counter=1
    local original_dest_path="$dest_path"
    while [[ -f "$dest_path" ]]; do
        local dir_path=$(dirname "$original_dest_path")
        local filename=$(basename "$original_dest_path")
        local name_without_ext="${filename%.*}"
        local ext="${filename##*.}"
        dest_path="$dir_path/${name_without_ext}_${counter}.${ext}"
        counter=$((counter + 1))
    done
    
    local dest_dir=$(dirname "$dest_path")
    
    if [[ $DRY_RUN -eq 1 ]]; then
        if [[ $KEEP_ORIGINALS -eq 1 ]]; then
            print_color "$BLUE" "Would copy: $file -> $dest_path"
        else
            print_color "$BLUE" "Would move: $file -> $dest_path"
        fi
        PROCESSED_FILES=$((PROCESSED_FILES + 1))
        PROCESSED_SIZE=$((PROCESSED_SIZE + file_size))
        return 0
    fi
    
    # Create destination directory
    if ! mkdir -p "$dest_dir"; then
        log_message "Could not create directory: $dest_dir" "ERROR"
        ERROR_FILES=$((ERROR_FILES + 1))
        return 1
    fi
    
    # Copy or move file
    if [[ $KEEP_ORIGINALS -eq 1 ]]; then
        if cp "$file" "$dest_path"; then
            log_message "Copied: $file -> $dest_path" "SUCCESS"
            PROCESSED_FILES=$((PROCESSED_FILES + 1))
            PROCESSED_SIZE=$((PROCESSED_SIZE + file_size))
            return 0
        else
            log_message "Failed to copy: $file" "ERROR"
            ERROR_FILES=$((ERROR_FILES + 1))
            return 1
        fi
    else
        if mv "$file" "$dest_path"; then
            log_message "Moved: $file -> $dest_path" "SUCCESS"
            PROCESSED_FILES=$((PROCESSED_FILES + 1))
            PROCESSED_SIZE=$((PROCESSED_SIZE + file_size))
            return 0
        else
            log_message "Failed to move: $file" "ERROR"
            ERROR_FILES=$((ERROR_FILES + 1))
            return 1
        fi
    fi
}

# Function to display final report
show_report() {
    local end_time=$(date +%s)
    local elapsed_time=$((end_time - START_TIME))
    local hours=$((elapsed_time / 3600))
    local minutes=$(((elapsed_time % 3600) / 60))
    local seconds=$((elapsed_time % 60))
    
    echo ""
    print_color "$GREEN" "=========================================="
    print_color "$GREEN" "           PROCESSING REPORT"
    print_color "$GREEN" "=========================================="
    echo ""
    
    echo "Files Summary:"
    echo "  Total files found:     $TOTAL_FILES"
    echo "  Successfully processed: $PROCESSED_FILES"
    echo "  Skipped (no date):     $SKIPPED_FILES"
    echo "  Errors:                $ERROR_FILES"
    echo ""
    
    echo "Size Summary:"
    echo "  Total size found:      $(format_size $TOTAL_SIZE)"
    echo "  Successfully processed: $(format_size $PROCESSED_SIZE)"
    echo ""
    
    echo "Time Summary:"
    printf "  Time elapsed:          %02d:%02d:%02d\n" $hours $minutes $seconds
    
    if [[ $elapsed_time -gt 0 && $PROCESSED_FILES -gt 0 ]]; then
        local files_per_second=$((PROCESSED_FILES / elapsed_time))
        local mb_per_second=$((PROCESSED_SIZE / elapsed_time / 1048576))
        echo "  Speed:                 $files_per_second files/sec, ${mb_per_second}MB/sec"
    fi
    
    echo ""
    
    if [[ $DRY_RUN -eq 1 ]]; then
        print_color "$YELLOW" "DRY RUN MODE - No files were actually moved/copied"
    elif [[ $KEEP_ORIGINALS -eq 1 ]]; then
        print_color "$BLUE" "COPY MODE - Original files were preserved"
    else
        print_color "$GREEN" "MOVE MODE - Files were moved to destination"
    fi
    
    echo ""
    print_color "$GREEN" "=========================================="
}

# Parse command line arguments
while [[ $# -gt 0 ]]; do
    case $1 in
        -o|--organization)
            ORGANIZATION="$2"
            if [[ ! "$ORGANIZATION" =~ ^[ymdh]$ ]]; then
                print_color "$RED" "Error: Invalid organization pattern. Must be one of: y, m, d, h"
                exit 1
            fi
            shift 2
            ;;
        --full-date)
            FORCE_FULL_DATE=1
            shift
            ;;
        --collect-unsortable)
            COLLECT_UNSORTABLE=1
            shift
            ;;
        -s|--source)
            SOURCE_PATTERNS+=("$2")
            shift 2
            ;;
        -d|--destination)
            DESTINATION="$2"
            shift 2
            ;;
        -k|--keep-originals)
            KEEP_ORIGINALS=1
            shift
            ;;
        --dry-run)
            DRY_RUN=1
            shift
            ;;
        -v|--verbose)
            VERBOSE=1
            shift
            ;;
        -h|--help)
            show_help
            exit 0
            ;;
        --version)
            show_version
            exit 0
            ;;
        *)
            print_color "$RED" "Error: Unknown option: $1"
            echo "Use -h or --help for usage information."
            exit 1
            ;;
    esac
done

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

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

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

# Display configuration
print_color "$GREEN" "$SCRIPT_NAME v$VERSION"
echo ""
echo "Configuration:"
echo "  Organization pattern: $ORGANIZATION"
echo "  Destination:         $DESTINATION"
echo "  Keep originals:      $([ $KEEP_ORIGINALS -eq 1 ] && echo "Yes" || echo "No")"
echo "  Dry run:             $([ $DRY_RUN -eq 1 ] && echo "Yes" || echo "No")"
echo "  Verbose:             $([ $VERBOSE -eq 1 ] && echo "Yes" || echo "No")"

if [[ ${#SOURCE_PATTERNS[@]} -gt 0 ]]; then
    echo "  Source patterns:"
    for pattern in "${SOURCE_PATTERNS[@]}"; do
        echo "    - $pattern"
    done
else
    echo "  Source patterns:     All media files in current directory"
fi

echo ""

# Check dependencies
check_dependencies

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

# Find all source files

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

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

print_color "$BLUE" "Found $TOTAL_FILES media files to process"
echo ""

# Process each file

FATAL_ERROR=0
for file in "${files[@]}"; do
    if [[ -f "$file" ]]; then
        process_file "$file"
        if [[ $FATAL_ERROR -eq 1 ]]; then
            print_color "$RED" "Fatal error encountered. Stopping further processing."
            break
        fi
    fi
done

# Show final report
show_report

# Exit with appropriate code
if [[ $ERROR_FILES -gt 0 ]]; then
    exit 1
else
    exit 0
fi