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"