#!/usr/bin/env python3 import os import sys import subprocess import re import shutil import tempfile from pathlib import Path from typing import Optional, Dict, Set def check_dependencies() -> bool: """Check if required commands are available.""" for cmd in ['file', 'dctfilename']: if not shutil.which(cmd): print(f"Error: {cmd} command not found") return False return True def is_processed_file(filename: str) -> bool: """Check if filename matches our format (NNNN_HASH.ext).""" pattern = r'^[0-9]{4}_[0-9a-f]{16}\.(jpg|jpeg|png)$' return bool(re.match(pattern, filename.lower())) def extract_hash(filename: str) -> Optional[str]: """Extract hash from processed filename.""" match = re.match(r'^[0-9]{4}_([0-9a-f]{16})\.[^.]+$', filename.lower()) return match.group(1) if match else None def get_file_hash(filepath: Path) -> str: """Get hash for a file using dctfilename.""" try: # Create a temporary file for processing with tempfile.NamedTemporaryFile(suffix=filepath.suffix) as tmp: temp_path = Path(tmp.name) try: # Try hardlink first os.link(str(filepath), tmp.name) except OSError: # Fall back to copy if hardlink fails shutil.copy2(str(filepath), tmp.name) result = subprocess.run(['dctfilename', tmp.name], capture_output=True, text=True, check=True) return result.stdout.strip() except subprocess.CalledProcessError as e: print(f"Error getting hash for {filepath}: {e}") return "" except Exception as e: print(f"Unexpected error processing {filepath}: {e}") return "" def process_directory(directory: Path): """Process a single directory.""" print(f"Processing directory: {directory}") # Track files and their information seen_hashes: Dict[str, Path] = {} # hash -> filepath hash_sizes: Dict[str, int] = {} # hash -> filesize # Get all image files in directory image_files = sorted( path for path in directory.iterdir() if path.is_file() and path.suffix.lower() in {'.jpg', '.jpeg', '.png'} ) # First pass: gather information and handle duplicates kept_files = [] # Files to keep and rename for filepath in image_files: print(f"Processing: {filepath.name}") # Get hash (either from filename or calculate) if is_processed_file(filepath.name): file_hash = extract_hash(filepath.name) print(f"Using existing hash from filename: {file_hash}") else: file_hash = get_file_hash(filepath) print(f"Calculated new hash: {file_hash}") if not file_hash: print(f"Skipping {filepath.name} due to hash error") continue filesize = filepath.stat().st_size if file_hash in seen_hashes: # Found duplicate if filesize > hash_sizes[file_hash]: print(f"Found larger duplicate: {filepath.name} replaces {seen_hashes[file_hash].name}") seen_hashes[file_hash].unlink() # Remove smaller file seen_hashes[file_hash] = filepath hash_sizes[file_hash] = filesize kept_files.append((filepath, file_hash)) else: print(f"Removing smaller duplicate: {filepath.name}") filepath.unlink() else: seen_hashes[file_hash] = filepath hash_sizes[file_hash] = filesize kept_files.append((filepath, file_hash)) # Second pass: rename files for index, (filepath, file_hash) in enumerate(sorted(kept_files, key=lambda x: x[0].name), 1): new_name = filepath.parent / f"{index:04d}_{file_hash}{filepath.suffix.lower()}" if filepath != new_name: print(f"Renaming: {filepath.name} -> {new_name.name}") filepath.rename(new_name) print(f"Done! Processed {len(kept_files)} files in {directory}") def main(): if not check_dependencies(): sys.exit(1) # Get directories to process if len(sys.argv) == 1: directories = [Path('.')] else: recursive = sys.argv[1] in {'-r', '--recursive'} start_idx = 2 if recursive else 1 if recursive: directories = [] for dir_arg in sys.argv[start_idx:]: path = Path(dir_arg) if path.is_dir(): directories.extend(p for p in path.rglob('.') if p.is_dir()) else: directories = [Path(dir_arg) for dir_arg in sys.argv[start_idx:] if Path(dir_arg).is_dir()] # Process each directory for directory in directories: try: process_directory(directory) except Exception as e: print(f"Error processing directory {directory}: {e}") if __name__ == "__main__": main()