mirror of
https://github.com/storytold/lightcraft.git
synced 2026-11-08 01:19:40 -06:00
- brand.toml is the only place the product name lives; every crate and app
reads it through dac-brand (env vars with legacy fallback, settings folder
with copy-only migration, default library, preset extensions, i18n {app})
- cargo xtask brand check|test, run|install|package, docs, rename-crates,
deny and immich; packaging files are templates rendered from brand.toml
- icons regenerated from brand/icon.svg; upstream branding removed
- MIT licence (segment and fetch stay Apache-2.0); cargo deny licence policy
- clean-room rules in AGENTS.md/CLAUDE.md; docs/upstream-merge.md
- plan/ rebuilt and committed; parity tracker reopens Classic rows, adds
PLAN.md section 2 rows and the IMM Immich section; plan/immich.md and a
pinned Immich test server
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
11 KiB
11 KiB
Immich research note
Immich is a self-hosted photo and video server. The app integrates with it (tracker section "IMM. Immich integration", PLAN.md §2.14) so the desktop catalog and an Immich server act as one library.
Checked against: Immich v3.3.1 (released 2026-10-08, the latest release on 2026-10-10), using the OpenAPI
document published with that tag (open-api/immich-openapi-specs.json, info.version 3.3.1) and a live v3.3.1
server started by cargo xtask immich up. Facts marked (verified) were confirmed against that server.
Licence note
- The Immich server, web app and mobile apps are AGPL-3.0.
- We only talk to a server over HTTP. Our client is written from the public API documentation and the OpenAPI description; we do not vendor Immich code, its generated TypeScript/Dart SDKs, or code generated from its OpenAPI file. Hand-written request/response structs for the endpoints we use are our own work.
- The test server in
xtask/immich/compose.ymlruns Immich's published container images unmodified; nothing from them is distributed with the app.
Versions
- Minimum supported: v3.0.0. The v3 line is what we test. Endpoints we rely on were all marked stable in v2, but behaviour we depend on (rating −1 = rejected, the v3 deprecations below) is checked only on v3. Older servers get a clear "please upgrade" message from IMM-CONNECT rather than partial behaviour.
- Version check:
GET /api/server/version→{major, minor, patch}(no auth needed);GET /api/server/ping→{"res":"pong"}for reachability. Store the version per server and gate features on it. - API history metadata: every operation in the OpenAPI file carries
x-immich-history(added / beta / stable / deprecated per version) andx-immich-permission. Use them when moving to a new release: diff the operations we call and fail the integration tests on a removed one. - Deprecations to watch (v3):
PUTupdates of single objects (PUT /assets/{id},PUT /assets,PUT /tags/{id},PUT /people/{id},PUT /stacks/{id},PUT /libraries/{id},PUT /api-keys/{id}) are marked deprecated with a replacement of the same operation id; they still work in v3.3.1 (verified forPUT /assets/{id}). Wrap each update behind one function so the switch is local.GET /server/configand/server/featuresare deprecated in favour of a public-config endpoint (v3.2.0). - Moving to a new release: update the four images in
xtask/immich/compose.ymlfrom that release's own compose file, re-run the integration tests, then update "Checked against" here.
Authentication, API keys and permissions
- API key (what the app stores): header
x-api-key: <secret>. Created by a logged-in user withPOST /api/api-keys {name, permissions[]}; the response carriessecretonce (verified). Keys are per user. - Permissions are fine-grained strings (
asset.read,asset.upload,asset.update,asset.download,album.create,albumAsset.create,tag.asset,person.read,sharedLink.create,library.read,stack.create, …) orall. IMM-CONNECT should ask for the minimal set per feature and explain which features a key with fewer permissions disables.GET /api/api-keys/mereads the current key's permissions. - Session login (test tooling only):
POST /api/auth/login {email, password}→accessToken, used asAuthorization: Bearer. The first user is created withPOST /api/auth/admin-sign-up {email, password, name}, which is refused once an admin exists (verified). - The key is a secret: stored in the OS keychain (
credentialscrate), never in the catalog, logs or XMP.
Concepts
| Concept | What it is | How we map it |
|---|---|---|
| Asset | one photo or video owned by one user (id UUID, type IMAGE/VIDEO, originalFileName, originalPath, fileCreatedAt, localDateTime, exifInfo, isFavorite, visibility, isTrashed, libraryId, stack, people, tags) |
a catalog photo, linked through the remote_identity table (server, asset id, checksum) |
| Checksum | SHA-1 of the original file, base64 encoded in AssetResponseDto.checksum *(verified: equals `openssl sha1 -binary |
base64`)*; inputs to the duplicate check also accept hex |
| Duplicate detection | an upload whose checksum matches an existing asset of the same user is not stored again: POST /assets answers status: "duplicate" with the existing id (verified); POST /assets/bulk-upload-check answers action: reject, reason: duplicate, assetId per item without uploading (verified) |
link before upload; never upload twice |
| Visibility | timeline (normal), archive (hidden from the timeline), hidden (e.g. the video half of a live photo), locked (locked folder, needs a PIN session) |
archive ↔ an "archived" flag in sync; never touch locked assets |
| Album | a named, ordered set of assets, possibly shared with other users | collection ↔ album (IMM-PUBLISH, IMM-SYNC) |
| Tag | hierarchical tags (parent/child values), assigned to assets |
keyword hierarchy ↔ tags |
| Person / face | people with names and face boxes per asset (/people, /faces), found by Immich's ML service |
IMM-PEOPLE: import names and regions; push names back |
| Stack | a group of assets with a primary (POST /stacks {assetIds}, first id = primary) |
our stacks; IMM-PUBLISH stacks a render with its original |
| External library | an admin-defined set of server-side folders (importPaths, exclusionPatterns) that Immich scans and indexes in place (read-only originals; isOffline when a file disappears) |
IMM-EXTLIB: the catalog and Immich index the same folders; link by checksum or by path mapping, never upload |
| Shared link | a public URL for an album (type: ALBUM) or for chosen assets (INDIVIDUAL), with optional expiry, password, download/upload permission, metadata visibility and custom slug |
IMM-SHARELINK |
| Trash | deleted assets go to trash first (isTrashed), restorable via /trash/restore/assets |
IMM-PUBLISH removes only by trashing, never force-deletes |
| Smart search | CLIP embedding search computed by the ML service | IMM-SEARCH |
Ratings and favourites
- Rating lives in the asset's EXIF info (
exifInfo.rating) and is set withUpdateAssetDto.rating. Valid values: 1–5 (stars), −1 (rejected), or null (unrated). 0 is refused with a validation error (verified on v3.3.1). Mapping: our 0 stars ↔ null; our reject flag ↔ −1 only when the photo has no star rating (one field cannot hold both, so the conflict rule in IMM-SYNC is: a reject wins on push, and a −1 from Immich sets reject and clears stars). - Immich reads the rating from the file's metadata (XMP/EXIF) on upload; the web UI shows ratings only when the user enables them in their preferences, but the API accepts them regardless.
- Favourite (
isFavorite) is a separate per-asset boolean, independent of rating. Mapping: favourite ↔ pick flag (configurable: pick or a chosen colour label), as in PLAN.md §2.14. - Description (
UpdateAssetDto.description, shown inexifInfo.description) ↔ our caption. Location (latitude,longitude) and capture time (dateTimeOriginal) are writable the same way. - Immich has no colour labels and no title field separate from description; those stay local (or go to tags if the user opts in).
Endpoints we need
All paths are under /api. Permission in brackets.
Connect (IMM-CONNECT)
GET /server/ping,GET /server/version,GET /server/about[server.about],GET /server/media-typesGET /users/me[user.read],GET /api-keys/me
Link and import (IMM-LINK, IMM-IMPORT)
POST /assets/bulk-upload-check[asset.upload]: checksum → existing asset id, without uploadingPOST /search/metadata[asset.read]: filter bychecksum,originalPath,libraryId,albumIds,personIds,tagIds,isFavorite,rating, dates,visibility; paged withpage/sizeGET /assets/{id}[asset.read],GET /assets/{id}/original[asset.download],GET /assets/{id}/thumbnail[asset.view]GET /albums/GET /albums/{id}[album.read]- Timeline browsing uses
/timeline/bucketsand/timeline/bucket, but they are marked internal (v1); preferPOST /search/metadatawith date ranges and only fall back to the buckets for speed.
External library (IMM-EXTLIB)
GET /libraries,GET /libraries/{id}[library.read],POST /libraries[library.create, admin],POST /libraries/{id}/scan[library.update],POST /libraries/{id}/validate
Publish (IMM-PUBLISH)
POST /assets[asset.upload] multipart:assetData,fileCreatedAt,fileModifiedAt(required),filename,isFavorite,visibility, optionalsidecarData; headerx-immich-checksum(SHA-1) lets the server reject a duplicate before reading the body (upload verified)POST /albums[album.create],PUT /albums/{id}/assetsandPUT /albums/assets[albumAsset.create],DELETE /albums/{id}/assets[albumAsset.delete],PATCH /albums/{id}[album.update]POST /stacks[stack.create],DELETE /stacks/{id}/assets/{assetId}[stack.update]DELETE /assets[asset.delete] (to trash),POST /trash/restore/assets
Sync (IMM-SYNC)
PUT /assets/{id}/PUT /assets[asset.update]:rating,isFavorite,description,latitude,longitude,dateTimeOriginal,visibility(deprecated method, see Versions)GET /tags,PUT /tags(upsert by value) [tag.create],PUT /tags/assets(bulk) /PUT /tags/{id}/assets/DELETE /tags/{id}/assets[tag.asset]- Change feed:
POST /sync/stream[sync.stream] withPOST /sync/ackcheckpoints is what Immich's own mobile app uses for incremental sync. Prefer it over polling once evaluated; until then poll withupdatedAfterinPOST /search/metadata.
People (IMM-PEOPLE)
GET /people,GET /people/{id},GET /people/{id}/thumbnail[person.read],GET /faces?id=<assetId>[face.read],PUT /people(bulk rename) [person.update],POST /people/merge(v3.2.1)
Search (IMM-SEARCH)
POST /search/smart[asset.read]:query(natural language), orqueryAssetId(find similar), plus the same filters as metadata search;GET /search/suggestions,GET /search/explore,GET /search/places
Shared links (IMM-SHARELINK)
POST /shared-links[sharedLink.create]:typeALBUM / INDIVIDUAL,albumIdorassetIds,expiresAt,password,allowDownload,allowUpload,showMetadata,description,slug;GET /shared-links,PATCH /shared-links/{id},DELETE /shared-links/{id}
Test server
cargo xtask immich up | seed | down [--volumes] (xtask/src/immich.rs):
xtask/immich/compose.ymlpinsimmich-serverandimmich-machine-learningtov3.3.1, and the Valkey 9 andimmich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0images by digest, exactly as that release's compose file does. Data lives in docker volumes; the server listens on127.0.0.1:2284(not the default 2283).seedcreatesadmin@example.invalid, logs in, creates an API key withallpermissions (written totarget/immich/api-key, mode 0600), uploads 8 small PNGs generated by the xtask (our own work, CC0) and puts them in the album "xtask fixtures". It is idempotent: later runs see the uploads as duplicates.IMMICH_URLpoints the seed at another test server.