From 0f73a6b53782d1a033420c030d9be76e98a73364 Mon Sep 17 00:00:00 2001 From: administrator Date: Thu, 4 Jun 2026 22:56:27 +0200 Subject: [PATCH] notebookVb --- 00 PictureCollector/migrate_add_wanted.py | 28 +++ 00 PictureCollector/migrate_category.py | 31 +++ 00 PictureCollector/preview_sample.py | 230 ++++++++++++++++++++ 00 PictureCollector/update_wanted.py | 136 ++++++++++++ 00 PictureCollector/update_wanted_batch2.py | 58 +++++ POSTUP.md | 100 +++++++++ 6 files changed, 583 insertions(+) create mode 100644 00 PictureCollector/migrate_add_wanted.py create mode 100644 00 PictureCollector/migrate_category.py create mode 100644 00 PictureCollector/preview_sample.py create mode 100644 00 PictureCollector/update_wanted.py create mode 100644 00 PictureCollector/update_wanted_batch2.py create mode 100644 POSTUP.md diff --git a/00 PictureCollector/migrate_add_wanted.py b/00 PictureCollector/migrate_add_wanted.py new file mode 100644 index 0000000..20f6094 --- /dev/null +++ b/00 PictureCollector/migrate_add_wanted.py @@ -0,0 +1,28 @@ +""" +migrate_add_wanted.py — přidá sloupec wanted BOOLEAN NOT NULL DEFAULT FALSE do tabulky photos. +Bezpečné spustit vícekrát (idempotentní). +""" + +import psycopg2 + +DB = dict(host="192.168.1.76", port=5432, user="vladimir.buzalka", + password="Vlado7309208104++", database="fotky_buzalkovi") + +SQL = """ +ALTER TABLE photos + ADD COLUMN IF NOT EXISTS wanted BOOLEAN NOT NULL DEFAULT FALSE; +""" + +conn = psycopg2.connect(**DB) +conn.autocommit = True +cur = conn.cursor() + +print("Přidávám sloupec wanted…") +cur.execute(SQL) +print("Hotovo.") + +cur.execute("SELECT COUNT(*) FROM photos WHERE wanted = FALSE") +n = cur.fetchone()[0] +print(f"Fotek s wanted=FALSE: {n:,}") + +conn.close() diff --git a/00 PictureCollector/migrate_category.py b/00 PictureCollector/migrate_category.py new file mode 100644 index 0000000..67355b5 --- /dev/null +++ b/00 PictureCollector/migrate_category.py @@ -0,0 +1,31 @@ +""" +migrate_category.py — přidá sloupec category do photos (idempotentní) +a provede konkrétní UPDATE podle pravidel. +""" +import psycopg2 + +DB = dict(host="192.168.1.76", port=5432, user="vladimir.buzalka", + password="Vlado7309208104++", database="fotky_buzalkovi") + +conn = psycopg2.connect(**DB) +conn.autocommit = True +cur = conn.cursor() + +# 1. Přidat sloupec category pokud neexistuje +cur.execute(""" + ALTER TABLE photos + ADD COLUMN IF NOT EXISTS category VARCHAR(100); +""") +print("Sloupec category OK.") + +# 2. BolyMedia SG520 → wanted=TRUE, category='Fotopast' +cur.execute(""" + UPDATE photos + SET wanted = TRUE, + category = 'Fotopast' + WHERE camera_make = 'BolyMedia' + AND camera_model = 'SG520'; +""") +print(f"SG520 aktualizováno: {cur.rowcount:,} řádků → wanted=TRUE, category='Fotopast'") + +conn.close() diff --git a/00 PictureCollector/preview_sample.py b/00 PictureCollector/preview_sample.py new file mode 100644 index 0000000..92ebf0f --- /dev/null +++ b/00 PictureCollector/preview_sample.py @@ -0,0 +1,230 @@ +""" +preview_sample.py — zobrazí náhled fotek podle ID z databáze. + +Použití: + python preview_sample.py 101 202 303 ... + python preview_sample.py --file ids.txt # jedno ID na řádek + python preview_sample.py --file ids.txt --thumb 400 +""" + +import argparse +import base64 +import io +import sys +import tempfile +import webbrowser +from pathlib import Path + +import psycopg2 +from PIL import Image + +# ── DB ──────────────────────────────────────────────────────────────────────── + +DB = dict(host="192.168.1.76", port=5432, user="vladimir.buzalka", + password="Vlado7309208104++", database="fotky_buzalkovi") + +# ── Cesty ───────────────────────────────────────────────────────────────────── + +NAS_LINUX_PREFIX = "/mnt/user/ZalohaVsechObrazku/" +NAS_WIN_UNC = r"\\Tower1\ZalohaVsechObrazku\\" + + +def linux_to_win(p: str) -> Path: + rel = p.removeprefix(NAS_LINUX_PREFIX).replace("/", "\\") + return Path(NAS_WIN_UNC + rel) + + +# ── Načtení dat z DB ────────────────────────────────────────────────────────── + +QUERY = """ + SELECT + z.id, + z.cesta_zalohy, + z.velikost, + p.taken_at, + p.taken_at_source, + p.camera_make, + p.camera_model, + p.width, + p.height, + p.megapixels, + p.gps_lat, + p.gps_lon, + COALESCE( + (SELECT string_agg(e.error_code, ', ') + FROM photo_errors e WHERE e.photo_id = p.id), + '' + ) AS chyby + FROM zaloha_obrazku z + JOIN photos p ON p.zaloha_id = z.id + WHERE z.id = ANY(%s) +""" + + +def fetch_rows(ids: list[int]) -> list: + conn = psycopg2.connect(**DB) + cur = conn.cursor() + cur.execute(QUERY, (ids,)) + rows = cur.fetchall() + conn.close() + # zachovej původní pořadí ID + order = {rid: i for i, rid in enumerate(ids)} + return sorted(rows, key=lambda r: order.get(r[0], 9999)) + + +# ── Thumbnail ───────────────────────────────────────────────────────────────── + +def make_thumb_b64(win_path: Path, size: int) -> str | None: + try: + img = Image.open(win_path) + img.thumbnail((size, size), Image.LANCZOS) + if img.mode not in ("RGB", "L"): + img = img.convert("RGB") + buf = io.BytesIO() + img.save(buf, format="JPEG", quality=82) + return base64.b64encode(buf.getvalue()).decode() + except Exception: + return None + + +# ── HTML ────────────────────────────────────────────────────────────────────── + +def format_size(n) -> str: + if n is None: + return "?" + for unit in ("B", "kB", "MB", "GB"): + if n < 1024: + return f"{n:.0f} {unit}" + n /= 1024 + return f"{n:.1f} GB" + + +def render_html(rows: list, ids: list[int], thumb_size: int) -> str: + cards = [] + ok = 0 + + for i, row in enumerate(rows, 1): + (rid, cesta, velikost, taken_at, taken_src, + make_, model, width, height, mp, lat, lon, chyby) = row + + win_path = linux_to_win(cesta) + b64 = make_thumb_b64(win_path, thumb_size) + + if b64: + ok += 1 + img_tag = f'foto' + card_cls = "card" + else: + img_tag = '
⚠ soubor nedostupný
zkontroluj přístup na Tower1
' + card_cls = "card broken" + + camera = " ".join(filter(None, [make_, model])) or "neznámá" + date_str = taken_at.strftime("%Y-%m-%d %H:%M") if taken_at else "?" + res = f"{width}×{height}" if width and height else "?" + mp_str = f"{mp:.1f} Mpx" if mp else "" + gps_link = ( + f'📍 {lat:.4f}, {lon:.4f}' + if lat and lon else "" + ) + chyby_html = f'
{chyby}
' if chyby else "" + path_short = str(win_path).replace(NAS_WIN_UNC, "…\\") + + cards.append(f""" +
+
{img_tag}
+
+
id={rid}  #{i}
+
{date_str} ({taken_src})
+
📷 {camera}
+
{res} {mp_str}   {format_size(velikost)}
+ {"
" + gps_link + "
" if gps_link else ""} + {chyby_html} +
{path_short}
+
+
""") + + missing_ids = set(ids) - {r[0] for r in rows} + missing_note = "" + if missing_ids: + missing_note = f'
ID nenalezena v DB: {sorted(missing_ids)}
' + + return f""" + + + +Preview fotek ({len(rows)}) + + + +

Preview fotek

+
+ Celkem ID: {len(ids)}  ·  + Načteno z DB: {len(rows)}  ·  + Obrázky dostupné: {ok}/{len(rows)} +
+{missing_note} +
+{"".join(cards)} +
+ +""" + + +# ── Main ────────────────────────────────────────────────────────────────────── + +def main(): + parser = argparse.ArgumentParser(description="Preview fotek podle ID z DB") + parser.add_argument("ids", nargs="*", type=int, help="ID fotek (zaloha_obrazku.id)") + parser.add_argument("--file", "-f", help="Soubor s ID (jedno na řádek)") + parser.add_argument("--thumb", "-t", type=int, default=320, help="Velikost thumbnailů v px (default 320)") + args = parser.parse_args() + + ids = list(args.ids) + + if args.file: + with open(args.file) as fh: + for line in fh: + line = line.strip() + if line and line.isdigit(): + ids.append(int(line)) + + if not ids: + print("Žádná ID. Zadej je jako argumenty nebo přes --file.", file=sys.stderr) + sys.exit(1) + + print(f"Načítám {len(ids)} fotek z DB…") + rows = fetch_rows(ids) + print(f"Generuji thumbnaily (--thumb {args.thumb}px)…") + html = render_html(rows, ids, args.thumb) + + tmp = tempfile.NamedTemporaryFile( + suffix=".html", delete=False, mode="w", encoding="utf-8" + ) + tmp.write(html) + tmp.close() + print(f"Otevírám: {tmp.name}") + webbrowser.open(f"file:///{tmp.name}") + + +if __name__ == "__main__": + main() diff --git a/00 PictureCollector/update_wanted.py b/00 PictureCollector/update_wanted.py new file mode 100644 index 0000000..cd268fa --- /dev/null +++ b/00 PictureCollector/update_wanted.py @@ -0,0 +1,136 @@ +""" +update_wanted.py — označuje fotky wanted=TRUE a nastavuje category. +Spusť a uprav seznam pravidel podle potřeby. +""" +import psycopg2 + +DB = dict(host="192.168.1.76", port=5432, user="vladimir.buzalka", + password="Vlado7309208104++", database="fotky_buzalkovi") + +conn = psycopg2.connect(**DB) +conn.autocommit = True +cur = conn.cursor() + +rules = [ + # (popis, SQL WHERE, category) + # ── Samsung ────────────────────────────────────────────────────────────── + ( + "Samsung SM-G935F / A520F / A525F / J500FN / I8190N / S7560 / S5230", + "camera_make IN ('samsung', 'SAMSUNG', 'Samsung') AND camera_model IN " + "('SM-G935F','SM-A520F','SM-A525F','SM-J500FN','GT-I8190N','GT-S7560','GT-S5230')", + "Rodina", + ), + # ── Profesionální / cizí ───────────────────────────────────────────────── + ( + "Nikon D5 / D4 / D700 / D300 / D800 / D500", + "camera_make = 'NIKON CORPORATION' AND camera_model IN " + "('NIKON D5','NIKON D4','NIKON D700','NIKON D300','NIKON D800','NIKON D500')", + "Rodina", + ), + ( + "Canon EOS-1D X III / 450D / 600D / R6 / 5D IV / 5D / 40D / 20D", + "camera_make = 'Canon' AND camera_model IN " + "('Canon EOS-1D X Mark III','Canon EOS 450D','Canon EOS 600D','Canon EOS R6'," + "'Canon EOS 5D Mark IV','Canon EOS 5D','Canon EOS 40D','Canon EOS 20D')", + "Rodina", + ), + ( + "Sony ILCE-1 (Alpha 1)", + "camera_make = 'SONY' AND camera_model = 'ILCE-1'", + "Rodina", + ), + ( + "Panasonic DC-S5", + "camera_make = 'Panasonic' AND camera_model = 'DC-S5'", + "Rodina", + ), + # ── Ostatní kompakty ───────────────────────────────────────────────────── + ( + "Panasonic DMC-FZ5", + "camera_make = 'Panasonic' AND camera_model = 'DMC-FZ5'", + "Rodina", + ), + ( + "Canon PowerShot S40", + "camera_make = 'Canon' AND camera_model = 'Canon PowerShot S40'", + "Rodina", + ), + ( + "Canon PowerShot A40", + "camera_make = 'Canon' AND camera_model = 'Canon PowerShot A40'", + "Rodina", + ), + ( + "Panasonic DMC-FX33", + "camera_make = 'Panasonic' AND camera_model = 'DMC-FX33'", + "Rodina", + ), + ( + "NIKON D80", + "camera_make = 'NIKON CORPORATION' AND camera_model = 'NIKON D80'", + "Rodina", + ), + ( + "GoPro HERO6 Black", + "camera_make = 'GoPro' AND camera_model = 'HERO6 Black'", + "Rodina", + ), + ( + "Microsoft Lumia 930", + "camera_make = 'Microsoft' AND camera_model = 'Lumia 930'", + "Rodina", + ), + ( + "Panasonic DMC-FX3", + "camera_make = 'Panasonic' AND camera_model = 'DMC-FX3'", + "Rodina", + ), + ( + "Sony DSC-T77", + "camera_make = 'SONY' AND camera_model = 'DSC-T77'", + "Rodina", + ), + ( + "Olympus C120/D380", + "camera_make = 'OLYMPUS OPTICAL CO.,LTD' AND camera_model = 'C120,D380'", + "Rodina", + ), + ( + "Sony DSC-WX50", + "camera_make = 'SONY' AND camera_model = 'DSC-WX50'", + "Rodina", + ), + ( + "Sony CYBERSHOT", + "camera_make = 'SONY' AND camera_model = 'CYBERSHOT'", + "Rodina", + ), + ( + "Casio EX-Z6", + "camera_make = 'CASIO COMPUTER CO.,LTD.' AND camera_model = 'EX-Z6'", + "Rodina", + ), + ( + "Olympus C765UZ", + "camera_make = 'OLYMPUS CORPORATION' AND camera_model = 'C765UZ'", + "Rodina", + ), + ( + "Nikon E990", + "camera_make = 'NIKON' AND camera_model = 'E990'", + "Rodina", + ), +] + +for popis, where, category in rules: + cur.execute(f""" + UPDATE photos + SET wanted = TRUE, + category = %s + WHERE {where} + AND (wanted = FALSE OR category IS DISTINCT FROM %s) + """, (category, category)) + print(f"{popis}: {cur.rowcount:,} řádků → wanted=TRUE, category='{category}'") + +conn.close() +print("Hotovo.") diff --git a/00 PictureCollector/update_wanted_batch2.py b/00 PictureCollector/update_wanted_batch2.py new file mode 100644 index 0000000..bcbb793 --- /dev/null +++ b/00 PictureCollector/update_wanted_batch2.py @@ -0,0 +1,58 @@ +"""update_wanted_batch2.py — označí další vlnu foťáků wanted=TRUE, category=Rodina""" +import psycopg2 + +DB = dict(host="192.168.1.76", port=5432, user="vladimir.buzalka", + password="Vlado7309208104++", database="fotky_buzalkovi") + +# (make, model) +CAMERAS = [ + ("Canon", "Canon DIGITAL IXUS 30"), + ("Xiaomi", "2201117TY"), + ("SONY", "DSC-H70"), + ("Panasonic", "DMC-FZ20"), + ("Panasonic", "DMC-FZ50"), + ("Panasonic", "NV-GS180"), + ("EASTMAN KODAK COMPANY", "KODAK C875 ZOOM DIGITAL CAMERA"), + ("SONY", "D2005"), + ("OLYMPUS IMAGING CORP.", "XZ-10"), + ("OLYMPUS CORPORATION", "C460ZdelSol"), + ("Canon", "Canon EOS DIGITAL REBEL"), + ("HTC", "HTC HD2 T8585"), + ("OLYMPUS OPTICAL CO.,LTD", "C4040Z"), + ("NIKON", "E950"), + ("NIKON", "E5400"), + ("Canon", "Canon PowerShot S300"), + ("Emgeton", "Flexaret Mini"), + ("SONY", "DCR-PC115E"), + ("Canon", "Canon PowerShot A720 IS"), + ("SONY", "HDR-TG3E"), + ("Canon", "Canon PowerShot A75"), + ("OLYMPUS OPTICAL CO.,LTD", "C3030Z"), + ("FUJIFILM", "FinePix F20"), + ("vivo", "vivo X100 Pro"), + ("NIKON", "E3200"), + ("Canon", "Canon PowerShot S3 IS"), + ("Canon", "Canon PowerShot G3"), + ("OLYMPUS OPTICAL CO.,LTD", "X-2,C-50Z"), + ("Canon", "Canon PowerShot S2 IS"), + ("Canon", "Canon PowerShot S500"), + ("Canon", "Canon DIGITAL IXUS 400"), + ("OLYMPUS OPTICAL CO.,LTD", "C5050Z"), +] + +conn = psycopg2.connect(**DB) +conn.autocommit = True +cur = conn.cursor() + +placeholders = ",".join(["(%s,%s)"] * len(CAMERAS)) +params = [v for pair in CAMERAS for v in pair] +cur.execute(f""" + UPDATE photos + SET wanted = TRUE, + category = 'Rodina' + WHERE (camera_make, camera_model) IN ({placeholders}) + AND wanted = FALSE +""", params) + +print(f"Označeno: {cur.rowcount:,} řádků → wanted=TRUE, category='Rodina'") +conn.close() diff --git a/POSTUP.md b/POSTUP.md new file mode 100644 index 0000000..75c9bf3 --- /dev/null +++ b/POSTUP.md @@ -0,0 +1,100 @@ +# FotkyBuzalkovi — pracovní deník + +> Živý dokument. Zapisujeme sem co jsme zjistili, co jsme rozhodli a co je na řadě. +> Technická reference → `CONTEXT.md`, návrh architektury → `NAVRH.md`. + +--- + +## Session 2026-06-04 + +### Stav DB na začátku + +| Tabulka | Řádky | +|---------|-------| +| `zaloha_obrazku` | 1 717 182 | +| `zdrojove_soubory` | 3 573 846 | +| `photos` | 1 717 175 | +| `photo_errors` | 3 185 319 | + +Všechny fotky mají `processing_status = 'pending'` — pipeline doběhla, další zpracování nezačalo. + +### Co jsme zjistili + +**Problém 1 — V záloze je spousta odpadu, ne jen rodinné fotky.** +Pipeline sebrala vše co má příponu `.jpg/.jpeg` — včetně: +- PhotoPrism cache thumbnailů (`appdata/photoprism/cache/`) — 229 521 ks, <10 kB +- Plex / Immich cache +- MP3 a LP obaly +- DVD obaly, eBook obálky +- ABC vystřihovánky (skenované na 1200 DPI → soubory 60–100 MB) +- Reprodukce obrazů z torrentů (Raffael, Rembrandt... v muzejní kvalitě) +- Stažené obrázky z webu (Dropbox/!!!Days/Stefajir/...) +- Windows AppData (Kindle covers, .NET watermark...) + +**Problém 2 — Rok pořízení je hodně porušený.** +- Rok 2024 má 985 754 fotek (>57 % všech) — zřejmě chybný fallback na mtime místo EXIF +- Rok 1863, 2031–4501 — garbage v EXIF +- Rok 2026 má 93 492 — suspektní + +**Sloupec `wanted`:** +Přidán `photos.wanted BOOLEAN NOT NULL DEFAULT FALSE` — všech 1 717 175 fotek má FALSE. +Účel: budeme označovat fotky které chceme zachovat / zpracovat. + +### Nástroje + +- `00 PictureCollector/preview_sample.py` — zobrazí náhled fotek podle ID + Použití: `python preview_sample.py 101 202 303 ...` + Claude vybere ID přes MCP dotazy, předá příkaz ke spuštění. + +- `00 PictureCollector/migrate_add_wanted.py` — přidal sloupec `wanted` (idempotentní) + +### Rozhodnutí + +#### Pravidla vyloučení cest — část 1 (2026-06-04) + +Tyto cesty **nechceme** — `wanted` zůstane FALSE, nezpracovávat: + +| Vzor cesty (obsahuje) | Důvod | Počet | +|---|---|---| +| `Torrents/Downloads/OOPS!!! International` | porno screenshoty | ~7 105 | +| `Torrents/Downloads/Tampons Pads Period` | porno | ~9 600 | +| `#ColdData/Porno/` | porno screenlists | — | +| `Porno1/` | porno | ~2 730 | +| `#ColdData/000 TORENT OBRAZKY/National Geographic Wallpapers` | stažené wallpapery | ~7 188 | +| `#ColdData/000 TORENT OBRAZKY/[OnlyFans]` | OnlyFans | ~1 377 | +| `#ColdData/000 TORENT OBRAZKY/Great Painters` | reprodukce obrazů | — | +| `UltraCC/` a obsahuje `/jpg` | Hot Wheels katalog a jiné torrent obrázky | ~3 484 | +| `Magentic/Runtime/UserPhotos/css` | webové ikonky | ~1 034 | +| `.Icecream Ebook Reader/` | obrázky z epub knih | — | +| `photoprism/sidecar/` | XMP sidecar soubory | — | + +> Otevřené: appdata/photoprism/cache, Immich thumbs, MP3/LP obaly, eBooks — vyřeší se v další části pravidel. + +### Schéma — nové sloupce v `photos` + +| Sloupec | Typ | Popis | +|---|---|---| +| `wanted` | `BOOLEAN NOT NULL DEFAULT FALSE` | chceme tuto fotku zachovat/zpracovat | +| `category` | `VARCHAR(100)` | kategorie: Fotopast, Rodina, Skeny, … | + +### Označené kategorie + +| Kamera / kritérium | wanted | category | Počet | +|---|---|---|---| +| BolyMedia SG520 | TRUE | Fotopast | 42 688 | + +### Na řadě + +- [ ] Prozkoumat co přesně je v záloze — jaký podíl jsou skutečné rodinné fotky +- [ ] Rozhodnout jak filtrovat odpad (path blacklist? size? absence kamery?) +- [ ] Vyřešit problém s roky — proč 57 % fotek padá do 2024 +- [ ] Označit první várku fotek jako `wanted = TRUE` + +--- + +## Backlog otevřených otázek + +1. Co s "sirotky" bez EXIF — `mtime` / odmítnout / označit? +2. Při shodě `sha256_pixels` — přeskočit / sloučit metadata / uložit oba? +3. Storage layout — nechat in-place / `archiv/YYYY/MM/` / content-addressable? +4. Jak poznat "rodinná fotka" od odpadu bez ruční kontroly?