#!/usr/bin/env python3 """ preview_server.py — HTTP server pro náhled fotek z DB. Spustit v Docker kontejneru python-runner na Tower. Spuštění: docker exec -d python-runner python /scripts/preview_server.py API: GET /preview?ids=101,202,303 GET /preview?ids=101,202,303&thumb=400 GET /health """ import base64 import io import socketserver import urllib.parse from http.server import BaseHTTPRequestHandler import psycopg from PIL import Image # ── Konfigurace ─────────────────────────────────────────────────────────────── PORT = 8765 DB = dict( host="192.168.1.76", port=5432, user="vladimir.buzalka", password="Vlado7309208104++", dbname="fotky_buzalkovi" ) # Prefix v DB (jak to zapsal collect_pictures.py na Tower1) NAS_LINUX_PREFIX = "/mnt/user/ZalohaVsechObrazku/" # Cesta uvnitř kontejneru (NFS mount přidaný v Unraid Docker UI) NAS_CONTAINER_PATH = "/mnt/ZalohaVsechObrazku/" # ── DB dotaz ────────────────────────────────────────────────────────────────── 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 linux_to_container(p: str) -> str: rel = p.removeprefix(NAS_LINUX_PREFIX) return NAS_CONTAINER_PATH + rel def fetch_rows(ids: list[int]) -> list: conn = psycopg.connect(**DB) cur = conn.cursor() cur.execute(QUERY, (ids,)) rows = cur.fetchall() conn.close() order = {rid: i for i, rid in enumerate(ids)} return sorted(rows, key=lambda r: order.get(r[0], 9999)) # ── Thumbnaily ──────────────────────────────────────────────────────────────── def make_thumb_b64(path: str, size: int) -> str | None: try: img = Image.open(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 container_path = linux_to_container(cesta) b64 = make_thumb_b64(container_path, thumb_size) if b64: ok += 1 img_tag = f'foto' card_cls = "card" else: img_tag = ( '
⚠ soubor nedostupný
' ) 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'' f'📍 {lat:.4f}, {lon:.4f}' if lat and lon else "" ) chyby_html = f'
{chyby}
' if chyby else "" 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}
{container_path}
""") 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)}
""" # ── HTTP handler ────────────────────────────────────────────────────────────── class Handler(BaseHTTPRequestHandler): def log_message(self, format, *args): print(f"[{self.address_string()}] {format % args}", flush=True) def do_GET(self): parsed = urllib.parse.urlparse(self.path) # ── /health ────────────────────────────────────────────────────────── if parsed.path == "/health": self._text(200, "OK") return # ── /image ─────────────────────────────────────────────────────────── if parsed.path == "/image": params = urllib.parse.parse_qs(parsed.query) try: zid = int(params.get("id", [""])[0]) except ValueError: self._text(400, "Chybí id") return conn = psycopg.connect(**DB) cur = conn.cursor() cur.execute( "SELECT z.cesta_zalohy FROM zaloha_obrazku z JOIN photos p ON p.zaloha_id = z.id WHERE z.id = %s", (zid,) ) row = cur.fetchone() conn.close() if not row: self._text(404, "ID nenalezeno") return path = linux_to_container(row[0]) try: data = open(path, "rb").read() self.send_response(200) self.send_header("Content-Type", "image/jpeg") self.send_header("Content-Length", str(len(data))) self.end_headers() self.wfile.write(data) except Exception as ex: self._text(404, f"Soubor nedostupný: {ex}") return # ── /preview ───────────────────────────────────────────────────────── if parsed.path != "/preview": self._text(404, "Not found. Použij /preview?ids=101,202,303") return params = urllib.parse.parse_qs(parsed.query) ids_raw = params.get("ids", [""])[0] thumb = int(params.get("thumb", ["320"])[0]) try: ids = [int(x.strip()) for x in ids_raw.split(",") if x.strip()] except ValueError: self._text(400, "Chybné IDs — očekávaný formát: ?ids=101,202,303") return if not ids: self._text(400, "Chybí IDs — např. /preview?ids=101,202,303") return print(f" → {len(ids)} IDs, thumb={thumb}px", flush=True) rows = fetch_rows(ids) html = render_html(rows, ids, thumb) encoded = html.encode("utf-8") self.send_response(200) self.send_header("Content-Type", "text/html; charset=utf-8") self.send_header("Content-Length", str(len(encoded))) self.end_headers() self.wfile.write(encoded) def _text(self, code: int, msg: str): body = msg.encode("utf-8") self.send_response(code) self.send_header("Content-Type", "text/plain; charset=utf-8") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) # ── Main ────────────────────────────────────────────────────────────────────── if __name__ == "__main__": print(f"Preview server běží na http://0.0.0.0:{PORT}", flush=True) print(f"Test: http://tower:{PORT}/health", flush=True) print(f"Použití: http://tower:{PORT}/preview?ids=101,202,303&thumb=400", flush=True) socketserver.TCPServer.allow_reuse_address = True with socketserver.TCPServer(("", PORT), Handler) as httpd: httpd.serve_forever()