Skip to content

Instantly share code, notes, and snippets.

@waka-wa
Created March 2, 2025 00:53
Show Gist options
  • Save waka-wa/72bc5caba1f04e172ed6eb5f20df944d to your computer and use it in GitHub Desktop.
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…
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