diff --git a/30 SběrDat/generate_thumbnails.py b/30 SběrDat/generate_thumbnails.py index 1994d1a..e8b1cde 100644 --- a/30 SběrDat/generate_thumbnails.py +++ b/30 SběrDat/generate_thumbnails.py @@ -45,7 +45,7 @@ for _env in (_here / ".env", _here.parent / ".env"): # ── Konfigurace ────────────────────────────────────────────────────────────── # Maximální počet fotek ke zpracování (0 = všechny) -MAX_PHOTOS = 10 +MAX_PHOTOS = 1000 # Pokud True, na začátku skriptu se smažou všechny thumbnaily (DB i soubory) RESET = True diff --git a/30 SběrDat/verify_photos.py b/30 SběrDat/verify_photos.py index ea5bcf3..9ae14bb 100644 --- a/30 SběrDat/verify_photos.py +++ b/30 SběrDat/verify_photos.py @@ -44,7 +44,7 @@ for _env in (_here / ".env", _here.parent / ".env"): # ── Konfigurace ────────────────────────────────────────────────────────────── # Maximální počet fotek ke zpracování (0 = všechny) -MAX_PHOTOS = 10 +MAX_PHOTOS = 0 # Pokud True, na začátku skriptu se smažou všechny záznamy v photo_errors # a vyresetuje verified_at v photos. Pak proběhne plná verifikace. diff --git a/40 Dedup/dedup.py b/40 Dedup/dedup.py new file mode 100644 index 0000000..b827c4d --- /dev/null +++ b/40 Dedup/dedup.py @@ -0,0 +1,152 @@ +""" +Deduplikace: zkontroluje obrázky ve vybrané složce oproti tabulce zaloha_obrazku. +Pokud je obrázek bezpečně zazálohován (blake3 sedí + záložní soubor existuje), smaže ho. +""" + +import sys +import os +import tkinter as tk +from tkinter import filedialog, messagebox +from pathlib import Path +import psycopg2 +import blake3 +from dotenv import load_dotenv + +load_dotenv() + +# ── DB připojení ──────────────────────────────────────────────────────────── +DB_DSN = ( + f"host={os.getenv('DB_HOST', 'localhost')} " + f"port={os.getenv('DB_PORT', '5432')} " + f"dbname={os.getenv('DB_NAME')} " + f"user={os.getenv('DB_USER')} " + f"password={os.getenv('DB_PASSWORD')}" +) + +IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".tif", + ".webp", ".heic", ".heif", ".raw", ".cr2", ".nef", ".arw"} + +# Překlad Linux cest na Windows UNC +LINUX_TO_WINDOWS = { + "/mnt/user/ZalohaVsechObrazku": r"\\Tower1\ZalohaVsechObrazku", + "/mnt/remotes/TOWER1.LAN_ZalohaVsechObrazku": r"\\Tower1\ZalohaVsechObrazku", +} + + +def linux_to_windows_path(linux_path: str) -> Path: + for prefix, unc in LINUX_TO_WINDOWS.items(): + if linux_path.startswith(prefix): + rest = linux_path[len(prefix):].replace("/", "\\") + return Path(unc + rest) + return Path(linux_path) + + +def compute_blake3(path: Path) -> str: + h = blake3.blake3() + with open(path, "rb") as f: + for chunk in iter(lambda: f.read(1024 * 1024), b""): + h.update(chunk) + return h.hexdigest() + + +def pick_folder() -> Path | None: + root = tk.Tk() + root.withdraw() + folder = filedialog.askdirectory(title="Vyber složku k deduplikaci") + root.destroy() + return Path(folder) if folder else None + + +def log(msg: str): + print(msg, flush=True) + + +def dedup(folder: Path): + images = [p for p in folder.rglob("*") if p.suffix.lower() in IMAGE_EXTENSIONS] + log(f"Nalezeno obrázků: {len(images)}") + + if not images: + log("Žádné obrázky k zpracování.") + return + + conn = psycopg2.connect(DB_DSN) + cur = conn.cursor() + + deleted = 0 + skipped = 0 + errors = 0 + + for img in images: + try: + size = img.stat().st_size + name = img.name + + # 1. Hledej podle velikosti + cur.execute( + "SELECT blake3_hash, cesta_zalohy, nazev_souboru FROM zaloha_obrazku " + "WHERE velikost = %s", + (size,) + ) + rows = cur.fetchall() + + if not rows: + log(f" PŘESKOČEN (žádná záloha se stejnou velikostí): {img.name}") + skipped += 1 + continue + + # 2. Spočítej blake3 lokálního souboru (jednou pro všechny kandidáty) + local_hash = compute_blake3(img) + + matched = False + for db_hash, cesta_zalohy, db_name in rows: + + if local_hash != db_hash: + continue + + # 3. Zkontroluj, že záložní soubor fyzicky existuje + backup_path = linux_to_windows_path(cesta_zalohy) + if not backup_path.exists(): + log(f" PŘESKOČEN (záložní soubor neexistuje): {cesta_zalohy}") + continue + + # 4. Zkontroluj blake3 záložního souboru + backup_hash = compute_blake3(backup_path) + if backup_hash != db_hash: + log(f" PŘESKOČEN (blake3 zálohy nesedí s DB): {cesta_zalohy}") + continue + + matched = True + break + + if matched: + img.unlink() + if img.name != db_name: + log(f" SMAZÁN [JINÉ JMÉNO]: {img.name} != záloha: {db_name} (binary identické)") + else: + log(f" SMAZÁN: {img}") + deleted += 1 + else: + log(f" PŘESKOČEN (žádná platná záloha neprošla): {img.name}") + skipped += 1 + + except Exception as e: + log(f" CHYBA ({img.name}): {e}") + errors += 1 + + cur.close() + conn.close() + + log(f"\n--- Hotovo ---") + log(f"Smazáno: {deleted}") + log(f"Přeskočeno: {skipped}") + log(f"Chyby: {errors}") + + +if __name__ == "__main__": + folder = pick_folder() + if not folder: + print("Žádná složka nevybrána. Končím.") + sys.exit(0) + + print(f"Zpracovávám složku: {folder}") + dedup(folder) diff --git a/mcp_postgres/README.md b/mcp_postgres/README.md new file mode 100644 index 0000000..5267264 --- /dev/null +++ b/mcp_postgres/README.md @@ -0,0 +1,53 @@ +# PostgreSQL MCP server — fotky_buzalkovi + +Umožní Claudovi volat tvůj lokální PostgreSQL přímo. + +## 1. Závislosti (jednou) + +```powershell +# Ve venv projektu (nebo globálně) +.\.venv\Scripts\pip install psycopg2-binary mcp +``` + +> `mcp` balíček není nutný — server používá čistý stdio JSON-RPC, žádná extra knihovna nepotřeba. Stačí `psycopg2-binary`. + +## 2. Přidání do Claude Desktop + +Otevři (nebo vytvoř) soubor: +``` +%APPDATA%\Claude\claude_desktop_config.json +``` + +A přidej do sekce `mcpServers`: + +```json +{ + "mcpServers": { + "postgres-fotky": { + "command": "C:\\Users\\vlado\\PycharmProjects\\fotkyBuzalkovi\\.venv\\Scripts\\python.exe", + "args": [ + "C:\\Users\\vlado\\PycharmProjects\\fotkyBuzalkovi\\mcp_postgres\\server.py" + ] + } + } +} +``` + +## 3. Restart Claude Desktop + +Po uložení config souboru restartuj Claude Desktop. Server se spustí automaticky. + +## 4. Dostupné nástroje + +| Tool | Popis | +|------|-------| +| `pg_query` | SELECT dotaz, vrátí JSON (max 200 řádků) | +| `pg_execute` | INSERT / UPDATE / DELETE / DDL | +| `pg_list_tables` | Seznam tabulek s velikostmi | +| `pg_describe_table` | Struktura tabulky (sloupce, indexy) | + +## Přímé otestování + +```powershell +echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1"}}}' | .\.venv\Scripts\python.exe mcp_postgres\server.py +``` diff --git a/mcp_postgres/server.py b/mcp_postgres/server.py new file mode 100644 index 0000000..d12445a --- /dev/null +++ b/mcp_postgres/server.py @@ -0,0 +1,226 @@ +""" +PostgreSQL MCP Server pro fotky_buzalkovi +Spustit: python server.py +""" +import json +import sys +import os +import psycopg2 +import psycopg2.extras +from typing import Any + +# Vynutit UTF-8 na stdout/stderr (Windows může defaultovat na cp1252) +if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8") +if hasattr(sys.stderr, "reconfigure"): + sys.stderr.reconfigure(encoding="utf-8") + +# --- MCP protocol helpers (stdio transport) --- + +def send(msg: dict): + line = json.dumps(msg, ensure_ascii=False, default=str) + sys.stdout.write(line + "\n") + sys.stdout.flush() + +def recv() -> dict: + line = sys.stdin.readline() + if not line: + raise EOFError + return json.loads(line) + +# --- DB connection --- + +DB = dict( + host=os.getenv("DB_HOST", "192.168.1.76"), + port=int(os.getenv("DB_PORT", 5432)), + user=os.getenv("DB_USER", "vladimir.buzalka"), + password=os.getenv("DB_PASSWORD", "Vlado7309208104++"), + dbname=os.getenv("DB_NAME", "fotky_buzalkovi"), +) + +def get_conn(): + return psycopg2.connect(**DB) + +# --- Tool implementations --- + +def tool_query(sql: str, params: list | None = None, limit: int = 200) -> str: + """SELECT dotaz, vrátí JSON.""" + sql = sql.strip().rstrip(";") + # Bezpečnost: pouze SELECT/EXPLAIN/WITH + first = sql.split()[0].upper() + if first not in ("SELECT", "EXPLAIN", "WITH", "TABLE", "SHOW"): + return "Chyba: tool_query je jen pro SELECT. Pro zápis použij tool_execute." + sql = f"SELECT * FROM ({sql}) _q LIMIT {limit}" if first == "SELECT" else sql + with get_conn() as conn: + with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur: + cur.execute(sql, params or []) + rows = cur.fetchall() + return json.dumps([dict(r) for r in rows], ensure_ascii=False, default=str) + +def tool_execute(sql: str, params: list | None = None) -> str: + """INSERT/UPDATE/DELETE/CREATE — vrátí počet ovlivněných řádků.""" + with get_conn() as conn: + with conn.cursor() as cur: + cur.execute(sql, params or []) + rowcount = cur.rowcount + conn.commit() + return f"OK, ovlivněno řádků: {rowcount}" + +def tool_list_tables() -> str: + """Vrátí seznam tabulek v public schema.""" + sql = """ + SELECT table_name, + pg_size_pretty(pg_total_relation_size(quote_ident(table_name))) AS size + FROM information_schema.tables + WHERE table_schema = 'public' + ORDER BY table_name + """ + with get_conn() as conn: + with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur: + cur.execute(sql) + rows = cur.fetchall() + return json.dumps([dict(r) for r in rows], ensure_ascii=False) + +def tool_describe_table(table: str) -> str: + """Vrátí sloupce, typy, nullable, default + indexy dané tabulky.""" + cols_sql = """ + SELECT column_name, data_type, is_nullable, column_default + FROM information_schema.columns + WHERE table_schema = 'public' AND table_name = %s + ORDER BY ordinal_position + """ + idx_sql = """ + SELECT indexname, indexdef + FROM pg_indexes + WHERE schemaname = 'public' AND tablename = %s + """ + with get_conn() as conn: + with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur: + cur.execute(cols_sql, [table]) + cols = cur.fetchall() + cur.execute(idx_sql, [table]) + idxs = cur.fetchall() + return json.dumps({ + "columns": [dict(r) for r in cols], + "indexes": [dict(r) for r in idxs], + }, ensure_ascii=False) + +# --- Tool registry --- + +TOOLS = [ + { + "name": "pg_query", + "description": "Spustí SELECT dotaz na fotky_buzalkovi a vrátí výsledky jako JSON. Max 200 řádků (nastavitelné parametrem limit).", + "inputSchema": { + "type": "object", + "properties": { + "sql": {"type": "string", "description": "SELECT SQL dotaz"}, + "params": {"type": "array", "description": "Poziční parametry (%s)", "default": []}, + "limit": {"type": "integer","description": "Max počet řádků (default 200)", "default": 200}, + }, + "required": ["sql"], + }, + }, + { + "name": "pg_execute", + "description": "Spustí INSERT/UPDATE/DELETE/DDL na fotky_buzalkovi. Vrátí počet ovlivněných řádků.", + "inputSchema": { + "type": "object", + "properties": { + "sql": {"type": "string", "description": "SQL příkaz (INSERT/UPDATE/DELETE/CREATE/ALTER)"}, + "params": {"type": "array", "description": "Poziční parametry (%s)", "default": []}, + }, + "required": ["sql"], + }, + }, + { + "name": "pg_list_tables", + "description": "Vrátí seznam všech tabulek v databázi fotky_buzalkovi s jejich velikostí.", + "inputSchema": {"type": "object", "properties": {}}, + }, + { + "name": "pg_describe_table", + "description": "Vrátí strukturu (sloupce, typy, indexy) dané tabulky.", + "inputSchema": { + "type": "object", + "properties": { + "table": {"type": "string", "description": "Název tabulky"}, + }, + "required": ["table"], + }, + }, +] + +# --- MCP message loop --- + +def handle(req: dict) -> dict | None: + method = req.get("method", "") + rid = req.get("id") + + if method == "initialize": + return { + "jsonrpc": "2.0", "id": rid, + "result": { + "protocolVersion": "2024-11-05", + "capabilities": {"tools": {}}, + "serverInfo": {"name": "postgres-fotky", "version": "1.0.0"}, + }, + } + + if method == "notifications/initialized": + return None # no response needed + + if method == "tools/list": + return { + "jsonrpc": "2.0", "id": rid, + "result": {"tools": TOOLS}, + } + + if method == "tools/call": + name = req["params"]["name"] + args = req["params"].get("arguments", {}) + try: + if name == "pg_query": + result = tool_query(args["sql"], args.get("params"), args.get("limit", 200)) + elif name == "pg_execute": + result = tool_execute(args["sql"], args.get("params")) + elif name == "pg_list_tables": + result = tool_list_tables() + elif name == "pg_describe_table": + result = tool_describe_table(args["table"]) + else: + result = f"Neznámý tool: {name}" + return { + "jsonrpc": "2.0", "id": rid, + "result": {"content": [{"type": "text", "text": result}]}, + } + except Exception as e: + return { + "jsonrpc": "2.0", "id": rid, + "result": {"content": [{"type": "text", "text": f"Chyba: {e}"}], "isError": True}, + } + + # Neznámá metoda + return { + "jsonrpc": "2.0", "id": rid, + "error": {"code": -32601, "message": f"Method not found: {method}"}, + } + + +if __name__ == "__main__": + # Ověření připojení při startu + try: + with get_conn() as c: + pass + sys.stderr.write("postgres-fotky MCP: připojeno k DB OK\n") + except Exception as e: + sys.stderr.write(f"postgres-fotky MCP: VAROVÁNÍ — nelze se připojit k DB: {e}\n") + + while True: + try: + req = recv() + except EOFError: + break + resp = handle(req) + if resp is not None: + send(resp) diff --git a/query_photo_errors.py b/query_photo_errors.py new file mode 100644 index 0000000..8aa2876 --- /dev/null +++ b/query_photo_errors.py @@ -0,0 +1,69 @@ +"""Rychlý summary tabulky photo_errors — spusť a zkopíruj výstup.""" +import psycopg2 +from dotenv import load_dotenv +import os + +load_dotenv() + +conn = psycopg2.connect( + host=os.getenv("DB_HOST", "192.168.1.76"), + port=int(os.getenv("DB_PORT", 5432)), + user=os.getenv("DB_USER", "vladimir.buzalka"), + password=os.getenv("DB_PASSWORD", "Vlado7309208104++"), + dbname=os.getenv("DB_NAME", "fotky_buzalkovi"), +) +cur = conn.cursor() + +# Struktura +cur.execute(""" + SELECT column_name, data_type + FROM information_schema.columns + WHERE table_name = 'photo_errors' + ORDER BY ordinal_position +""") +print("=== SLOUPCE ===") +for row in cur.fetchall(): + print(f" {row[0]:30s} {row[1]}") + +# Celkový počet +cur.execute("SELECT COUNT(*) FROM photo_errors") +total = cur.fetchone()[0] +print(f"\n=== CELKEM ZÁZNAMŮ: {total} ===") + +# Počty podle error_type (pokud sloupec existuje) +try: + cur.execute(""" + SELECT error_type, COUNT(*) AS pocet + FROM photo_errors + GROUP BY error_type + ORDER BY pocet DESC + """) + rows = cur.fetchall() + if rows: + print("\n=== TYPY CHYB ===") + for r in rows: + print(f" {str(r[0]):50s} {r[1]:>8,}") +except Exception as e: + print(f"\n(error_type sloupec neexistuje nebo chyba: {e})") + +# Ukázka prvních 5 řádků +cur.execute("SELECT * FROM photo_errors LIMIT 5") +cols = [d[0] for d in cur.description] +rows = cur.fetchall() +print(f"\n=== UKÁZKA (prvních 5) ===") +print(" " + " | ".join(cols)) +for r in rows: + print(" " + " | ".join(str(v)[:60] for v in r)) + +# Timeline (pokud je sloupec s datumem) +date_cols = [d[0] for d in cur.description if "dat" in d[0].lower() or "time" in d[0].lower() or "created" in d[0].lower()] +if date_cols: + dc = date_cols[0] + cur.execute(f"SELECT MIN({dc}), MAX({dc}) FROM photo_errors") + mn, mx = cur.fetchone() + print(f"\n=== ČASOVÝ ROZSAH ({dc}) ===") + print(f" Od: {mn}") + print(f" Do: {mx}") + +conn.close() +print("\nHotovo.") diff --git a/requirements.txt b/requirements.txt index 03eaf9f..a3f0a27 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ blake3 +python-dotenv exifread imagehash pandas