Skip to content

Repository files navigation

Photos Organizer

A CLI tool that organizes photos and videos into a dated folder structure, detects duplicates, and keeps originals safe. I built it to organize my own personal photos, and organized more than 100k photos with this tool so far. If you encounter any problems open an issue and I will try to fix it quickly.

Disclaimer

This tool moves and modifies photos/videos. Use at your own risk — no warranty is provided. Always run with dry_run: true first and keep backups of irreplaceable photos.

Quick Start

# 1. Install dependencies
pip install -r requirements.txt

# 2. Create a config file (see config.example.json for all options)

# 3. Always do a dry run first — nothing is moved, just previewed
python main.py config.json

# 4. Run for real
python main.py -y config.json

-y skips the confirmation prompt. dry_run defaults to true — change it to false in config when ready.

Output Structure

target/
├── 2024/
│   ├── 04-April/
│   │   ├── photo.jpg           ← files land here by default
│   │   └── 14/                 ← day subfolder only when ≥ 40 photos on that day
│   │       └── event.jpg
│   └── 05-May/
│       └── video.mp4
├── duplicates/                 ← duplicate files (never deleted, just moved aside)
├── already_exist/              ← files already in target
├── corrupted/                  ← unreadable files
└── shortcuts/                  ← flat symlink/hardlink directory (if enabled)

Installation

Core (required):

pip install -r requirements.txt   # Pillow, pillow-heif, tqdm

Optional GPU acceleration (install only what matches your hardware):

pip install cupy-cuda12x    # NVIDIA (or cupy-cuda11x for older CUDA)
pip install openvino        # Intel
pip install coremltools     # Apple Silicon

Optional system tool for video date extraction:

# Install ffmpeg (provides ffprobe) — without it, videos use filename/filesystem dates
sudo apt install ffmpeg     # Linux
brew install ffmpeg         # macOS

Configuration

All settings live in a JSON file. See config.example.json for a complete reference with all options and their defaults.

Config Options Reference

Top-level paths

Field Required Description
input Yes Array of source directories to scan
target Yes Destination directory for organized files
input_ignored No Subdirectories to skip during scan
input_additional_duplicates_check No Extra dirs to check for duplicates (not organized, just compared)
target_duplicates No Where duplicates go (default: target/duplicates)
target_already_exist No Files already in target (default: target/already_exist)
target_corrupted No Unreadable files (default: target/corrupted)
target_shortcuts No Flat shortcut directory (default: target/shortcuts)
log_directory No Log and report output (default: ./logs)

Options

Option Type Default Description
dry_run bool true Preview only — no files are moved or copied
move_not_copy bool true Move files instead of copying them
organizing_enabled bool true Enable date-based folder organization
duplicates_enabled bool true Enable duplicate detection and relocation
duplicate_strategy string "hash" "hash" (content, most accurate), "filename", or "skip"
shortcuts_enabled bool false Create a flat symlink/hardlink directory alongside organized files
shortcut_type string "symlink" "symlink" or "hardlink"
rename_enabled bool false Rename files to img_YYYY_MM_DD_HH_MM_SS_... / vid_... format
date_source string "modified" Filesystem date fallback: "modified", "created", "accessed", "oldest"
date_priority string "filesystem_only" "exif_first", "exif_only", "filesystem_first", "filesystem_only"
set_modified_to_capture_date bool true Stamp each organized file's mtime with its computed capture date
pictures_folder_per_day int 40 Minimum photos on a single day to create a DD/ subfolder
gpu_enabled bool true Enable GPU acceleration for hashing and date extraction
gpu_method string "auto" "auto", "nvidia", "intel", "mac_arm64", or "disabled"
multiprocessing_level int 100 CPU usage: 0 (single-threaded), 25, 50, 75, or 100 (all cores)
multiprocessing_gpu_level int 100 GPU batch size multiplier: 0, 25, 50, 75, or 100
hash_cache_enabled bool true Cache file hashes in SQLite to speed up repeat runs
hash_cache_path string "./cache/hash_cache.db" Path to the hash cache database
force_rehash bool false Ignore cached hashes and recalculate everything
corruption_check_enabled bool false Check each file is readable before processing
ignore_subdirectories bool false Only scan top-level files in input directories
ignore_dotfiles bool true Skip hidden files (names starting with .)
separate_dotfiles bool true Move dotfiles to a separate folder instead of skipping
ignore_thumbnail bool false Skip files that look like thumbnails
ignore_thumbnail_less_than_size int 100 Thumbnail size threshold in KB
ignore_thumbnail_name_contain string "thumb" Filename pattern identifying thumbnails
verbose_logging bool false Print detailed per-file output to console
matchModifiedToCreated bool false Set mtime = creation time for each file (mutually exclusive with the option below)
matchCreatedToModified bool false Set creation time = mtime for each file

Recommended settings

date_source: "oldest" + date_priority: "exif_first" is the safest combination: it prefers the embedded capture date and, when falling back to filesystem, takes the older of modified vs. birth time — self-correcting against dates pushed forward by copies or backups.

Supported File Types

Images: JPG, JPEG, PNG, GIF, BMP, TIFF, TIF, HEIC, HEIF, WEBP
Videos: MP4, MOV, AVI, MKV, WMV, M4V, 3GP, FLV, MTS, M2TS

Date Fix Script

repair_dates.py repairs files whose filesystem dates were pushed forward. For each media file it determines the true capture date (EXIF → ffprobe video metadata → filename pattern) and, if the file's current date is newer than the true date, resets mtime/atime to the true date.

python repair_dates.py /path/to/library            # dry run — shows what would change
python repair_dates.py /path/to/library --apply    # apply the fixes
python repair_dates.py /path/to/library --apply --verbose #Verbose output
python repair_dates.py /path/to/library --apply --rename    # Just rename the files to img/vid_date_format

Note: On Linux, userspace cannot set a file's true birth/creation time. Only mtime and atime can be reset — but mtime is what the organizer reads when filing, so this is sufficient.

Tips

  • Always start with "dry_run": true and verify where files would land before running for real.
  • Enable hash_cache_enabled for large libraries — subsequent runs are dramatically faster.
  • Use multiprocessing_level: 0 + verbose_logging: true for debugging.

Contributing

Contributions are welcome. One hard rule: do not change the duplicate-detection logic (get_file_hash, detect_duplicates_optimized, build_target_file_hashes, and the related handlers in main.py) without extensive testing. That code went through a long cycle of manual review and rework to reach its current state — a seemingly small change can silently break deduplication on real libraries where mistakes are not easily undone.

License

License: MPL 2.0

This project is licensed under the Mozilla Public License 2.0 — see the LICENSE file for details. Copyright (c) 2026 aalzubidy.

About

CLI tool that organizes photos and videos into dated folders, detects duplicates via MD5 hashing, and extracts capture dates from EXIF and video metadata. Supports GPU acceleration and multiprocessing.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages