Batch Image Renamer
Build a command-line Perl tool that renames photos based on the date and time they were taken, read from their EXIF metadata. For example, IMG_4821.JPG becomes 2024-07-14_18-32-05.jpg. If two photos share the same timestamp, the tool adds a suffix (_1, _2) instead of overwriting anything. Every rename is recorded in a small SQLite database through DBI, so you can undo the last run, and photos with GPS coordinates can get the place name added using LWP::UserAgent and a free geocoding API. The tool only shows what it would do by default and renames files only when you pass --apply. You'll practice reading EXIF data with Image::ExifTool, parsing command-line options, working safely with the filesystem, and using DBI and LWP.
- Estimate
- ~14.5h
- Steps
- 8
- Completed by
- 0
- Proposed by
- codeseed.app
DBI · LWP::UserAgent
Project roadmap
- 01
Set up the script and its options
~1.5hCreate rename_photos.pl with use strict and use warnings. Parse options with Getopt::Long: a directory path (required), --apply (really rename; without it the script only prints what it would do), --recursive and --help. Print a usage message and exit with a non-zero code if the directory is missing or doesn't exist. List Image::ExifTool, DBI, DBD::SQLite and LWP::UserAgent in a cpanfile. Done when: --help, or an invalid path, prints a clear message.
- 02
Find the image files
~1.5hCollect all files with the extensions .jpg, .jpeg, .png and .heic (case-insensitive) in the given directory, and in its subdirectories when --recursive is set, using File::Find. Skip hidden files and anything that isn't a regular file. Sort the list by path so every run is predictable. Done when: the script prints the images it found in a test folder.
- 03
Read the capture date
~2hFor each image, use Image::ExifTool to read DateTimeOriginal, falling back to CreateDate if it's missing. Turn the value (format 2024:07:14 18:32:05) into the new base name 2024-07-14_18-32-05 and keep the original extension in lowercase. Files with no date are left alone and reported as 'skipped: no EXIF date'. Done when: the dry run prints the old name and proposed new name for every image.
- 04
Avoid name collisions
~2hCompute the final name of every file before renaming anything. If a target name is already taken, by an existing file or by another photo in the same run, add _1, _2 and so on until the name is free. A file that already has the correct name is left alone. Never overwrite a file. Done when: a test folder with three photos taken in the same second produces three different names in the dry run.
- 05
Rename files and log them with DBI
~2hWith --apply, rename the files and record each rename through DBI in a SQLite database (renames.db in the target folder), in a table with the run id, old path, new path and time. Wrap each run in a transaction so the log always matches what happened. Print a summary with the number of files renamed, skipped and failed. Done when: after an --apply run, the files are renamed and every rename is stored in renames.db.
- 06
Add undo
~1.5hAdd an --undo option that reads the most recent run from renames.db and renames each file back to its original name, in reverse order. If a file was moved or deleted since, report it and continue with the others. Mark the run as undone so it can't be undone twice. Done when: running --apply and then --undo restores the original file names.
- 07
Add the place name with LWP::UserAgent
~2hAdd a --with-place option. When a photo has GPS coordinates in its EXIF data, call the OpenStreetMap Nominatim reverse geocoding API with LWP::UserAgent and add the city name to the file name, e.g. 2024-07-14_18-32-05_Paris.jpg. Follow Nominatim's usage policy: send a descriptive User-Agent and make at most one request per second. If the request fails, rename without the place. Done when: photos with GPS data get a place name, and network errors don't stop the run.
- 08
Write tests
~2hWrite tests in t/ with Test::More, using a few small sample images with known EXIF dates committed as fixtures. Check the date-to-name conversion, the collision suffixes, that files without a date are skipped, that a dry run changes nothing on disk, and that undo restores the names. Run them with prove -l t/. Done when: prove passes.
Ready to build this?
Get a GitHub repo and start building. Your AI reviewer checks each step as you go.
Tech stack