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>
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user