diff --git a/00 PictureCollector/immich_upload.py b/00 PictureCollector/immich_upload.py new file mode 100644 index 0000000..3c61f32 --- /dev/null +++ b/00 PictureCollector/immich_upload.py @@ -0,0 +1,125 @@ +#!/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() diff --git a/00 PictureCollector/mark_cache_unwanted.py b/00 PictureCollector/mark_cache_unwanted.py new file mode 100644 index 0000000..9d17385 --- /dev/null +++ b/00 PictureCollector/mark_cache_unwanted.py @@ -0,0 +1,47 @@ +""" +mark_cache_unwanted.py — Označí celé adresáře Tower/appdata a Tower/Sabnzbd +jako nechceme (cache PhotoPrism/Immich + usenet download cache). + +wanted=FALSE, category='Odpad-cache' + +Idempotentní: díky filtru `category IS DISTINCT FROM 'Odpad-cache'` přepíše +jen řádky, které ještě nejsou označené (vč. těch, co camera-rules omylem +označily jako Rodina — jde o duplicitní cache kopie, ne originály). + +Jediná DB session, autocommit — žádné paralelní běhy (deadlock). +""" +import sys +import psycopg2 + +sys.stdout.reconfigure(encoding="utf-8") + +DB = dict(host="192.168.1.76", port=5432, user="vladimir.buzalka", + password="Vlado7309208104++", database="fotky_buzalkovi") + +DIRS = [ + ("Tower/appdata (PhotoPrism/Immich cache)", + "/mnt/user/ZalohaVsechObrazku/Tower/appdata%"), + ("Tower/Sabnzbd (usenet download cache)", + "/mnt/user/ZalohaVsechObrazku/Tower/Sabnzbd%"), +] + +conn = psycopg2.connect(**DB) +conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT) +cur = conn.cursor() + +total = 0 +for popis, like in DIRS: + cur.execute(""" + UPDATE photos p + SET wanted = FALSE, + category = 'Odpad-cache' + FROM zaloha_obrazku z + WHERE p.zaloha_id = z.id + AND z.cesta_zalohy LIKE %s + AND p.category IS DISTINCT FROM 'Odpad-cache' + """, (like,)) + print(f" {popis}: {cur.rowcount:,} řádků aktualizováno") + total += cur.rowcount + +print(f"\nHotovo. Celkem aktualizováno: {total:,} řádků.") +conn.close() diff --git a/00 PictureCollector/mark_colddata_junk.py b/00 PictureCollector/mark_colddata_junk.py new file mode 100644 index 0000000..5a7788e --- /dev/null +++ b/00 PictureCollector/mark_colddata_junk.py @@ -0,0 +1,48 @@ +""" +mark_colddata_junk.py — Označí jasně odpadní podsložky v Tower1/#ColdData +jako nechceme. NEoznačuje SynologyMaly, Honza, VladkoSoubory, Qnap, Tatinek, +DedupPhotos (ty se zkoumají zvlášť). + +Kategorie: + Porno*, MadelineIsWicked -> 'Porno' + 000 TORENT OBRAZKY, MoMA, Dali, + hudební alba, eBooky -> 'Odpad-torrent' +""" +import sys +import psycopg2 + +sys.stdout.reconfigure(encoding="utf-8") + +DB = dict(host="192.168.1.76", port=5432, user="vladimir.buzalka", + password="Vlado7309208104++", database="fotky_buzalkovi") + +BASE = "/mnt/user/ZalohaVsechObrazku/Tower1/#ColdData/" + +# (popis, LIKE vzor, kategorie) +RULES = [ + ("Porno", BASE + "Porno/%", "Porno"), + ("Porno1", BASE + "Porno1/%", "Porno"), + ("MadelineIsWicked", BASE + "MadelineIsWicked/%", "Porno"), + ("000 TORENT OBRAZKY",BASE + "000 TORENT OBRAZKY/%","Odpad-torrent"), + ("Museum of Modern Art NY", BASE + "Museum of Modern Art NY/%", "Odpad-torrent"), +] + +conn = psycopg2.connect(**DB) +conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT) +cur = conn.cursor() + +total = 0 +for popis, like, kat in RULES: + cur.execute(""" + UPDATE photos p + SET wanted = FALSE, category = %s + FROM zaloha_obrazku z + WHERE p.zaloha_id = z.id + AND z.cesta_zalohy LIKE %s + AND p.category IS DISTINCT FROM %s + """, (kat, like, kat)) + print(f" {popis:28s} -> {kat:14s}: {cur.rowcount:,}") + total += cur.rowcount + +print(f"\nHotovo. Celkem: {total:,} řádků označeno jako odpad.") +conn.close() diff --git a/00 PictureCollector/mark_colddata_zbytek.py b/00 PictureCollector/mark_colddata_zbytek.py new file mode 100644 index 0000000..28791cd --- /dev/null +++ b/00 PictureCollector/mark_colddata_zbytek.py @@ -0,0 +1,29 @@ +""" +mark_colddata_zbytek.py — Zbytek #ColdData (vše dosud neroztříděné): +DropBox (software help obrázky), hudební alba, Salvador Dali, eBooky, +Truecrypt, Delikatesy ... -> odpad. +wanted=FALSE, category='Odpad-cache' +""" +import sys +import psycopg2 + +sys.stdout.reconfigure(encoding="utf-8") + +DB = dict(host="192.168.1.76", port=5432, user="vladimir.buzalka", + password="Vlado7309208104++", database="fotky_buzalkovi") + +conn = psycopg2.connect(**DB) +conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT) +cur = conn.cursor() + +cur.execute(""" + UPDATE photos p + SET wanted = FALSE, category = 'Odpad-cache' + FROM zaloha_obrazku z + WHERE p.zaloha_id = z.id + AND z.cesta_zalohy LIKE '/mnt/user/ZalohaVsechObrazku/Tower1/#ColdData/%' + AND p.category IS NULL +""") + +print(f"#ColdData zbytek -> Odpad-cache: {cur.rowcount:,} řádků") +conn.close() diff --git a/00 PictureCollector/mark_dedup_rodina.py b/00 PictureCollector/mark_dedup_rodina.py new file mode 100644 index 0000000..ee64b42 --- /dev/null +++ b/00 PictureCollector/mark_dedup_rodina.py @@ -0,0 +1,27 @@ +""" +mark_dedup_rodina.py — DedupPhotos (Samsung S7 Edge 2019, 285 ks). +Ověřeno: všechny unikátní, žádné duplikáty jinde -> Rodina. +""" +import sys +import psycopg2 + +sys.stdout.reconfigure(encoding="utf-8") + +DB = dict(host="192.168.1.76", port=5432, user="vladimir.buzalka", + password="Vlado7309208104++", database="fotky_buzalkovi") + +conn = psycopg2.connect(**DB) +conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT) +cur = conn.cursor() + +cur.execute(""" + UPDATE photos p + SET wanted = TRUE, category = 'Rodina' + FROM zaloha_obrazku z + WHERE p.zaloha_id = z.id + AND z.cesta_zalohy LIKE %s + AND (p.wanted = FALSE OR p.category IS DISTINCT FROM 'Rodina') +""", ('/mnt/user/ZalohaVsechObrazku/Tower1/#ColdData/DedupPhotos/%',)) + +print(f"DedupPhotos -> Rodina: {cur.rowcount:,} řádků") +conn.close() diff --git a/00 PictureCollector/mark_ltbs_212.py b/00 PictureCollector/mark_ltbs_212.py new file mode 100644 index 0000000..281001e --- /dev/null +++ b/00 PictureCollector/mark_ltbs_212.py @@ -0,0 +1,32 @@ +""" +mark_ltbs_212.py — Potvrzeno uživatelem: + LTBS -> Rodina (rodinné fotky, názvy obsahují BUZALKA) + 212 -> Odpad-dokumenty (skeny smluv Confidentiality Agreement) +""" +import sys +import psycopg2 + +sys.stdout.reconfigure(encoding="utf-8") + +DB = dict(host="192.168.1.76", port=5432, user="vladimir.buzalka", + password="Vlado7309208104++", database="fotky_buzalkovi") + +B = "/mnt/user/ZalohaVsechObrazku/Tower1/#ColdData/" +RULES = [ + ("LTBS", B + "LTBS/%", True, "Rodina"), + ("212", B + "212/%", False, "Odpad-dokumenty"), +] + +conn = psycopg2.connect(**DB) +conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT) +cur = conn.cursor() +for popis, like, wanted, kat in RULES: + cur.execute(""" + UPDATE photos p SET wanted=%s, category=%s + FROM zaloha_obrazku z + WHERE p.zaloha_id=z.id AND z.cesta_zalohy LIKE %s + AND (p.wanted IS DISTINCT FROM %s OR p.category IS DISTINCT FROM %s) + """, (wanted, kat, like, wanted, kat)) + print(f" {popis:6s} -> {kat:16s} (wanted={wanted}): {cur.rowcount:,}") +conn.close() +print("Hotovo.") diff --git a/00 PictureCollector/mark_pomoc.py b/00 PictureCollector/mark_pomoc.py new file mode 100644 index 0000000..f1ede3f --- /dev/null +++ b/00 PictureCollector/mark_pomoc.py @@ -0,0 +1,41 @@ +""" +mark_pomoc.py — Tower1/#ColdData/pomoc, potvrzeno uživatelem: + Samsung GT-S5230 (s EXIF) -> Rodina + porno snapshoty (bez EXIF) -> Porno +""" +import sys +import psycopg2 + +sys.stdout.reconfigure(encoding="utf-8") + +DB = dict(host="192.168.1.76", port=5432, user="vladimir.buzalka", + password="Vlado7309208104++", database="fotky_buzalkovi") + +P = "/mnt/user/ZalohaVsechObrazku/Tower1/#ColdData/pomoc/%" + +conn = psycopg2.connect(**DB) +conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT) +cur = conn.cursor() + +# Samsung (s EXIF) -> Rodina +cur.execute(""" + UPDATE photos p SET wanted=TRUE, category='Rodina' + FROM zaloha_obrazku z + WHERE p.zaloha_id=z.id AND z.cesta_zalohy LIKE %s + AND p.camera_make IS NOT NULL + AND (p.wanted=FALSE OR p.category IS DISTINCT FROM 'Rodina') +""", (P,)) +print(f" pomoc Samsung -> Rodina: {cur.rowcount:,}") + +# porno (bez EXIF) -> Porno +cur.execute(""" + UPDATE photos p SET wanted=FALSE, category='Porno' + FROM zaloha_obrazku z + WHERE p.zaloha_id=z.id AND z.cesta_zalohy LIKE %s + AND p.camera_make IS NULL + AND p.category IS DISTINCT FROM 'Porno' +""", (P,)) +print(f" pomoc porno -> Porno : {cur.rowcount:,}") + +conn.close() +print("Hotovo.") diff --git a/00 PictureCollector/mark_qnap_exif_odpad.py b/00 PictureCollector/mark_qnap_exif_odpad.py new file mode 100644 index 0000000..276b10d --- /dev/null +++ b/00 PictureCollector/mark_qnap_exif_odpad.py @@ -0,0 +1,29 @@ +""" +mark_qnap_exif_odpad.py — Qnap fotky s EXIF jsou CD inlety/obaly -> odpad. +Potvrzeno uživatelem přes náhled (81 ks). +wanted=FALSE, category='Odpad-obaly' +""" +import sys +import psycopg2 + +sys.stdout.reconfigure(encoding="utf-8") + +DB = dict(host="192.168.1.76", port=5432, user="vladimir.buzalka", + password="Vlado7309208104++", database="fotky_buzalkovi") + +conn = psycopg2.connect(**DB) +conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT) +cur = conn.cursor() + +cur.execute(""" + UPDATE photos p + SET wanted = FALSE, category = 'Odpad-obaly' + FROM zaloha_obrazku z + WHERE p.zaloha_id = z.id + AND z.cesta_zalohy LIKE %s + AND p.camera_make IS NOT NULL + AND p.category IS DISTINCT FROM 'Odpad-obaly' +""", ('/mnt/user/ZalohaVsechObrazku/Tower1/#ColdData/Qnap/%',)) + +print(f"Qnap (EXIF) -> Odpad-obaly: {cur.rowcount:,} řádků") +conn.close() diff --git a/00 PictureCollector/mark_qnap_nepotrebne.py b/00 PictureCollector/mark_qnap_nepotrebne.py new file mode 100644 index 0000000..fb9fe4f --- /dev/null +++ b/00 PictureCollector/mark_qnap_nepotrebne.py @@ -0,0 +1,28 @@ +""" +mark_qnap_nepotrebne.py — Qnap_nepotrebne (COMPLETED TORRENTS: komiksy, +učebnice, software) -> odpad. Potvrzeno uživatelem. +wanted=FALSE, category='Odpad-torrent' +""" +import sys +import psycopg2 + +sys.stdout.reconfigure(encoding="utf-8") + +DB = dict(host="192.168.1.76", port=5432, user="vladimir.buzalka", + password="Vlado7309208104++", database="fotky_buzalkovi") + +conn = psycopg2.connect(**DB) +conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT) +cur = conn.cursor() + +cur.execute(""" + UPDATE photos p + SET wanted = FALSE, category = 'Odpad-torrent' + FROM zaloha_obrazku z + WHERE p.zaloha_id = z.id + AND z.cesta_zalohy LIKE %s + AND p.category IS DISTINCT FROM 'Odpad-torrent' +""", ('/mnt/user/ZalohaVsechObrazku/Tower1/#ColdData/Qnap_nepotrebne/%',)) + +print(f"Qnap_nepotrebne -> Odpad-torrent: {cur.rowcount:,} řádků") +conn.close() diff --git a/00 PictureCollector/mark_qnap_zbytek_odpad.py b/00 PictureCollector/mark_qnap_zbytek_odpad.py new file mode 100644 index 0000000..2579e7e --- /dev/null +++ b/00 PictureCollector/mark_qnap_zbytek_odpad.py @@ -0,0 +1,29 @@ +""" +mark_qnap_zbytek_odpad.py — Zbytek Qnap bez EXIF (obaly alb, downloads, +kurzové thumbnaily) -> odpad. Potvrzeno uživatelem. +wanted=FALSE, category='Odpad-cache' +""" +import sys +import psycopg2 + +sys.stdout.reconfigure(encoding="utf-8") + +DB = dict(host="192.168.1.76", port=5432, user="vladimir.buzalka", + password="Vlado7309208104++", database="fotky_buzalkovi") + +conn = psycopg2.connect(**DB) +conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT) +cur = conn.cursor() + +cur.execute(""" + UPDATE photos p + SET wanted = FALSE, category = 'Odpad-cache' + FROM zaloha_obrazku z + WHERE p.zaloha_id = z.id + AND z.cesta_zalohy LIKE %s + AND p.camera_make IS NULL + AND p.category IS DISTINCT FROM 'Odpad-cache' +""", ('/mnt/user/ZalohaVsechObrazku/Tower1/#ColdData/Qnap/%',)) + +print(f"Qnap (zbytek bez EXIF) -> Odpad-cache: {cur.rowcount:,} řádků") +conn.close() diff --git a/00 PictureCollector/mark_synology_photo_rodina.py b/00 PictureCollector/mark_synology_photo_rodina.py new file mode 100644 index 0000000..7ccc421 --- /dev/null +++ b/00 PictureCollector/mark_synology_photo_rodina.py @@ -0,0 +1,32 @@ +""" +mark_synology_photo_rodina.py — Označí celý rodinný fotoarchiv +Tower1/#ColdData/SynologyMaly/photo/ jako Rodina. + +Potvrzeno uživatelem přes náhled. Foťáky 1998-2015 (Canon S40/A40/IXUS, +Nikon D80, iPhony, Lumia...). 99 % má EXIF, archiv je unikátní (bez duplikátů). + +wanted=TRUE, category='Rodina' +""" +import sys +import psycopg2 + +sys.stdout.reconfigure(encoding="utf-8") + +DB = dict(host="192.168.1.76", port=5432, user="vladimir.buzalka", + password="Vlado7309208104++", database="fotky_buzalkovi") + +conn = psycopg2.connect(**DB) +conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT) +cur = conn.cursor() + +cur.execute(""" + UPDATE photos p + SET wanted = TRUE, category = 'Rodina' + FROM zaloha_obrazku z + WHERE p.zaloha_id = z.id + AND z.cesta_zalohy LIKE %s + AND (p.wanted = FALSE OR p.category IS DISTINCT FROM 'Rodina') +""", ('/mnt/user/ZalohaVsechObrazku/Tower1/#ColdData/SynologyMaly/photo/%',)) + +print(f"SynologyMaly/photo -> Rodina: {cur.rowcount:,} řádků (wanted=TRUE)") +conn.close() diff --git a/00 PictureCollector/mark_synology_rodina.py b/00 PictureCollector/mark_synology_rodina.py new file mode 100644 index 0000000..9e9ba53 --- /dev/null +++ b/00 PictureCollector/mark_synology_rodina.py @@ -0,0 +1,42 @@ +""" +mark_synology_rodina.py — #Synology fotoarchivy potvrzené uživatelem -> Rodina: + #SERVER/Y/HD02#FOTKY (~30 789) + #SERVER/Y/HD02#FOTKY MAMKA (~1 167) + DropboxFotky (~14 882) + HD02#DVD OSOBNI (~5 834) + Fotky (~2 138) +""" +import sys +import psycopg2 + +sys.stdout.reconfigure(encoding="utf-8") + +DB = dict(host="192.168.1.76", port=5432, user="vladimir.buzalka", + password="Vlado7309208104++", database="fotky_buzalkovi") + +S = "/mnt/user/ZalohaVsechObrazku/Tower1/#Synology/Public/" +LIKES = [ + ("HD02#FOTKY", S + "#SERVER/Y/HD02#FOTKY/%"), + ("HD02#FOTKY MAMKA", S + "#SERVER/Y/HD02#FOTKY MAMKA/%"), + ("DropboxFotky", S + "DropboxFotky/%"), + ("HD02#DVD OSOBNI", S + "HD02#DVD OSOBNI/%"), + ("Fotky", S + "Fotky/%"), +] + +conn = psycopg2.connect(**DB) +conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT) +cur = conn.cursor() + +total = 0 +for popis, like in LIKES: + cur.execute(""" + UPDATE photos p SET wanted=TRUE, category='Rodina' + FROM zaloha_obrazku z + WHERE p.zaloha_id=z.id AND z.cesta_zalohy LIKE %s + AND (p.wanted=FALSE OR p.category IS DISTINCT FROM 'Rodina') + """, (like,)) + print(f" {popis:18s}: {cur.rowcount:,}") + total += cur.rowcount + +print(f"\nHotovo. Celkem nově/změněno na Rodina: {total:,}") +conn.close() diff --git a/00 PictureCollector/mark_synology_x_odpad.py b/00 PictureCollector/mark_synology_x_odpad.py new file mode 100644 index 0000000..50ec0fe --- /dev/null +++ b/00 PictureCollector/mark_synology_x_odpad.py @@ -0,0 +1,30 @@ +""" +mark_synology_x_odpad.py — Označí Tower1/#ColdData/SynologyMaly/Public/X/ +jako odpad (skenovaná série publikací e0xxx, 3744x5616, bez EXIF). +Potvrzeno uživatelem přes náhled. + +wanted=FALSE, category='Odpad-sken' +""" +import sys +import psycopg2 + +sys.stdout.reconfigure(encoding="utf-8") + +DB = dict(host="192.168.1.76", port=5432, user="vladimir.buzalka", + password="Vlado7309208104++", database="fotky_buzalkovi") + +conn = psycopg2.connect(**DB) +conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT) +cur = conn.cursor() + +cur.execute(""" + UPDATE photos p + SET wanted = FALSE, category = 'Odpad-sken' + FROM zaloha_obrazku z + WHERE p.zaloha_id = z.id + AND z.cesta_zalohy LIKE %s + AND p.category IS DISTINCT FROM 'Odpad-sken' +""", ('/mnt/user/ZalohaVsechObrazku/Tower1/#ColdData/SynologyMaly/Public/X/%',)) + +print(f"SynologyMaly/Public/X -> Odpad-sken: {cur.rowcount:,} řádků") +conn.close() diff --git a/00 PictureCollector/mark_synology_zbytek.py b/00 PictureCollector/mark_synology_zbytek.py new file mode 100644 index 0000000..ba20f39 --- /dev/null +++ b/00 PictureCollector/mark_synology_zbytek.py @@ -0,0 +1,39 @@ +""" +mark_synology_zbytek.py — Dokončí SynologyMaly: + Public/Dropbox -> Rodina (chceme, potvrzeno uživatelem) + music/web/video -> Odpad-cache (obaly, web obrázky) +""" +import sys +import psycopg2 + +sys.stdout.reconfigure(encoding="utf-8") + +DB = dict(host="192.168.1.76", port=5432, user="vladimir.buzalka", + password="Vlado7309208104++", database="fotky_buzalkovi") + +BASE = "/mnt/user/ZalohaVsechObrazku/Tower1/#ColdData/SynologyMaly/" + +RULES = [ + ("Public/Dropbox", BASE + "Public/Dropbox/%", True, "Rodina"), + ("music", BASE + "music/%", False, "Odpad-cache"), + ("web", BASE + "web/%", False, "Odpad-cache"), + ("video", BASE + "video/%", False, "Odpad-cache"), +] + +conn = psycopg2.connect(**DB) +conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT) +cur = conn.cursor() + +for popis, like, wanted, kat in RULES: + cur.execute(""" + UPDATE photos p + SET wanted = %s, category = %s + FROM zaloha_obrazku z + WHERE p.zaloha_id = z.id + AND z.cesta_zalohy LIKE %s + AND (p.wanted IS DISTINCT FROM %s OR p.category IS DISTINCT FROM %s) + """, (wanted, kat, like, wanted, kat)) + print(f" {popis:16s} -> {kat:12s} (wanted={wanted}): {cur.rowcount:,}") + +conn.close() +print("Hotovo.") diff --git a/00 PictureCollector/mark_tatinek_cast1.py b/00 PictureCollector/mark_tatinek_cast1.py new file mode 100644 index 0000000..213cf88 --- /dev/null +++ b/00 PictureCollector/mark_tatinek_cast1.py @@ -0,0 +1,40 @@ +""" +mark_tatinek_cast1.py — Tatinek U, rozhodnuté části (potvrzeno uživatelem): + Fractal (iPhone 13) -> Rodina (chceme) + DarthAnihilator + Screenshots-> Odpad-screenshot + Duplicates/Compass (dokumenty)-> Odpad-dokumenty +""" +import sys +import psycopg2 + +sys.stdout.reconfigure(encoding="utf-8") + +DB = dict(host="192.168.1.76", port=5432, user="vladimir.buzalka", + password="Vlado7309208104++", database="fotky_buzalkovi") + +T = "/mnt/user/ZalohaVsechObrazku/Tower1/#ColdData/Tatinek U/" + +RULES = [ + ("Fractal (iPhone)", T + "D/!!!Days/%Fractal%", True, "Rodina"), + ("DarthAnihilator", T + "DarthAnihilator/%", False, "Odpad-screenshot"), + ("Screenshots", T + "Screenshots/%", False, "Odpad-screenshot"), + ("Compass dokumenty", T + "Duplicates/Compass/%",False, "Odpad-dokumenty"), +] + +conn = psycopg2.connect(**DB) +conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT) +cur = conn.cursor() + +for popis, like, wanted, kat in RULES: + cur.execute(""" + UPDATE photos p + SET wanted = %s, category = %s + FROM zaloha_obrazku z + WHERE p.zaloha_id = z.id + AND z.cesta_zalohy LIKE %s + AND (p.wanted IS DISTINCT FROM %s OR p.category IS DISTINCT FROM %s) + """, (wanted, kat, like, wanted, kat)) + print(f" {popis:20s} -> {kat:18s} (wanted={wanted}): {cur.rowcount:,}") + +conn.close() +print("Hotovo.") diff --git a/00 PictureCollector/mark_tatinek_zbytek.py b/00 PictureCollector/mark_tatinek_zbytek.py new file mode 100644 index 0000000..a87b22a --- /dev/null +++ b/00 PictureCollector/mark_tatinek_zbytek.py @@ -0,0 +1,32 @@ +""" +mark_tatinek_zbytek.py — Zbytek Tatinek U (produktové letáky, Sudoku, +screenshoty, web obrázky) -> odpad. Potvrzeno uživatelem. +Vše krom Fractal/DarthAnihilator/Screenshots/Compass -> Odpad-cache. +""" +import sys +import psycopg2 + +sys.stdout.reconfigure(encoding="utf-8") + +DB = dict(host="192.168.1.76", port=5432, user="vladimir.buzalka", + password="Vlado7309208104++", database="fotky_buzalkovi") + +conn = psycopg2.connect(**DB) +conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT) +cur = conn.cursor() + +cur.execute(""" + UPDATE photos p + SET wanted = FALSE, category = 'Odpad-cache' + FROM zaloha_obrazku z + WHERE p.zaloha_id = z.id + AND z.cesta_zalohy LIKE '/mnt/user/ZalohaVsechObrazku/Tower1/#ColdData/Tatinek U/%' + AND z.cesta_zalohy NOT LIKE '%Fractal%' + AND z.cesta_zalohy NOT LIKE '%/DarthAnihilator/%' + AND z.cesta_zalohy NOT LIKE '%/Screenshots/%' + AND z.cesta_zalohy NOT LIKE '%/Duplicates/Compass/%' + AND p.category IS DISTINCT FROM 'Odpad-cache' +""") + +print(f"Tatinek U (zbytek) -> Odpad-cache: {cur.rowcount:,} řádků") +conn.close() diff --git a/00 PictureCollector/mark_torrents_unwanted.py b/00 PictureCollector/mark_torrents_unwanted.py new file mode 100644 index 0000000..0449fff --- /dev/null +++ b/00 PictureCollector/mark_torrents_unwanted.py @@ -0,0 +1,42 @@ +""" +mark_torrents_unwanted.py — Označí celé Tower/Torrents a Tower1/Torrents +jako nechceme (stažený torrent obsah: komiksy, vystřihovánky, speleologie, +porno, textury...). Žádné rodinné originály (ověřeno pixel-hashem + adresáři). + +wanted=FALSE, category='Odpad-torrent' + +Idempotentní + jedna session, autocommit (žádné paralelní běhy → bez deadlocku). +""" +import sys +import psycopg2 + +sys.stdout.reconfigure(encoding="utf-8") + +DB = dict(host="192.168.1.76", port=5432, user="vladimir.buzalka", + password="Vlado7309208104++", database="fotky_buzalkovi") + +DIRS = [ + ("Tower/Torrents", "/mnt/user/ZalohaVsechObrazku/Tower/Torrents%"), + ("Tower1/Torrents", "/mnt/user/ZalohaVsechObrazku/Tower1/Torrents%"), +] + +conn = psycopg2.connect(**DB) +conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT) +cur = conn.cursor() + +total = 0 +for popis, like in DIRS: + cur.execute(""" + UPDATE photos p + SET wanted = FALSE, + category = 'Odpad-torrent' + FROM zaloha_obrazku z + WHERE p.zaloha_id = z.id + AND z.cesta_zalohy LIKE %s + AND p.category IS DISTINCT FROM 'Odpad-torrent' + """, (like,)) + print(f" {popis}: {cur.rowcount:,} řádků") + total += cur.rowcount + +print(f"\nHotovo. Celkem: {total:,} řádků → wanted=FALSE, category='Odpad-torrent'") +conn.close() diff --git a/00 PictureCollector/mark_webs.py b/00 PictureCollector/mark_webs.py new file mode 100644 index 0000000..a77ef69 --- /dev/null +++ b/00 PictureCollector/mark_webs.py @@ -0,0 +1,44 @@ +""" +mark_webs.py — #Synology/.../#SERVER/Y/WEBS (starý web Buzalka.cz/.com): + galerijní alba (g1/Albums, g2data/albums) -> Rodina (potvrzeno uživatelem) + zbytek (web šablony, moduly, skiny, e-shop) -> Odpad-cache +""" +import sys +import psycopg2 + +sys.stdout.reconfigure(encoding="utf-8") + +DB = dict(host="192.168.1.76", port=5432, user="vladimir.buzalka", + password="Vlado7309208104++", database="fotky_buzalkovi") + +WEBS = "/mnt/user/ZalohaVsechObrazku/Tower1/#Synology/Public/#SERVER/Y/WEBS/%" + +conn = psycopg2.connect(**DB) +conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT) +cur = conn.cursor() + +# Galerijní alba -> Rodina +cur.execute(""" + UPDATE photos p SET wanted=TRUE, category='Rodina' + FROM zaloha_obrazku z + WHERE p.zaloha_id=z.id + AND z.cesta_zalohy LIKE %s + AND (z.cesta_zalohy LIKE '%%/Albums/%%' OR z.cesta_zalohy LIKE '%%/g2data/albums/%%') + AND (p.wanted=FALSE OR p.category IS DISTINCT FROM 'Rodina') +""", (WEBS,)) +print(f" WEBS galerie -> Rodina: {cur.rowcount:,}") + +# Zbytek WEBS -> Odpad-cache +cur.execute(""" + UPDATE photos p SET wanted=FALSE, category='Odpad-cache' + FROM zaloha_obrazku z + WHERE p.zaloha_id=z.id + AND z.cesta_zalohy LIKE %s + AND z.cesta_zalohy NOT LIKE '%%/Albums/%%' + AND z.cesta_zalohy NOT LIKE '%%/g2data/albums/%%' + AND p.category IS DISTINCT FROM 'Odpad-cache' +""", (WEBS,)) +print(f" WEBS zbytek -> Odpad-cache: {cur.rowcount:,}") + +conn.close() +print("Hotovo.") diff --git a/00 PictureCollector/mark_xperie_vladko.py b/00 PictureCollector/mark_xperie_vladko.py new file mode 100644 index 0000000..be92f8c --- /dev/null +++ b/00 PictureCollector/mark_xperie_vladko.py @@ -0,0 +1,38 @@ +""" +mark_xperie_vladko.py — Osobní složky v #ColdData, chceme zachovat: + Honza fotky z Xperie -> kamarad Jan Luxemburk -> category 'Kamaradi' + VladkoSoubory -> syn Vladimir Buzalka ml -> category 'Rodina' +Obojí wanted=TRUE (potvrzeno uživatelem). +""" +import sys +import psycopg2 + +sys.stdout.reconfigure(encoding="utf-8") + +DB = dict(host="192.168.1.76", port=5432, user="vladimir.buzalka", + password="Vlado7309208104++", database="fotky_buzalkovi") + +BASE = "/mnt/user/ZalohaVsechObrazku/Tower1/#ColdData/" + +RULES = [ + ("Honza fotky z Xperie", BASE + "Honza fotky z Xperie/%", "Kamaradi"), + ("VladkoSoubory", BASE + "VladkoSoubory/%", "Rodina"), +] + +conn = psycopg2.connect(**DB) +conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT) +cur = conn.cursor() + +for popis, like, kat in RULES: + cur.execute(""" + UPDATE photos p + SET wanted = TRUE, category = %s + FROM zaloha_obrazku z + WHERE p.zaloha_id = z.id + AND z.cesta_zalohy LIKE %s + AND (p.wanted = FALSE OR p.category IS DISTINCT FROM %s) + """, (kat, like, kat)) + print(f" {popis:22s} -> {kat:10s}: {cur.rowcount:,}") + +conn.close() +print("Hotovo.") diff --git a/00 PictureCollector/restore_maminka.py b/00 PictureCollector/restore_maminka.py new file mode 100644 index 0000000..1195d5b --- /dev/null +++ b/00 PictureCollector/restore_maminka.py @@ -0,0 +1,34 @@ +""" +restore_maminka.py — Vrátí jedinečné iPhone fotky z +Tower/appdata/photoprism/sidecar/Maminka zpět na wanted=TRUE, category='Rodina'. + +Tyto soubory jsem omylem označil jako Odpad-cache, ačkoli jde o jediné kopie +rodinných fotek v záloze (žádný originál jinde neexistuje). + +Filtr camera_make IS NOT NULL = bereme jen skutečné fotky, ne případné +technické soubory v té složce. +""" +import sys +import psycopg2 + +sys.stdout.reconfigure(encoding="utf-8") + +DB = dict(host="192.168.1.76", port=5432, user="vladimir.buzalka", + password="Vlado7309208104++", database="fotky_buzalkovi") + +conn = psycopg2.connect(**DB) +conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT) +cur = conn.cursor() + +cur.execute(""" + UPDATE photos p + SET wanted = TRUE, + category = 'Rodina' + FROM zaloha_obrazku z + WHERE p.zaloha_id = z.id + AND z.cesta_zalohy LIKE %s + AND p.camera_make IS NOT NULL +""", ('/mnt/user/ZalohaVsechObrazku/Tower/appdata/photoprism/sidecar/Maminka%',)) + +print(f"Vráceno na Rodina: {cur.rowcount:,} řádků (wanted=TRUE, category='Rodina')") +conn.close() diff --git a/POSTUP.md b/POSTUP.md index c5cb93b..d5a190f 100644 --- a/POSTUP.md +++ b/POSTUP.md @@ -104,6 +104,121 @@ Tyto cesty **nechceme** — `wanted` zůstane FALSE, nezpracovávat: --- +## Session 2026-06-05 + +### Nástroje — nový web preview na Toweru + +- **`00 PictureCollector/preview_server.py`** — HTTP server běžící v Docker kontejneru + `python-runner` na Toweru. Renderuje thumbnaily lokálně (rychlé), má lightbox + (klik = plné rozlišení přes `/image?id=`). + - URL: `http://192.168.1.76:8766/preview?ids=101,202,303&thumb=320` + - Kontejner má mount `/mnt/remotes/TOWER1.LAN_ZalohaVsechObrazku` → `/mnt/ZalohaVsechObrazku:ro` + - Knihovny v image: `psycopg` (v3!), `Pillow` (zabakováno přes `docker commit`) +- **MCP konektor `fotky-buzalkovi`** — `query/tables/describe_table/stats`. SQL dotazy + pouštím přímo, bez mezikroku. + +### Pracovní metodika (důležité!) + +- **NEoznačovat nechtěné proaktivně** — vždy jen po explicitním pokynu uživatele. +- Před každým hromadným označením složky za odpad: **ověřit jedinečnost přes + pixel/file-hash** (`sha256_pixels`, `sha256_file`). V cache/odpadních složkách se + můžou skrývat unikátní rodinné fotky (viz „Maminka" níže). +- UPDATE pouštět vždy jako **jedna session, autocommit** — paralelní běhy způsobují + deadlock. Před každým během kontrola `pg_stat_activity` na zaseklé UPDATE. +- Marker skripty: `mark_*.py` v `00 PictureCollector/` (idempotentní přes filtr + `category IS DISTINCT FROM ...`). + +### Kategorie (rozšířený číselník) + +| category | wanted | význam | +|---|---|---| +| `Rodina` | TRUE | rodinné fotky | +| `Kamaradi` | TRUE | fotky kamarádů (Jan Luxemburk – Xperie) | +| `Fotopast` | TRUE | fotopast | +| `Odpad-cache` | FALSE | cache aplikací, obaly, web obrázky | +| `Odpad-torrent` | FALSE | stažené torrenty (komiksy, vystřihovánky, software) | +| `Odpad-sken` | FALSE | skenované publikace (série stránek) | +| `Odpad-obaly` | FALSE | CD inlety/obaly s EXIF | +| `Odpad-dokumenty` | FALSE | skeny dokumentů/smluv | +| `Odpad-screenshot` | FALSE | screenshoty z her/aplikací | +| `Porno` | FALSE | porno | +| `Skener-nechceme` | FALSE | skenery (viz session 2026-06-04) | + +### Zpracované velké složky + +| Složka | Fotek | Výsledek | +|---|---:|---| +| **Tower/appdata** | 1 006 032 | odpad (Odpad-cache); **zachráněno 2 274** fotek Maminky (iPhone 12) z `photoprism/sidecar/Maminka` → Rodina | +| **Tower/Sabnzbd** | 48 442 | celé odpad (usenet downloads, profi Nikon D5/D4 = stažené, ne rodina) | +| **Tower/Torrents + Tower1/Torrents** | 100 938 | celé odpad (Odpad-torrent) | +| **Tower1/#ColdData** | 84 374 | roztříděno 100 %, viz níže | +| **Tower1/#Synology** | 66 462 | rozpracováno (~95 %), viz níže | + +### #Synology — rozpracováno (Public/...) + +Velký rodinný NAS fotoarchiv. Hash-check potvrdil, že `HD02#FOTKY` je z 99,6 % +**unikátní** (jen 132 fotek má kopii jinde) — není to duplikát jiné zálohy. + +**Rodina (chceme) — HOTOVO:** +- `#SERVER/Y/HD02#FOTKY` (30 789) — hlavní archiv 2008–2020 +- `#SERVER/Y/HD02#FOTKY MAMKA` (1 167) +- `DropboxFotky` (14 882) +- `HD02#DVD OSOBNI` (5 834) +- `Fotky` (2 138) +- `#SERVER/Y/WEBS` galerijní alba (906) — starý web Buzalka.com/Gallery2, + alba akcí (USA 2006, lyžování Stoderzinken 2007, Orlík, Střetávka…) + +**Odpad — HOTOVO:** +- `#SERVER/Y/WEBS` zbytek (2 153) — web šablony/skiny/moduly Buzalka.cz e-shop → Odpad-cache + +**JEŠTĚ NEROZHODNUTO (zbývá ~9 200):** +- `AFotkyFotky` (4 550, 2 245 EXIF) — pravděpodobně fotky +- `#SERVER/JNJ` (1 835, 45 modelů) — smíšené +- `#SERVER/E` (1 629) — smíšené +- `VideoMichalkaiCloud` (277) — Michalka iCloud, pravděpodobně rodina +- `GoPro` (70) — pravděpodobně rodina +- drobnosti (`###StatSoft`, `#SOFTWARE`, `HD02#AUDIO CD A DVD`, `#Kurzy`, `X`…) ~220 → odpad + +### #ColdData — detailní rozpad + +**Rodina (chceme):** +- `SynologyMaly/photo` (24 198) + `SynologyMaly/Public/Dropbox` (447) — rodinný archiv 1998–2015 (Canon S40/A40/IXUS, Nikon D80, iPhony, Lumia). Unikátní (bez duplikátů). +- `VladkoSoubory` (871) — syn Vladimír Buzalka ml. +- `DedupPhotos` (285) — Samsung S7 Edge 2019, ověřeno unikátní. +- `LTBS` (40) — rodina (názvy souborů obsahují BUZALKA). +- `Tatinek U/.../Fractal` (19) — iPhone 13, táta. +- `pomoc` Samsung GT-S5230 (26) — rodina. + +**Kamaradi (chceme):** `Honza fotky z Xperie` (2 047) — kamarád Jan Luxemburk. + +**Odpad:** +- `Porno`, `Porno1`, `MadelineIsWicked` → Porno +- `000 TORENT OBRAZKY`, `Qnap_nepotrebne` (X-Men komiksy, učebnice, software) → Odpad-torrent +- `SynologyMaly/Public/X` (9 134, série e0xxx skenů) → Odpad-sken +- `Qnap` (MP3 obaly, downloads, 81 EXIF CD inletů) → Odpad-cache/Odpad-obaly +- `Museum of Modern Art NY`, hudební alba, Salvador Dali, eBooky, Truecrypt, `DropBox` (software help obrázky) → Odpad-cache +- `Tatinek U` Compass dokumenty + `212` smlouvy → Odpad-dokumenty; DarthAnihilator/Screenshots → Odpad-screenshot +- `pomoc` LegalPorno/Kink snapshoty (28) → Porno + +**Výsledek #ColdData:** Rodina 25 889 · Kamaradi 2 047 · Porno 23 502 · Odpad-torrent 21 300 · Odpad-sken 9 134 · Odpad-cache 2 269 · ostatní odpad 228. + +### Poučení + +- **Cache/download složky ≠ čistý odpad.** PhotoPrism si do `sidecar/` ukládá unikátní + JPEG kopie fotek, jejichž originály v záloze nejsou → před smazáním vždy hash-check. +- Camera-rules (např. „všechny Nikon D5/D4 = Rodina") dávají **false-positives** ve + stažených složkách → path má přednost, ale ověřit obsah. + +### Na řadě + +- [ ] **Dokončit #Synology** — zbývá ~9 200: `AFotkyFotky`, `JNJ`, `E`, + `VideoMichalkaiCloud`, `GoPro` (náhledy → rozhodnout), drobnosti → odpad +- [ ] Velké zbývající: `TW22/D` (63 706), `Tower1/#Pomoc` (34 500), `Tower1/#Synologymaly` (29 934) +- [ ] Dokončit Canony (a pak ostatní značky foťáků) +- [ ] „BEZ KAMERY" skupina (path-based) + +--- + ## Backlog otevřených otázek 1. Co s "sirotky" bez EXIF — `mtime` / odmítnout / označit?