[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:
parent
0080597858
commit
11ede38d1b
107
nova.py
107
nova.py
|
@ -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
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
jinja2
|
jinja2
|
||||||
markdown
|
markdown
|
||||||
|
mutagen
|
||||||
|
|
Loading…
Reference in New Issue