"""Open Food Facts Lookup-Client.""" from __future__ import annotations import json import re import httpx from .config import get_settings settings = get_settings() # Faktoren, um eine Einheit in die Basiseinheit (Gramm bzw. Milliliter) umzurechnen. _UNIT_TO_BASE: dict[str, tuple[str, float]] = { "kg": ("gram", 1000.0), "g": ("gram", 1.0), "mg": ("gram", 0.001), "l": ("milliliter", 1000.0), "dl": ("milliliter", 100.0), "cl": ("milliliter", 10.0), "ml": ("milliliter", 1.0), } _QUANTITY_RE = re.compile(r"([\d]+(?:[.,]\d+)?)\s*(kg|mg|g|dl|cl|ml|l)\b", re.IGNORECASE) def parse_quantity(quantity: str | None) -> tuple[str, float | None]: """Ermittelt Basiseinheit und Packungsgröße aus dem OFF-Feld 'quantity'. Beispiele: '500 g' -> ('gram', 500), '1 kg' -> ('gram', 1000), '1,5 l' -> ('milliliter', 1500), '6 Stück' -> ('piece', None). """ if not quantity: return "piece", None match = _QUANTITY_RE.search(quantity) if not match: return "piece", None amount = float(match.group(1).replace(",", ".")) base_unit, factor = _UNIT_TO_BASE[match.group(2).lower()] package_size = round(amount * factor, 3) return base_unit, (package_size if package_size > 0 else None) def lookup_barcode(barcode: str) -> dict | None: """Fragt Open Food Facts nach einem Barcode. Gibt ein vorbefülltes Produkt-Dict zurück oder None, wenn nicht gefunden. """ # v0-API: liefert bei Treffer status=1 + product, bei Nicht-Treffer status=0 # (HTTP 200). Die v2-API hat kein status-Feld und antwortet mit HTTP 404, # weshalb wir bewusst v0 nutzen. url = f"{settings.off_base_url}/api/v0/product/{barcode}.json" try: resp = httpx.get( url, timeout=settings.off_timeout_seconds, headers={"User-Agent": "Vorrania-Selfhosted/1.0"}, follow_redirects=True, ) except httpx.HTTPError: return None if resp.status_code != 200: return None try: data = resp.json() except ValueError: return None if data.get("status") != 1: return None product = data.get("product", {}) name = ( product.get("product_name_de") or product.get("product_name") or product.get("generic_name") or "" ).strip() if not name: return None categories = product.get("categories") or "" category_tags = product.get("categories_tags") or [] base_unit, package_size = parse_quantity(product.get("quantity")) return { "barcode": barcode, "name": name, "brand": (product.get("brands") or "").strip() or None, "image_url": product.get("image_front_url") or product.get("image_url") or None, "base_unit": base_unit, "package_size": package_size, "quantity_text": product.get("quantity"), "category_suggestion": categories.split(",")[0].strip() if categories else None, "category_tags": category_tags, "source": "off", "off_raw": json.dumps(product)[:20000], }