Skip to content
Infrastructure

Fixing iCloud photos that all landed in Immich dated today

By Victor Da Luz
homelab immich icloud exiftool photos self-hosted

After deploying Immich I started pulling my iCloud history into it. The import worked. The timeline did not: thousands of photos spanning more than a decade were stacked at the top of the timeline as if I had taken all of them today. A photo from a 2014 vacation sat next to this morning’s screenshot, both claiming to be brand new.

The import pipeline

New photos reach Immich through its mobile app, but the iCloud back catalog needed a bulk path. That is icloudpd, the iCloud Photos downloader, which I run as a Docker container (the boredazfcuk/icloudpd image) next to Immich. It logs into the iCloud account, downloads originals, and files them into a year/month folder structure:

/media/photos/icloud/2014/07/IMG_2034.JPG
/media/photos/icloud/2014/08/IMG_2101.HEIC

Immich watches that path as an external library, so everything icloudpd downloads shows up in the timeline.

Why everything was dated today

Immich orders the timeline by the capture date in the photo’s EXIF metadata, DateTimeOriginal. When that tag is missing, it falls back to file dates, and the file modification time of a freshly downloaded photo is the moment it was downloaded. The photos I pulled from iCloud were arriving without DateTimeOriginal set, so as far as Immich could tell, my entire photo history had been captured the day the importer ran.

icloudpd has a flag for exactly this: set_exif_datetime, which writes DateTimeOriginal from the photo’s creation date when the tag is missing. It is off by default, and I had not enabled it. The data to date every photo correctly existed in iCloud the whole time; the importer just was not writing it into the files.

Fixing future downloads

The forward fix is one setting. In my Ansible role for icloudpd it is a default that flows into the container’s environment:

icloud_folder_structure: '{:%Y/%m}' # year/month folders
icloud_set_exif_datetime: true # write DateTimeOriginal when missing
# docker-compose.yml (boredazfcuk/icloudpd)
environment:
  - set_exif_datetime=${ICLOUD_SET_EXIF_DATETIME:-true}

Restart the container and every photo downloaded from then on carries a real capture date.

Fixing the photos already on disk

That left the thousands of photos already downloaded without the tag. Two options:

  1. Delete them and re-download everything with the flag enabled. Correct, complete, and slow; a full re-download of a decade of originals takes hours and re-transfers data I already have.
  2. Repair the metadata in place.

I went with repair, and the trick is that the dates were not entirely lost: icloudpd had been filing photos into YYYY/MM folders all along, using iCloud’s knowledge of when each photo was taken. The folder structure itself was a usable, month-accurate record of capture dates.

So the fix is ExifTool, run from a Docker container (ai2ys/exiftool:latest) so nothing gets installed on the host. The heart of the script is one ExifTool invocation per month folder:

docker run --rm -v /media/photos/icloud:/data ai2ys/exiftool:latest \
  exiftool -overwrite_original -r \
  -if 'not $DateTimeOriginal' \
  '-DateTimeOriginal=2014:07:01 00:00:00' \
  -ext jpg -ext heic -ext png /data/2014/07

The -if 'not $DateTimeOriginal' guard is what makes this safe: files that already have a capture date are untouched, so the script is idempotent and photos downloaded after the flag was enabled keep their exact timestamps. The wrapper script walks every YYYY/MM folder, derives the date from the path (2014/07 becomes 2014:07:01 00:00:00), scans for files missing the tag, fixes them, and prints per-folder results before moving on. Processing one month at a time keeps the output readable and makes it obvious where you are if you need to stop.

In my library that loop covered 219 month folders, and every file missing DateTimeOriginal came out with a date.

Telling Immich about it

Immich reads EXIF when it ingests an asset, so rewriting tags on disk does not update the timeline by itself. The last step is making Immich re-extract metadata: in the admin UI under Administration and Jobs, run the metadata extraction job (or rescan the external library). Once that finished, the timeline reorganized itself and the 2014 vacation went back to 2014.

Lessons

  • Immich is only as good as the EXIF it reads. Missing DateTimeOriginal does not error; it silently becomes “taken today,” which is worse than failing loudly.
  • Turn on set_exif_datetime before the first icloudpd run, not after. The flag costs nothing and its absence cost me a batch repair job.
  • The folder structure saved the day. YYYY/MM paths meant month-accurate dates were recoverable without re-downloading anything. A flat download directory would have left re-downloading as the only fix.
  • First-of-the-month is a tradeoff. Repaired photos sort into the right month, not the right day. For a back catalog that is good enough; for anything where exact dates matter, re-downloading is the correct fix.
  • Guard batch metadata writes with a condition. -if 'not $DateTimeOriginal' made the script re-runnable and protected every file that already had good data.
  • Rewriting files is half the job; the indexer has to be told. Immich needed a metadata re-extraction pass before any of it showed in the timeline.

Related reading

Infrastructure

Researching self-hosted game library consolidation

My games are scattered across Steam, GOG, Epic, Xbox, PlayStation, and a Switch. Before building anything, I went looking for a self-hosted, web-based way to see them all in one place. Here is what I evaluated, why nothing fit, and the custom build I talked myself into.

Read
Infrastructure

Self-hosting Backlogia, and fixing it before running it

Backlogia is a self-hosted app that pulls your game libraries from Steam, GOG, Epic and more into one place. Before I would run it I read the code, found four security gaps, and forked it. Then Starlette and a CORS bug had opinions too.

Read

Ready to Transform Your Career?

Let's work together to unlock your potential and achieve your professional goals.