···
1
+
"""OPML generation for thicket."""
3
+
import xml.etree.ElementTree as ET
4
+
from datetime import datetime
5
+
from pathlib import Path
6
+
from typing import Optional
7
+
from xml.dom import minidom
9
+
from ..models import UserMetadata
12
+
class OPMLGenerator:
13
+
"""Generates OPML files from feed collections."""
15
+
def __init__(self) -> None:
16
+
"""Initialize the OPML generator."""
21
+
users: dict[str, UserMetadata],
22
+
title: str = "Thicket Feeds",
23
+
output_path: Optional[Path] = None,
25
+
"""Generate OPML XML content from user metadata.
28
+
users: Dictionary of username -> UserMetadata
29
+
title: Title for the OPML file
30
+
output_path: Optional path to write the OPML file
33
+
OPML XML content as string
35
+
# Create root OPML element
36
+
opml = ET.Element("opml", version="2.0")
38
+
# Create head section
39
+
head = ET.SubElement(opml, "head")
40
+
title_elem = ET.SubElement(head, "title")
41
+
title_elem.text = title
43
+
date_created = ET.SubElement(head, "dateCreated")
44
+
date_created.text = datetime.now().strftime("%a, %d %b %Y %H:%M:%S %z")
46
+
date_modified = ET.SubElement(head, "dateModified")
47
+
date_modified.text = datetime.now().strftime("%a, %d %b %Y %H:%M:%S %z")
49
+
# Create body section
50
+
body = ET.SubElement(opml, "body")
52
+
# Add each user as an outline with their feeds as sub-outlines
53
+
for username, user_metadata in sorted(users.items()):
54
+
user_outline = ET.SubElement(body, "outline")
55
+
user_outline.set("text", user_metadata.display_name or username)
56
+
user_outline.set("title", user_metadata.display_name or username)
58
+
# Add user metadata as attributes if available
59
+
if user_metadata.homepage:
60
+
user_outline.set("htmlUrl", user_metadata.homepage)
61
+
if user_metadata.email:
62
+
user_outline.set("email", user_metadata.email)
64
+
# Add each feed as a sub-outline
65
+
for feed_url in sorted(user_metadata.feeds):
66
+
feed_outline = ET.SubElement(user_outline, "outline")
67
+
feed_outline.set("type", "rss")
68
+
feed_outline.set("text", feed_url)
69
+
feed_outline.set("title", feed_url)
70
+
feed_outline.set("xmlUrl", feed_url)
71
+
feed_outline.set("htmlUrl", feed_url)
73
+
# Convert to pretty-printed XML string
74
+
xml_str = self._prettify_xml(opml)
76
+
# Write to file if path provided
78
+
output_path.write_text(xml_str, encoding="utf-8")
82
+
def _prettify_xml(self, elem: ET.Element) -> str:
83
+
"""Return a pretty-printed XML string for the Element."""
84
+
rough_string = ET.tostring(elem, encoding="unicode")
85
+
reparsed = minidom.parseString(rough_string)
86
+
return reparsed.toprettyxml(indent=" ")
88
+
def generate_flat_opml(
90
+
users: dict[str, UserMetadata],
91
+
title: str = "Thicket Feeds (Flat)",
92
+
output_path: Optional[Path] = None,
94
+
"""Generate a flat OPML file with all feeds at the top level.
96
+
This format may be more compatible with some feed readers.
99
+
users: Dictionary of username -> UserMetadata
100
+
title: Title for the OPML file
101
+
output_path: Optional path to write the OPML file
104
+
OPML XML content as string
106
+
# Create root OPML element
107
+
opml = ET.Element("opml", version="2.0")
109
+
# Create head section
110
+
head = ET.SubElement(opml, "head")
111
+
title_elem = ET.SubElement(head, "title")
112
+
title_elem.text = title
114
+
date_created = ET.SubElement(head, "dateCreated")
115
+
date_created.text = datetime.now().strftime("%a, %d %b %Y %H:%M:%S %z")
117
+
date_modified = ET.SubElement(head, "dateModified")
118
+
date_modified.text = datetime.now().strftime("%a, %d %b %Y %H:%M:%S %z")
120
+
# Create body section
121
+
body = ET.SubElement(opml, "body")
123
+
# Collect all feeds with their associated user info
125
+
for username, user_metadata in users.items():
126
+
for feed_url in user_metadata.feeds:
130
+
"username": username,
131
+
"display_name": user_metadata.display_name or username,
132
+
"homepage": user_metadata.homepage,
136
+
# Sort feeds by URL for consistency
137
+
all_feeds.sort(key=lambda f: f["url"] or "")
139
+
# Add each feed as a top-level outline
140
+
for feed_info in all_feeds:
141
+
feed_outline = ET.SubElement(body, "outline")
142
+
feed_outline.set("type", "rss")
144
+
# Create a descriptive title that includes the user
145
+
title_text = f"{feed_info['display_name']}: {feed_info['url']}"
146
+
feed_outline.set("text", title_text)
147
+
feed_outline.set("title", title_text)
148
+
url = feed_info["url"] or ""
149
+
feed_outline.set("xmlUrl", url)
150
+
homepage_url = feed_info.get("homepage") or url
151
+
feed_outline.set("htmlUrl", homepage_url or "")
153
+
# Add custom attributes for user info
154
+
feed_outline.set("thicketUser", feed_info["username"] or "")
155
+
homepage = feed_info.get("homepage")
157
+
feed_outline.set("thicketHomepage", homepage)
159
+
# Convert to pretty-printed XML string
160
+
xml_str = self._prettify_xml(opml)
162
+
# Write to file if path provided
164
+
output_path.write_text(xml_str, encoding="utf-8")