Added RSS feed generator

Signed-off-by: Markus Birth <markus@birth-online.de>
This commit is contained in:
2026-01-29 12:38:33 +00:00
parent 72457df5bd
commit 2fcf3e3bf2
7 changed files with 301 additions and 0 deletions
+2
View File
@@ -7,6 +7,8 @@
*.jpg
*.gif
*.png
RSS/index.rss2
!RSS/rsslogo.jpg
# macOS
.DS_Store
+11
View File
@@ -0,0 +1,11 @@
RSS Generator
=============
Copy these files to the station root folder, then run:
uv run ./generate_rss.py
This will generate a file `index.rss2` containing entries for all the audio
files. Host everything on a web server and add the link to the `index.rss2`
to your favourite podcast app.
+233
View File
@@ -0,0 +1,233 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
print("Started.")
import glob
import re
import xml.etree.ElementTree as ET
import xml.dom.minidom
from datetime import datetime
from os.path import basename, getsize, isfile, splitext
from pytz import timezone
from urllib.parse import quote
from mutagen.mp4 import MP4
from rich import print
from rich.progress import track
BASEURL = "https://rpi4.mbirth.uk/wsqk/"
RFC822 = "%a, %d %b %Y %H:%M:%S %z"
TEASER = {
"Late Night Crew": {
"title": "The Late Night Crew with Vance Goodman",
"desc": "Midnight belongs to the night owls of Hawkins. Join The Late Night Crew, 12 to 3 am on The Squawk",
},
"s Early Risers": {
"title": "Mindy's Early Risers",
"desc": "For the bakers, the night shifters, and the can't-sleep crew - Keeping Hawkins company from 3 to 6 am",
},
"Weekend Early Risers": {
"title": "Mindy's Weekend Early Risers",
"desc" :"Getting up or just getting in? Keeping Hawkins company from 3 to 6 am",
},
"K Wake Up": {
"title": "The WSQK Wake Up with Vance Goodman",
"desc": "Playing you Hawkins hottest hits to eat your morning waffles on The Squawk.",
},
"Weekend Wake Up": {
"title": "The Weekend Wake Up with Vance Goodman",
"desc": "Playing you Hawkins hottest hits to eat your morning waffles on The Squawk.",
},
"Rewind At 9": {
"title": "The REWIND at 9",
"desc": "The ultimate test, Mindy plays a track, but it's been turned upside down & played backwards, but what is it?",
},
"in the Mornings": {
"title": "Mindy Flare in the Mornings",
"desc": "Roll through your morning with Mindy, 9 to 12, the heartbeat of Hawkins and the queen of mid-morning hits.",
},
"Afternoon Hangout": {
"title": "The Afternoon Hangout with Vance Goodman",
"desc": "12 to 4, blasting Hawkins biggest hits straight from the Tower of Power on WSQK The Squawk.",
},
"Talk To Tammy": {
"title": "Talk to Tammy",
"desc": "Need a hug and some real talk? You gotta problem? You need your mammy? It's time to ... Talk to Tammy",
},
"Hawkins Homerun": {
"title": "The Hawkins Home Run with Mindy Flare",
"desc": "Clock out and crank up, we're blasting the best drive-time hits from 4 to 7 on WSQK The Squawk.",
},
"Weekend Homerun": {
"title": "The Weekend Homerun With Mindy Flare",
"desc": "Back from the big game? or out shopping at the Mall? We have best tracks for your weekend afternoon!",
},
"Dial-A-Dedication": {
"title": "Dial-A-Dedication",
"desc": "Your chance to take over the airwaves every single night at 7 with Vance Goodman, on WSQK, The Squawk",
},
"Evenings": {
"title": "Evenings with Vance Goodman",
"desc": "Bringing the glow of Hawkins nights alive, 7 to 9 on WSQK The Squawk",
},
"Late Nights": {
"title": "Late Nights with Mindy Flare",
"desc": "9pm to midnight with relaxing vibes, soft lights, and the hottest night-time hits.",
},
"New Year": {
"title": "The Squawks New Years Eve Party",
"desc": "Counting down to New Year across Hawkins, with a BANG!",
},
"ʎɐᗡ ʇsɐ⅂ ǝuO": {
"title": "ʎɐᗡ ʇsɐ⅂ ǝuO",
"desc": "One Last Day with Vance Goodman and Mindy Flare, broadcasting your Dial A Dedications and keeping the hits blasting until 11pm",
},
}
def appendTextElement(parent_element, tag_name, text):
tmp = ET.SubElement(parent_element, tag_name)
tmp.text = text
return tmp
def secondsToHMS(seconds):
hours = int(seconds / 3600)
remainder = seconds % 3600
minutes = int(remainder / 60)
seconds = int(remainder % 60)
return "{:02d}:{:02d}:{:02d}".format(hours, minutes, seconds)
def appendMP4Item(parent_element, mp4_filename):
namenoext = splitext(mp4_filename)[0]
title = basename(namenoext)
size = getsize(mp4_filename)
duration = MP4(mp4_filename).info.length
hms_stamp = secondsToHMS(duration)
#print(repr(duration))
#print(hms_stamp)
# Cut off ".." and audio parent directory
#file_url = "/".join(mp4_filename.split("/")[2:])
file_url = mp4_filename
# Fix time in title ("16-00-00" --> "04:00pm")
try:
(t_hms, t_t) = title.split(" ", 1)
t_h = int(t_hms[0:2])
t_ap = "am"
if t_h>11:
t_ap = "pm"
if t_h>12:
t_h -= 12
elif t_h==0:
t_h = 12
title = f"{t_h}:{t_hms[3:5]}{t_ap} {t_t}"
except:
pass
url = BASEURL + quote(file_url)
txt = namenoext + ".txt"
item = ET.SubElement(parent_element, "item")
appendTextElement(item, "guid", url)
appendTextElement(item, "title", title)
appendTextElement(item, "itunes:duration", hms_stamp)
description = ""
for i in TEASER:
if i in title:
description += TEASER[i]["desc"]
if isfile(txt):
f = open(txt, "r")
desc = f.read()
desc = desc.replace("\n", "<br />")
description += "\n\n" + desc
#appendTextElement(item, "description", desc)
#appendTextElement(item, "content:encoded", desc)
if description:
appendTextElement(item, "description", description)
enc = ET.SubElement(item, "enclosure")
enc.set("url", url)
enc.set("length", str(size))
enc.set("type", "audio/mp4")
date = re.search(r"(\d{4})-(\d\d)-(\d\d)", namenoext)
time = re.search(r"/(\d\d)-(\d\d)-(\d\d) ", namenoext)
if date:
if time:
pubdate = datetime(int(date.group(1)), int(date.group(2)), int(date.group(3)), int(time.group(1)), int(time.group(2)), int(time.group(3)))
else:
pubdate = datetime(int(date.group(1)), int(date.group(2)), int(date.group(3)), 23, 59, 59)
pubdate = timezone("Europe/London").localize(pubdate)
appendTextElement(item, "pubDate", pubdate.strftime(RFC822))
ET.register_namespace("content", "http://purl.org/rss/1.0/modules/content/")
ET.register_namespace("atom", "http://www.w3.org/2005/Atom")
ET.register_namespace("itunes", "http://www.itunes.com/dtds/podcast-1.0.dtd")
ET.register_namespace("spotify", "http://www.spotify.com/ns/rss")
ET.register_namespace("psc", "https://podlove.org/simple-chapters/")
tree = ET.ElementTree(ET.Element("rss"))
root = tree.getroot()
root.set("version", "2.0")
root.set("xmlns:content", "http://purl.org/rss/1.0/modules/content/")
root.set("xmlns:atom", "http://www.w3.org/2005/Atom")
root.set("xmlns:itunes", "http://www.itunes.com/dtds/podcast-1.0.dtd")
root.set("xmlns:spotify", "http://www.spotify.com/ns/rss")
root.set("xmlns:psc", "https://podlove.org/simple-chapters/")
channel = ET.SubElement(root, "channel")
appendTextElement(channel, "title", "WSQK - The Squawk - 94.5FM")
appendTextElement(channel, "link", BASEURL + "index.rss2")
appendTextElement(channel, "copyright", "Netflix &amp; Global Radio")
appendTextElement(channel, "itunes:author", "Netflix &amp; Global Radio")
ET.SubElement(channel, "atom:link", {"rel": "self", "type": "application/rss+xml", "href": BASEURL + "index.rss2"})
appendTextElement(channel, "description", "Introducing Hawkins' own radio station: WSQK 'The Squawk'. From November 24th until January 1st, you will be transported straight into the heart of Hawkins, 24/7.")
appendTextElement(channel, "language", "en-uk")
img = ET.SubElement(channel, "image")
appendTextElement(img, "url", BASEURL + "rsslogo.jpg")
appendTextElement(img, "title", "WSQK - The Squawk - 94.5FM")
appendTextElement(img, "link", BASEURL + "index.rss2")
appendTextElement(img, "width", "512")
appendTextElement(img, "height", "512")
ET.SubElement(channel, "itunes:category", {"text": "Music"})
appendTextElement(channel, "itunes:explicit", "No")
# <pubDate>Tue, 10 Jun 2003 04:00:00 GMT</pubDate>
appendTextElement(channel, "lastBuildDate", datetime.now(timezone("Europe/Berlin")).strftime(RFC822)) # RFC 822 format
appendTextElement(channel, "docs", "http://blogs.law.harvard.edu/tech/rss")
appendTextElement(channel, "generator", "Handcrafted with love")
manEd = appendTextElement(channel, "managingEditor", "markus@birth-online.de (Markus Birth)")
appendTextElement(channel, "webMaster", manEd.text)
print("Scanning for audio files...")
for m4a in track(sorted(glob.glob("20*/*.m4a")), description="Parsing audio files..."):
appendMP4Item(channel, m4a)
rawxml = ET.tostring(root, "utf-8")
reparsed = xml.dom.minidom.parseString(rawxml)
prettyxml = reparsed.toprettyxml()
print("Writing RSS file...")
with open("index.rss2", "wt") as f:
f.write(prettyxml)
#tree.write("index.rss2", "utf-8", True)
#print(ET.tostring(root, "utf-8").decode("utf-8"))
print("All done.")
+11
View File
@@ -0,0 +1,11 @@
[project]
name = "generate_rss"
version = "0.1.0"
description = "Generates RSS2 file to serve WSQK audio files"
readme = "README.md"
requires-python = ">=3.14"
dependencies = [
"mutagen>=1.47.0",
"pytz>=2025.2",
"rich>=14.3.1",
]
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

+5
View File
@@ -8,3 +8,8 @@ dependencies = [
"requests>=2.32.5",
"rich>=14.2.0",
]
[tool.uv.workspace]
members = [
"RSS",
]
Generated
+39
View File
@@ -2,6 +2,12 @@ version = 1
revision = 3
requires-python = ">=3.14"
[manifest]
members = [
"generate-rss",
"globalcatchup",
]
[[package]]
name = "certifi"
version = "2025.11.12"
@@ -36,6 +42,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
]
[[package]]
name = "generate-rss"
version = "0.1.0"
source = { virtual = "RSS" }
dependencies = [
{ name = "mutagen" },
{ name = "pytz" },
]
[package.metadata]
requires-dist = [
{ name = "mutagen", specifier = ">=1.47.0" },
{ name = "pytz", specifier = ">=2025.2" },
]
[[package]]
name = "globalcatchup"
version = "0.1.0"
@@ -81,6 +102,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
]
[[package]]
name = "mutagen"
version = "1.47.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/81/e6/64bc71b74eef4b68e61eb921dcf72dabd9e4ec4af1e11891bbd312ccbb77/mutagen-1.47.0.tar.gz", hash = "sha256:719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99", size = 1274186, upload-time = "2023-09-03T16:33:33.411Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b0/7a/620f945b96be1f6ee357d211d5bf74ab1b7fe72a9f1525aafbfe3aee6875/mutagen-1.47.0-py3-none-any.whl", hash = "sha256:edd96f50c5907a9539d8e5bba7245f62c9f520aef333d13392a79a4f70aca719", size = 194391, upload-time = "2023-09-03T16:33:29.955Z" },
]
[[package]]
name = "pygments"
version = "2.19.2"
@@ -90,6 +120,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
]
[[package]]
name = "pytz"
version = "2025.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
]
[[package]]
name = "requests"
version = "2.32.5"