Photo Renamer
lupaxa-photo-renamer safely copies or moves photographs and videos into
consistent, date-based filenames. The console command is photo-renamer.
It reads image EXIF or video metadata when available, falls back to filesystem modification time in the default mode, detects common source apps from filenames, and reports progress with Rich.
Installation
Python 3.13 or newer is required.
pip install lupaxa-photo-renamer
Using a virtual environment is recommended:
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
python -m pip install lupaxa-photo-renamer
For local development:
git clone https://github.com/lupaxa-miscellaneous-toolbox/photo-renamer.git
cd photo-renamer
python -m venv .venv
source .venv/bin/activate
python -m pip install -e ".[dev]"
Video timestamp extraction uses MediaInfo. If it is missing, install it with
your operating system's package manager (brew install mediainfo on macOS,
apt install mediainfo on Debian/Ubuntu). auto still falls back to
filesystem modification time.
Quick Start
PATH is the directory to scan. Preview the work first:
photo-renamer --dry-run --recursive ~/Pictures
Review the startup panel (root, output, operation, format, timestamp mode, file
count). Then run the same command without --dry-run:
photo-renamer --recursive ~/Pictures
By default, files are copied, originals remain untouched, and results are
written below PATH/renamed/. Only files directly inside PATH are scanned
unless you add --recursive. Existing relative directories are preserved:
Pictures/holiday/day-1/IMG-20260801-WA0001.JPG
→ Pictures/renamed/holiday/day-1/2026-08-01_14-55-22.jpg
Use --move only when you intentionally want to remove each successfully
processed source.
Common Examples
# Include a detected source label in each filename
photo-renamer --recursive --format source ~/Pictures
# --preserve-source is an alias for --format source
photo-renamer --preserve-source ~/Pictures
# Put a source folder inside each preserved relative path
photo-renamer --recursive --organise ~/Pictures
# → renamed/holiday/day-1/WhatsApp/2026-08-01_14-55-22.jpg
# Drop relative paths, retaining only source folders
photo-renamer --recursive --flatten --organise ~/Pictures
# → renamed/WhatsApp/2026-08-01_14-55-22.jpg
# Choose another output root and move instead of copy
photo-renamer --recursive --output /Volumes/Archive --move ~/Pictures
# Limit extensions and always use modification time
photo-renamer --recursive --include jpg,jpeg,heic --timestamp filesystem ~/Pictures
A relative --output resolves below PATH. --verbose and --quiet cannot
be used together.
Flag Summary
| Flag | Purpose |
|---|---|
--output DIR |
Output root; defaults to renamed under PATH |
--recursive |
Scan subdirectories; off by default |
--dry-run |
Plan and report without writing |
--move |
Move files instead of copying |
--timestamp auto|exif|filesystem |
Select the timestamp strategy |
--format datetime|source|source-first |
Select the filename format |
--preserve-source |
Alias for --format source |
--organise |
Nest the detected source inside the relative path |
--flatten |
Remove relative path segments from destinations |
--include EXT,... |
Process only listed supported extensions |
--exclude EXT,... |
Exclude listed extensions |
--skip-existing |
Skip files already matching a target naming pattern |
--force |
Process matching names even with --skip-existing |
--timezone ZONE |
Convert timestamps to an IANA zone before naming |
--log-file PATH |
Append a tab-separated audit log on real runs |
--workers N |
Concurrent copy/move workers (default 1) |
--yes / -y |
Assume yes for over-cap workers confirmation |
--verbose / --quiet |
Increase or suppress terminal output |
If both --format and --preserve-source are supplied, the explicit
--format value wins.
--workers only parallelises copy or move. Planning stays sequential so
collision-safe names stay stable. If --workers is greater than twice the
detected CPU count, photo-renamer asks for confirmation (use --yes in CI).
--log-file is not written during --dry-run. Each line is
timestamp, action, source, destination, source label,
timestamp origin, and message.
Run photo-renamer --help for the authoritative command syntax.
Filename Formats
| Format | Example |
|---|---|
datetime |
2026-08-01_14-55-22.jpg |
source |
2026-08-01_14-55-22_WhatsApp.jpg |
source-first |
WhatsApp_2026-08-01_14-55-22.jpg |
Extensions are preserved and lowercased. datetime is the default.
For PATH/vacation/day-1/IMG-20260801-WA0001.jpg:
| Options | Destination below output |
|---|---|
| none | vacation/day-1/2026-08-01_14-55-22.jpg |
--organise |
vacation/day-1/WhatsApp/2026-08-01_14-55-22.jpg |
--flatten |
2026-08-01_14-55-22.jpg |
--flatten --organise |
WhatsApp/2026-08-01_14-55-22.jpg |
--organise nests the detected source inside the preserved relative path.
Supported Formats
| Kind | Extensions |
|---|---|
| Images | JPG, JPEG, PNG, HEIC, HEIF, WebP, TIF, TIFF |
| Videos | MP4, MOV, AVI, MKV, M4V, 3GP |
Matching is case-insensitive. --include and --exclude filter this supported
set; they do not enable arbitrary formats.
Timestamps
Every output name uses a resolved timestamp as YYYY-MM-DD_HH-MM-SS.
| Mode | Behaviour |
|---|---|
auto |
Embedded metadata when available, otherwise filesystem mtime |
exif |
Embedded only (EXIF or MediaInfo); missing dates fail that file |
filesystem |
Always use filesystem mtime |
Image priority: EXIF DateTimeOriginal, then DateTimeDigitized, then
DateTime, then filesystem mtime when the mode allows fallback.
Video priority: MediaInfo recorded, creation, encoded, then tagged dates, then filesystem mtime.
--timezone ZONE accepts IANA identifiers such as Europe/London. The tool
reads metadata only; it never rewrites EXIF or video tags.
Source Detection
Rules are case-insensitive; the first match wins. Detection uses the original filename only — not pixels or maker tags.
| Label | Patterns |
|---|---|
IMG-…-WA…, VID-…-WA… |
|
| Telegram | Photo_…, Video_… |
| Signal | signal-…, or a name ending with _signal before its extension |
| Pixel | PXL_… |
| Samsung | eight digits then _ (for example 20260801_…) |
| iPhone | IMG_ followed by four digits |
| Screenshot | Screenshot…, Screen Shot…, Screen_… |
| Camera | DSC_…, DSCF… |
| Unknown | anything else |
Safety
| Guarantee | Behaviour |
|---|---|
| Copy by default | Moving requires --move |
| Dry run | --dry-run performs no file or log writes |
| Never overwrite | Collisions receive _001, _002, and so on |
| Output excluded | The resolved output tree is never scanned |
| Continue on errors | Per-file I/O errors are reported; later files still process |
| Non-zero on failure | The command exits non-zero if any file failed |
Unreadable subdirectories are skipped without aborting the run. A mistyped
PATH is a configuration error.
Exit Codes
| Code | Meaning |
|---|---|
0 |
Every scanned file succeeded or was intentionally skipped |
1 |
At least one file failed (including missing metadata in exif mode) |
2 |
Invalid configuration (unknown timezone, quiet+verbose, bad PATH, …) |
Troubleshooting
| Symptom | What to try |
|---|---|
| No files scanned | Add --recursive; check --include / --exclude |
| Missing metadata | Use --timestamp auto or --timestamp filesystem |
| Video dates not found | Install MediaInfo; auto still falls back to mtime |
| Wrong wall-clock time | Pass an IANA zone such as --timezone Europe/London |
Unexpected _001 suffix |
Destination already existed or was reserved in the same run |
Files inside the resolved output directory are ignored. Symlinks are not
followed. --timestamp exif fails any file that has no embedded date.
FAQ
| Question | Answer |
|---|---|
| Does it change metadata? | No. It copies or moves files and changes destination names only. |
| Does it find duplicate photos? | No. Collisions prevent overwrites; they are not content hashes. |
| Can I undo a move? | Not in v1. Prefer copy mode, --dry-run, and --log-file. |
What does --preserve-source preserve? |
The source label in the filename (--format source), not originals. |
| Where do unknown sources go? | They use the Unknown label when naming or organising by source. |
| Can it overwrite a file? | No. Unique suffixes and exclusive destination creation prevent it. |
Testing
make init
make python-install-dev
make python-check
Or without Make:
python -m pip install -e ".[test]"
pytest
Coverage for lupaxa.photo_renamer is reported by default.
This project is released under the MIT License; see LICENCE.
Metadata
Release files for lupaxa-photo-renamer 0.1.6
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| lupaxa_photo_renamer-0.1.6.tar.gz | 18.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| lupaxa_photo_renamer-0.1.6-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 42.4 kB
Release files / lupaxa_photo_renamer-0.1.6.tar.gz
| Download URL | lupaxa_photo_renamer-0.1.6.tar.gz |
|---|---|
| Size | 18.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b855b0afcbadb45d179c9e290ca886c9d2ad55589eaab6cbeb55084e82834e45
|
|
BLAKE2b-256 checksum How to use checksums |
e94aea2bcbfe4e3ed13f0024cf0172f9ae9e41ae0e2762b014672336cecfea3e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 30, 2026.
Transparency logRelease files / lupaxa_photo_renamer-0.1.6-py3-none-any.whl
| Download URL | lupaxa_photo_renamer-0.1.6-py3-none-any.whl |
|---|---|
| Size | 24.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
87cac57b662ecf41c5237f042cf946eaa68c97c38d01500e4a3f1e1f58b1831f
|
|
BLAKE2b-256 checksum How to use checksums |
cd844af37415db8a471d687e2fb7ddf1e0a45fda4e6812e5b9a95c281ad9a172
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 30, 2026.
Transparency log