Files
dimon 3f9b108482 RSS/Atom -> ntfy bridge with web UI, OPML import/export and RU/EN localization
Web-managed fork of nurefexc/rss-bridge-ntfy: Flask UI + REST API, background
sync engine (SQLite dedup, quiet hours, filters, flood protection, images),
OPML import/export and switchable interface/notification language.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 19:34:53 +08:00

87 lines
3.3 KiB
Python

"""OPML import/export.
Feeds are exported as an OPML 2.0 outline grouped by ntfy topic. ntfy-specific
options are stored as custom ``ntfy*`` attributes so an export/import round-trip
preserves everything. Import also accepts plain OPML files from other readers:
feeds without an ``ntfyTopic`` attribute inherit the parent outline's text as the
topic (falling back to ``rss``).
"""
import xml.etree.ElementTree as ET
def _int(value, default):
try:
return int(value)
except (TypeError, ValueError):
return default
def feeds_to_opml(feeds):
opml = ET.Element("opml", version="2.0")
head = ET.SubElement(opml, "head")
ET.SubElement(head, "title").text = "rss-bridge-ntfy feeds"
body = ET.SubElement(opml, "body")
# Group feeds by topic, preserving insertion order.
groups = {}
for feed in feeds:
groups.setdefault(feed.get("topic", "") or "feeds", []).append(feed)
for topic, items in groups.items():
group = ET.SubElement(body, "outline", text=topic, title=topic)
for feed in items:
label = feed.get("name") or feed.get("url", "")
ET.SubElement(group, "outline", **{
"type": "rss",
"text": label,
"title": label,
"xmlUrl": feed.get("url", ""),
"htmlUrl": feed.get("url", ""),
"ntfyTopic": feed.get("topic", "") or "",
"ntfyPriority": str(feed.get("priority", 3)),
"ntfyIcon": feed.get("icon", "") or "",
"ntfyQuietHours": feed.get("quiet_hours", "") or "",
"ntfyQuietPriority": str(feed.get("quiet_priority", 1)),
"ntfyInclude": feed.get("include_regex", "") or "",
"ntfyExclude": feed.get("exclude_regex", "") or "",
"ntfyEnabled": "true" if feed.get("enabled", True) else "false",
})
xml = ET.tostring(opml, encoding="unicode")
return '<?xml version="1.0" encoding="UTF-8"?>\n' + xml
def opml_to_feeds(xml_str):
root = ET.fromstring(xml_str)
body = root.find("body")
if body is None:
return []
feeds = []
def walk(node, parent_topic):
for outline in node.findall("outline"):
xml_url = outline.get("xmlUrl") or outline.get("xmlurl")
text = outline.get("text") or outline.get("title") or ""
if xml_url:
topic = outline.get("ntfyTopic") or parent_topic or text or "rss"
feeds.append({
"name": text or xml_url,
"url": xml_url,
"topic": topic,
"priority": _int(outline.get("ntfyPriority"), 3),
"icon": outline.get("ntfyIcon", "") or "",
"quiet_hours": outline.get("ntfyQuietHours", "") or "",
"quiet_priority": _int(outline.get("ntfyQuietPriority"), 1),
"include_regex": outline.get("ntfyInclude", "") or "",
"exclude_regex": outline.get("ntfyExclude", "") or "",
"enabled": (outline.get("ntfyEnabled", "true").lower() != "false"),
})
# A container outline (no xmlUrl) defines the topic for its children.
child_topic = text if (not xml_url and text) else parent_topic
walk(outline, child_topic)
walk(body, "")
return feeds