#!/bin/bash # organize-images -- Sort images into AI/, Photos/, Screenshots/, Unsorted/ # Default: sub-categorizes AI by model + LoRA combo (uses extract-ai-meta). # Use --flat for one flat AI/ folder. TARGET="" DRY_RUN=true VERBOSE=false INSTALL=false FLAT=false LIMIT=0 usage() { echo "Usage: $(basename "$0") [path] [-e|--execute] [-v|--verbose] [-i|--install] [--flat]" echo " path Directory to organize (default: ~/Pictures)" echo " -e Execute moves (default: dry-run only)" echo " -v Verbose: show per-file classification" echo " -i Install this script to ~/.local/bin/" echo " --flat Don't sub-categorize by model/LoRA (flat AI/ folder)" echo " --limit N Only process N files (for batching)" exit 0 } ARGS=() LIMIT_NEXT=false for arg in "$@"; do if [ "$LIMIT_NEXT" = true ]; then LIMIT="$arg" LIMIT_NEXT=false continue fi case "$arg" in -e|--execute) DRY_RUN=false ;; -v|--verbose) VERBOSE=true ;; -i|--install) INSTALL=true ;; --flat) FLAT=true ;; --limit) LIMIT_NEXT=true ;; --limit=*) LIMIT="${arg#*=}" ;; -h|--help) usage ;; *) ARGS+=("$arg") ;; esac done if [ "$INSTALL" = true ]; then SRC="$(cd "$(dirname "$0")" && pwd -P)/$(basename "$0")" DEST="$HOME/.local/bin/$(basename "$0")" mkdir -p "$HOME/.local/bin" if [ "$SRC" = "$DEST" ]; then echo "Already installed at $DEST" else cp "$SRC" "$DEST" chmod +x "$DEST" echo "Installed to $DEST" fi exit 0 fi TARGET="${ARGS[0]:-$HOME/Pictures}" TARGET="${TARGET%/}" if [ ! -d "$TARGET" ]; then echo "Error: '$TARGET' is not a directory" >&2 exit 1 fi echo "organize-images — Classifying images in: $TARGET" [ "$DRY_RUN" = true ] && echo " DRY RUN (use -e to execute)" || echo " EXECUTING" [ "$LIMIT" -gt 0 ] && echo " Limit: first $LIMIT files" # --- Pre-scan: extract AI metadata for model/LoRA routing --- AI_META="" if [ "$FLAT" = false ]; then META_SCRIPT="$(cd "$(dirname "$0")" && pwd -P)/extract-ai-meta" if [ -x "$META_SCRIPT" ]; then echo " Scanning AI metadata for model/LoRA routing..." AI_META=$(mktemp) if ! "$META_SCRIPT" "$TARGET" > "$AI_META" 2>/dev/null; then echo " Warning: AI metadata scan failed, falling back to flat AI/" FLAT=true rm -f "$AI_META" elif [ ! -s "$AI_META" ]; then FLAT=true rm -f "$AI_META" fi else echo " Warning: extract-ai-meta not found, falling back to flat AI/" FLAT=true fi fi [ "$FLAT" = false ] && echo " AI sub-categorization: by model/LoRA (use --flat to disable)" echo classify() { local f="$1" local name name=$(basename "$f") case "$name" in CleanShot*|Screen_Shot*|Screenshot*) echo "Screenshots"; return ;; esac local comment comment=$(mdls -name kMDItemComment "$f" 2>/dev/null) if echo "$comment" | grep -q '= "Screenshot"'; then echo "Screenshots"; return fi case "$name" in ComfyUI_*|*_DEFI_*|R.*.png|schmeeve-AI-*) echo "AI"; return ;; esac if mdls -name kMDItemAcquisitionMake "$f" 2>/dev/null | grep -q '= "'; then echo "Photos"; return fi local ext="${f##*.}" ext=$(echo "$ext" | tr '[:upper:]' '[:lower:]') case "$ext" in dng|cr2|nef|arw|orf|rw2|raf|3fr) echo "Photos"; return ;; esac if [ "$ext" = "png" ]; then local dims w h area ratio dims=$(sips --getProperty pixelWidth --getProperty pixelHeight "$f" 2>/dev/null | grep -E 'pixel(Width|Height)' | awk '{print $NF}') w=$(echo "$dims" | head -1) h=$(echo "$dims" | tail -1) if [ -n "$w" ] && [ -n "$h" ] && [ "$w" -gt 0 ] && [ "$h" -gt 0 ]; then area=$((w * h)) ratio=$(echo "scale=4; $w / $h" | bc 2>/dev/null || echo 1) if [ "$area" -gt 1048576 ] && [ "$(echo "$ratio > 0.75" | bc -l)" = 1 ] && [ "$(echo "$ratio < 1.33" | bc -l)" = 1 ]; then echo "AI"; return fi fi fi echo "Unsorted" } # --- Pre-scan: collect all image files --- FILES=() for f in "$TARGET"/*; do [ -f "$f" ] || continue ext="${f##*.}"; ext=$(echo "$ext" | tr '[:upper:]' '[:lower:]') case "$ext" in jpg|jpeg|png|webp|gif|bmp|tiff|tif|heic|heif|dng|cr2|nef|arw|orf|rw2|raf|3fr) FILES+=("$f") ;; esac done TOTAL=${#FILES[@]} if [ "$LIMIT" -gt 0 ] && [ "$LIMIT" -lt "$TOTAL" ]; then FILES=("${FILES[@]:0:$LIMIT}") TOTAL=$LIMIT fi if [ "$TOTAL" -eq 0 ]; then echo " No image files found." exit 0 fi mkdir -p "$TARGET/AI" "$TARGET/Photos" "$TARGET/Screenshots" "$TARGET/Unsorted" cAI=0; cPhotos=0; cScreenshots=0; cUnsorted=0 progress_bar() { local current=$1 total=$2 w=40 local pct=0 filled=0 empty=0 [ "$total" -gt 0 ] && pct=$((current * 100 / total)) [ "$total" -gt 0 ] && filled=$((current * w / total)) empty=$((w - filled)) printf "\r [" for ((i=0; i/dev/null) || sub="" if [ -z "$sub" ]; then dest="$TARGET/AI/unknown/" else dest="$TARGET/$sub" fi else dest="$TARGET/$cat/" fi if [ "$VERBOSE" = true ]; then short="${dest#$TARGET/}" printf " (%d/%d) %s → %s\n" "$idx" "$TOTAL" "$(basename "$f")" "$short" else progress_bar "$idx" "$TOTAL" fi if [ "$DRY_RUN" = false ]; then mkdir -p "$dest" mv -f "$f" "$dest" fi case "$cat" in AI) cAI=$((cAI + 1)) ;; Photos) cPhotos=$((cPhotos + 1)) ;; Screenshots) cScreenshots=$((cScreenshots + 1)) ;; Unsorted) cUnsorted=$((cUnsorted + 1)) ;; esac done if [ "$VERBOSE" = false ]; then printf "\r %-60s\n" "Done." fi echo echo " Results:" if [ "$FLAT" = false ] && [ -n "$AI_META" ]; then echo " AI (by model/LoRA): $cAI" else echo " AI: $cAI" fi echo " Photos: $cPhotos" echo " Screenshots: $cScreenshots" echo " Unsorted: $cUnsorted" echo if [ "$DRY_RUN" = true ]; then echo " Dry run complete. Re-run with -e to execute." fi