"""Open-Graph-Bild-Rendering via Playwright (#141). Rendert /v2/og-template?drucksache=X als PNG 1200×630. Cache in data/og-cache/ mit Key SHA256(drucksache + updated_at). Öffentliche API: ``render_og_card(drucksache, updated_at, base_url)`` → PNG-Bytes oder None bei Fehler ``cache_key(drucksache, updated_at)`` → Hex-String (SHA-256 Kurzform, 16 Zeichen) ``get_cached(drucksache, updated_at, cache_dir)`` → Path der gecacheten Datei oder None """ from __future__ import annotations import hashlib import logging from pathlib import Path from typing import Optional logger = logging.getLogger(__name__) _DEFAULT_CACHE_DIR = Path(__file__).resolve().parent.parent / "data" / "og-cache" def cache_key(drucksache: str, updated_at: str) -> str: """Berechnet den Cache-Schlüssel als 16-stelligen SHA-256-Präfix. Args: drucksache: Drucksachen-ID (z.B. "NRW-18/1234"). updated_at: ISO-Zeitstempel des letzten Updates aus der Datenbank. Returns: 16 Hex-Zeichen (64-Bit-Präfix des SHA-256). """ raw = f"{drucksache}|{updated_at}" return hashlib.sha256(raw.encode()).hexdigest()[:16] def _cache_path(drucksache: str, updated_at: str, cache_dir: Path) -> Path: key = cache_key(drucksache, updated_at) safe_name = drucksache.replace("/", "_").replace(" ", "_") return cache_dir / f"{safe_name}_{key}.png" def get_cached( drucksache: str, updated_at: str, cache_dir: Optional[Path] = None, ) -> Optional[Path]: """Gibt den Pfad der gecacheten PNG-Datei zurück, wenn sie existiert. Args: drucksache: Drucksachen-ID. updated_at: ISO-Zeitstempel — ändert sich dieser, ist der Cache ungültig. cache_dir: Verzeichnis für den Cache. Standard: data/og-cache/. Returns: Path-Objekt wenn Treffer, sonst None. """ cache_dir = cache_dir or _DEFAULT_CACHE_DIR path = _cache_path(drucksache, updated_at, cache_dir) return path if path.exists() else None def render_og_card( drucksache: str, updated_at: str, base_url: str = "http://127.0.0.1:8000", cache_dir: Optional[Path] = None, ) -> Optional[bytes]: """Rendert die OG-Karte als PNG via Playwright und legt sie im Cache ab. Bei Cache-Hit wird das Rendering übersprungen. Args: drucksache: Drucksachen-ID (URL-kodierbar). updated_at: ISO-Zeitstempel für den Cache-Key. base_url: Interne Basis-URL der App (Playwright greift darauf zu). cache_dir: Cache-Verzeichnis. Standard: data/og-cache/. Returns: PNG-Bytes bei Erfolg, None bei Fehler. """ cache_dir = cache_dir or _DEFAULT_CACHE_DIR cache_dir.mkdir(parents=True, exist_ok=True) cached = get_cached(drucksache, updated_at, cache_dir) if cached: logger.debug("OG-Cache-Hit für %s", drucksache) return cached.read_bytes() dest = _cache_path(drucksache, updated_at, cache_dir) try: from playwright.sync_api import sync_playwright import urllib.parse encoded = urllib.parse.quote(drucksache, safe="") url = f"{base_url}/v2/og-template?drucksache={encoded}" with sync_playwright() as pw: browser = pw.chromium.launch(args=["--no-sandbox"]) page = browser.new_page(viewport={"width": 1200, "height": 630}) page.goto(url, wait_until="networkidle", timeout=15000) png_bytes = page.screenshot( clip={"x": 0, "y": 0, "width": 1200, "height": 630}, type="png", ) browser.close() dest.write_bytes(png_bytes) logger.info("OG-Karte gerendert: %s → %s", drucksache, dest.name) return png_bytes except Exception: logger.exception("Playwright-Render fehlgeschlagen für %s", drucksache) return None