"""RSS feed ingestion — filtered for kant-en-klaar & supermarkt only.""" from __future__ import annotations import re import xml.etree.ElementTree as ET from datetime import datetime, timezone from email.utils import parsedate_to_datetime from typing import Any, Optional from urllib.request import Request, urlopen from app.db import execute, execute_returning, fetch_all, fetch_one USER_AGENT = "Foodlinkk-Intel/1.0" INCLUDE_KEYWORDS = ( "kant en klaar", "kant-en-klaar", "kant&klaa", "ready meal", "ready-to-eat", "maaltijd", "maaltijden", "supermarkt", "supermarket", "retail", "jumbo", "albert heijn", "ah ", " plus ", "lidl", "aldi", "dirk", "halal", "convenience", "schap", "filiaal", "foodservice", "vers", "meal", "grocery", "food retail", "kant-en-klaar", ) EXCLUDE_KEYWORDS = ( "voetbal", "sport", "politiek", "verkiezing", "trump", "bbc", "oorlog", "crypto", "bitcoin", "aandelenbeurs", "beurs ", "weerbericht", ) def _parse_date(raw: Optional[str]) -> Optional[datetime]: if not raw: return None try: return parsedate_to_datetime(raw).astimezone(timezone.utc) except Exception: pass try: return datetime.fromisoformat(raw.replace("Z", "+00:00")) except Exception: return None def _strip_html(text: str) -> str: return re.sub(r"<[^>]+>", "", text or "").strip()[:2000] def is_relevant(title: str, summary: Optional[str] = None) -> bool: blob = f"{title} {summary or ''}".lower() for bad in EXCLUDE_KEYWORDS: if bad in blob: return False for good in INCLUDE_KEYWORDS: if good in blob: return True return False def _fetch_xml(url: str) -> ET.Element: req = Request(url, headers={"User-Agent": USER_AGENT}) with urlopen(req, timeout=25) as resp: data = resp.read() return ET.fromstring(data) def _skip_keyword_filter(category: Optional[str]) -> bool: return category in ("regelgeving", "cbs", "markt") def refresh_feed(feed_id: int) -> dict[str, Any]: feed = fetch_one("SELECT * FROM rss_feeds WHERE id = %s AND is_active = TRUE", (feed_id,)) if not feed: return {"error": "feed not found"} skip_filter = _skip_keyword_filter(feed.get("category")) root = _fetch_xml(feed["url"]) items = root.findall(".//item") or root.findall(".//{http://www.w3.org/2005/Atom}entry") inserted = skipped = 0 for item in items[:50]: title = (item.findtext("title") or item.findtext("{http://www.w3.org/2005/Atom}title") or "").strip() link = (item.findtext("link") or "").strip() if not link: link_el = item.find("{http://www.w3.org/2005/Atom}link") if link_el is not None: link = link_el.get("href") or "" summary = item.findtext("description") or item.findtext("summary") or item.findtext("{http://www.w3.org/2005/Atom}summary") or "" pub = item.findtext("pubDate") or item.findtext("published") or item.findtext("{http://www.w3.org/2005/Atom}published") if not title or not link: continue clean_summary = _strip_html(summary) cat = (feed.get("category") or "").lower() if cat not in ("regelgeving", "cbs", "markt") and not is_relevant(title, clean_summary): skipped += 1 continue try: execute_returning( """INSERT INTO rss_items (feed_id, title, link, summary, published_at) VALUES (%s, %s, %s, %s, %s) RETURNING id""", (feed_id, title[:500], link[:1000], clean_summary, _parse_date(pub)), ) inserted += 1 except Exception: pass execute( "UPDATE rss_feeds SET last_fetch_at = NOW(), last_status = 'ok' WHERE id = %s", (feed_id,), ) return {"feed": feed["name"], "inserted": inserted, "skipped": skipped} def refresh_all_feeds() -> dict[str, Any]: feeds = fetch_all("SELECT id, name FROM rss_feeds WHERE is_active = TRUE") results = [] for f in feeds: try: results.append(refresh_feed(int(f["id"]))) except Exception as exc: # noqa: BLE001 execute("UPDATE rss_feeds SET last_status = %s WHERE id = %s", (str(exc)[:32], f["id"])) results.append({"feed": f["name"], "error": str(exc)}) return {"feeds": len(feeds), "results": results} def list_live_feed(limit: int = 40, category: Optional[str] = None) -> list[dict[str, Any]]: params: list[Any] = [] if category and category.lower() in ("regelgeving", "cbs", "markt"): base = """ SELECT i.*, f.name AS feed_name, f.category, f.url AS feed_url FROM rss_items i JOIN rss_feeds f ON f.id = i.feed_id AND f.is_active = TRUE WHERE f.category = %s """ params.append(category.lower()) else: like_clauses = " OR ".join( f"(i.title ILIKE %s OR COALESCE(i.summary,'') ILIKE %s)" for _ in INCLUDE_KEYWORDS[:12] ) for kw in INCLUDE_KEYWORDS[:12]: p = f"%{kw}%" params.extend([p, p]) base = f""" SELECT i.*, f.name AS feed_name, f.category, f.url AS feed_url FROM rss_items i JOIN rss_feeds f ON f.id = i.feed_id AND f.is_active = TRUE WHERE ({like_clauses}) """ if category: base += " AND f.category = %s" params.append(category) base += " ORDER BY i.published_at DESC NULLS LAST, i.fetched_at DESC LIMIT %s" params.append(limit) rows = fetch_all(base, tuple(params)) skip_filter = category and category.lower() in ("regelgeving", "cbs", "markt") if skip_filter: return [dict(r) for r in rows] return [dict(r) for r in rows if is_relevant(r.get("title") or "", r.get("summary"))]