MediaImporter / RPI / scripts / autonas-media-importer.sh
1 contributor
438 lines | 14.969kb
#!/bin/bash

# AutoNAS Media Importer
# Advanced media import engine that processes, organizes and imports media files from cameras
# Usage: autonas-media-importer.sh <source_mount> <destination_path>

# Global configuration
LOG_TAG="autonas-import"

# Function to log messages
log_message() {
    local message="$1"
    local priority="${2:-info}"  # Default priority is info
    
    # Log to syslog with facility local0 and specified priority
    logger -p "local0.$priority" -t "$LOG_TAG" "$message"
    
    # Also echo to stdout/stderr for interactive use
    if [ -t 1 ]; then
        echo "$(date '+%Y-%m-%d %H:%M:%S') - $message"
    fi
}

# Usage function
usage() {
    echo "Usage: $0 <source_mount> <destination_path> [options]"
    echo ""
    echo "Arguments:"
    echo "  source_mount     - Mount point of the camera (e.g., /mnt/autonas/camera)"
    echo "  destination_path - Destination directory for imported files"
    echo ""
    echo "Options:"
    echo "  --dry-run        - Show what would be done without actually doing it"
    echo "  --keep-originals - Keep original files on camera after import"
    echo "  --verbose        - Enable verbose output"
    echo "  --limit N        - Process only N files (useful for testing)"
    echo "  --help           - Show this help"
    echo ""
    echo "Examples:"
    echo "  $0 /mnt/autonas/camera /mnt/autonas/photos/imported"
    echo "  $0 /mnt/autonas/camera /mnt/autonas/photos/imported --dry-run --verbose"
}

# Parse command line arguments
SOURCE_MOUNT=""
DESTINATION=""
DRY_RUN=0
KEEP_ORIGINALS=0
VERBOSE=0
FILE_LIMIT=0

while [[ $# -gt 0 ]]; do
    case $1 in
        --dry-run)
            DRY_RUN=1
            shift
            ;;
        --keep-originals)
            KEEP_ORIGINALS=1
            shift
            ;;
        --verbose)
            VERBOSE=1
            shift
            ;;
        --limit)
            FILE_LIMIT="$2"
            if ! [[ "$FILE_LIMIT" =~ ^[0-9]+$ ]]; then
                echo "Error: --limit requires a number"
                usage
                exit 1
            fi
            shift 2
            ;;
        --help)
            usage
            exit 0
            ;;
        -*)
            echo "Unknown option: $1"
            usage
            exit 1
            ;;
        *)
            if [[ -z "$SOURCE_MOUNT" ]]; then
                SOURCE_MOUNT="$1"
            elif [[ -z "$DESTINATION" ]]; then
                DESTINATION="$1"
            else
                echo "Too many arguments"
                usage
                exit 1
            fi
            shift
            ;;
    esac
done

# Validate arguments
if [[ -z "$SOURCE_MOUNT" || -z "$DESTINATION" ]]; then
    echo "Error: Both source_mount and destination_path are required"
    usage
    exit 1
fi

# Check if source exists and is mounted
if [[ ! -d "$SOURCE_MOUNT" ]]; then
    log_message "Error: Source mount point does not exist: $SOURCE_MOUNT" "err"
    exit 1
fi

# Check if source is actually mounted
if ! mountpoint -q "$SOURCE_MOUNT" 2>/dev/null; then
    log_message "Warning: Source path is not a mount point: $SOURCE_MOUNT" "warning"
fi

# Create destination directory if it doesn't exist
if [[ $DRY_RUN -eq 0 ]]; then
    mkdir -p "$DESTINATION"
    if [[ ! -d "$DESTINATION" ]]; then
        log_message "Error: Cannot create destination directory: $DESTINATION" "err"
        exit 1
    fi
fi

# Check for required tools
if ! command -v exiftool &> /dev/null; then
    log_message "Error: exiftool is required but not installed" "err"
    exit 1
fi

# Function to process a single file
process_file() {
    local file="$1"
    local relative_path="${file#$SOURCE_MOUNT/}"
    
    # Check if source mount is still available
    if ! mountpoint -q "$SOURCE_MOUNT" 2>/dev/null; then
        log_message "Error: Source mount point is no longer available: $SOURCE_MOUNT" "err"
        return 1
    fi
    
    # Check if file still exists
    if [[ ! -f "$file" ]]; then
        log_message "Error: File no longer exists: $relative_path" "err"
        log_message "Camera appears to be disconnected, stopping import" "warning"
        exit 1
    fi
    
    if [[ $VERBOSE -eq 1 ]]; then
        log_message "Processing: $relative_path" "info"
    fi
    
    # Check which group CreateDate comes from to determine correct handling
    create_date_info=$(exiftool -G1 -s -CreateDate "$file" 2>/dev/null | grep CreateDate | head -1)
    
    # Check if exiftool failed (possible if device disconnected)
    local exiftool_exit_code=$?
    if [[ $exiftool_exit_code -ne 0 ]] && [[ $exiftool_exit_code -ne 1 ]]; then
        log_message "Error: Cannot read file (device may be disconnected): $relative_path" "err"
        log_message "Camera appears to be disconnected, stopping import" "warning"
        exit 1
    fi
    
    if [[ -z "$create_date_info" ]]; then
        log_message "Warning: No CreateDate found in $relative_path, using file modification time" "warning"
        # Fallback to file modification time
        local file_date=$(date -r "$file" "+%Y-%m-%d %H:%M:%S" 2>/dev/null)
        if [[ -n "$file_date" ]]; then
            create_date_value="$file_date"
            create_date_group="FileSystem"
        else
            log_message "Error: Cannot determine date for $relative_path" "err"
            return 1
        fi
    else
        create_date_group=$(echo "$create_date_info" | cut -d']' -f1 | cut -d'[' -f2)
        create_date_value=$(echo "$create_date_info" | cut -d':' -f2- | xargs)
        # Convert EXIF date format (YYYY:MM:DD HH:MM:SS) to standard format (YYYY-MM-DD HH:MM:SS)
        create_date_value=$(echo "$create_date_value" | sed 's/^\([0-9]\{4\}\):\([0-9]\{2\}\):\([0-9]\{2\}\)/\1-\2-\3/')
    fi
    
    if [[ $VERBOSE -eq 1 ]]; then
        echo -n "  Date: [$create_date_value] from $create_date_group "
    fi
    
    # Extract file extension
    local filename=$(basename "$file")
    local extension="${filename##*.}"
    
    # For QuickTime files, the CreateDate is in UTC and needs conversion to local time
    if [[ "$create_date_group" == "QuickTime" ]]; then
        # Convert UTC time to local time
        local utc_timestamp=$(date -d "$create_date_value UTC" "+%s" 2>/dev/null)
        if [[ -n "$utc_timestamp" ]]; then
            create_date_value=$(date -d "@$utc_timestamp" "+%Y-%m-%d %H:%M:%S" 2>/dev/null)
            if [[ $VERBOSE -eq 1 ]]; then
                echo -n "(converted from UTC) "
            fi
        fi
    fi
    
    # Create output directory structure
    local date_dir=$(date -d "$create_date_value" "+%Y-%m-%d" 2>/dev/null)
    if [[ -z "$date_dir" ]]; then
        log_message "Error: Invalid date format for $relative_path: $create_date_value" "err"
        return 1
    fi
    
    local output_dir="$DESTINATION/$date_dir"
    
    if [[ $DRY_RUN -eq 0 ]]; then
        mkdir -p "$output_dir"
    fi
    
    # Generate output filename with timestamp
    local timestamp=$(date -d "$create_date_value" "+%Y-%m-%d_%H-%M-%S" 2>/dev/null)
    local output_filename="${timestamp}.${extension,,}"  # Convert extension to lowercase
    local output_path="$output_dir/$output_filename"
    
    # Handle filename conflicts
    local counter=1
    local base_output_path="$output_path"
    while [[ -f "$output_path" ]] && [[ $DRY_RUN -eq 0 ]]; do
        local name_without_ext="${timestamp}_${counter}"
        output_path="$output_dir/${name_without_ext}.${extension,,}"
        counter=$((counter + 1))
    done
    
    if [[ $DRY_RUN -eq 1 ]]; then
        if [[ $KEEP_ORIGINALS -eq 1 ]]; then
            echo "Would copy: $relative_path -> ${output_path#$DESTINATION/}"
        else
            echo "Would move: $relative_path -> ${output_path#$DESTINATION/}"
        fi
    else
        # Perform the actual file operation
        if [[ $KEEP_ORIGINALS -eq 1 ]]; then
            if cp "$file" "$output_path"; then
                if [[ $VERBOSE -eq 1 ]]; then
                    echo "✓ Copied"
                fi
                log_message "Copied: $relative_path -> ${output_path#$DESTINATION/}" "info"
                return 0
            else
                if [[ $VERBOSE -eq 1 ]]; then
                    echo "✗ Copy failed"
                fi
                log_message "Error: Failed to copy $relative_path" "err"
                return 1
            fi
        else
            if mv "$file" "$output_path"; then
                if [[ $VERBOSE -eq 1 ]]; then
                    echo "✓ Moved"
                fi
                log_message "Moved: $relative_path -> ${output_path#$DESTINATION/}" "info"
                return 0
            else
                if [[ $VERBOSE -eq 1 ]]; then
                    echo "✗ Move failed"
                fi
                log_message "Error: Failed to move $relative_path" "err"
                return 1
            fi
        fi
    fi
}

# Function to find camera directories
find_camera_directories() {
    local search_patterns=("DCIM" "PRIVATE" "MP_ROOT" "AVCHD" "Photos" "Videos")
    local found_dirs=()
    
    # Test if the mount point is accessible with a timeout
    if ! timeout 3 ls "$SOURCE_MOUNT" >/dev/null 2>&1; then
        log_message "Error: Mount point is not accessible (device likely disconnected): $SOURCE_MOUNT" "err"
        exit 1
    fi
    
    for pattern in "${search_patterns[@]}"; do
        while IFS= read -r -d '' dir; do
            found_dirs+=("$dir")
        done < <(timeout 5 find "$SOURCE_MOUNT" -maxdepth 3 -type d -iname "$pattern" -print0 2>/dev/null)
    done
    
    # If no camera directories found, search for common media file extensions
    if [[ ${#found_dirs[@]} -eq 0 ]]; then
        log_message "No camera directories found, searching for media files..." "info"
        local media_extensions=("*.jpg" "*.jpeg" "*.png" "*.tiff" "*.cr2" "*.nef" "*.arw" "*.mp4" "*.mov" "*.avi" "*.mts" "*.m2ts")
        
        for ext in "${media_extensions[@]}"; do
            while IFS= read -r -d '' file; do
                local dir=$(dirname "$file")
                if [[ ! " ${found_dirs[@]} " =~ " ${dir} " ]]; then
                    found_dirs+=("$dir")
                fi
            done < <(timeout 5 find "$SOURCE_MOUNT" -maxdepth 3 -type f -iname "$ext" -print0 2>/dev/null)
        done
    fi
    
    printf '%s\n' "${found_dirs[@]}" | sort -u
}

# Main execution
log_message "Starting camera import from $SOURCE_MOUNT to $DESTINATION" "info"

if [[ $DRY_RUN -eq 1 ]]; then
    log_message "DRY RUN MODE - No files will be actually moved/copied" "info"
fi

if [[ $KEEP_ORIGINALS -eq 1 ]]; then
    log_message "KEEP ORIGINALS MODE - Files will be copied instead of moved" "info"
fi

# Find camera directories
log_message "Scanning for camera directories..." "info"
camera_dirs=$(find_camera_directories)

if [[ -z "$camera_dirs" ]]; then
    log_message "No camera directories or media files found in $SOURCE_MOUNT" "warning"
    exit 0
fi

echo "Found camera directories:"
echo "$camera_dirs" | while IFS= read -r dir; do
    echo "  $dir"
done

# Process files
total_files=0
processed_files=0
error_files=0

# Delete GLV files (Garmin video preview files) - they're usually not needed
log_message "Cleaning up GLV preview files..." "info"
glv_count=0
while IFS= read -r dir; do
    if [[ $DRY_RUN -eq 1 ]]; then
        glv_files=$(find "$dir" -type f -iname "*.glv" 2>/dev/null | wc -l)
        if [[ $glv_files -gt 0 ]]; then
            echo "Would delete $glv_files GLV files from $dir"
            glv_count=$((glv_count + glv_files))
        fi
    else
        while IFS= read -r -d '' glv_file; do
            if rm "$glv_file" 2>/dev/null; then
                glv_count=$((glv_count + 1))
            fi
        done < <(find "$dir" -type f -iname "*.glv" -print0 2>/dev/null)
    fi
done <<< "$camera_dirs"

if [[ $glv_count -gt 0 ]]; then
    if [[ $DRY_RUN -eq 1 ]]; then
        log_message "Would delete $glv_count GLV preview files" "info"
    else
        log_message "Deleted $glv_count GLV preview files" "info"
    fi
fi

# Process media files
log_message "Processing media files..." "info"
media_extensions=("*.jpg" "*.jpeg" "*.png" "*.tiff" "*.tif" "*.cr2" "*.nef" "*.arw" "*.dng" "*.mp4" "*.mov" "*.avi" "*.mts" "*.m2ts" "*.mkv" "*.wmv")

# In dry-run mode, limit output to avoid overwhelming logs
max_files_to_show=20
files_shown=0
files_processed_count=0

while IFS= read -r dir; do
    # Check if mount point is still available before processing each directory
    if ! mountpoint -q "$SOURCE_MOUNT" 2>/dev/null; then
        log_message "Camera disconnected, stopping import process" "warning"
        break
    fi
    
    for ext in "${media_extensions[@]}"; do
        while IFS= read -r -d '' file; do
            total_files=$((total_files + 1))
            files_processed_count=$((files_processed_count + 1))
            
            # Check if camera is still connected before processing each file
            if ! mountpoint -q "$SOURCE_MOUNT" 2>/dev/null; then
                log_message "Camera disconnected during processing, stopping import" "warning"
                exit 1
            fi
            
            # Check file limit
            if [[ $FILE_LIMIT -gt 0 && $files_processed_count -gt $FILE_LIMIT ]]; then
                echo "Reached file limit of $FILE_LIMIT files, stopping processing..."
                break 3
            fi
            
            # In dry-run mode, limit verbose output
            if [[ $DRY_RUN -eq 1 && $files_shown -ge $max_files_to_show ]]; then
                if [[ $files_shown -eq $max_files_to_show ]]; then
                    echo "... (limiting output in dry-run mode, processing continues)"
                    files_shown=$((files_shown + 1))
                fi
                # Still process but don't show details
                processed_files=$((processed_files + 1))
            else
                if [[ $DRY_RUN -eq 1 ]]; then
                    files_shown=$((files_shown + 1))
                fi
                
                if process_file "$file"; then
                    processed_files=$((processed_files + 1))
                else
                    error_files=$((error_files + 1))
                fi
            fi
        done < <(find "$dir" -type f -iname "$ext" -print0 2>/dev/null)
    done
done <<< "$camera_dirs"

# Summary
log_message "Import completed: $processed_files/$total_files files processed successfully" "info"
if [[ $error_files -gt 0 ]]; then
    log_message "Import had errors: $error_files files failed to process" "warning"
fi

echo ""
echo "=== Import Summary ==="
echo "Total files found: $total_files"
echo "Successfully processed: $processed_files"
echo "Errors: $error_files"
if [[ $glv_count -gt 0 ]]; then
    echo "GLV files cleaned up: $glv_count"
fi

# Exit with error code if there were errors
if [[ $error_files -gt 0 ]]; then
    exit 1
else
    exit 0
fi