297 lines
9.5 KiB
Bash
297 lines
9.5 KiB
Bash
#!/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
|