""" News-Router: aggregiert kuratierte RSS/Atom-Feeds rund um lokale LLMs. Serverseitig geholt (httpx) + mit stdlib geparst (kein neuer Dep), In-memory-Cache (30 min). Degradiert sauber: einzelne Feeds dürfen fehlschlagen, der Rest kommt trotzdem. """ import time import xml.etree.ElementTree as ET from datetime import datetime from email.utils import parsedate_to_datetime import httpx from fastapi import APIRouter, Depends from auth import auth router = APIRouter(prefix="/api", dependencies=[Depends(auth)]) FEEDS = [ ("HuggingFace", "https://huggingface.co/blog/feed.xml"), ("llama.cpp", "https://github.com/ggml-org/llama.cpp/releases.atom"), ("Ollama", "https://github.com/ollama/ollama/releases.atom"), ("r/LocalLLaMA", "https://www.reddit.com/r/LocalLLaMA/.rss"), ("Simon Willison", "https://simonwillison.net/atom/everything/"), ] _UA = {"User-Agent": "Mozilla/5.0 (MissionControl NewsBot)"} _TTL = 1800 # 30 min _CACHE = {"ts": 0.0, "items": []} _RELEVANT = ("rocm", "gfx1151", "strix", "amd", "vulkan", "llama.cpp", "gguf", "quant") def _tag(t): return t.split("}")[-1] def _epoch(s): if not s: return 0.0 try: return parsedate_to_datetime(s).timestamp() except Exception: # noqa: BLE001 pass try: return datetime.fromisoformat(s.replace("Z", "+00:00")).timestamp() except Exception: # noqa: BLE001 return 0.0 def _parse(source, xml_text): out = [] try: root = ET.fromstring(xml_text) except Exception: # noqa: BLE001 return out for el in root.iter(): if _tag(el.tag) not in ("item", "entry"): continue title = link = date = "" for ch in el: t = _tag(ch.tag) if t == "title": title = (ch.text or "").strip() elif t == "link": link = ch.get("href") or (ch.text or "").strip() or link elif t in ("pubDate", "published", "updated") and not date: date = (ch.text or "").strip() if title: low = title.lower() out.append({ "source": source, "title": title, "link": link, "ts": _epoch(date), "relevant": any(k in low for k in _RELEVANT), }) return out[:8] def get_news(): now = time.time() if now - _CACHE["ts"] < _TTL and _CACHE["items"]: return _CACHE["items"] items = [] with httpx.Client(timeout=8.0, headers=_UA, follow_redirects=True) as c: for source, url in FEEDS: try: r = c.get(url) r.raise_for_status() items += _parse(source, r.text) except Exception: # noqa: BLE001 continue items.sort(key=lambda x: x["ts"], reverse=True) items = items[:24] _CACHE.update(ts=now, items=items) return items @router.get("/news") def news(): return {"items": get_news()}