Compare commits
29 Commits
979d3f50b7
...
master
Author | SHA1 | Date | |
---|---|---|---|
5d1ef1c364 | |||
5e369994be | |||
bc2ecc1cff | |||
329039541a | |||
401e1b64f0 | |||
f7582b1b2b | |||
802acb5cc1 | |||
11ede38d1b | |||
0080597858 | |||
1cbf2e9554 | |||
c1b3690df8 | |||
3d8a816b66 | |||
2729f74683 | |||
93bdd1885d | |||
917d3addb6 | |||
5b13ce2a2f | |||
5636815aaf | |||
5041f4c221 | |||
f69d922f32 | |||
6a33095635 | |||
c54b12e2b1 | |||
2507bc7596 | |||
2aaea11e8d | |||
1b4e1f7640 | |||
32a1aa9712 | |||
8b9a367fb4 | |||
97c898acdb | |||
d0b7922c9b | |||
e6139b4c33 |
7
archives.html
Normal file
7
archives.html
Normal file
@@ -0,0 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% block left_content %}
|
||||
<ul class="archives">
|
||||
{% for episode in episodes %}
|
||||
<li><a href="../{{ episode.slug }}">{{ episode.title }}</a></li>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
Binary file not shown.
Binary file not shown.
BIN
assets/fonts/Lato-Regular.ttf
Normal file
BIN
assets/fonts/Lato-Regular.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Montserrat-Bold.ttf
Normal file
BIN
assets/fonts/Montserrat-Bold.ttf
Normal file
Binary file not shown.
12
assets/fork-awesome/css/fork-awesome.min.css
vendored
Normal file
12
assets/fork-awesome/css/fork-awesome.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
assets/fork-awesome/fonts/forkawesome-webfont.eot
Normal file
BIN
assets/fork-awesome/fonts/forkawesome-webfont.eot
Normal file
Binary file not shown.
2849
assets/fork-awesome/fonts/forkawesome-webfont.svg
Normal file
2849
assets/fork-awesome/fonts/forkawesome-webfont.svg
Normal file
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 470 KiB |
BIN
assets/fork-awesome/fonts/forkawesome-webfont.ttf
Normal file
BIN
assets/fork-awesome/fonts/forkawesome-webfont.ttf
Normal file
Binary file not shown.
BIN
assets/fork-awesome/fonts/forkawesome-webfont.woff
Normal file
BIN
assets/fork-awesome/fonts/forkawesome-webfont.woff
Normal file
Binary file not shown.
BIN
assets/fork-awesome/fonts/forkawesome-webfont.woff2
Normal file
BIN
assets/fork-awesome/fonts/forkawesome-webfont.woff2
Normal file
Binary file not shown.
85
base.html
Normal file
85
base.html
Normal file
@@ -0,0 +1,85 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<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>{% 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" />
|
||||
<meta name="twitter:title" content="Redacted Life" />
|
||||
<meta name="twitter:description" content="An audiocast on Linux and libre software with a hard spin on personal privacy and security" />
|
||||
<meta name="twitter:image" content="/assets/media/cover-site.png" />
|
||||
|
||||
<!-- Social: OpenGraph -->
|
||||
<meta property="og:locale" content="en_US">
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:title" content="Redacted Life">
|
||||
<meta property="og:description" content="An audiocast on Linux and libre software with a hard spin on personal privacy and security">
|
||||
<meta property="og:url" content="https://redacted.life">
|
||||
<meta property="og:site_name" content="Redacted Life">
|
||||
<meta property="og:image" content="/assets/media/cover-site.png" />
|
||||
|
||||
<!-- Favicons -->
|
||||
<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="{{ relative }}/assets/favicons/browserconfig.xml">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<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="{{relative}}">Redacted Life</a></h1>
|
||||
{% block left_content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bottom">
|
||||
<div class="wrapper">
|
||||
<div class="content">
|
||||
<a class="button" href="{{relative}}/subscribe/">Subscribe</a>
|
||||
<a class="button" href="mailto:hello@redacted.life">Contact</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 />
|
||||
Licensed under <a href="https://git.webionite.com/ceda_ei/redacted.life/src/branch/master/LICENSE">MIT</a>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
<script src="{{ relative }}/assets/plyr/plyr.js"></script>
|
||||
<script>
|
||||
const player = new Plyr("#player", {
|
||||
iconUrl: "{{ relative }}/assets/plyr/plyr.svg",
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
9
donate.html
Normal file
9
donate.html
Normal 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 %}
|
57
index.html
57
index.html
@@ -1,55 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ title }} | Redacted Life</title>
|
||||
<link href="assets/css/index.css" rel="stylesheet" type="text/css">
|
||||
<link rel="stylesheet" href="assets/plyr/plyr.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<img src="assets/images/cloud.svg" class="clouds" id="cloud1" />
|
||||
<img src="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>Redacted Life</h1>
|
||||
<div class="player">
|
||||
{% extends "base.html" %}
|
||||
{% block left_content %}
|
||||
<div class="player">
|
||||
<video id="player" poster="{{ thumbnail_src }}" controls>
|
||||
<source src="{{ video_src }}" type="video/mp4" />
|
||||
</video>
|
||||
</div>
|
||||
<details class="shownotes">
|
||||
</div>
|
||||
<details class="shownotes">
|
||||
<summary>Show Notes</summary>
|
||||
{{ show_notes }}
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bottom">
|
||||
<div class="wrapper">
|
||||
<div class="content">
|
||||
<a class="button" href="/feed.xml">Subscribe</a>
|
||||
<a class="button" href="mailto:hello@redacted.life">Contact</a>
|
||||
<a class="button" href="/archives/">Archives</a>
|
||||
</div>
|
||||
<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 />
|
||||
Licensed under <a href="https://git.webionite.com/ceda_ei/redacted.life/src/branch/master/LICENSE">MIT</a>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
<script src="assets/plyr/plyr.js"></script>
|
||||
<script>
|
||||
const player = new Plyr("#player", {
|
||||
iconUrl: "assets/plyr/plyr.svg",
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</details>
|
||||
{% endblock %}
|
||||
|
365
nova.py
365
nova.py
@@ -1,100 +1,393 @@
|
||||
#!/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
|
||||
import sys
|
||||
|
||||
import jinja2
|
||||
import markdown
|
||||
from mutagen.mp3 import MP3
|
||||
|
||||
|
||||
def gen_name(date, slug):
|
||||
"Returns to file name"
|
||||
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):
|
||||
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):
|
||||
def sort(self, *_args, **_kwargs):
|
||||
"Sorts the EpisodeList"
|
||||
super().sort(key=lambda x: x.date, reverse=False)
|
||||
|
||||
def generate_thumnails(self):
|
||||
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"):
|
||||
os.mkdir(self.output + "assets/thumbnails")
|
||||
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_site(self):
|
||||
"Generates the entire "
|
||||
def generate_archives(self):
|
||||
"Generates archives page"
|
||||
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[::-1]]
|
||||
file.write(self.archives.render(episodes=episodes,
|
||||
title="Archives",
|
||||
relative=".."
|
||||
))
|
||||
|
||||
def generate_site(self, root):
|
||||
"Generates the entire site"
|
||||
logging.info("Generating CSS from SCSS")
|
||||
subprocess.run(["sass", "--update", f"{root}scss:{root}assets/css"],
|
||||
check=True)
|
||||
logging.info("Copy the existing assets")
|
||||
shutil.copytree(root + "assets", self.output + "assets",
|
||||
dirs_exist_ok=True)
|
||||
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)
|
||||
|
||||
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")
|
||||
video = (self.output + "assets/videos/" +
|
||||
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)
|
||||
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 = 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):
|
||||
def __init__(self, date, slug, title, show_notes, video_src, audio_src, config):
|
||||
self.date = date
|
||||
self.slug = slug
|
||||
self.title = title
|
||||
self.show_notes = show_notes
|
||||
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, video_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,
|
||||
video_src=video_src
|
||||
relative=relative,
|
||||
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]
|
||||
logging.info(f"Running {' '.join(args)}")
|
||||
subprocess.run(args, check=False)
|
||||
|
||||
def __len__(self):
|
||||
return int(self.length)
|
||||
|
||||
def main():
|
||||
"Main method"
|
||||
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("/") + "/"
|
||||
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
|
||||
|
||||
|
||||
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, url = args
|
||||
logging.info(f"Input directory: {input_dir}")
|
||||
logging.info(f"Output directory: {output_dir}")
|
||||
logging.info(f"URL: {url}")
|
||||
|
||||
# Input validation
|
||||
if not all(path.isdir(i) for i in (input_dir, input_dir + "md",
|
||||
input_dir + "videos")):
|
||||
print("Invalid Input", file=sys.stderr)
|
||||
return
|
||||
paths = [
|
||||
input_dir,
|
||||
input_dir + "md",
|
||||
input_dir + "videos",
|
||||
input_dir + "audio",
|
||||
]
|
||||
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
|
||||
|
||||
if not path.isdir(args.output_dir):
|
||||
os.mkdir(args.output_dir)
|
||||
logging.info("Creating output directory if it doesn't exist")
|
||||
if not path.isdir(output_dir):
|
||||
os.mkdir(output_dir)
|
||||
|
||||
template = jinja2.Environment(
|
||||
loader=jinja2.FileSystemLoader(input_dir),
|
||||
env = jinja2.Environment(
|
||||
loader=jinja2.FileSystemLoader(root),
|
||||
autoescape=jinja2.select_autoescape("html")
|
||||
).get_template("index.html")
|
||||
)
|
||||
|
||||
podcast = EpisodeList([], output_dir, template)
|
||||
logging.info("Creating EpisodeList")
|
||||
podcast = EpisodeList(
|
||||
url,
|
||||
[],
|
||||
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>.*))")
|
||||
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 = match.group("date")
|
||||
date = datetime.strptime(match.group("date"), "%Y-%m-%d")
|
||||
logging.info(f"Date: {date}")
|
||||
slug = match.group("slug")
|
||||
with open(file) as f: # pylint: disable=invalid-name
|
||||
title = f.readline()
|
||||
show_notes = f.read()
|
||||
podcast.append(Episode(date, slug, title, show_notes))
|
||||
logging.info(f"Slug: {slug}")
|
||||
with open(input_dir + "md/" + file) as episode:
|
||||
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,
|
||||
config["title"],
|
||||
show_notes,
|
||||
input_dir + "videos/" + gen_name(date, slug) + ".mp4",
|
||||
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()
|
||||
podcast.generate_thumnails()
|
||||
podcast.generate_atom()
|
||||
podcast.generate_site()
|
||||
logging.info("Generating thumbnails")
|
||||
podcast.generate_thumbnails()
|
||||
logging.info("Generating archives pages")
|
||||
podcast.generate_archives()
|
||||
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()))
|
||||
|
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
jinja2
|
||||
markdown
|
||||
mutagen
|
@@ -1,28 +1,23 @@
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Comfortaa';
|
||||
font-family: 'Lato';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url(../fonts/Comfortaa-Latin-Light.woff) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
src: url(../fonts/Lato-Regular.ttf);
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Comfortaa';
|
||||
font-family: 'Montserrat';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url(../fonts/Comfortaa-Latin.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
src: url(../fonts/Montserrat-Bold.ttf);
|
||||
}
|
||||
|
||||
@mixin body-font {
|
||||
font-family: "Comfortaa", sans-serif;
|
||||
font-weight: 300;
|
||||
font-family: "Lato", sans-serif;
|
||||
}
|
||||
|
||||
@mixin title-font {
|
||||
font-family: "Comfortaa", sans-serif;
|
||||
font-weight: 700;
|
||||
font-family: "Montserrat", sans-serif;
|
||||
}
|
||||
|
133
scss/index.scss
133
scss/index.scss
@@ -44,7 +44,7 @@ body {
|
||||
}
|
||||
.bottom_bg {
|
||||
background-image: linear-gradient(#26a4c4, #afdde9);
|
||||
height: 100 - $top-height;
|
||||
height: 100vh - $top-height;
|
||||
width: 100vw;
|
||||
position: fixed;
|
||||
top: $top-height;
|
||||
@@ -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,21 +69,86 @@ 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;
|
||||
}
|
||||
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;
|
||||
text-align: center;
|
||||
li {
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #000;
|
||||
transition-duration: 0.2s;
|
||||
}
|
||||
a:hover, a:active {
|
||||
transition-duration: 0.2s;
|
||||
background-color: #000;
|
||||
color: #FFF;
|
||||
padding: 0.3em;
|
||||
border-radius: 1.6em;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottom {
|
||||
min-height: 100vh - $top-height;
|
||||
min-height: 95vh - $top-height;
|
||||
margin-bottom: 5vh;
|
||||
.wrapper {
|
||||
min-height: 100vh - $top-height;
|
||||
min-height: 95vh - $top-height;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
padding-top: 2em;
|
||||
padding-top: 1em;
|
||||
height: 100%;
|
||||
.content {
|
||||
display: flex;
|
||||
@@ -98,12 +163,27 @@ body {
|
||||
border-style: solid;
|
||||
border-radius: 1.5em;
|
||||
text-decoration: none;
|
||||
transition-duration: 0.4s;
|
||||
}
|
||||
.button:hover, .button:active {
|
||||
transition-duration: 0.4s;
|
||||
background-color: #000;
|
||||
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;
|
||||
@@ -113,11 +193,13 @@ body {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,10 +223,32 @@ h1 {
|
||||
@include title-font;
|
||||
font-size: 4em;
|
||||
text-align: center;
|
||||
a {
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
body {
|
||||
.top {
|
||||
.wrapper {
|
||||
.subscribe {
|
||||
width: 400px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
body {
|
||||
.top_bg {
|
||||
height: $top-height-phone;
|
||||
}
|
||||
.bottom_bg {
|
||||
height: 100vh - $top-height-phone;
|
||||
top: $top-height-phone;
|
||||
}
|
||||
.top {
|
||||
min-height: $top-height-phone;
|
||||
.wrapper {
|
||||
@@ -159,9 +263,9 @@ h1 {
|
||||
}
|
||||
}
|
||||
.bottom {
|
||||
min-height: 100vh - $top-height-phone;
|
||||
min-height: 95vh - $top-height-phone;
|
||||
.wrapper {
|
||||
min-height: 100vh - $top-height-phone;
|
||||
min-height: 95vh - $top-height-phone;
|
||||
.content {
|
||||
.button {
|
||||
font-size: 1em;
|
||||
@@ -176,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
15
subscribe.html
Normal 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 %}
|
Reference in New Issue
Block a user