Organize photos and videos by capture date while preserving metadata and resumable workflows.
Project description
DateFrame
Tools for organizing photo and video libraries by their real capture date while preserving useful metadata and making long operations resumable.
Use this project to:
- copy media from iCloud Photos on Windows while preserving dates from Windows Shell metadata
- rename photos and videos from camera folders using embedded metadata or associated sidecars
- write filename-based capture dates back into media metadata
- inspect metadata and file extensions before processing a library
- recover safely from interrupted long-running operations using CSV logs and checkpoints
dateframe import-icloud is Windows-only. The other commands are designed to work across operating systems when their required external tools are installed.
Installing the package provides one dateframe command with the subcommands shown below. The corresponding Python modules are kept at the repository root, while shared implementation modules live in media_tools/.
Quick start
Install the dependencies described in Requirements, then choose the workflow that matches your library.
Copy media directly from iCloud Photos on Windows:
dateframe import-icloud "C:\Users\You\Pictures\iCloud Photos\Photos" "C:\Users\You\Pictures\iCloud-Renamed"
Copy and rename media from a regular camera or export folder:
dateframe rename --copy "C:\Users\You\Pictures\Originals" "C:\Users\You\Pictures\Renamed"
Write dates from renamed filenames into metadata:
dateframe write-dates "C:\Users\You\Pictures\Renamed"
Continue a previous interrupted run using its latest CSV or checkpoint:
dateframe rename --resume-csv ".\logs\rename_media_2026-05-22T13-37-05.csv"
dateframe import-icloud --resume-csv ".\logs\copy_icloud_2026-05-21T22-54-12_checkpoint.csv"
Included commands
dateframe extensions (list_extensions.py)
Lists file extensions found in a folder. Useful for quickly checking which file types exist before processing them.
It prints counts per extension and can also read paths from a TXT file.
dateframe extensions "C:\Users\You\Pictures"
dateframe extensions --input-txt ".\files-to-check.txt"
dateframe inspect (read_metadata.py)
Reads and prints file metadata using multiple methods:
Wand/ ImageMagickPillowffmpeg- Windows filesystem dates with
-w - associated XMP/XML sidecars with
--sidecars
This is useful for diagnosing which dates or tags are actually present in an image or video before renaming files or writing metadata.
By default it runs all readers that are appropriate for the current operating system. The Windows filesystem reader is included by default only on Windows. Pass one or more reader flags to run only those methods.
dateframe inspect "C:\Users\You\Pictures\IMG_1234.ARW"
dateframe inspect --ffmpeg --sidecars "C:\Users\You\Pictures\VID_1234.MP4"
dateframe rename (rename_media.py)
Copies or moves media files from a source folder to a destination folder and renames them using the best date found in metadata.
- uses
Wand/Pillowfor images - uses
ffmpegfor videos - can use Windows file dates as a fallback with
--windows - uses
shutil.copy2when copying, to preserve file metadata - keeps folder structure with
--keep-structure - can take files from a TXT input list with
--input-txt - keeps associated
.xmp/.xmlsidecars together with their media file - keeps Apple Live Photo image/
.MOVpairs on a common renamed timestamp - always writes a CSV log that can be used to resume later
- writes a TXT summary log
- writes periodic checkpoint CSVs during long runs
- uses 4 workers by default, which is safer for RAW-heavy folders than automatic thread scaling
Basic copy usage:
dateframe rename --copy "C:\Users\You\Pictures\Originals" "C:\Users\You\Pictures\Renamed"
Move recursively while keeping the source folder structure:
dateframe rename --move --recursive --keep-structure "C:\Users\You\Pictures\Unsorted" "C:\Users\You\Pictures\Sorted"
TXT input list usage:
dateframe rename --copy --input-txt ".\files-to-rename.txt" "C:\Users\You\Pictures\Renamed"
Resume from the latest CSV log:
dateframe rename --resume-csv ".\logs\rename_media_2026-05-22T13-37-05.csv"
Resume from a checkpoint after a power loss:
dateframe rename --resume-csv ".\logs\rename_media_2026-05-22T13-37-05_checkpoint.csv"
When resuming, src and dest can be omitted if the CSV includes run context. You can still override operational flags such as --workers, --windows, --quiet, --copy, and --move.
dateframe rename writes logs to .\logs by default. Change this with --log-path.
The CSV log records each source path, destination path, detected date, media type, action (copy or move), date source, whether processing succeeded, errors, and run context. Successful rows are skipped when resuming; failed rows are retried if the source path still exists.
When a sidecar contains a date with timezone offset, the local capture time is used for the filename and the offset is preserved in the CSV. The script does not convert dates to UTC.
Associated sidecars are renamed using the full renamed media filename plus the sidecar extension. For example:
C0001.MP4 -> 2026-03-02T03-20-52.MP4
C0001M01.XML -> 2026-03-02T03-20-52.MP4.M01.XML
IMG_1234.ARW -> 2026-03-02T03-20-52.ARW
IMG_1234.XMP -> 2026-03-02T03-20-52.ARW.XMP
The script detects regular same-stem sidecars such as IMG_1234.XMP, and Sony-style XML sidecars such as C0001M01.XML for C0001.MP4.
Apple Live Photo .HEIC, .JPG, or .JPEG images and .MOV files are checked with ExifTool. They are treated as a pair only when the image identifier (Apple:ContentIdentifier / MediaGroupUUID) matches the video QuickTime:ContentIdentifier, even if their original filenames differ. The image capture date is preferred for both output names so the pair stays visibly associated:
IMG_1234.HEIC -> 2026-03-02T03-20-52.HEIC
IMG_1234.MOV -> 2026-03-02T03-20-52.MOV
If the image does not provide a usable date, the .MOV date is used for both files. Files without a matching embedded identifier remain independent media files. Use --no-live-photos to disable this detection; the setting and confirmed pair identifier are recorded and preserved when resuming. Change the ExifTool executable with --exiftool when needed.
When a detected Live Photo has a regular same-stem .XMP or .XML sidecar, that sidecar is kept with the image member of the pair. Sony-style video sidecars such as C0001M01.XML continue to stay with their video.
Useful CSV columns:
source: original file pathdest: copied or moved destination pathdate: local capture time used for the filenamedate_offset: timezone offset found in a sidecar, if availablemedia_type:image,video,sidecar, orotherdate_source: where the date came from, such aswand:dng:create.dateorsidecar:...:creationdatepair_type:live_photofor image/video pairs detected as an Apple Live Photopair_id: matching embedded identifier used to confirm a Live Photo pairpaired_source: original path of the other file in a Live Photo pairprocessed_ok:Truewhen the row completed successfully
dateframe write-dates (write_dates.py)
Takes files already renamed in the format YYYY-MM-DDTHH-mm-SS.ext and writes that date into the file using ExifTool.
- supports recursive processing
- can take files from a TXT input list with
--input-txt - can write only missing tags with
--if-missing - can also update filesystem dates with
--set-filetime - always writes a CSV log that can be used to resume later
- writes a TXT summary log
- writes periodic checkpoint CSVs during long runs
- uses 2 workers and a 90-second per-file ExifTool timeout by default
Basic usage:
dateframe write-dates "C:\Users\You\Pictures\Renamed"
Resume from the latest CSV log:
dateframe write-dates --resume-csv ".\logs\write_dates_2026-05-22T12-58-10.csv"
Resume with explicit conservative settings:
dateframe write-dates --resume-csv ".\logs\write_dates_2026-05-22T12-58-10.csv" --workers 1 --timeout 90
Resume from a checkpoint after a power loss:
dateframe write-dates --resume-csv ".\logs\write_dates_2026-05-22T12-58-10_checkpoint.csv"
TXT input list usage:
dateframe write-dates --input-txt ".\files-to-write.txt"
dateframe write-dates writes logs to .\logs by default. Change this with --log-path.
The CSV log records each source path, the date parsed from the filename, whether metadata writing succeeded, the write target (embedded, generated .xmp, or dry-run), errors, and run context. On resume, successful rows and definitive skips such as missing dates in filenames are skipped; failed metadata writes are retried.
Generated XMP sidecars use the full media filename plus .xmp, for example 2026-03-02T03-20-52.AVI.xmp.
If a media filename does not contain a date, write_dates.py can read a date from an associated sidecar such as file.ext.xmp, file.xmp, or preserved XML metadata. If the filename already contains a date and an associated sidecar has the same date with a timezone offset, the filename remains the source of truth and the offset is still preserved.
When an offset is available, the local time is written and the offset is written separately where supported. The script does not convert dates to UTC.
Dry-run rows are logged, but they are not treated as completed for later non-dry-run resumes.
Useful CSV columns:
source: file path being updateddate: local date written, formatted asYYYY-MM-DD HH:mm:ssdate_offset: timezone offset found in a sidecar, if availabledate_source:filename,sidecar:..., orfilename+sidecar:...metadata_ok:Truewhen metadata writing finished successfullywrite_target:embedded, generated.xmp, ordry-runerror: empty for success; otherwise contains skip/error detail
dateframe import-icloud (copy_icloud.py)
Windows-only script designed for iCloud Photos on Windows.
It depends on Windows Shell / COM metadata through pywin32, so it is not expected to run on Linux or macOS.
- reads the Windows Shell / iCloud date first
- preserves exact embedded seconds when they match the Windows/iCloud date to the minute
- falls back to embedded capture date when Windows does not expose one
- filters by date range with
--from-dateand--to-date - copies only files that pass the filter
- renames files using the selected capture date
- writes embedded metadata with
ExifTool - can optionally create
.xmpsidecar files with--write-xmp - can skip embedded metadata entirely with
--no-metadata - can verify written date metadata with
--verify - can skip video metadata writing with
--skip-video-metadata - always writes a CSV log that can be used to resume later
- writes a TXT summary log
- writes periodic checkpoint CSVs during long runs
- limits concurrent iCloud downloads with
--copy-workersto avoid overloading iCloud
Basic usage:
dateframe import-icloud "C:\Users\You\Pictures\iCloud Photos\Photos" "C:\Users\You\Pictures\iCloud Renamed"
Date filter usage:
dateframe import-icloud --from-date 2020-01-01 --to-date 2020-12-31 "C:\Users\You\Pictures\iCloud Photos\Photos" "C:\Users\You\Pictures\iCloud Renamed"
Low-resource usage:
dateframe import-icloud --workers 2 --copy-workers 1 --checkpoint-seconds 300 --quiet "C:\Users\You\Pictures\iCloud Photos\Photos" "C:\Users\You\Pictures\iCloud Renamed"
TXT input list usage:
dateframe import-icloud --input-txt ".\files-to-process.txt" "D:\Media\Pictures\iCloud"
When --input-txt is used, each non-empty, non-comment line is treated as one source file path. Date filters still apply.
Resume from the latest CSV log:
dateframe import-icloud --resume-csv ".\logs\copy_icloud_2026-05-21T22-54-12.csv"
Resume from a checkpoint after a power loss:
dateframe import-icloud --resume-csv ".\logs\copy_icloud_2026-05-21T22-54-12_checkpoint.csv"
When resuming, src and dest can be omitted if the CSV includes run context. You can still override operational flags such as --workers, --copy-workers, --exiftool, --write-xmp, --no-metadata, --verify, and --quiet.
Date filters saved in the resume CSV are reused by default. Passing new --from-date or --to-date values overrides the saved filters. Use --clear-date-filter to remove saved date filters when resuming.
dateframe import-icloud logs
By default logs are written to .\logs. Change this with --log-path.
Each run writes:
copy_icloud_<start-time>.csv: main log and resume sourcecopy_icloud_<start-time>.txt: summary log
During a running operation, the script also writes:
copy_icloud_<start-time>_checkpoint.csv
The checkpoint CSV is overwritten periodically according to --checkpoint-seconds and contains the accumulated history up to the last checkpoint. It is useful if the computer loses power or the process ends instantly before the final logs are written.
If the run ends normally or with Ctrl + C, the final CSV/TXT logs are written and the checkpoint file is removed.
The checkpoint CSV includes one extra column:
run_checkpoint_at: timestamp of the checkpoint write
The final CSV does not include run_checkpoint_at.
Important CSV columns:
source: original file pathdest: copied destination path, when a copy existsdate: effective capture date used for naming and metadatacopied_ok:Trueif the file was copied; empty for non-copy skips such as date filters or missing usable datemetadata_ok:Trueif embedded metadata finished successfully,Falseif metadata failed, empty when metadata was intentionally skippederror: empty for success; otherwise contains the skip/error reasonrun_resume_csv: CSV used as the resume source for that row's runrun_interrupted: whether that row's run ended after interruption
Generated XMP sidecars use the full copied media filename plus .xmp, for example 2026-03-02T03-20-52.AVI.xmp.
During processing, a copied file may temporarily appear as:
copied_ok=True
metadata_ok=False
error=metadata pending
This protects long runs from power loss: if a file was copied but metadata had not finished yet, a later resume can retry metadata on the already-copied destination instead of copying the file again.
Rows with copy or metadata errors are retried automatically when using --resume-csv. Rows that completed successfully, were outside the date range, were not media, or had no usable date are skipped on resume.
If date filters change when resuming, rows that were previously skipped as outside date range are reevaluated with the new effective filter.
Logging and resume behavior
dateframe import-icloud, dateframe rename, and dateframe write-dates share the same logging model:
- every run writes a CSV log and a TXT summary log
- logs are named with the run start time
- logs go to
.\logsby default and can be changed with--log-path - CSV logs are the source of truth for resume
- TXT logs contain run summary counts, start/end time, interruption state, and the effective command
- periodic checkpoint CSVs are written during long runs
- checkpoint CSVs can be passed to
--resume-csvafter a power loss - final CSV/TXT logs remove the checkpoint after normal completion or
Ctrl + C
Use the most recent CSV log when resuming. Each new resume CSV includes accumulated rows from the previous CSV plus newly processed rows, so the newest CSV becomes the next resume point.
Privacy and safety
These scripts are designed to process personal media libraries. Before processing a large collection, test the command on a small copied sample first and review the produced CSV/TXT logs.
Logs can contain full local file paths, folder names, filenames, selected metadata values, errors, and effective commands. Before sharing logs in an issue, discussion, forum post, or pull request, review and redact any private information.
When using copy or move operations, keep an independent backup of important media until you have verified the output.
Requirements
Most scripts support Windows, Linux, and macOS when the required external tools are installed.
dateframe import-icloud is the exception: it is Windows-only because it depends on Windows Shell metadata and pywin32.
1. Install Python
Install Python 3.10 or newer from:
During installation, enable Add Python to PATH.
Verify:
python --version
pip --version
2. Install ffmpeg
You need the ffmpeg and ffprobe executables, because the scripts use ffmpeg-python to read video metadata.
You can get it from:
Make sure both ffmpeg and ffprobe are available in PATH.
Verify:
ffmpeg -version
ffprobe -version
3. Install ImageMagick
Wand requires ImageMagick to be installed on Windows.
You can download it from:
During installation, enable the command-line integration needed by Wand.
4. Install ExifTool
dateframe write-dates and dateframe import-icloud use ExifTool to write metadata. dateframe rename uses it to confirm Apple Live Photo pairs through their embedded identifiers.
You can download it from:
Make sure the executable is available in PATH, or pass its full path with --exiftool.
Verify:
exiftool -ver
5. Install DateFrame
From a cloned project folder:
pip install .
After a PyPI release is available, install the published package with:
pip install dateframe
Notes:
- If you only use
dateframe import-icloud, you need Python,pywin32, andExifTool.ffmpegand ImageMagick are used by the other commands. pywin32is required fordateframe import-icloudand is only available on WindowsWandrequires ImageMagick to already be installedffmpeg-pythonis only a wrapper and does not replace theffmpegbinaries
Clone and install
Clone this repository, then install DateFrame:
git clone https://github.com/fyulita/dateframe.git
cd dateframe
pip install .
If you are not using git, copying the project folder and running pip install . inside it is enough.
Contributing
Bug reports and pull requests are welcome. See CONTRIBUTING.md for development setup, validation guidance, and what information is most useful in a report.
License
This project is licensed under the GNU General Public License v3.0. See LICENSE.md.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file dateframe-0.1.0.tar.gz.
File metadata
- Download URL: dateframe-0.1.0.tar.gz
- Upload date:
- Size: 67.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b98a106da1a695941cd2f50472fcfb12929756035d19ee2741bb3265094259f5
|
|
| MD5 |
556bdb6159a44b068575a8a2857c89b8
|
|
| BLAKE2b-256 |
0c1afd1eaf1539b18423cf34b5ab5dad5b66a99747052a8712d08d4af1d34c0c
|
File details
Details for the file dateframe-0.1.0-py3-none-any.whl.
File metadata
- Download URL: dateframe-0.1.0-py3-none-any.whl
- Upload date:
- Size: 59.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
23c38834b6f342d8bc26aa5fd4afc5b28be2f552e3d271b29e8dba34351f0ad3
|
|
| MD5 |
56d76b8eb78a783b5becb11c1a0c9bc5
|
|
| BLAKE2b-256 |
4cbed1e4833b0486d0e611207883ea740e0d446cea46b6a4c5d3d5498cd9ecb9
|