From 0fd9cc8bbd9b971980de150cd6aab6a83ba90926 Mon Sep 17 00:00:00 2001 From: John Doty Date: Sat, 20 Sep 2025 10:52:23 -0700 Subject: [PATCH] OPML export --- cry/cli.py | 16 ++++++++++++++++ cry/opml.py | 22 ++++++++++++++++++++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/cry/cli.py b/cry/cli.py index e6f9c8e..ad9eb35 100644 --- a/cry/cli.py +++ b/cry/cli.py @@ -295,3 +295,19 @@ def serve(): def sync(): local_db = database.Database.local() database.sync(local_db) + + +@cli.command(name="export") +@click.argument("opml_file", type=click.File("wb")) +def export_opml(opml_file): + "Export the specified OPML file." + + db = database.Database.local() + feeds = db.load_all(feed_limit=0) + if len(feeds) == 0: + click.echo("Not subscribed to any feeds.") + return 0 + + feeds.sort(key=lambda f: f.title) + + opml.write_opml(opml_file, feeds) diff --git a/cry/opml.py b/cry/opml.py index 7960f0c..544ca8a 100644 --- a/cry/opml.py +++ b/cry/opml.py @@ -1,12 +1,30 @@ import pathlib -import xml.etree.ElementTree +import xml.etree.ElementTree as ET + +from . import feed def parse_opml(opml: str) -> list[str]: - f = xml.etree.ElementTree.fromstring(opml) + f = ET.fromstring(opml) return [e.attrib["xmlUrl"] for e in f.iterfind(".//*[@xmlUrl]")] def load_opml(path: pathlib.Path) -> list[str]: with open(path, "r", encoding="utf-8") as f: return parse_opml(f.read()) + + +def write_opml(f, feeds: list[feed.Feed]): + root = ET.Element("opml", {"version": "2.0"}) + body = ET.SubElement(root, "body") + head = ET.SubElement(root, "head") + title = ET.SubElement(head, "title") + title.text = "Exported from cry" + + for feed in feeds: + ET.SubElement( + body, + "outline", + {"type": "rss", "text": feed.title, "xmlUrl": feed.meta.url}, + ) + ET.ElementTree(root).write(f)