Photo Pipeline — migrate Google Photos to a private archive, serve via Immich, back up offsite.
A Prefect-orchestrated pipeline that downloads Google Takeout exports, fingerprints + dedups every image, applies quality analysis (blurry/dark), flows through a human review dashboard, merges approved photos into a date-sorted master archive (by_date), imports them into Immich, and protects everything with layered backups (Borg, Kopia, AWS S3).
🗺️ The pipeline (Mermaid)
flowchart LR
TO[Google Takeout<br/>134GB / 14 zips] -->|drop manifest| W[photo-watch<br/>every 15 min]
W -->|download + extract| I[photo-ingest<br/>sha256 + dhash dedup]
I -->|fingerprints| Q[quality-scan<br/>PIL blurry/dark]
Q -->|verdicts| R[Review dashboard<br/>photo-filter.home.lab]
R -->|approved| M[merge_orphans<br/>EXIF date routing]
M --> B[(by_date master<br/>35K files)]
B -->|incremental import| IM[Immich<br/>.35 library]
R -->|rejected| T[(trash → purge)]
B -.->|offsite| S3[AWS S3<br/>Glacier lifecycle]
A self-hosted Google Photos replacement pipeline. Photos leave Google via Takeout, are verified + deduplicated + quality-checked locally, reviewed by a human, then land in a date-sorted master archive (the single source of truth) and are served through Immich (a self-hosted photo server). The goal is full independence from Google + a robust local + offsite backup story.
Why it matters
Content-based dedup by sha256 — re-importing never doubles photos
Quality verdicts are deterministic (PIL image math, not ML) — reliable at 35K+ scale
Human gate — nothing merges until a person approves it
Incremental import — only new photos go to Immich (no 376GB re-uploads)
Safe cleanup — rejected → trash (recoverable) → purge only on confirmation
PHOTOS-TO-DELETE album — remove unwanted photos from Immich + by_date in one action
Dashboard service (on .13): systemctl --user restart photo-dashboard (users must refresh browser after code changes — stale JS causes "buttons do nothing")
Prefect worker: systemctl --user status prefect-worker (runs photo-pool)
The 3TB archive drive is USB — got unplugged once (Aug 21-26 2026 → backup failures); replugging resolved. Remount if /mnt/ubuntu_storage_3TB missing: the disk is sde1 (UUID 037a542c-…), sudo mount /mnt/ubuntu_storage_3TB
Immich API key lives at ~/.ssh/../photo-pipeline/.immich-key (0600) — needs user.read + asset.read + asset.upload; skip server.about scope (not granted)
382 HEIC files skip the PIL quality scan (no HEIF plugin) — safe on disk, handle with pillow-heif later
Known gap: phone-only photos uploaded directly to Immich are NOT in by_date and NOT backed up — decision deferred
Gitea repo pushes use SSH port 2222; .13 SSH can be intermittently flaky — retry after 30s if a push times out