Merge remote-tracking branch 'origin/main'

This commit is contained in:
administrator
2026-06-04 22:56:46 +02:00
7 changed files with 503 additions and 2 deletions
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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.
+152
View File
@@ -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)
+53
View File
@@ -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
```
+226
View File
@@ -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)
+69
View File
@@ -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.")
+1
View File
@@ -1,4 +1,5 @@
blake3
python-dotenv
exifread
imagehash
pandas