Added RSS feed generator
Signed-off-by: Markus Birth <markus@birth-online.de>
This commit is contained in:
@@ -7,6 +7,8 @@
|
||||
*.jpg
|
||||
*.gif
|
||||
*.png
|
||||
RSS/index.rss2
|
||||
!RSS/rsslogo.jpg
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
@@ -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.
|
||||
|
||||
Executable
+233
@@ -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", "<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 & 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")
|
||||
|
||||
# <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.")
|
||||
Executable
+11
@@ -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",
|
||||
]
|
||||
Executable
BIN
Binary file not shown.
|
After Width: | Height: | Size: 113 KiB |
@@ -8,3 +8,8 @@ dependencies = [
|
||||
"requests>=2.32.5",
|
||||
"rich>=14.2.0",
|
||||
]
|
||||
|
||||
[tool.uv.workspace]
|
||||
members = [
|
||||
"RSS",
|
||||
]
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user