diff --git a/.gitignore b/.gitignore
index 356513a..c9f02db 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,8 @@
*.jpg
*.gif
*.png
+RSS/index.rss2
+!RSS/rsslogo.jpg
# macOS
.DS_Store
diff --git a/RSS/README.md b/RSS/README.md
new file mode 100644
index 0000000..52fc75c
--- /dev/null
+++ b/RSS/README.md
@@ -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.
+
diff --git a/RSS/generate_rss.py b/RSS/generate_rss.py
new file mode 100755
index 0000000..8dd0750
--- /dev/null
+++ b/RSS/generate_rss.py
@@ -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 Squawk’s New Year’s 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", "
")
+ 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 & Global Radio")
+appendTextElement(channel, "itunes:author", "Netflix & 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")
+
+# Tue, 10 Jun 2003 04:00:00 GMT
+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.")
diff --git a/RSS/pyproject.toml b/RSS/pyproject.toml
new file mode 100755
index 0000000..755a248
--- /dev/null
+++ b/RSS/pyproject.toml
@@ -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",
+]
diff --git a/RSS/rsslogo.jpg b/RSS/rsslogo.jpg
new file mode 100755
index 0000000..9e01955
Binary files /dev/null and b/RSS/rsslogo.jpg differ
diff --git a/pyproject.toml b/pyproject.toml
index e033c6c..7f5176e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -8,3 +8,8 @@ dependencies = [
"requests>=2.32.5",
"rich>=14.2.0",
]
+
+[tool.uv.workspace]
+members = [
+ "RSS",
+]
diff --git a/uv.lock b/uv.lock
index 238d92f..d71c67e 100644
--- a/uv.lock
+++ b/uv.lock
@@ -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"