From 82de38f02ad56e0ed892cbb7d2101e5e53bbc5d6 Mon Sep 17 00:00:00 2001 From: administrator Date: Fri, 5 Jun 2026 07:03:21 +0200 Subject: [PATCH] notebookVb --- 00 PictureCollector/preview_server.py | 338 ++++++++++++++++++++++++++ POSTUP.md | 12 + 2 files changed, 350 insertions(+) create mode 100644 00 PictureCollector/preview_server.py diff --git a/00 PictureCollector/preview_server.py b/00 PictureCollector/preview_server.py new file mode 100644 index 0000000..3425748 --- /dev/null +++ b/00 PictureCollector/preview_server.py @@ -0,0 +1,338 @@ +#!/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() diff --git a/POSTUP.md b/POSTUP.md index 75c9bf3..c5cb93b 100644 --- a/POSTUP.md +++ b/POSTUP.md @@ -82,6 +82,18 @@ Tyto cesty **nechceme** — `wanted` zůstane FALSE, nezpracovávat: | Kamera / kritérium | wanted | category | Počet | |---|---|---|---| | BolyMedia SG520 | TRUE | Fotopast | 42 688 | +| Apple iPhones + iPad (viz update_wanted.py) | TRUE | Rodina | — | +| 15 kompaktů (Panasonic, Canon, GoPro, …) | TRUE | Rodina | — | +| Samsung + profesionální (Nikon D5/D4…) | TRUE | Rodina | — | +| 32 dalších fotoaparátů (update_wanted_batch2.py) | TRUE | Rodina | 13 056 | + +### Vyloučené skenery (wanted=FALSE, category='Skener-nechceme') + +| Skener | Počet | Poznámka | +|---|---|---| +| CanoScan 8800F (`make IS NULL`) | 1 567 | Skeny, nechceme | + +> Ostatní skenery (LiDE 300/210/100, 4400F, HP Scanjet G2710, pls3015, djf300/2100/2200, MP210/190) — **dosud nerozhodnuto**. ### Na řadě