Created
June 30, 2025 08:26
-
-
Save melalj/e97b8ddc53e939094e32e0a5d6cc58c4 to your computer and use it in GitHub Desktop.
πΈ Bulk Photo Renamer: EXIF datetime β YYYY-MM-DD_HH-MM-SS with dry-run mode
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# Photo Rename Script - Renames photos based on EXIF datetime | |
# Usage: ./rename_photos.sh [directory] [--dry-run] | |
# | |
# Requirements: exiftool (install with: sudo apt install libimage-exiftool-perl) | |
set -uo pipefail | |
# Default values | |
TARGET_DIR="${1:-.}" | |
DRY_RUN=false | |
COUNTER=1 | |
# Check for dry-run flag | |
if [[ "${2:-}" == "--dry-run" ]] || [[ "${1:-}" == "--dry-run" ]]; then | |
DRY_RUN=true | |
echo "π DRY RUN MODE - No files will be renamed" | |
echo | |
fi | |
# Color codes for output | |
RED='\033[0;31m' | |
GREEN='\033[0;32m' | |
YELLOW='\033[1;33m' | |
BLUE='\033[0;34m' | |
NC='\033[0m' # No Color | |
# Function to print colored output | |
print_status() { | |
local color=$1 | |
local message=$2 | |
echo -e "${color}${message}${NC}" | |
} | |
# Check if exiftool is installed | |
if ! command -v exiftool &> /dev/null; then | |
print_status "$RED" "β Error: exiftool is not installed." | |
echo "Install it with:" | |
echo " Ubuntu/Debian: sudo apt install libimage-exiftool-perl" | |
echo " macOS: brew install exiftool" | |
echo " Arch: sudo pacman -S perl-image-exiftool" | |
exit 1 | |
fi | |
# Check if target directory exists | |
if [[ ! -d "$TARGET_DIR" ]]; then | |
print_status "$RED" "β Error: Directory '$TARGET_DIR' does not exist." | |
exit 1 | |
fi | |
print_status "$BLUE" "πΈ Photo Renaming Script" | |
print_status "$BLUE" "Directory: $TARGET_DIR" | |
echo | |
# Supported image extensions | |
EXTENSIONS=("jpg" "jpeg" "png" "tiff" "tif" "raw" "cr2" "nef" "arw" "dng" "heic" "webp") | |
# Build find command with all extensions | |
FIND_PATTERN="" | |
for ext in "${EXTENSIONS[@]}"; do | |
if [[ -n "$FIND_PATTERN" ]]; then | |
FIND_PATTERN="$FIND_PATTERN -o" | |
fi | |
FIND_PATTERN="$FIND_PATTERN -iname \"*.$ext\"" | |
done | |
# Count total files first | |
TOTAL_FILES=$(eval "find \"$TARGET_DIR\" -type f \\( $FIND_PATTERN \\)" | wc -l) | |
print_status "$BLUE" "Found $TOTAL_FILES image files to process" | |
echo | |
# Initialize counters | |
RENAMED=0 | |
SKIPPED=0 | |
ERRORS=0 | |
# Function to generate safe filename | |
generate_safe_filename() { | |
local datetime="$1" | |
local extension="$2" | |
local base_name="$3" | |
local dir="$4" | |
# Convert datetime to filename format: YYYY-MM-DD_HH-MM-SS | |
local formatted_date=$(echo "$datetime" | sed 's/[: ]/_/g' | sed 's/__/_/g') | |
local new_name="${formatted_date}.${extension}" | |
local full_path="${dir}/${new_name}" | |
# If file exists, add counter | |
local counter=1 | |
while [[ -e "$full_path" ]]; do | |
new_name="${formatted_date}_${counter}.${extension}" | |
full_path="${dir}/${new_name}" | |
((counter++)) | |
done | |
echo "$new_name" | |
} | |
# Function to process a single file | |
process_file() { | |
local filepath="$1" | |
local filename=$(basename "$filepath") | |
local dirname=$(dirname "$filepath") | |
local extension="${filename##*.}" | |
printf "[$COUNTER/$TOTAL_FILES] Processing: %s... " "$filename" | |
# Extract datetime from EXIF data | |
local datetime=$(exiftool -s3 -DateTimeOriginal -CreateDate -d "%Y-%m-%d %H:%M:%S" "$filepath" 2>/dev/null | head -n1) | |
if [[ -z "$datetime" || "$datetime" == "-" ]]; then | |
print_status "$YELLOW" "β οΈ No datetime found, skipping" | |
SKIPPED=$((SKIPPED + 1)) | |
return 0 | |
fi | |
# Generate new filename | |
local new_name=$(generate_safe_filename "$datetime" "$extension" "$filename" "$dirname") | |
local new_path="${dirname}/${new_name}" | |
# Check if rename is needed | |
if [[ "$filename" == "$new_name" ]]; then | |
print_status "$BLUE" "β Already correctly named" | |
SKIPPED=$((SKIPPED + 1)) | |
return 0 | |
fi | |
# Perform rename | |
if [[ "$DRY_RUN" == true ]]; then | |
print_status "$GREEN" "π Would rename to: $new_name" | |
RENAMED=$((RENAMED + 1)) | |
else | |
if mv "$filepath" "$new_path" 2>/dev/null; then | |
print_status "$GREEN" "β Renamed to: $new_name" | |
RENAMED=$((RENAMED + 1)) | |
else | |
print_status "$RED" "β Failed to rename" | |
ERRORS=$((ERRORS + 1)) | |
return 0 | |
fi | |
fi | |
return 0 | |
} | |
# Process all image files | |
while IFS= read -r -d '' file; do | |
process_file "$file" | |
COUNTER=$((COUNTER + 1)) | |
done < <(eval "find \"$TARGET_DIR\" -type f \\( $FIND_PATTERN \\) -print0") | |
# Print summary | |
echo | |
print_status "$BLUE" "π Summary:" | |
if [[ "$DRY_RUN" == true ]]; then | |
echo " Files that would be renamed: $RENAMED" | |
else | |
echo " Files renamed: $RENAMED" | |
fi | |
echo " Files skipped: $SKIPPED" | |
echo " Errors: $ERRORS" | |
echo " Total processed: $TOTAL_FILES" | |
if [[ "$DRY_RUN" == true ]]; then | |
echo | |
print_status "$YELLOW" "π‘ Run without --dry-run to actually rename files" | |
fi | |
if [[ $ERRORS -gt 0 ]]; then | |
exit 1 | |
fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment