"""Clean file names (snake_case) Renames every file in one folder to a clean, lowercase, web-safe name: "My Photo (Final) #2.JPG" becomes "my_photo_final_2.jpg". Free script from LinearAndLoom Icons: https://linearandloomicons.netlify.app/scripts/ Provided as is. Renaming and zipping cannot be undone, so try it on a copy of your folder first. """ import re import unicodedata from pathlib import Path def to_snake_case_filename(filename: str, max_length: int = 180) -> str: """ Convert filename to clean SEO-friendly snake_case. Preserves extension and removes invalid characters. """ path = Path(filename) stem = path.stem suffix = path.suffix.lower() # Normalize unicode → ASCII stem = unicodedata.normalize("NFKD", stem) stem = stem.encode("ascii", "ignore").decode("ascii") # Lowercase for snake_case stem = stem.lower() # Remove invalid filename chars (Windows + cross-platform safe) stem = re.sub(r'[<>:"/\\|?*\x00-\x1F]', "", stem) # Replace any non-alphanumeric sequence with underscore stem = re.sub(r"[^a-z0-9]+", "_", stem) # Remove repeated underscores stem = re.sub(r"_+", "_", stem) # Strip leading/trailing underscores stem = stem.strip("_") # Prevent empty names if not stem: stem = "file" # Trim total length safely max_stem_len = max_length - len(suffix) stem = stem[:max_stem_len].rstrip("_") return stem + suffix def normalize_folder_filenames(folder_path: str): folder = Path(folder_path) if not folder.exists() or not folder.is_dir(): print("Invalid folder path.") return used_names = set() this_script = Path(__file__).resolve() # List the files first, so files renamed along the way are not picked up twice for file in sorted(f for f in folder.iterdir() if f.is_file()): # Leave hidden system files (.DS_Store, .gitignore) and this script alone if file.name.startswith(".") or file.resolve() == this_script: continue new_name = to_snake_case_filename(file.name) new_path = folder / new_name # Handle duplicates base_stem = Path(new_name).stem suffix = Path(new_name).suffix counter = 1 # samefile() keeps "Photo.JPG" -> "photo.jpg" working on Windows and macOS, # where both names point to the same file while new_name in used_names or (new_path.exists() and not new_path.samefile(file)): new_name = f"{base_stem}_{counter}{suffix}" new_path = folder / new_name counter += 1 used_names.add(new_name) if file.name != new_name: file.rename(new_path) print(f"Renamed: {file.name} -> {new_name}") if __name__ == "__main__": folder_path = input("Enter folder path: ").strip().strip('"').strip("'") normalize_folder_filenames(folder_path)