Add RSS feed loader for episode page generation
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import html
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
from dataclasses import dataclass
|
||||
from email.utils import parsedate_to_datetime
|
||||
|
||||
ITUNES_NS = {"itunes": "http://www.itunes.com/dtds/podcast-1.0.dtd"}
|
||||
|
||||
TAG_RE = re.compile(r"<[^>]+>")
|
||||
WHITESPACE_RE = re.compile(r"\s+")
|
||||
|
||||
|
||||
@dataclass
|
||||
class Episode:
|
||||
number: int | None
|
||||
slug: str
|
||||
title: str
|
||||
description: str
|
||||
published_iso: str
|
||||
duration_seconds: int | None
|
||||
audio_url: str
|
||||
|
||||
|
||||
def parse_feed(xml_text: str) -> list[Episode]:
|
||||
"""Turn RSS feed XML into Episode records, skipping items with no usable link."""
|
||||
channel = ET.fromstring(xml_text).find("channel")
|
||||
if channel is None:
|
||||
return []
|
||||
|
||||
episodes: list[Episode] = []
|
||||
for item in channel.findall("item"):
|
||||
slug = _slug_from_link(item.findtext("link"))
|
||||
if not slug:
|
||||
continue
|
||||
enclosure = item.find("enclosure")
|
||||
episodes.append(
|
||||
Episode(
|
||||
number=_as_int(item.findtext("itunes:episode", namespaces=ITUNES_NS)),
|
||||
slug=slug,
|
||||
title=(item.findtext("title") or "").strip(),
|
||||
description=_plain_text(item.findtext("description")),
|
||||
published_iso=_iso_date(item.findtext("pubDate")),
|
||||
duration_seconds=_as_int(item.findtext("itunes:duration", namespaces=ITUNES_NS)),
|
||||
audio_url=(enclosure.get("url") or "") if enclosure is not None else "",
|
||||
)
|
||||
)
|
||||
return episodes
|
||||
|
||||
|
||||
def _slug_from_link(link: str | None) -> str:
|
||||
slug = (link or "").strip().rstrip("/")
|
||||
if "/episodes/" not in slug:
|
||||
return ""
|
||||
return slug.rsplit("/episodes/", 1)[-1]
|
||||
|
||||
|
||||
def _plain_text(raw: str | None) -> str:
|
||||
text = TAG_RE.sub(" ", raw or "")
|
||||
return WHITESPACE_RE.sub(" ", html.unescape(text)).strip()
|
||||
|
||||
|
||||
def _iso_date(raw: str | None) -> str:
|
||||
if not raw:
|
||||
return ""
|
||||
try:
|
||||
return parsedate_to_datetime(raw).isoformat()
|
||||
except (TypeError, ValueError):
|
||||
return ""
|
||||
|
||||
|
||||
def _as_int(raw: str | None) -> int | None:
|
||||
try:
|
||||
return int((raw or "").strip())
|
||||
except ValueError:
|
||||
return None
|
||||
Reference in New Issue
Block a user