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%
| .forgejo/workflows | ||
| src | ||
| Dockerfile | ||
| README.md | ||
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_cmdruns 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
RegionInfostruct from theasset_face+persontables, in the same shape Immich itself reads on import. exiftool with-use MWGwrites 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 aPeople/<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_serverat 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 exceptTARGET=allis 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 sortedexif:DateTimeOriginal/photoshop:DateCreated(with UTC offset where Immich knows the timezone)exif:GPSLatitude/exif:GPSLongitudexmp:Ratingmwg-rs:RegionInfo— face regions for every named person on the asset, with normalized coordinates andType=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
AppliedToDimensionsare taken fromimageWidth/imageHeighton theasset_facerow, 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 againstAppliedToDimensions, so this is correct as long as you don't hand-edit the values. Verified against Immich's own MWG import code inserver/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 inverseorientRegionInfoper Immich's metadata.service.ts). - Schema column/table case is
asset_face(singular), matching the v2-eraassets→assetrename. If your Immich is older and usesasset_faces(plural), the SQL needs a one-token edit. Verify with\dtin 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.