[SSG] Generate RSS file. Fix extension handling. URL in EpisodeList.

Episode.audio now takes path without extension. Parse url as part of
argument parsing.
This commit is contained in:
Ceda EI 2020-02-05 04:59:57 +05:30
parent 0080597858
commit 11ede38d1b
2 changed files with 99 additions and 9 deletions

107
nova.py
View File

@ -14,6 +14,7 @@ import sys
import jinja2 import jinja2
import markdown import markdown
from mutagen.mp3 import MP3
def gen_name(date, slug): def gen_name(date, slug):
@ -21,10 +22,17 @@ def gen_name(date, slug):
return date.strftime("%Y-%m-%d-") + slug return date.strftime("%Y-%m-%d-") + slug
def seconds_to_str(seconds):
"Convert seconds to a string of hh:mm:ss"
seconds = int(seconds)
return f"{seconds // 3600:02}:{(seconds % 3600) // 60:02}:{seconds % 60:02}"
class EpisodeList(UserList): class EpisodeList(UserList):
"Represents list of episodes" "Represents list of episodes"
def __init__(self, data, output, template, archives): def __init__(self, url, data, output, template, archives):
super().__init__(data) super().__init__(data)
self.url = url
self.output = output self.output = output
self.template = template self.template = template
self.archives = archives self.archives = archives
@ -44,8 +52,80 @@ class EpisodeList(UserList):
gen_name(episode.date, episode.slug) + ".jpg") gen_name(episode.date, episode.slug) + ".jpg")
episode.store_thumbnail(location) episode.store_thumbnail(location)
def generate_atom(self): def generate_rss(self, header):
"Generates the Atom feed" "Generates the RSS Feed"
with open(self.output + "feed_mp3.rss", "w") as mp3, \
open(self.output + "feed_ogg.rss", "w") as ogg:
# pylint: disable = invalid-name
for s, ext in ((mp3, "mp3"), (ogg, "ogg")):
s.write(header)
for ep in self.data:
s.write("<item>")
s.write("\n")
# Title
s.write(f"<title><![CDATA[{ep.title}]]></title>")
s.write("\n")
# Description
s.write("<description><![CDATA["
f"{ep.config['description']}]]></description>")
s.write("\n")
# Date
datestring = ep.date.strftime(
'%a, %d %b, %Y %H:%M:%Sz GMT'
)
s.write(f"<pubDate>{datestring}</pubDate>")
s.write("\n")
# iTunes: explicit, author, subtitle, keywords
s.write(f"<itunes:explicit>{ep.config['explicit']}"
"</itunes:explicit>")
s.write("\n")
s.write(
f"<itunes:author><![CDATA[{ep.config['author']}]]>"
"</itunes:author>"
)
s.write("\n")
s.write(
"<itunes:subtitle><![CDATA["
f"{ep.config['subtitle']}]]></itunes:subtitle>"
)
s.write("\n")
s.write(
f"<itunes:keywords>{','.join(ep.config['tags'])}"
"</itunes:keywords>"
)
s.write("\n")
s.write(f"<itunes:duration>{seconds_to_str(len(ep))}"
"</itunes:duration>")
s.write("\n")
# Content (show_notes)
s.write(
f"<content:encoded><![CDATA[{ep.show_notes}]]>"
"</content:encoded>"
)
s.write("\n")
# GUID
s.write(
f"<guid isPermaLink=\"true\">{self.url}{ep.slug}"
".html</guid>"
)
s.write("\n")
# Enclosure
audio = f'{self.url}assets/music/{ep.slug}.{ext}'
size = path.getsize(f"{ep.audio}.{ext}")
s.write(
f'<enclosure url="{audio}" type="audio/{ext}" '
f'length="{size}" />'
)
s.write("\n")
# Categories
for tag in ep.config["tags"]:
s.write(f"<category><![CDATA[{tag}]]></category>")
s.write("\n")
s.write("</item>")
s.write("\n")
s.write("</channel>")
s.write("\n")
s.write("</rss>")
def generate_archives(self): def generate_archives(self):
"Generates archives page" "Generates archives page"
@ -85,7 +165,8 @@ class EpisodeList(UserList):
audio = (self.output + "assets/audio/" + audio = (self.output + "assets/audio/" +
gen_name(episode.date, episode.slug) + ".mp3") gen_name(episode.date, episode.slug) + ".mp3")
shutil.copy2(episode.video, video) shutil.copy2(episode.video, video)
shutil.copy2(episode.audio, audio) shutil.copy2(episode.audio + ".mp3", audio)
shutil.copy2(episode.audio + ".ogg", audio)
with open(html, "w") as file: with open(html, "w") as file:
file.write(episode.render(self.template, thumbnail)) file.write(episode.render(self.template, thumbnail))
@ -104,6 +185,7 @@ class Episode:
self.video = video_src self.video = video_src
self.audio = audio_src self.audio = audio_src
self.config = config self.config = config
self.length = MP3(audio_src + ".mp3").info.length
def render(self, template, thumbnail_src): def render(self, template, thumbnail_src):
"Renders the Episode with the given template" "Renders the Episode with the given template"
@ -120,23 +202,28 @@ class Episode:
"1", location] "1", location]
subprocess.run(args, check=False) subprocess.run(args, check=False)
def __len__(self):
return int(self.length)
def parse_args(): def parse_args():
"Parses arguments" "Parses arguments"
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("input_dir", help="Input directory") parser.add_argument("input_dir", help="Input directory")
parser.add_argument("output_dir", help="Output directory") parser.add_argument("output_dir", help="Output directory")
parser.add_argument("url", help="Base URL of website")
args = parser.parse_args() args = parser.parse_args()
input_dir = args.input_dir.rstrip("/") + "/" input_dir = args.input_dir.rstrip("/") + "/"
output_dir = args.output_dir.rstrip("/") + "/" output_dir = args.output_dir.rstrip("/") + "/"
return input_dir, output_dir url = args.url.rstrip("/") + "/"
return input_dir, output_dir, url
class ParseError(ValueError): class ParseError(ValueError):
"Error raised while parsing a file" "Error raised while parsing a file"
def parse_file(file, array_keys=("categories")): def parse_file(file, array_keys=("tags")):
"Parses a file" "Parses a file"
config = {} config = {}
kv_re = re.compile(r"(?P<key>\w+):\s*(?P<value>.*)") kv_re = re.compile(r"(?P<key>\w+):\s*(?P<value>.*)")
@ -160,7 +247,7 @@ def parse_file(file, array_keys=("categories")):
def main(args): def main(args):
"Main method" "Main method"
root = path.dirname(sys.argv[0]).rstrip("/") + "/" root = path.dirname(sys.argv[0]).rstrip("/") + "/"
input_dir, output_dir = args input_dir, output_dir, url = args
# Input validation # Input validation
paths = [ paths = [
@ -182,6 +269,7 @@ def main(args):
) )
podcast = EpisodeList( podcast = EpisodeList(
url,
[], [],
output_dir, output_dir,
env.get_template("index.html"), env.get_template("index.html"),
@ -210,7 +298,7 @@ def main(args):
config["title"], config["title"],
show_notes, show_notes,
input_dir + "videos/" + gen_name(date, slug) + ".mp4", input_dir + "videos/" + gen_name(date, slug) + ".mp4",
input_dir + "audio/" + gen_name(date, slug) + ".mp3", input_dir + "audio/" + gen_name(date, slug),
config config
) )
) )
@ -230,7 +318,8 @@ def main(args):
podcast.sort() podcast.sort()
podcast.generate_thumbnails() podcast.generate_thumbnails()
podcast.generate_archives() podcast.generate_archives()
podcast.generate_atom() with open(input_dir + "header.rss") as header:
podcast.generate_rss(header.read())
podcast.generate_site(root) podcast.generate_site(root)
shutil.copytree(input_dir + "overrides", output_dir, dirs_exist_ok=True) shutil.copytree(input_dir + "overrides", output_dir, dirs_exist_ok=True)
return 0 return 0

View File

@ -1,2 +1,3 @@
jinja2 jinja2
markdown markdown
mutagen