redacted.life/nova.py

188 lines
6.0 KiB
Python
Executable File

#!/usr/bin/env python3
"Creates a static site for redacted.life"
import argparse
from collections import UserList
from datetime import datetime
import os
import os.path as path
import re
import subprocess
import shutil
import sys
import jinja2
import markdown
def gen_name(date, slug):
"Returns to file name"
return date.strftime("%Y-%m-%d-") + slug
class EpisodeList(UserList):
"Represents list of episodes"
def __init__(self, data, output, template, archives):
super().__init__(data)
self.output = output
self.template = template
self.archives = archives
def sort(self, *_args, **_kwargs):
"Sorts the EpisodeList"
super().sort(key=lambda x: x.date, reverse=True)
def generate_thumbnails(self):
"Generates thumbnails for all the videos"
if not path.isdir(self.output + "assets"):
os.mkdir(self.output + "assets")
if not path.isdir(self.output + "assets/thumbnails"):
os.mkdir(self.output + "assets/thumbnails")
for episode in self.data:
location = (self.output + "assets/thumbnails/" +
gen_name(episode.date, episode.slug) + ".jpg")
episode.store_thumbnail(location)
def generate_atom(self):
"Generates the Atom feed"
def generate_archives(self):
"Generates archives page"
with open(self.output + "archives.html", "w") as file:
episodes = [{
"slug": gen_name(i.date, i.slug) + ".html",
"title": i.title
} for i in self.data]
file.write(self.archives.render(episodes=episodes))
def generate_site(self, root):
"Generates the entire site"
# Generate CSS from SCSS
subprocess.run(["sass", "--update", f"{root}scss:{root}assets/css"],
check=True)
# Copy the existing assets
shutil.copytree(root + "assets", self.output + "assets",
dirs_exist_ok=True)
# Create the required directories
paths = [
"assets",
"assets/audios",
"assets/videos",
]
for directory in paths:
if not path.isdir(self.output + directory):
os.mkdir(self.output + directory)
# Render episodes and copy data
for episode in self.data:
html = f"{self.output}{gen_name(episode.date, episode.slug)}.html"
thumbnail = ("assets/thumbnails/" +
gen_name(episode.date, episode.slug) + ".jpg")
video = (self.output + "assets/videos/" +
gen_name(episode.date, episode.slug) + ".mp4")
audio = (self.output + "assets/audios/" +
gen_name(episode.date, episode.slug) + ".mp3")
shutil.copy2(episode.video, video)
shutil.copy2(episode.audio, audio)
with open(html, "w") as file:
file.write(episode.render(self.template, thumbnail))
last = self.data[-1]
last_name = self.output + gen_name(last.date, last.slug) + ".html"
shutil.copy2(last_name, self.output + "index.html")
class Episode:
"Represents one episode of podcast"
def __init__(self, date, slug, title, show_notes, video_src, audo_src):
self.date = date
self.slug = slug
self.title = title.strip()
self.show_notes = markdown.markdown(show_notes)
self.video = video_src
self.audio = audo_src
def render(self, template, thumbnail_src):
"Renders the Episode with the given template"
return template.render(
title=self.title,
show_notes=jinja2.Markup(self.show_notes),
thumbnail_src=thumbnail_src,
video_src=f"assets/videos/{path.basename(self.video)}"
)
def store_thumbnail(self, location):
"Stores the thumbnail for given image at path"
args = ["ffmpeg", "-i", self.video, "-ss", "00:00:01.000", "-vframes",
"1", location]
subprocess.run(args, check=False)
def main():
"Main method"
root = path.dirname(sys.argv[0]).rstrip("/") + "/"
parser = argparse.ArgumentParser()
parser.add_argument("input_dir", help="Input directory")
parser.add_argument("output_dir", help="Output directory")
args = parser.parse_args()
input_dir = args.input_dir.rstrip("/") + "/"
output_dir = args.output_dir.rstrip("/") + "/"
# Input validation
paths = [
input_dir,
input_dir + "md",
input_dir + "videos",
input_dir + "audios",
]
if not all(path.isdir(i) for i in paths):
print("Invalid Input", file=sys.stderr)
return
if not path.isdir(args.output_dir):
os.mkdir(args.output_dir)
env = jinja2.Environment(
loader=jinja2.FileSystemLoader(root),
autoescape=jinja2.select_autoescape("html")
)
podcast = EpisodeList(
[],
output_dir,
env.get_template("index.html"),
env.get_template("archives.html")
)
split = re.compile(r"((?P<date>\d{4}-[01]?\d-[0123]?\d)-(?P<slug>.*).md)")
for file in os.listdir(input_dir + "md"):
match = split.match(file)
if not match:
print(f"Invalid filename: {file}", file=sys.stderr)
continue
date = datetime.strptime(match.group("date"), "%Y-%M-%d")
slug = match.group("slug")
with open(input_dir + "md/" + file) as episode:
title = episode.readline()
show_notes = episode.read()
podcast.append(
Episode(
date,
slug,
title,
show_notes,
input_dir + "videos/" + gen_name(date, slug) + ".mp4",
input_dir + "audios/" + gen_name(date, slug) + ".mp3"
)
)
podcast.sort()
podcast.generate_thumbnails()
podcast.generate_archives()
podcast.generate_atom()
podcast.generate_site(root)
if __name__ == "__main__":
main()