Created
March 2, 2025 00:53
-
-
Save waka-wa/72bc5caba1f04e172ed6eb5f20df944d to your computer and use it in GitHub Desktop.
A Python script that organizes your photos by moving files from an "Unsorted" folder into year-based directories (using file creation/modification dates). It then updates a central "All" folder with symlinks to each file, providing an aggregated view. The script features interactive prompts for setting the year range and confirming symlink updat…
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
import os | |
import shutil | |
from datetime import datetime | |
from pathlib import Path | |
def get_file_year(file_path, min_year, max_year): | |
"""Determine a file's year based on creation or modification date.""" | |
try: | |
ctime = os.path.getctime(file_path) | |
mtime = os.path.getmtime(file_path) | |
earlier_time = min(ctime, mtime) | |
year = datetime.fromtimestamp(earlier_time).year | |
# Bound the year between min_year and max_year | |
if year < min_year: | |
year = min_year | |
elif year > max_year: | |
year = max_year | |
return year | |
except Exception as e: | |
print(f"Error retrieving year for {file_path}: {e}") | |
return None | |
def move_files_by_year(base_path, min_year, max_year): | |
"""Move files from the 'Unsorted' folder to year-based folders.""" | |
base_dir = Path(base_path) | |
unsorted_dir = base_dir / "Unsorted" | |
if not unsorted_dir.exists(): | |
print(f"'Unsorted' folder not found at {unsorted_dir}.") | |
create_choice = input("Would you like to create an 'Unsorted' folder? (Y/n): ").strip().lower() | |
if create_choice in ("", "y", "yes"): | |
unsorted_dir.mkdir(parents=True, exist_ok=True) | |
print(f"Created 'Unsorted' folder at {unsorted_dir}. Please add your files and run the script again.") | |
return False | |
# Check if there are any files to process | |
files_found = any(files for _, _, files in os.walk(unsorted_dir)) | |
if not files_found: | |
print("No files found in the 'Unsorted' folder. Please add files and run the script again.") | |
return False | |
# Create year folders upfront | |
for year in range(min_year, max_year + 1): | |
(base_dir / str(year)).mkdir(parents=True, exist_ok=True) | |
moved_count = 0 | |
error_count = 0 | |
# Walk through all files in the Unsorted directory and its subdirectories | |
for root, _, files in os.walk(unsorted_dir): | |
for file in files: | |
source_file = Path(root) / file | |
file_year = get_file_year(source_file, min_year, max_year) | |
if file_year is None: | |
error_count += 1 | |
continue | |
rel_path = Path(root).relative_to(unsorted_dir) | |
dest_dir = base_dir / str(file_year) / rel_path | |
dest_file = dest_dir / file | |
dest_dir.mkdir(parents=True, exist_ok=True) | |
# Check for duplicate files at the destination | |
if dest_file.exists(): | |
print(f"Warning: {dest_file} already exists. Skipping {source_file}.") | |
error_count += 1 | |
continue | |
try: | |
shutil.move(str(source_file), str(dest_file)) | |
print(f"Moved {source_file} to {dest_file}") | |
moved_count += 1 | |
except Exception as e: | |
print(f"Error moving {source_file} to {dest_file}: {e}") | |
error_count += 1 | |
print(f"\nFile moving complete: {moved_count} files moved, {error_count} errors/skips.") | |
return moved_count > 0 | |
def update_symlinks(base_path, min_year, max_year): | |
"""Update the 'All' folder with symlinks to files in year folders.""" | |
base_dir = Path(base_path) | |
all_dir = base_dir / "All" | |
all_dir.mkdir(parents=True, exist_ok=True) | |
# Remove broken symlinks | |
broken_symlinks = 0 | |
for root, _, files in os.walk(all_dir, topdown=False): | |
for file in files: | |
symlink_path = Path(root) / file | |
if symlink_path.is_symlink() and not symlink_path.exists(): | |
try: | |
symlink_path.unlink() | |
print(f"Removed broken symlink: {symlink_path}") | |
broken_symlinks += 1 | |
except Exception as e: | |
print(f"Error removing symlink {symlink_path}: {e}") | |
created_symlinks = 0 | |
# Create new symlinks from year folders to 'All' | |
for year in range(min_year, max_year + 1): | |
year_dir = base_dir / str(year) | |
if not year_dir.exists(): | |
continue | |
for root, _, files in os.walk(year_dir): | |
for file in files: | |
try: | |
original_file = Path(root) / file | |
rel_path = Path(root).relative_to(year_dir) | |
symlink_dir = all_dir / rel_path | |
symlink_dir.mkdir(parents=True, exist_ok=True) | |
symlink_name = f"{year}_{file}" | |
symlink_path = symlink_dir / symlink_name | |
if not symlink_path.exists(): | |
target_path = os.path.relpath(original_file, symlink_path.parent) | |
symlink_path.symlink_to(target_path) | |
print(f"Created symlink: {symlink_path} -> {target_path}") | |
created_symlinks += 1 | |
except Exception as e: | |
print(f"Error creating symlink for {original_file}: {e}") | |
print(f"\nSymlink update complete: {broken_symlinks} broken symlinks removed, {created_symlinks} new symlinks created.") | |
def get_user_input(): | |
"""Collect user input for configuration.""" | |
default_min = 1990 | |
default_max = 2025 | |
print("\n===== Photo Library Organizer =====\n") | |
try: | |
min_year_input = input(f"Enter minimum year (default: {default_min}): ").strip() | |
min_year = int(min_year_input) if min_year_input else default_min | |
except ValueError: | |
print("Invalid input for minimum year. Using default.") | |
min_year = default_min | |
try: | |
max_year_input = input(f"Enter maximum year (default: {default_max}): ").strip() | |
max_year = int(max_year_input) if max_year_input else default_max | |
except ValueError: | |
print("Invalid input for maximum year. Using default.") | |
max_year = default_max | |
if min_year > max_year: | |
print("Minimum year cannot be greater than maximum year. Swapping values.") | |
min_year, max_year = max_year, min_year | |
update_links = input("Update symlinks after organizing files? (Y/n): ").strip().lower() not in ("n", "no") | |
return min_year, max_year, update_links | |
def main(): | |
base_path = os.getcwd() | |
min_year, max_year, update_links = get_user_input() | |
print(f"\nWorking in directory: {base_path}") | |
print(f"Year range: {min_year} to {max_year}") | |
files_moved = move_files_by_year(base_path, min_year, max_year) | |
if update_links: | |
# If no files were moved, ask if the user still wants to update symlinks. | |
if not files_moved: | |
proceed = input("\nNo files were moved. Still update symlinks? (Y/n): ").strip().lower() | |
if proceed in ("n", "no"): | |
print("Symlink update cancelled.") | |
return | |
print("\nUpdating symlinks...") | |
update_symlinks(base_path, min_year, max_year) | |
print("\n===== Operation Complete =====") | |
if __name__ == "__main__": | |
try: | |
main() | |
except KeyboardInterrupt: | |
print("\nOperation cancelled by user.") | |
except Exception as e: | |
print(f"\nAn unexpected error occurred: {e}") | |
input("\nPress Enter to exit...") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment