#!/bin/bash # AutoNAS Media Importer # Advanced media import engine that processes, organizes and imports media files from cameras # Usage: autonas-media-importer.sh # 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 [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