1 contributor
#!/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