"""OPML import/export for feed subscriptions.""" from __future__ import annotations from xml.etree import ElementTree as ET from .models import Feed def export_opml(feeds: list[Feed]) -> str: """Render feeds as an OPML 2.0 document.""" opml = ET.Element("opml", version="2.0") head = ET.SubElement(opml, "head") ET.SubElement(head, "title").text = "RSS → ntfy subscriptions" body = ET.SubElement(opml, "body") for feed in feeds: attrs = { "type": "rss", "text": feed.title or feed.url, "title": feed.title or feed.url, "xmlUrl": feed.url, } # Stash the ntfy topic so a re-import keeps the routing. if feed.ntfy_topic: attrs["ntfyTopic"] = feed.ntfy_topic if feed.ntfy_server: attrs["ntfyServer"] = feed.ntfy_server ET.SubElement(body, "outline", attrs) return '\n' + ET.tostring( opml, encoding="unicode" ) def parse_opml(content: str) -> list[dict]: """Extract feed definitions from an OPML document. Returns a list of dicts ready to build Feed rows. Raises ValueError on malformed XML. """ try: root = ET.fromstring(content) except ET.ParseError as exc: raise ValueError(f"Некорректный OPML: {exc}") from exc feeds: list[dict] = [] for outline in root.iter("outline"): url = outline.get("xmlUrl") or outline.get("xmlurl") if not url: continue feeds.append( { "url": url.strip(), "title": (outline.get("title") or outline.get("text") or "").strip(), "ntfy_topic": (outline.get("ntfyTopic") or "").strip(), "ntfy_server": (outline.get("ntfyServer") or "").strip(), } ) return feeds