LostSynapse fork of skatsubo/immich-metadata-exporter Reads metadata directly from Immich Postgres and writes XMP sidecars via exiftool.
  • Shell 94.4%
  • Dockerfile 5.6%
Find a file
LostSynapse d4892320cf
All checks were successful
build / Validate (PR build-only) (push) Has been skipped
build / Build and push multi-arch image (push) Successful in 22m3s
i really want to murder claude
2026-04-30 11:53:48 -04:00
.forgejo/workflows i really want to murder claude 2026-04-30 11:53:48 -04:00
src initial commit 2026-04-30 11:11:47 -04:00
Dockerfile i really want to murder claude 2026-04-30 11:53:48 -04:00
README.md initial commit 2026-04-30 11:11:47 -04:00

immich-metadata-exporter

LostSynapse fork of skatsubo/immich-metadata-exporter. Reads metadata directly from Immich Postgres and writes XMP sidecars via exiftool.

What's different from upstream

Upstream is a host-shell tool that wraps docker exec calls into the immich_postgres and immich_server containers. That model doesn't fit Kubernetes, so this fork:

  • Replaces docker-exec wrappers with direct invocation. psql_cmd runs psql against the database using libpq env vars. The sidecar shell helper runs in-pod against a mounted external library path. No docker dependency.
  • Adds face region export. New SQL CTE builds an mwg-rs RegionInfo struct from the asset_face + person tables, in the same shape Immich itself reads on import. exiftool with -use MWG writes it directly from the JSON struct - no exiftool config required. Coordinates are normalized 0-1 with top-left origin, matching digiKam's convention.
  • Adds People/<Name> keyword tags. Every named person attached to an asset gets a People/<Name> entry merged into the existing TagsList alongside human/AI tags, in the digiKam hierarchical-tag style.
  • Bakes runtime deps into the image. Upstream apt-installs exiftool/jq/colordiff/rsync inside immich_server at runtime; we pre-install everything in the image build.

What's identical

  • The CLI surface: --target {known|unknown|all}, --filter <SQL>, --preview, --debug, --help. Same defaults except TARGET=all is now the image default.
  • The two-phase architecture: SQL extracts metadata to JSON, exiftool consumes the JSON and writes sidecars.
  • Preview mode: writes to /var/tmp/immich-metadata-exporter/preview/... and produces a colordiff plan. No write to the original location.
  • Skatsubo's tag/exif/datetime/GPS/rating/description handling, including the Luxon-tz-to-UTC-offset gymnastics.

What this writes per asset

  • dc:description / tiff:ImageDescription (description)
  • digiKam:TagsList — user tags + AI tags + People/<Name> entries, deduplicated and sorted
  • exif:DateTimeOriginal / photoshop:DateCreated (with UTC offset where Immich knows the timezone)
  • exif:GPSLatitude / exif:GPSLongitude
  • xmp:Rating
  • mwg-rs:RegionInfo — face regions for every named person on the asset, with normalized coordinates and Type=Face

What this does NOT write

  • Faces with personId IS NULL (unrecognized clusters). Skatsubo doesn't output empty-name regions and neither does this fork — empty Names clutter sidecars and digiKam treats them as placeholders.
  • Album membership. Immich exposes albums as a separate concept from tags and this tool follows that.

Caveats inherited from upstream

  • The tool rewrites entire sidecar files even if no field changed. exiftool output XML layout differs from what other tools (digiKam exiv2, Picasa) produce. File mtimes change on every run.
  • Filenames containing newlines aren't supported.
  • The UTC offset calculation is best-effort and may produce wrong offsets in edge cases (DST transitions, named-zone assets without explicit offset).

Caveats specific to this fork

  • The mwg-rs AppliedToDimensions are taken from imageWidth / imageHeight on the asset_face row, not from the original file. Immich stores face coords against the dimensions of the buffer it sent to the ML model, which is not the same as the original file dimensions. digiKam normalizes against AppliedToDimensions, so this is correct as long as you don't hand-edit the values. Verified against Immich's own MWG import code in server/src/services/metadata.service.ts — the import side normalizes the same way.
  • Image orientation: Immich's MWG import code applies an orientation transform when the image has a non-default EXIF orientation tag. This fork emits coordinates as Immich stores them, without applying the inverse transform on the way out. If you have rotated source files with EXIF Orientation set, regions in the resulting sidecars will be rotated relative to digiKam's expectation. Test on a small batch first. If this is wrong for your library, the fix lives in metadata.sql (apply the inverse orientRegionInfo per Immich's metadata.service.ts).
  • Schema column/table case is asset_face (singular), matching the v2-era assetsasset rename. If your Immich is older and uses asset_faces (plural), the SQL needs a one-token edit. Verify with \dt in psql.

Files

.
├── Dockerfile
├── README.md                      <- this file
├── .forgejo/
│   └── workflows/
│       └── build.yaml             <- multi-arch image build
└── src/
    ├── export.sh                  <- entrypoint (replaces upstream's docker-exec wrappers)
    ├── metadata.sql               <- extended SQL (faces + person tags)
    ├── sidecars.sh                <- in-pod sidecar runner (no apt-install)
    ├── export.sh.upstream         <- skatsubo original, for reference / diff
    ├── metadata.sql.upstream
    ├── sidecars.sh.upstream
    └── README.md.upstream

Building locally

podman build -t git.lost-synapse.com/lostsynapse/immich-metadata-exporter:dev .
podman push   git.lost-synapse.com/lostsynapse/immich-metadata-exporter:dev

CI builds and pushes on push to main via Forgejo Actions.

License & attribution

  • Upstream code (all files in src/*.upstream): Sergey Katsubo, license per the original repository.
  • Fork modifications: see repo LICENSE.