A CLI tool that catalogs photos, Live Photos, videos and audio
recordings from a source folder (e.g. freshly imported from a phone) into a
date-structured library like 2023/04/. Capture dates come from embedded
metadata — EXIF, video container creation dates, audio tags — parsed in pure
Rust, with no external tools (no exiftool, no ffmpeg) required at runtime.
sifter organize <SRC> <DEST> [options]
sifter organize --from <SRC> --to <DEST> [options]
The command scans the source, prints a pre-flight report (what will move, what is already in the library, what has no date), asks for confirmation, shows progress, and finishes with a summary.
Scanning /media/phone ... 812 media files found
Plan: organize into /home/user/Pictures using {year}/{month}
Photos 640 (12 RAW) Videos 130 Audio 18
Live Photo pairs: 22
New files to move: 771 (3.2 GiB)
Already in library: 28 (identical content, will skip)
Undated: 5 (no capture date, will skip)
Unsupported (left as-is): 8 (.pdf, .txt)
Date from: metadata 780 · filename 20 · mtime 4
Proceed with moving 771 files? [y/N]
| Option | Default | Description |
|---|---|---|
-n, --dry-run | off | Show the plan without changing anything |
-c, --copy | move | Copy files instead of moving them |
-o, --on-conflict <skip|rename|overwrite|ask> | skip | What to do when a different file with the same name exists |
-d, --on-no-date <skip|mtime|unsorted> | skip | What to do with files whose capture date can't be determined |
-t, --template <TPL> | {year}/{month} | Destination directory layout |
-s, --stamp-date | off | Append the capture date to file names that don't already contain one |
-r, --recursive | top level only | Scan subfolders of the source too |
-y, --yes | off | Skip the confirmation prompt (needed in scripts) |
-v, --verbose[=FILE] | off | Table of every planned mapping (type, size, date source, resolution, paths); with =FILE, written to that file instead of stdout |
- Embedded metadata — EXIF
DateTimeOriginal/CreateDatefor photos and RAW, container creation date for videos (MP4/MOV/3GP/MKV/WebM), recording-date tags for audio (ID3v2, MP4, Vorbis, RIFF). - Filename patterns —
IMG_20230101_120000,PXL_…,IMG-20230101-WA0012,Screenshot_…,signal-2023-01-01-12-00-00, Unix timestamps, and similar. - Neither found — the file is skipped (default), stamped with its
filesystem mtime (
--on-no-date mtime), or moved to<DEST>/unsorted/(--on-no-date unsorted).
With --stamp-date, a file whose name carries no date of its own is renamed
to <name>_YYYYMMDD_HHMMSS.<ext> on the way into the library — IMG_0001.jpg
taken on 2023-04-07 12:00:00 lands as IMG_0001_20230407_120000.jpg. Names
that already contain a recognizable date (IMG_20230101_120000.jpg,
PXL_…, signal-2023-01-01-…) are left untouched, so nothing gets stamped
twice. Live Photo pairs keep matching names, and duplicate and conflict
detection both use the stamped name.
If the destination already holds a file with the same name and identical
content (size + BLAKE3 hash), the source is skipped as a duplicate
regardless of --on-conflict — running sifter twice is safe. Only files
with the same name but different content count as conflicts. Nothing is
ever overwritten unless you pass --on-conflict overwrite.
An IMG_1234.HEIC + IMG_1234.MOV pair (same folder, same stem) is
detected and kept together: the video inherits the photo's capture date, so
the pair never splits across a month boundary, and a conflict rename keeps
both stems identical.
Tokens: {year}{month}{day}{year_short}{month_name}{type}
(Photos/Videos/Audio). / separates directories. Examples:
--template "{year}/{month}" # 2023/04 (default)
--template "{year}/{year}-{month}-{day}" # 2023/2023-04-07
--template "{type}/{year}/{month_name}" # Photos/2023/April
0 success · 1 error · 2 aborted by user · 3 finished with per-file errors
cargo build --release
All dependencies are pure Rust, so a fully static binary is one target away:
cargo build --release --target x86_64-unknown-linux-musl
cargo test runs the unit suite plus end-to-end tests that exercise the
binary against the tiny committed media fixtures in tests/fixtures/
(generated once with ffmpeg and a small Python script — those tools are not
needed to build, test, or run sifter).
GPL-3.0-or-later