Metadata-Version: 2.5
Name: immich-export
Version: 1.0.1
Summary: Export everything out of Immich — files plus albums, people, tags, descriptions — into a redundant, human-readable local folder tree.
Project-URL: Homepage, https://github.com/fileworks/immich-export
Project-URL: Repository, https://github.com/fileworks/immich-export
Project-URL: Issues, https://github.com/fileworks/immich-export/issues
Project-URL: Changelog, https://github.com/fileworks/immich-export/blob/main/CHANGELOG.md
Author: gykonik
License-Expression: MIT
License-File: LICENSE
Keywords: backup,export,immich,photos,xmp
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: End Users/Desktop
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: System :: Archiving :: Backup
Requires-Python: >=3.12
Requires-Dist: httpx>=0.28
Requires-Dist: pydantic>=2.9
Requires-Dist: typer>=0.15
Description-Content-Type: text/markdown

<img src=".github/icon.svg" alt="" width="72" height="72" align="left">

# immich-export

## Overview

Export supported originals and metadata from [Immich](https://immich.app) —
albums, people, tags, descriptions, favorites, and coordinates — into a
human-readable local folder tree.

The result is useful for inspection, migration, and an additional local copy.
It is not a replacement for independent, tested backups of Immich and its
database.

```
immich-export/
  library/2024/03/IMG_1234.jpg          # primary tree (self-contained mode)
  library/2024/03/IMG_1234.jpg.xmp      # sidecar: tags, people, albums, description, geo, favorite
  albums/Japan-2019/IMG_1234.jpg        # → symlink into library/
  people/Anna/IMG_1234.jpg              # → symlink into library/
  manifest.jsonl                        # append-only verified-state history
  manifest-current.jsonl                # authoritative current verified set
  manifest.csv                          # human-readable current projection
  export-report.txt                     # counts, warnings, errors, timing
```

## Status

The latest verified public release is 1.0.0 across GitHub Releases, PyPI, and
the Fileworks Homebrew tap.

## Install

```sh
pipx install immich-export
# or
brew install fileworks/tap/immich-export
```

Released **1.0.0** on
[PyPI](https://pypi.org/project/immich-export/1.0.0/),
[GitHub](https://github.com/fileworks/immich-export/releases/tag/v1.0.0), and
`fileworks/tap` (verified 2026-08-12).

## Quick start

```sh
export IMMICH_SERVER=https://immich.local:2283
export IMMICH_API_KEY=…            # never passed on the command line
immich-export --out ~/immich-export
```

The first run copies originals and writes sidecars. Every later run rehashes
what is already there and transfers only what changed.

## Usage

```sh
export IMMICH_SERVER=https://immich.local:2283
export IMMICH_API_KEY=...   # Immich → Account Settings → API Keys

# full portable export (copies originals)
immich-export --out ./immich-export

# verified re-run: local originals are rehashed; only missing/changed bytes download
immich-export --out ./immich-export

# sidecar mode: you already have the Storage-Template tree mounted —
# only write .xmp sidecars + album/people views next to it
immich-export --mode sidecar --library-root /volume1/photos --out /volume1/photos

# custom primary tree
immich-export --layout "{year}/{album}" --out ./export
```

Key flags (see `immich-export --help` for all):

| Flag | Default | Meaning |
|---|---|---|
| `--mode` | `self-contained` | `self-contained` copies originals; `sidecar` only writes XMP + views next to an existing tree |
| `--layout` | `{year}/{month}` | primary tree; tokens `{year} {month} {day} {album} {type}`; `{album}` falls back to `Unsorted` |
| `--album-view` / `--people-view` | on | build `albums/` and `people/` symlink views |
| `--sidecars` | `xmp` | `xmp` or `none` |
| `--since` | — | only assets taken on/after this date |
| `--resume` | on | use prior state as a resume/migration hint; local bytes are still rehashed |
| `--include-hidden` | off | also export hidden + locked-folder assets |
| `--stale-assets` | `keep` | keep/report absent outputs or explicitly move owned outputs to `quarantine` |
| `--concurrency` | `4` | bound concurrent API work, downloads, and local verification |
| `--manifest-batch-size` | `128` | verified history records per durable synchronization |
| `--manifest-flush-interval` | `0.1` | maximum seconds before a partial durable group is synchronized |
| `--history-max-records` | `100000` | rotate the bounded active history at this record count |
| `--history-max-bytes` | `134217728` | rotate the active history at this byte count |
| `--log-file` | `<out>/immich-export.log` | bounded rotating timestamped diagnostics |

## Verified behavior

- **Read-only against Immich.** Never writes back.
- **Verified originals.** Downloads are SHA-1 checked before atomic promotion,
  and existing local originals are rehashed on every run in both modes.
- **Canonical metadata and XMP.** All persisted/path/XMP fields share one typed
  state. Missing, malformed, or stale required XMP is atomically refreshed.
- **History versus current.** `manifest.jsonl` is bounded active audit history;
  digest-linked verified archives live under `manifest-history/`.
  `manifest-current.jsonl`, its CSV projection, and generated views contain only
  assets verified by the latest completed compatible scan.
- **Partial runs are explicit.** Asset-specific integrity failures are reported,
  excluded from current state, and return exit code `1` (`PARTIAL`); run-level
  failures do not replace the prior current snapshot.
- **Conservative reconciliation.** A compatible full scan removes absent assets
  from current state and views. Their files remain reported orphans by default.
  Explicit quarantine moves only manifest-owned outputs; sidecar mode never
  moves Immich-managed originals.
- **Bounded work and visible progress.** Assets are paged, concurrent work is
  bounded, and terminal/log progress remains available.

These checks cover the API fields and files the exporter supports. They cannot
detect metadata Immich does not expose, storage failures that occur after a
successful verification, or prove that an export can restore an entire Immich
installation. Keep separate backups and test restoration procedures.

## Exit codes

`immich-export`, `paperless-export` and `unpacksort` share one exit-code
vocabulary, so a script can branch on the code without knowing which tool it
ran. The class of outcome is the same everywhere; the specific condition is
this tool's, and the table below is what it means here.

| Code | Name | Meaning |
|---|---|---|
| 0 | `SUCCESS` | everything asked for was done |
| 1 | `PARTIAL` | the run finished with one or more asset failures; successes are published |
| 2 | `USAGE` | bad flags, paths, or credentials the server rejected — nothing was attempted |
| 3 | `CONFLICT` | the Immich server is unreachable |
| 4 | `FATAL` | unexpected failure, or output that could not be written (re-run with `--verbose`) |
| 130 | `INTERRUPTED` | cancelled by the operator |

## Sidecar format

Standard XMP wherever a standard slot exists — `dc:subject` (tags),
`Iptc4xmpExt:PersonInImage` (people), `dc:description`, `photoshop:DateCreated`,
`exif:GPSLatitude/Longitude`, `xmp:Rating` (favorite → 5) — so digiKam,
Lightroom and exiftool can read them. Album membership and Immich ids live in a
custom `immich:` namespace in the same file.

When XMP is enabled, an asset is current only after its canonical sidecar
matches the manifest state. Exact generated-state validation removes metadata
that was deleted in Immich rather than retaining stale XMP nodes.

## Immich API compatibility

Built against the **Immich v3 API** (spec version 3.0.1). Instead of a
generated client, the exact API slice used is declared in
`src/immich_export/api_contract.py` and checked in CI against a vendored,
pruned copy of the official OpenAPI spec. To check a new Immich release:

```sh
uv run python scripts/refresh_api_spec.py --ref v3.1.0
uv run pytest tests/test_contract.py
```

A removed endpoint or field fails the tests *before* it breaks at runtime.

## Configuration

Everything is configured through environment variables and flags; there is no
configuration file to keep in sync.

| Variable | Purpose |
|---|---|
| `IMMICH_SERVER` | Base URL of your Immich instance |
| `IMMICH_API_KEY` | API key. Read from the environment only, never from argv |
| `IMMICH_EXPORT_CONCURRENCY` | Parallel downloads (default: conservative) |
| `IMMICH_EXPORT_MANIFEST_BATCH_SIZE` | Records per durable history group |
| `IMMICH_EXPORT_MANIFEST_FLUSH_INTERVAL` | Maximum partial-group delay in seconds |
| `IMMICH_EXPORT_HISTORY_MAX_RECORDS` | Active-history record rotation threshold |
| `IMMICH_EXPORT_HISTORY_MAX_BYTES` | Active-history byte rotation threshold |
| `IMMICH_EXPORT_LOG_FILE` | Rotating logfile path |

Flags are documented under [Usage](#usage); `--help` is authoritative.

## Troubleshooting

**The run stops with a network error.** Re-run it. The manifest records what was
verified, so a repeat run resumes rather than restarting.

**A previous run was interrupted and the manifest looks damaged.** Damaged lines
are skipped and reported; the export continues from the last intact state.

**Nothing is downloaded and the count is zero.** Check that `IMMICH_SERVER`
points at the API root and that the key has library access — a wrong base URL
authenticates fine and returns nothing.

**Symlinks fail on the target.** Album and people views require symlinks.
Disable them with `--no-album-view --no-people-view` on FAT/exFAT or cloud
folders that cannot represent links; the verified primary export remains
available.

## Development

```sh
uv sync --locked --all-extras --all-groups
uv run ruff check . && uv run ruff format --check .   # lint
uv run mypy                                           # strict types
uv run pytest                                         # tests (mock Immich API)
uv build                                              # sdist + wheel
```

Pull requests run linting, strict types, tests, and builds on Python 3.12, 3.13,
and 3.14, plus documentation links, dependency auditing, and an installed-wheel
smoke test. Renovate batches routine dependency changes into one Monday PR and
automerge is enabled; a per-repository concurrency limit of one prevents stacked
dependency branches or PRs.

A separate Sunday workflow runs the real disk-backed reconcile and publication
budget at 500,000 assets, keeping schedule-only work out of pull-request and
`main` CI checks.

Conventional Commits drive `python-semantic-release`. After CI passes on a
`main` push, the release workflow pins the tested SHA, verifies the staged
source, tag, distributions, and installed CLI, then atomically pushes the
release commit and tag. Publication proceeds through the protected
`github-release`, `pypi`, and `homebrew` environments in that order; PyPI uses
trusted publishing and GitHub Releases carry the verified wheel and sdist.

For per-clone paths, commands, or preferences, create an ignored
`CLAUDE.local.md` at the repository root. Do not put credentials or other
secrets in it.

## Security

Report vulnerabilities privately as described in [SECURITY.md](SECURITY.md).
The API key is read from the environment and never written to disk, a log line,
or the manifest.

## License

MIT
