diff --git a/scripts/shutterlinksort-usr-bin b/scripts/shutterlinksort-usr-bin new file mode 100644 index 0000000..cf00334 --- /dev/null +++ b/scripts/shutterlinksort-usr-bin @@ -0,0 +1,296 @@ +#!/bin/bash +# ====================================================================== +# shutterlinksort +# ---------------- +# Camera image downloader and organizer for PTP/IP-connected devices. +# +# Connects to a camera over network (using gphoto2), downloads new +# photos, extracts EXIF metadata (date, shutter count), and sorts +# images into folders by date. +# +# Usage: +# shutterlinksort +# Dependencies: +# gphoto2, exiftool, bash +# +# Author: James Blackmore (reclusejay) +# Version: 1.0 +# ====================================================================== + +# --- Safety options --- +# -e : Exit immediately if a command exits with non-zero status. +# -u : Treat unset variables as an error and exit. +# -o pipefail : Fail a pipeline if *any* command in it fails. +set -euo pipefail + +# ============================================================ +# Cleanup handler — runs on interrupt/termination +# ============================================================ +cleanup() { + # Disable any special globbing options we enabled + shopt -u nullglob nocaseglob || true + + # Delete any 0-byte (incomplete) downloads if present + find "$cam_dl_dir" -type f -size 0 -delete 2>/dev/null || true + + log "Caught interrupt, exiting" + exit 0 +} +# Trap cleanup on Ctrl+C, kill, or quit signals +trap cleanup SIGINT SIGTERM SIGQUIT + +# ============================================================ +# Function: php_get +# Pulls a config value from the PHP $CONFIG array +# ============================================================ +php_get() { + if ! command -v php >/dev/null 2>&1; then + # fallback to php-cli + if command -v php-cli >/dev/null 2>&1; then + php-cli -r "include('../src/includes/config.php'); print \$CONFIG['$1'];" + else + echo "Error: Neither 'php' nor 'php-cli' is available." + exit 1 + fi + else + php -r "include('../src/includes/config.php'); print \$CONFIG['$1'];" + fi + return 0 +} + +# ============================================================ +# Load all settings from PHP config +# ============================================================ +cam_uuid=$(php_get "camera_uuid") +cam_dl_dir=$(php_get "upload_dir") +cam_sort_dir=$(php_get "sorted_img_dir") +cam_index_file=$(php_get "index_file") +cam_xml_url=$(php_get "camera_xml_url") +cam_ext_format=$(php_get "camera_ext_formats") +cam_mac=$(php_get "camera_mac") +g2_port=$(php_get "gphoto2_config_port") +g2_model=$(php_get "gphoto2_config_model") +g2_guid=$(php_get "gphoto2_config_guid") +g2_conf=$(php_get "gphoto2_config_file") + +# Create the index file if missing +[ -f "$cam_index_file" ] || echo 0 > "$cam_index_file" + +# ============================================================ +# Global variables +# ============================================================ +debug=true +ver="v.25.10.1" +STATUS="INITIALIZING" +CAM_IP="" + +# ============================================================ +# Function: log +# Provides timestamped console and syslog logging +# ============================================================ +log() { + local msg lev full_msg + if $debug; then + msg="${1:-NO MESSAGE!}" + lev="${2:-info}" + + full_msg="[debug $lev] Version: $ver - $(date +[%Y/%m/%d-%H:%M:%S]) - $msg" + + # Always print to stdout for debugging + echo "$full_msg" + + # Try syslog first; fallback to a file log if logger fails + logger -t camera-download-sort "$full_msg" 2>/dev/null || \ + echo "$full_msg" >> /var/log/camera-download-sort.log + fi +} + +# ============================================================ +# Function: find_ip_by_mac +# Locates the IP address of a device by its MAC address +# ============================================================ +find_ip_by_mac() { + local mac ip file + + # Normalize MAC to lowercase for comparison + mac=$(echo "$1" | tr '[:upper:]' '[:lower:]') + [ -z "$mac" ] && log "No MAC provided" && return 1 + + # 1️⃣ Primary source: /proc/net/arp (most reliable) + ip=$(awk -v mac="$mac" 'NR>1 && tolower($4)==mac {print $1; exit}' /proc/net/arp) + + # 2️⃣ Fallback: BusyBox arp command output + if [ -z "$ip" ] && command -v arp >/dev/null 2>&1; then + ip=$(arp | awk -v mac="$mac" 'NR>1 && tolower($4)==mac {print $1; exit}') + fi + + # 3️⃣ Fallback: dnsmasq lease files + if [ -z "$ip" ]; then + for file in /tmp/dhcp.leases /var/lib/misc/dnsmasq.leases; do + [ -f "$file" ] || continue + ip=$(awk -v mac="$mac" 'tolower($2)==mac {print $3; exit}' "$file") + [ -n "$ip" ] && break + done + fi + + # If found, set global CAM_IP variable + if [ -n "$ip" ]; then + CAM_IP="$ip" + log "Camera found at $CAM_IP" + return 0 + fi + + # Not found + return 1 +} + +# ============================================================ +# Function: is_camera_connected +# Checks whether the camera’s MAC address is visible +# ============================================================ +is_camera_connected() { + if find_ip_by_mac "$cam_mac"; then + if [ "$STATUS" = "WAITING" ]; then + # Already in waiting state — skip + return 1 + fi + STATUS="CONNECTED" + return 0 + fi + STATUS="DISCONNECTED" + return 1 +} + +# ============================================================ +# Function: download_images +# Uses gphoto2 (PTP/IP) to pull new images from camera +# ============================================================ +download_images() { + local wd="$PWD" + STATUS="WAITING" + cd "$cam_dl_dir" || return 1 + + # Create gphoto2 config file if it doesn’t exist + if [ ! -f "$g2_conf" ]; then + { + echo "$g2_port$CAM_IP" + echo "$g2_model" + echo "$g2_guid$cam_uuid" + } > "$g2_conf" + fi + + # Check for gphoto2 availability + if command -v gphoto2 >/dev/null 2>&1; then + # Download all new files, skipping ones that already exist + if gphoto2 --port "ptpip:$CAM_IP" -P --skip-existing; then + log "Downloaded new images from $CAM_IP" + cd "$wd" || true + return 0 + fi + log "gphoto2 download failed" error + else + log "gphoto2 not found — skipping download" warn + fi + + cd "$wd" || true + return 1 +} + +# ============================================================ +# Function: sort_images +# Sorts downloaded images into folders by EXIF date +# ============================================================ +sort_images() { + local ext img datetime shuttercount yyyy mm dd timepart hhmmss ext_lower \ + output_dir new_filename new_file uid index + + STATUS="SORTING" + log "$STATUS" + + # Ensure exiftool exists + command -v exiftool >/dev/null 2>&1 || { + log "exiftool missing — cannot sort" error + return 1 + } + + # Get current image index number + index=$(cat "$cam_index_file") + + # Loop over each configured image extension (e.g. JPG ARW) + for ext in $cam_ext_format; do + # Enable flexible globbing (case-insensitive, no literal fail) + shopt -s nullglob nocaseglob + + # Iterate through each matching file + for img in "$cam_dl_dir"/*."$ext"; do + + # Pull EXIF date and shutter count + readarray -t exif_data < <(exiftool -s -s -s -DateTimeOriginal -ShutterCount "$img") + datetime="${exif_data[0]:-0000:00:00 00:00:00}" + shuttercount="${exif_data[1]:-}" + + log "Processing $img (index $index)" + + # Generate a unique ID if no shuttercount found + if [ -z "$shuttercount" ]; then + uid="rn${RANDOM}idx${index}" + log "No shuttercount in $img, uid=$uid" + else + uid="sc${shuttercount}idx${index}" + log "Shuttercount=$shuttercount uid=$uid" + fi + + # Parse EXIF date into components + yyyy=$(echo "$datetime" | cut -d':' -f1) + mm=$(echo "$datetime" | cut -d':' -f2) + dd=$(echo "$datetime" | cut -d':' -f3 | cut -d' ' -f1) + timepart=$(echo "$datetime" | cut -d' ' -f2) + hhmmss=$(echo "$timepart" | tr -d ':') + + # Normalize extension to lowercase + ext_lower=${ext,,} + + # Build destination folder structure + output_dir="$cam_sort_dir/$ext_lower/$yyyy/$mm/$dd" + mkdir -p "$output_dir" || { log "mkdir failed: $output_dir" error; return 1; } + + # Construct new filename + new_filename="${hhmmss}_${uid}.${ext_lower}" + new_file="$output_dir/$new_filename" + + log "Moving $img -> $new_file" + + # Move and increment index + if mv "$img" "$new_file"; then + index=$((index + 1)) + else + log "Failed to move $img" error + fi + done + + # Update index file after processing each extension type + printf '%s\n' "$index" > "$cam_index_file" + done + + STATUS="DONE" + log "$STATUS" + return 0 +} + +# ============================================================ +# Main loop — runs forever +# ============================================================ +log "Camera script started" + +while :; do + # If camera detected and not already downloading/sorting + if is_camera_connected && [ "$STATUS" != "WAITING" ] && [ "$STATUS" != "SORTING" ]; then + download_images || log "Download step failed" error + # If camera disconnected, begin sorting + elif [ "$STATUS" = "DISCONNECTED" ]; then + sort_images || log "Sort step failed" error + fi + + # Avoid hammering system; check every 2 seconds + sleep 2 +done