#!/usr/bin/env python3 """ immich_upload.py — nahrava wanted fotky z DB fotky_buzalkovi do Immiche. Cte primo ze zalohy (NFS), nahrava KOPII pres Immich API. Zalohu nesaha. Resumable: tabulka immich_upload (zaloha_id -> asset id, status). Spousti se v kontejneru python-runner: docker exec python-runner python3 /scripts/immich_upload.py --like '...%' --limit 200 """ import os import sys import argparse from datetime import datetime, timezone import requests import psycopg sys.stdout.reconfigure(encoding="utf-8") DB = dict(host="192.168.1.76", port=5432, user="vladimir.buzalka", password="Vlado7309208104++", dbname="fotky_buzalkovi") IMMICH = "http://192.168.1.76:8888" APIKEY = "UQV5PS1Td50hKOZTItnXEcXVkfQSUxcUH0XHZYxc" SRC_PREFIX = "/mnt/user/ZalohaVsechObrazku" # jak je cesta v DB DST_PREFIX = "/mnt/ZalohaVsechObrazku" # jak ji vidi kontejner ap = argparse.ArgumentParser() ap.add_argument("--like", default=None, help="filtr na z.cesta_zalohy LIKE") ap.add_argument("--category", default=None, help="filtr na p.category") ap.add_argument("--limit", type=int, default=0) ap.add_argument("--dry-run", action="store_true") args = ap.parse_args() conn = psycopg.connect(**DB, autocommit=True) cur = conn.cursor() cur.execute(""" CREATE TABLE IF NOT EXISTS immich_upload( zaloha_id bigint PRIMARY KEY, immich_id uuid, status text, uploaded_at timestamptz DEFAULT now() ) """) sql = """ SELECT z.id, z.cesta_zalohy, p.taken_at FROM photos p JOIN zaloha_obrazku z ON p.zaloha_id = z.id LEFT JOIN immich_upload iu ON iu.zaloha_id = z.id WHERE p.wanted = TRUE AND iu.zaloha_id IS NULL """ params = [] if args.category: sql += " AND p.category = %s"; params.append(args.category) if args.like: sql += " AND z.cesta_zalohy LIKE %s"; params.append(args.like) sql += " ORDER BY z.cesta_zalohy" if args.limit: sql += " LIMIT %s"; params.append(args.limit) cur.execute(sql, params) rows = cur.fetchall() total = len(rows) print(f"K nahrani (jeste nenahrano): {total:,}") if args.dry_run: miss = sum(1 for _, c, _ in rows if not os.path.isfile(c.replace(SRC_PREFIX, DST_PREFIX, 1))) print(f"DRY-RUN: chybejicich souboru: {miss}") sys.exit(0) sess = requests.Session() sess.headers.update({"x-api-key": APIKEY, "Accept": "application/json"}) created = dup = err = 0 for i, (zid, cesta, taken) in enumerate(rows, 1): path = cesta.replace(SRC_PREFIX, DST_PREFIX, 1) if not os.path.isfile(path): err += 1 print("MISSING:", path) continue st = os.stat(path) fmod = datetime.fromtimestamp(st.st_mtime, tz=timezone.utc) fcreated = taken if taken else fmod if fcreated.tzinfo is None: fcreated = fcreated.replace(tzinfo=timezone.utc) try: with open(path, "rb") as fh: r = sess.post( IMMICH + "/api/assets", files={"assetData": (os.path.basename(path), fh, "application/octet-stream")}, data={ "deviceAssetId": f"fb-{zid}", "deviceId": "fotky-buzalkovi", "fileCreatedAt": fcreated.isoformat(), "fileModifiedAt": fmod.isoformat(), "isFavorite": "false", }, timeout=300, ) j = r.json() status = j.get("status") aid = j.get("id") cur.execute( """INSERT INTO immich_upload(zaloha_id, immich_id, status) VALUES (%s,%s,%s) ON CONFLICT (zaloha_id) DO UPDATE SET immich_id=EXCLUDED.immich_id, status=EXCLUDED.status, uploaded_at=now()""", (zid, aid, status), ) if status == "created": created += 1 elif status == "duplicate": dup += 1 else: err += 1 print("? neznamy status:", status, j) except Exception as e: err += 1 print("ERR", zid, repr(e)) if i % 50 == 0 or i == total: print(f"... {i}/{total} created={created} dup={dup} err={err}", flush=True) print(f"\nHOTOVO. created={created} dup={dup} err={err}") conn.close()