autoNAS / scripts / autonas-media-importer.sh
Newer Older
5b5a565 3 months ago History
438 lines | 14.969kb
Bogdan Timofte authored 3 months ago
1
#!/bin/bash
2

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

            
7
# Global configuration
8
LOG_TAG="autonas-import"
9

            
10
# Function to log messages
11
log_message() {
12
    local message="$1"
13
    local priority="${2:-info}"  # Default priority is info
14

            
15
    # Log to syslog with facility local0 and specified priority
16
    logger -p "local0.$priority" -t "$LOG_TAG" "$message"
17

            
18
    # Also echo to stdout/stderr for interactive use
19
    if [ -t 1 ]; then
20
        echo "$(date '+%Y-%m-%d %H:%M:%S') - $message"
21
    fi
22
}
23

            
24
# Usage function
25
usage() {
26
    echo "Usage: $0 <source_mount> <destination_path> [options]"
27
    echo ""
28
    echo "Arguments:"
29
    echo "  source_mount     - Mount point of the camera (e.g., /mnt/autonas/camera)"
30
    echo "  destination_path - Destination directory for imported files"
31
    echo ""
32
    echo "Options:"
33
    echo "  --dry-run        - Show what would be done without actually doing it"
34
    echo "  --keep-originals - Keep original files on camera after import"
35
    echo "  --verbose        - Enable verbose output"
36
    echo "  --limit N        - Process only N files (useful for testing)"
37
    echo "  --help           - Show this help"
38
    echo ""
39
    echo "Examples:"
40
    echo "  $0 /mnt/autonas/camera /mnt/autonas/photos/imported"
41
    echo "  $0 /mnt/autonas/camera /mnt/autonas/photos/imported --dry-run --verbose"
42
}
43

            
44
# Parse command line arguments
45
SOURCE_MOUNT=""
46
DESTINATION=""
47
DRY_RUN=0
48
KEEP_ORIGINALS=0
49
VERBOSE=0
50
FILE_LIMIT=0
51

            
52
while [[ $# -gt 0 ]]; do
53
    case $1 in
54
        --dry-run)
55
            DRY_RUN=1
56
            shift
57
            ;;
58
        --keep-originals)
59
            KEEP_ORIGINALS=1
60
            shift
61
            ;;
62
        --verbose)
63
            VERBOSE=1
64
            shift
65
            ;;
66
        --limit)
67
            FILE_LIMIT="$2"
68
            if ! [[ "$FILE_LIMIT" =~ ^[0-9]+$ ]]; then
69
                echo "Error: --limit requires a number"
70
                usage
71
                exit 1
72
            fi
73
            shift 2
74
            ;;
75
        --help)
76
            usage
77
            exit 0
78
            ;;
79
        -*)
80
            echo "Unknown option: $1"
81
            usage
82
            exit 1
83
            ;;
84
        *)
85
            if [[ -z "$SOURCE_MOUNT" ]]; then
86
                SOURCE_MOUNT="$1"
87
            elif [[ -z "$DESTINATION" ]]; then
88
                DESTINATION="$1"
89
            else
90
                echo "Too many arguments"
91
                usage
92
                exit 1
93
            fi
94
            shift
95
            ;;
96
    esac
97
done
98

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

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

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

            
117
# Create destination directory if it doesn't exist
118
if [[ $DRY_RUN -eq 0 ]]; then
119
    mkdir -p "$DESTINATION"
120
    if [[ ! -d "$DESTINATION" ]]; then
121
        log_message "Error: Cannot create destination directory: $DESTINATION" "err"
122
        exit 1
123
    fi
124
fi
125

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

            
132
# Function to process a single file
133
process_file() {
134
    local file="$1"
135
    local relative_path="${file#$SOURCE_MOUNT/}"
136

            
137
    # Check if source mount is still available
138
    if ! mountpoint -q "$SOURCE_MOUNT" 2>/dev/null; then
139
        log_message "Error: Source mount point is no longer available: $SOURCE_MOUNT" "err"
140
        return 1
141
    fi
142

            
143
    # Check if file still exists
144
    if [[ ! -f "$file" ]]; then
145
        log_message "Error: File no longer exists: $relative_path" "err"
146
        log_message "Camera appears to be disconnected, stopping import" "warning"
147
        exit 1
148
    fi
149

            
150
    if [[ $VERBOSE -eq 1 ]]; then
151
        log_message "Processing: $relative_path" "info"
152
    fi
153

            
154
    # Check which group CreateDate comes from to determine correct handling
155
    create_date_info=$(exiftool -G1 -s -CreateDate "$file" 2>/dev/null | grep CreateDate | head -1)
156

            
157
    # Check if exiftool failed (possible if device disconnected)
158
    local exiftool_exit_code=$?
159
    if [[ $exiftool_exit_code -ne 0 ]] && [[ $exiftool_exit_code -ne 1 ]]; then
160
        log_message "Error: Cannot read file (device may be disconnected): $relative_path" "err"
161
        log_message "Camera appears to be disconnected, stopping import" "warning"
162
        exit 1
163
    fi
164

            
165
    if [[ -z "$create_date_info" ]]; then
166
        log_message "Warning: No CreateDate found in $relative_path, using file modification time" "warning"
167
        # Fallback to file modification time
168
        local file_date=$(date -r "$file" "+%Y-%m-%d %H:%M:%S" 2>/dev/null)
169
        if [[ -n "$file_date" ]]; then
170
            create_date_value="$file_date"
171
            create_date_group="FileSystem"
172
        else
173
            log_message "Error: Cannot determine date for $relative_path" "err"
174
            return 1
175
        fi
176
    else
177
        create_date_group=$(echo "$create_date_info" | cut -d']' -f1 | cut -d'[' -f2)
178
        create_date_value=$(echo "$create_date_info" | cut -d':' -f2- | xargs)
179
        # Convert EXIF date format (YYYY:MM:DD HH:MM:SS) to standard format (YYYY-MM-DD HH:MM:SS)
180
        create_date_value=$(echo "$create_date_value" | sed 's/^\([0-9]\{4\}\):\([0-9]\{2\}\):\([0-9]\{2\}\)/\1-\2-\3/')
181
    fi
182

            
183
    if [[ $VERBOSE -eq 1 ]]; then
184
        echo -n "  Date: [$create_date_value] from $create_date_group "
185
    fi
186

            
187
    # Extract file extension
188
    local filename=$(basename "$file")
189
    local extension="${filename##*.}"
190

            
191
    # For QuickTime files, the CreateDate is in UTC and needs conversion to local time
192
    if [[ "$create_date_group" == "QuickTime" ]]; then
193
        # Convert UTC time to local time
194
        local utc_timestamp=$(date -d "$create_date_value UTC" "+%s" 2>/dev/null)
195
        if [[ -n "$utc_timestamp" ]]; then
196
            create_date_value=$(date -d "@$utc_timestamp" "+%Y-%m-%d %H:%M:%S" 2>/dev/null)
197
            if [[ $VERBOSE -eq 1 ]]; then
198
                echo -n "(converted from UTC) "
199
            fi
200
        fi
201
    fi
202

            
203
    # Create output directory structure
204
    local date_dir=$(date -d "$create_date_value" "+%Y-%m-%d" 2>/dev/null)
205
    if [[ -z "$date_dir" ]]; then
206
        log_message "Error: Invalid date format for $relative_path: $create_date_value" "err"
207
        return 1
208
    fi
209

            
210
    local output_dir="$DESTINATION/$date_dir"
211

            
212
    if [[ $DRY_RUN -eq 0 ]]; then
213
        mkdir -p "$output_dir"
214
    fi
215

            
216
    # Generate output filename with timestamp
217
    local timestamp=$(date -d "$create_date_value" "+%Y-%m-%d_%H-%M-%S" 2>/dev/null)
218
    local output_filename="${timestamp}.${extension,,}"  # Convert extension to lowercase
219
    local output_path="$output_dir/$output_filename"
220

            
221
    # Handle filename conflicts
222
    local counter=1
223
    local base_output_path="$output_path"
224
    while [[ -f "$output_path" ]] && [[ $DRY_RUN -eq 0 ]]; do
225
        local name_without_ext="${timestamp}_${counter}"
226
        output_path="$output_dir/${name_without_ext}.${extension,,}"
227
        counter=$((counter + 1))
228
    done
229

            
230
    if [[ $DRY_RUN -eq 1 ]]; then
231
        if [[ $KEEP_ORIGINALS -eq 1 ]]; then
232
            echo "Would copy: $relative_path -> ${output_path#$DESTINATION/}"
233
        else
234
            echo "Would move: $relative_path -> ${output_path#$DESTINATION/}"
235
        fi
236
    else
237
        # Perform the actual file operation
238
        if [[ $KEEP_ORIGINALS -eq 1 ]]; then
239
            if cp "$file" "$output_path"; then
240
                if [[ $VERBOSE -eq 1 ]]; then
241
                    echo "✓ Copied"
242
                fi
243
                log_message "Copied: $relative_path -> ${output_path#$DESTINATION/}" "info"
244
                return 0
245
            else
246
                if [[ $VERBOSE -eq 1 ]]; then
247
                    echo "✗ Copy failed"
248
                fi
249
                log_message "Error: Failed to copy $relative_path" "err"
250
                return 1
251
            fi
252
        else
253
            if mv "$file" "$output_path"; then
254
                if [[ $VERBOSE -eq 1 ]]; then
255
                    echo "✓ Moved"
256
                fi
257
                log_message "Moved: $relative_path -> ${output_path#$DESTINATION/}" "info"
258
                return 0
259
            else
260
                if [[ $VERBOSE -eq 1 ]]; then
261
                    echo "✗ Move failed"
262
                fi
263
                log_message "Error: Failed to move $relative_path" "err"
264
                return 1
265
            fi
266
        fi
267
    fi
268
}
269

            
270
# Function to find camera directories
271
find_camera_directories() {
272
    local search_patterns=("DCIM" "PRIVATE" "MP_ROOT" "AVCHD" "Photos" "Videos")
273
    local found_dirs=()
274

            
275
    # Test if the mount point is accessible with a timeout
276
    if ! timeout 3 ls "$SOURCE_MOUNT" >/dev/null 2>&1; then
277
        log_message "Error: Mount point is not accessible (device likely disconnected): $SOURCE_MOUNT" "err"
278
        exit 1
279
    fi
280

            
281
    for pattern in "${search_patterns[@]}"; do
282
        while IFS= read -r -d '' dir; do
283
            found_dirs+=("$dir")
284
        done < <(timeout 5 find "$SOURCE_MOUNT" -maxdepth 3 -type d -iname "$pattern" -print0 2>/dev/null)
285
    done
286

            
287
    # If no camera directories found, search for common media file extensions
288
    if [[ ${#found_dirs[@]} -eq 0 ]]; then
289
        log_message "No camera directories found, searching for media files..." "info"
290
        local media_extensions=("*.jpg" "*.jpeg" "*.png" "*.tiff" "*.cr2" "*.nef" "*.arw" "*.mp4" "*.mov" "*.avi" "*.mts" "*.m2ts")
291

            
292
        for ext in "${media_extensions[@]}"; do
293
            while IFS= read -r -d '' file; do
294
                local dir=$(dirname "$file")
295
                if [[ ! " ${found_dirs[@]} " =~ " ${dir} " ]]; then
296
                    found_dirs+=("$dir")
297
                fi
298
            done < <(timeout 5 find "$SOURCE_MOUNT" -maxdepth 3 -type f -iname "$ext" -print0 2>/dev/null)
299
        done
300
    fi
301

            
302
    printf '%s\n' "${found_dirs[@]}" | sort -u
303
}
304

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

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

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

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

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

            
325
echo "Found camera directories:"
326
echo "$camera_dirs" | while IFS= read -r dir; do
327
    echo "  $dir"
328
done
329

            
330
# Process files
331
total_files=0
332
processed_files=0
333
error_files=0
334

            
335
# Delete GLV files (Garmin video preview files) - they're usually not needed
336
log_message "Cleaning up GLV preview files..." "info"
337
glv_count=0
338
while IFS= read -r dir; do
339
    if [[ $DRY_RUN -eq 1 ]]; then
340
        glv_files=$(find "$dir" -type f -iname "*.glv" 2>/dev/null | wc -l)
341
        if [[ $glv_files -gt 0 ]]; then
342
            echo "Would delete $glv_files GLV files from $dir"
343
            glv_count=$((glv_count + glv_files))
344
        fi
345
    else
346
        while IFS= read -r -d '' glv_file; do
347
            if rm "$glv_file" 2>/dev/null; then
348
                glv_count=$((glv_count + 1))
349
            fi
350
        done < <(find "$dir" -type f -iname "*.glv" -print0 2>/dev/null)
351
    fi
352
done <<< "$camera_dirs"
353

            
354
if [[ $glv_count -gt 0 ]]; then
355
    if [[ $DRY_RUN -eq 1 ]]; then
356
        log_message "Would delete $glv_count GLV preview files" "info"
357
    else
358
        log_message "Deleted $glv_count GLV preview files" "info"
359
    fi
360
fi
361

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

            
366
# In dry-run mode, limit output to avoid overwhelming logs
367
max_files_to_show=20
368
files_shown=0
369
files_processed_count=0
370

            
371
while IFS= read -r dir; do
372
    # Check if mount point is still available before processing each directory
373
    if ! mountpoint -q "$SOURCE_MOUNT" 2>/dev/null; then
374
        log_message "Camera disconnected, stopping import process" "warning"
375
        break
376
    fi
377

            
378
    for ext in "${media_extensions[@]}"; do
379
        while IFS= read -r -d '' file; do
380
            total_files=$((total_files + 1))
381
            files_processed_count=$((files_processed_count + 1))
382

            
383
            # Check if camera is still connected before processing each file
384
            if ! mountpoint -q "$SOURCE_MOUNT" 2>/dev/null; then
385
                log_message "Camera disconnected during processing, stopping import" "warning"
386
                exit 1
387
            fi
388

            
389
            # Check file limit
390
            if [[ $FILE_LIMIT -gt 0 && $files_processed_count -gt $FILE_LIMIT ]]; then
391
                echo "Reached file limit of $FILE_LIMIT files, stopping processing..."
392
                break 3
393
            fi
394

            
395
            # In dry-run mode, limit verbose output
396
            if [[ $DRY_RUN -eq 1 && $files_shown -ge $max_files_to_show ]]; then
397
                if [[ $files_shown -eq $max_files_to_show ]]; then
398
                    echo "... (limiting output in dry-run mode, processing continues)"
399
                    files_shown=$((files_shown + 1))
400
                fi
401
                # Still process but don't show details
402
                processed_files=$((processed_files + 1))
403
            else
404
                if [[ $DRY_RUN -eq 1 ]]; then
405
                    files_shown=$((files_shown + 1))
406
                fi
407

            
408
                if process_file "$file"; then
409
                    processed_files=$((processed_files + 1))
410
                else
411
                    error_files=$((error_files + 1))
412
                fi
413
            fi
414
        done < <(find "$dir" -type f -iname "$ext" -print0 2>/dev/null)
415
    done
416
done <<< "$camera_dirs"
417

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

            
424
echo ""
425
echo "=== Import Summary ==="
426
echo "Total files found: $total_files"
427
echo "Successfully processed: $processed_files"
428
echo "Errors: $error_files"
429
if [[ $glv_count -gt 0 ]]; then
430
    echo "GLV files cleaned up: $glv_count"
431
fi
432

            
433
# Exit with error code if there were errors
434
if [[ $error_files -gt 0 ]]; then
435
    exit 1
436
else
437
    exit 0
438
fi