Fixes Immich timeline dates by looping through every asset in a library and, for each one:
- Downloads the original file and reads its real EXIF date with
exiftool. Immich's own reported EXIF date (exifInfo.dateTimeOriginal) has been observed in the wild to silently diverge from what's actually embedded in the file — one confirmed case had correct camera fields (make/model/lens/ ISO/etc.) but adateTimeOriginalthat matched neither the file's realDateTimeOriginalnor any other visible timestamp, and re-running Immich's own "refresh-metadata" job did not correct it. So this tool never trusts Immich's stored EXIF data — it always checks the actual file. - If that real EXIF date differs from the asset's current timeline date, updates it.
- If the file has no usable EXIF date at all (e.g. WhatsApp exports,
screenshots), falls back to guessing a date from the filename (e.g.
IMG_20230115_143022.jpg,PXL_..., Unix-epoch filenames) or, failing that, the containing folder path (e.g..../2023-01-15 vacation/photo.jpg). This fallback can be disabled with--no-filename-fallback. - If neither yields a plausible date, the asset is left untouched and logged
as
unresolved.
Every asset that's already correct is left alone (no-op writes are skipped). Every processed asset — exif, filename-guess, skipped, unresolved, or failed — is written to a CSV audit report, along with a live progress line on stdout.
Because every asset requires downloading the original file, a full run is
bandwidth- and time-heavier than a metadata-only tool. Use --limit to test
on a small batch first, and --concurrency to process several assets in
parallel on a large library.
Requires uv and the exiftool binary on your
PATH (e.g. brew install exiftool).
uv syncuv run immich-timeline-fixer --host https://immich.example.com --api-key <key>By default this applies changes immediately. Use --dry-run to preview
what would change first, optionally combined with --limit to test on a
small batch:
uv run immich-timeline-fixer --host https://immich.example.com --api-key <key> --dry-run --limit 20Then inspect the printed report / CSV before running for real without
--dry-run.
| Flag | Env var | Description |
|---|---|---|
--host |
IMMICH_HOST |
Immich server URL |
--api-key |
IMMICH_API_KEY |
Immich API key |
--timezone |
IMMICH_TIMEZONE |
IANA timezone applied to dates that don't carry their own offset (default: system timezone) |
--config |
Path to a TOML config file | |
--dry-run |
Report changes without writing them | |
--limit |
Only process the first N assets | |
--min-year |
Reject implausible dates before this year (default: 1990) | |
--filename-fallback / --no-filename-fallback |
Guess a date from the filename/path when the file has no EXIF date (default: enabled) | |
--report-path |
Where to write the CSV audit report (default: timestamped file in the current directory) | |
--concurrency |
Number of assets to download+process in parallel (default: 1, sequential) |
Settings are resolved with precedence CLI flags > environment variables > config file > built-in defaults.
# immich-timeline-fixer.toml
host = "https://immich.example.com"
api_key = "..."
timezone = "Europe/Amsterdam"
min_year = 1990
filename_fallback = true
concurrency = 4uv run immich-timeline-fixer --config immich-timeline-fixer.tomlThe CSV audit report has one row per processed asset:
| Column | Description |
|---|---|
asset_id |
Immich asset ID |
filename |
Original filename |
action |
One of exif, filename-guess, skipped-unchanged, unresolved, failed |
old_date |
The asset's timeline date before processing |
new_date |
The date it was updated to (empty unless action is exif or filename-guess) |
error |
Error message (only set when action is failed) |
The same information is also printed live to stdout as each asset is processed, followed by a summary count per action when the run finishes.
uv run pytest