Compare commits

...

12 Commits

Author SHA1 Message Date
5d1ef1c364 Fix typo. music->audio 2020-08-25 13:13:02 +05:30
5e369994be Change subscribe page style. Fix typo. Fix spacing. Change anchor style 2020-04-04 17:46:13 +05:30
bc2ecc1cff Add metadata 2020-03-17 22:56:06 +05:30
329039541a [SSG] Move archives, subscribe, donate into directories.
Allow templates to be relative using {{ relative }} which can be set by
caller.
2020-03-10 21:21:54 +05:30
401e1b64f0 [SSG][Fix] Fix %M to %m. Add -v for logging. 2020-03-10 12:41:54 +05:30
f7582b1b2b [Fix] Resolve relative paths to absolute paths. 2020-03-09 23:43:22 +05:30
802acb5cc1 [SSG] Strip spaces in values in file headers. 2020-02-05 05:02:51 +05:30
11ede38d1b [SSG] Generate RSS file. Fix extension handling. URL in EpisodeList.
Episode.audio now takes path without extension. Parse url as part of
argument parsing.
2020-02-05 04:59:57 +05:30
0080597858 [Design][SSG] Add donate page. 2020-02-02 03:35:27 +05:30
1cbf2e9554 [Design] Change buttons on hover on subscribe 2020-02-02 02:42:57 +05:30
c1b3690df8 [SSG][Design] Add Subscribe page. 2020-02-01 15:29:36 +05:30
3d8a816b66 [SSG] Fix generate_site. parse_file for new file format.
Change sort order. Move argument parsing outside main. Better error
reporting. Better exit codes.
2020-01-30 05:43:13 +05:30
13 changed files with 3222 additions and 56 deletions

View File

@@ -2,6 +2,6 @@
{% block left_content %}
<ul class="archives">
{% for episode in episodes %}
<li><a href="{{ episode.slug }}">{{ episode.title }}</a></li>
<li><a href="../{{ episode.slug }}">{{ episode.title }}</a></li>
{% endfor %}
{% endblock %}

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 470 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -5,15 +5,18 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>{{ title }} | Redacted Life</title>
<link href="assets/css/index.css" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="assets/plyr/plyr.css" />
<title>{% block title %}{{ title }} | Redacted Life{% endblock %}</title>
<link href="{{ relative }}/assets/css/index.css" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="{{ relative }}/assets/plyr/plyr.css" />
<!-- Metadata -->
<meta name="description" content="An audiocast on Linux and libre software with a hard spin on personal privacy and security">
<meta http-equiv="X-Clacks-Overhead" content="GNU Terry Pratchett, Lloyd Barlowe" />
<link rel="canonical" href="https://redacted.life/">
<link rel="me" href="https://masto.nixnet.xyz/@amolith">
<link rel="me" href="https://masto.nixnet.xyz/@RedactedLife">
<link rel="me" href="https://social.nixnet.services/@RedactedLife">
{% block stylesheets %}{% endblock %}
<!-- Social: Twitter -->
<meta name="twitter:card" content="summary_large_image" />
@@ -31,26 +34,26 @@
<meta property="og:image" content="/assets/media/cover-site.png" />
<!-- Favicons -->
<link rel="apple-touch-icon" sizes="180x180" href="/assets/favicons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/assets/favicons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/assets/favicons/favicon-16x16.png">
<link rel="manifest" href="/assets/favicons/site.webmanifest">
<link rel="mask-icon" href="/assets/favicons/safari-pinned-tab.svg" color="#46ad83">
<link rel="shortcut icon" href="/assets/favicons/favicon.ico">
<link rel="apple-touch-icon" sizes="180x180" href="{{ relative }}/assets/favicons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="{{ relative }}/assets/favicons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="{{ relative }}/assets/favicons/favicon-16x16.png">
<link rel="manifest" href="{{ relative }}/assets/favicons/site.webmanifest">
<link rel="mask-icon" href="{{ relative }}/assets/favicons/safari-pinned-tab.svg" color="#46ad83">
<link rel="shortcut icon" href="{{ relative }}/assets/favicons/favicon.ico">
<meta name="msapplication-TileColor" content="#2b5797">
<meta name="msapplication-config" content="/assets/favicons/browserconfig.xml">
<meta name="msapplication-config" content="{{ relative }}/assets/favicons/browserconfig.xml">
<meta name="theme-color" content="#ffffff">
</head>
<body>
<img src="assets/images/cloud.svg" class="clouds" id="cloud1" />
<img src="assets/images/cloud.svg" class="clouds" id="cloud2" />
<img src="{{ relative }}/assets/images/cloud.svg" class="clouds" id="cloud1" />
<img src="{{ relative }}/assets/images/cloud.svg" class="clouds" id="cloud2" />
<div class="top_bg"></div>
<div class="bottom_bg"></div>
<div class="top">
<div class="wrapper">
<div class="content">
<h1><a href=".">Redacted Life</a></h1>
<h1><a href="{{relative}}">Redacted Life</a></h1>
{% block left_content %}
{% endblock %}
</div>
@@ -59,10 +62,11 @@
<div class="bottom">
<div class="wrapper">
<div class="content">
<a class="button" href="feed.xml">Subscribe</a>
<a class="button" href="{{relative}}/subscribe/">Subscribe</a>
<a class="button" href="mailto:hello@redacted.life">Contact</a>
<a class="button" href="archives.html">Archives</a>
<a class="button" href="{{relative}}/archives/">Archives</a>
</div>
<a class="donate-link" href="{{relative}}/donate/">Consider supporting these individuals and organisations</a>
<footer>
Designed by <a href="https://webionite.com">Ceda EI</a><br />
Source available on <a href="https://git.webionite.com/ceda_ei/redacted.life">Webionite</a><br />
@@ -70,10 +74,10 @@
</footer>
</div>
</div>
<script src="assets/plyr/plyr.js"></script>
<script src="{{ relative }}/assets/plyr/plyr.js"></script>
<script>
const player = new Plyr("#player", {
iconUrl: "assets/plyr/plyr.svg",
iconUrl: "{{ relative }}/assets/plyr/plyr.svg",
});
</script>
</body>

9
donate.html Normal file
View File

@@ -0,0 +1,9 @@
{% extends "base.html" %}
{% block title %}Donate | Redacted Life{% endblock %}
{% block left_content %}
<ul class="donate">
{% for donate in donations %}
<li><a href="{{ donate.link }}">{{ donate.text }}</a></li>
{% endfor %}
</ul>
{% endblock %}

262
nova.py
View File

@@ -1,11 +1,14 @@
#!/usr/bin/env python3
# pylint: disable=logging-format-interpolation
"Creates a static site for redacted.life"
import argparse
from collections import UserList
from datetime import datetime
import json
import os
import os.path as path
import logging
import re
import subprocess
import shutil
@@ -13,6 +16,7 @@ import sys
import jinja2
import markdown
from mutagen.mp3 import MP3
def gen_name(date, slug):
@@ -20,20 +24,29 @@ def gen_name(date, 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):
"Represents list of episodes"
def __init__(self, data, output, template, archives):
def __init__(self, url, data, output, template, archives):
super().__init__(data)
self.url = url
self.output = output
self.template = template
self.archives = archives
logging.info(f"New EpisodeList: {url=} {output=} {template=} {archives=}")
def sort(self, *_args, **_kwargs):
"Sorts the EpisodeList"
super().sort(key=lambda x: x.date, reverse=True)
super().sort(key=lambda x: x.date, reverse=False)
def generate_thumbnails(self):
"Generates thumbnails for all the videos"
logging.info(f"Creating missing directories")
if not path.isdir(self.output + "assets"):
os.mkdir(self.output + "assets")
if not path.isdir(self.output + "assets/thumbnails"):
@@ -41,41 +54,125 @@ class EpisodeList(UserList):
for episode in self.data:
location = (self.output + "assets/thumbnails/" +
gen_name(episode.date, episode.slug) + ".jpg")
logging.info(f"Creating thumbnail for {episode=} at {location}")
episode.store_thumbnail(location)
def generate_atom(self):
"Generates the Atom feed"
def generate_rss(self, header):
"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")):
logging.info(f"Writing header for {ext}")
s.write(header)
for ep in self.data:
logging.info(f"Writing item for episode {ep} with {ext=}")
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/audio/{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")
logging.info(f"Writing end for {ext}")
s.write("</channel>")
s.write("\n")
s.write("</rss>")
def generate_archives(self):
"Generates archives page"
with open(self.output + "archives.html", "w") as file:
if not path.isdir(self.output + "archives"):
logging.info("Creating directory archives")
os.mkdir(self.output + "archives")
with open(self.output + "archives/index.html", "w") as file:
episodes = [{
"slug": gen_name(i.date, i.slug) + ".html",
"title": i.title
} for i in self.data]
} for i in self.data[::-1]]
file.write(self.archives.render(episodes=episodes,
title="Archives"))
title="Archives",
relative=".."
))
def generate_site(self, root):
"Generates the entire site"
# Generate CSS from SCSS
logging.info("Generating CSS from SCSS")
subprocess.run(["sass", "--update", f"{root}scss:{root}assets/css"],
check=True)
# Copy the existing assets
logging.info("Copy the existing assets")
shutil.copytree(root + "assets", self.output + "assets",
dirs_exist_ok=True)
# Create the required directories
logging.info("Create the required directories")
paths = [
"assets",
"assets/audio",
"assets/videos",
]
logging.info("Creating missing directories")
for directory in paths:
if not path.isdir(self.output + directory):
logging.info(f"Creating directory {directory}")
os.mkdir(self.output + directory)
# Render episodes and copy data
logging.info("Render episodes and copy data")
for episode in self.data:
logging.info(f"Rendering episode {episode}")
html = f"{self.output}{gen_name(episode.date, episode.slug)}.html"
thumbnail = ("assets/thumbnails/" +
gen_name(episode.date, episode.slug) + ".jpg")
@@ -83,32 +180,43 @@ class EpisodeList(UserList):
gen_name(episode.date, episode.slug) + ".mp4")
audio = (self.output + "assets/audio/" +
gen_name(episode.date, episode.slug) + ".mp3")
logging.info(f"Copying {episode.video} to {video}")
shutil.copy2(episode.video, video)
shutil.copy2(episode.audio, audio)
logging.info(f"Copying {episode.audio}.mp3 to {audio}")
shutil.copy2(episode.audio + ".mp3", audio)
logging.info(f"Copying {episode.audio}.ogg to {audio}")
shutil.copy2(episode.audio + ".ogg", audio)
logging.info(f"Writing to {html}")
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"
last_name = f"{self.output}{gen_name(last.date, last.slug)}.html"
logging.info(f"Copying last one ({last}) to index.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, audio_src):
def __init__(self, date, slug, title, show_notes, video_src, audio_src, config):
self.date = date
self.slug = slug
self.title = title.strip()
self.title = title
self.show_notes = markdown.markdown(show_notes)
self.video = video_src
self.audio = audio_src
self.config = config
self.length = MP3(audio_src + ".mp3").info.length
logging.info(f"New episode: {date=} {slug=} {title=} {self.video=} "
f"{self.audio=} {config=} {self.length=} {self.show_notes=}")
def render(self, template, thumbnail_src):
def render(self, template, thumbnail_src, relative="."):
"Renders the Episode with the given template"
return template.render(
title=self.title,
show_notes=jinja2.Markup(self.show_notes),
thumbnail_src=thumbnail_src,
relative=relative,
video_src=f"assets/videos/{path.basename(self.video)}"
)
@@ -116,24 +224,68 @@ class Episode:
"Stores the thumbnail for given image at path"
args = ["ffmpeg", "-i", self.video, "-ss", "00:00:01.000", "-vframes",
"1", location]
logging.info(f"Running {' '.join(args)}")
subprocess.run(args, check=False)
def __len__(self):
return int(self.length)
def __str__(self):
return f"{self.slug}: {self.title}"
def __repr__(self):
return str(self)
def parse_args():
"Parses arguments"
parser = argparse.ArgumentParser()
parser.add_argument("input_dir", help="Input directory")
parser.add_argument("output_dir", help="Output directory")
parser.add_argument("url", help="Base URL of website")
parser.add_argument("--verbose", "-v", action="store_true", help="Verbose Logging")
args = parser.parse_args()
input_dir = args.input_dir.rstrip("/") + "/"
output_dir = args.output_dir.rstrip("/") + "/"
return input_dir, output_dir
input_dir = path.abspath(args.input_dir.rstrip("/")) + "/"
output_dir = path.abspath(args.output_dir.rstrip("/")) + "/"
if args.verbose:
logging.basicConfig(level=logging.DEBUG)
else:
logging.basicConfig()
url = args.url.rstrip("/") + "/"
return input_dir, output_dir, url
def main():
class ParseError(ValueError):
"Error raised while parsing a file"
def parse_file(file, array_keys=("tags")):
"Parses a file"
config = {}
kv_re = re.compile(r"(?P<key>\w+):\s*(?P<value>.*)")
while line := file.readline():
if line.rstrip("\n") == "---":
break
if line.strip() == "":
continue
if match := kv_re.match(line):
if match.group("key").strip().lower() in array_keys:
config[match.group("key")] = [i.strip() for i in
match.group("value").split(",")]
else:
config[match.group("key")] = match.group("value").strip()
else:
raise ParseError(f"Invalid line {line}")
return (config, file.read())
def main(args):
"Main method"
root = path.dirname(sys.argv[0]).rstrip("/") + "/"
input_dir, output_dir = parse_args()
input_dir, output_dir, url = args
logging.info(f"Input directory: {input_dir}")
logging.info(f"Output directory: {output_dir}")
logging.info(f"URL: {url}")
# Input validation
paths = [
@@ -142,10 +294,12 @@ def main():
input_dir + "videos",
input_dir + "audio",
]
if not all(path.isdir(i) for i in paths):
print("Invalid Input", file=sys.stderr)
return
logging.info("Checking if all paths exist.")
if not all(path.isdir((fail := i)) for i in paths):
logging.error(f"Invalid Input. {fail} is not a directory.")
return 1
logging.info("Creating output directory if it doesn't exist")
if not path.isdir(output_dir):
os.mkdir(output_dir)
@@ -154,7 +308,9 @@ def main():
autoescape=jinja2.select_autoescape("html")
)
logging.info("Creating EpisodeList")
podcast = EpisodeList(
url,
[],
output_dir,
env.get_template("index.html"),
@@ -162,34 +318,76 @@ def main():
)
split = re.compile(r"((?P<date>\d{4}-[01]?\d-[0123]?\d)-(?P<slug>.*).md)")
logging.info(f"Parsing all files in {input_dir}md")
for file in os.listdir(input_dir + "md"):
logging.info(f"File: {file}")
match = split.match(file)
logging.info(f"Match: {match}")
if not match:
print(f"Invalid filename: {file}", file=sys.stderr)
logging.error(f"Invalid filename: {file}")
continue
date = datetime.strptime(match.group("date"), "%Y-%M-%d")
date = datetime.strptime(match.group("date"), "%Y-%m-%d")
logging.info(f"Date: {date}")
slug = match.group("slug")
logging.info(f"Slug: {slug}")
with open(input_dir + "md/" + file) as episode:
title = episode.readline()
show_notes = episode.read()
try:
config, show_notes = parse_file(episode)
logging.info(f"Config: {config}")
logging.info(f"Show Notes: {show_notes}")
except ParseError as err:
logging.error(f"Error while parsing file: {file}")
logging.error(err)
return 2
logging.info("Appending to EpisodeList")
podcast.append(
Episode(
date,
slug,
title,
config["title"],
show_notes,
input_dir + "videos/" + gen_name(date, slug) + ".mp4",
input_dir + "audio/" + gen_name(date, slug) + ".mp3"
input_dir + "audio/" + gen_name(date, slug),
config
)
)
if not path.isdir(output_dir + "subscribe"):
os.mkdir(output_dir + "subscribe")
logging.info("Generating subscribe page")
with open(input_dir + "subscribe.json") as subscribe, \
open(output_dir + "subscribe/index.html", "w") as html:
html.write(env.get_template("subscribe.html").render(
relative="..",
subscriptions=json.load(subscribe)
))
if not path.isdir(output_dir + "donate"):
os.mkdir(output_dir + "donate")
logging.info("Generating donate page")
with open(input_dir + "donate.json") as donate, \
open(output_dir + "donate/index.html", "w") as html:
html.write(env.get_template("donate.html").render(
relative="..",
donations=json.load(donate)
))
logging.info("Sorting podcasts")
podcast.sort()
logging.info("Generating thumbnails")
podcast.generate_thumbnails()
logging.info("Generating archives pages")
podcast.generate_archives()
podcast.generate_atom()
logging.info("Generating RSS feeds")
with open(input_dir + "header.rss") as header:
podcast.generate_rss(header.read())
logging.info("Generating Site")
podcast.generate_site(root)
logging.info("Copying Overrides")
shutil.copytree(input_dir + "overrides", output_dir, dirs_exist_ok=True)
logging.info("Done")
return 0
if __name__ == "__main__":
main()
sys.exit(main(parse_args()))

View File

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

View File

@@ -56,7 +56,7 @@ body {
.wrapper {
min-height: $top-height;
align-items: flex-end;
padding-bottom: 2em;
padding-bottom: 1em;
.player {
margin: auto;
width: 80%;
@@ -69,11 +69,23 @@ body {
}
}
.shownotes {
padding: 2em 1em 1em 1em;
padding: 2em 1em 0;
margin: auto;
width: 80%;
a {
color: #292929;
padding: 0.3em;
border-radius: 1.6em;
transition-duration: 0.2s;
}
ul.archives {
a:hover {
color: #FFF;
background-color: #000;
text-decoration: none;
transition-duration: 0.2s;
}
}
ul.archives, ul.donate {
list-style-type: none;
width: 100%;
padding: 0;
@@ -94,6 +106,37 @@ body {
margin: 0.5em 1em;
}
}
.subscribe {
display: flex;
width: 600px;
justify-content: space-between;
flex-wrap: wrap;
margin: auto;
.subscription {
width: 200px;
color: #000;
text-align: center;
transition-duration: 0.2em;
padding: 1rem 0;
span {
color: inherit;
font-size: 100px;
text-decoration: none;
}
p {
@include title-font;
width: 100%;
font-size: 1rem;
}
}
.subscription:hover, .subscription:active {
text-decoration: none;
transition-duration: 0.2em;
background-color: #000;
color: #FFF;
border-radius: 30px;
}
}
}
}
@@ -105,7 +148,7 @@ body {
align-items: center;
justify-content: space-between;
flex-direction: column;
padding-top: 2em;
padding-top: 1em;
height: 100%;
.content {
display: flex;
@@ -128,6 +171,19 @@ body {
color: #FFF;
}
}
a.donate-link {
margin: 1em 0;
color: #292929;
padding: 0.3em;
border-radius: 1.6em;
transition-duration: 0.2s;
}
a:hover {
color: #FFF;
background-color: #000;
text-decoration: none;
transition-duration: 0.2s;
}
footer {
margin-top: 1em;
padding: 0 1em;
@@ -173,6 +229,17 @@ h1 {
}
}
@media (max-width: 800px) {
body {
.top {
.wrapper {
.subscribe {
width: 400px;
}
}
}
}
}
@media (max-width: 600px) {
body {
.top_bg {
@@ -213,6 +280,17 @@ h1 {
}
}
@media (max-width: 480px) {
body {
.top {
.wrapper {
.subscribe {
width: 200px;
}
}
}
}
}
@keyframes move_right {
from {left: 0}

15
subscribe.html Normal file
View File

@@ -0,0 +1,15 @@
{% extends "base.html" %}
{% block title %}Subscribe | Redacted Life{% endblock %}
{% block stylesheets %}
<link href="{{relative}}/assets/fork-awesome/css/fork-awesome.min.css" rel="stylesheet" type="text/css">
{% endblock %}
{% block left_content %}
<div class="subscribe">
{% for subscription in subscriptions %}
<a href={{subscription.link}} class="subscription">
<span class="fa fa-{{subscription.icon}}">
<p>{{subscription.text}}</p>
</a>
{% endfor %}
</div>
{% endblock %}