Compare commits
27 Commits
d0b7922c9b
...
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 |
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 %}
|
67
index.html
67
index.html
@@ -1,55 +1,12 @@
|
|||||||
<!DOCTYPE html>
|
{% extends "base.html" %}
|
||||||
<html lang="en">
|
{% block left_content %}
|
||||||
|
<div class="player">
|
||||||
<head>
|
<video id="player" poster="{{ thumbnail_src }}" controls>
|
||||||
<meta charset="UTF-8">
|
<source src="{{ video_src }}" type="video/mp4" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
</video>
|
||||||
<title>{{ title }} | Redacted Life</title>
|
</div>
|
||||||
<link href="assets/css/index.css" rel="stylesheet" type="text/css">
|
<details class="shownotes">
|
||||||
<link rel="stylesheet" href="assets/plyr/plyr.css" />
|
<summary>Show Notes</summary>
|
||||||
</head>
|
{{ show_notes }}
|
||||||
|
</details>
|
||||||
<body>
|
{% endblock %}
|
||||||
<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">
|
|
||||||
<video id="player" poster="{{ thumbnail_src }}" controls>
|
|
||||||
<source src="{{ video_src }}" type="video/mp4" />
|
|
||||||
</video>
|
|
||||||
</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>
|
|
||||||
|
337
nova.py
337
nova.py
@@ -1,16 +1,22 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
# pylint: disable=logging-format-interpolation
|
||||||
"Creates a static site for redacted.life"
|
"Creates a static site for redacted.life"
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
from collections import UserList
|
from collections import UserList
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import os.path as path
|
import os.path as path
|
||||||
|
import logging
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import jinja2
|
import jinja2
|
||||||
|
import markdown
|
||||||
|
from mutagen.mp3 import MP3
|
||||||
|
|
||||||
|
|
||||||
def gen_name(date, slug):
|
def gen_name(date, slug):
|
||||||
@@ -18,19 +24,29 @@ 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):
|
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
|
||||||
|
logging.info(f"New EpisodeList: {url=} {output=} {template=} {archives=}")
|
||||||
|
|
||||||
def sort(self, *_args, **_kwargs):
|
def sort(self, *_args, **_kwargs):
|
||||||
"Sorts the EpisodeList"
|
"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):
|
def generate_thumbnails(self):
|
||||||
"Generates thumbnails for all the videos"
|
"Generates thumbnails for all the videos"
|
||||||
|
logging.info(f"Creating missing directories")
|
||||||
if not path.isdir(self.output + "assets"):
|
if not path.isdir(self.output + "assets"):
|
||||||
os.mkdir(self.output + "assets")
|
os.mkdir(self.output + "assets")
|
||||||
if not path.isdir(self.output + "assets/thumbnails"):
|
if not path.isdir(self.output + "assets/thumbnails"):
|
||||||
@@ -38,85 +54,340 @@ class EpisodeList(UserList):
|
|||||||
for episode in self.data:
|
for episode in self.data:
|
||||||
location = (self.output + "assets/thumbnails/" +
|
location = (self.output + "assets/thumbnails/" +
|
||||||
gen_name(episode.date, episode.slug) + ".jpg")
|
gen_name(episode.date, episode.slug) + ".jpg")
|
||||||
|
logging.info(f"Creating thumbnail for {episode=} at {location}")
|
||||||
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")):
|
||||||
|
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):
|
def generate_archives(self):
|
||||||
"Generates the entire "
|
"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:
|
class Episode:
|
||||||
"Represents one episode of podcast"
|
"Represents one episode of podcast"
|
||||||
def __init__(self, date, slug, title, show_notes, video_src):
|
def __init__(self, date, slug, title, show_notes, video_src, audio_src, config):
|
||||||
self.date = date
|
self.date = date
|
||||||
self.slug = slug
|
self.slug = slug
|
||||||
self.title = title
|
self.title = title
|
||||||
self.show_notes = show_notes
|
self.show_notes = markdown.markdown(show_notes)
|
||||||
self.video = video_src
|
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"
|
"Renders the Episode with the given template"
|
||||||
return template.render(
|
return template.render(
|
||||||
title=self.title,
|
title=self.title,
|
||||||
show_notes=jinja2.Markup(self.show_notes),
|
show_notes=jinja2.Markup(self.show_notes),
|
||||||
thumbnail_src=thumbnail_src,
|
thumbnail_src=thumbnail_src,
|
||||||
video_src=self.video
|
relative=relative,
|
||||||
|
video_src=f"assets/videos/{path.basename(self.video)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def store_thumbnail(self, location):
|
def store_thumbnail(self, location):
|
||||||
"Stores the thumbnail for given image at path"
|
"Stores the thumbnail for given image at path"
|
||||||
args = ["ffmpeg", "-i", self.video, "-ss", "00:00:01.000", "-vframes",
|
args = ["ffmpeg", "-i", self.video, "-ss", "00:00:01.000", "-vframes",
|
||||||
"1", location]
|
"1", location]
|
||||||
|
logging.info(f"Running {' '.join(args)}")
|
||||||
subprocess.run(args, check=False)
|
subprocess.run(args, check=False)
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return int(self.length)
|
||||||
|
|
||||||
def main():
|
def __str__(self):
|
||||||
"Main method"
|
return f"{self.slug}: {self.title}"
|
||||||
root = path.dirname(sys.argv[0])
|
|
||||||
|
def __repr__(self):
|
||||||
|
return str(self)
|
||||||
|
|
||||||
|
def parse_args():
|
||||||
|
"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")
|
||||||
|
parser.add_argument("--verbose", "-v", action="store_true", help="Verbose Logging")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
input_dir = args.input_dir.rstrip("/") + "/"
|
input_dir = path.abspath(args.input_dir.rstrip("/")) + "/"
|
||||||
output_dir = args.output_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
|
# Input validation
|
||||||
if not all(path.isdir(i) for i in (input_dir, input_dir + "md",
|
paths = [
|
||||||
input_dir + "videos")):
|
input_dir,
|
||||||
print("Invalid Input", file=sys.stderr)
|
input_dir + "md",
|
||||||
return
|
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):
|
logging.info("Creating output directory if it doesn't exist")
|
||||||
os.mkdir(args.output_dir)
|
if not path.isdir(output_dir):
|
||||||
|
os.mkdir(output_dir)
|
||||||
|
|
||||||
template = jinja2.Environment(
|
env = jinja2.Environment(
|
||||||
loader=jinja2.FileSystemLoader(root),
|
loader=jinja2.FileSystemLoader(root),
|
||||||
autoescape=jinja2.select_autoescape("html")
|
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>.*).md)")
|
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"):
|
for file in os.listdir(input_dir + "md"):
|
||||||
|
logging.info(f"File: {file}")
|
||||||
match = split.match(file)
|
match = split.match(file)
|
||||||
|
logging.info(f"Match: {match}")
|
||||||
if not match:
|
if not match:
|
||||||
print(f"Invalid filename: {file}", file=sys.stderr)
|
logging.error(f"Invalid filename: {file}")
|
||||||
continue
|
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")
|
slug = match.group("slug")
|
||||||
|
logging.info(f"Slug: {slug}")
|
||||||
with open(input_dir + "md/" + file) as episode:
|
with open(input_dir + "md/" + file) as episode:
|
||||||
title = episode.readline()
|
try:
|
||||||
show_notes = episode.read()
|
config, show_notes = parse_file(episode)
|
||||||
video = input_dir + "videos/" + gen_name(date, slug) + ".mp4"
|
logging.info(f"Config: {config}")
|
||||||
podcast.append(Episode(date, slug, title, show_notes, video))
|
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.sort()
|
||||||
|
logging.info("Generating thumbnails")
|
||||||
podcast.generate_thumbnails()
|
podcast.generate_thumbnails()
|
||||||
podcast.generate_atom()
|
logging.info("Generating archives pages")
|
||||||
podcast.generate_site()
|
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__":
|
if __name__ == "__main__":
|
||||||
main()
|
sys.exit(main(parse_args()))
|
||||||
|
@@ -1,2 +1,3 @@
|
|||||||
jinja2
|
jinja2
|
||||||
markdown
|
markdown
|
||||||
|
mutagen
|
||||||
|
@@ -1,28 +1,23 @@
|
|||||||
/* latin */
|
/* latin */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Comfortaa';
|
font-family: 'Lato';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(../fonts/Comfortaa-Latin-Light.woff) format('woff2');
|
src: url(../fonts/Lato-Regular.ttf);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
/* latin */
|
/* latin */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Comfortaa';
|
font-family: 'Montserrat';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 700;
|
font-display: swap;
|
||||||
font-display: swap;
|
src: url(../fonts/Montserrat-Bold.ttf);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin body-font {
|
@mixin body-font {
|
||||||
font-family: "Comfortaa", sans-serif;
|
font-family: "Lato", sans-serif;
|
||||||
font-weight: 300;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin title-font {
|
@mixin title-font {
|
||||||
font-family: "Comfortaa", sans-serif;
|
font-family: "Montserrat", sans-serif;
|
||||||
font-weight: 700;
|
|
||||||
}
|
}
|
||||||
|
133
scss/index.scss
133
scss/index.scss
@@ -44,7 +44,7 @@ body {
|
|||||||
}
|
}
|
||||||
.bottom_bg {
|
.bottom_bg {
|
||||||
background-image: linear-gradient(#26a4c4, #afdde9);
|
background-image: linear-gradient(#26a4c4, #afdde9);
|
||||||
height: 100 - $top-height;
|
height: 100vh - $top-height;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: $top-height;
|
top: $top-height;
|
||||||
@@ -56,7 +56,7 @@ body {
|
|||||||
.wrapper {
|
.wrapper {
|
||||||
min-height: $top-height;
|
min-height: $top-height;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
padding-bottom: 2em;
|
padding-bottom: 1em;
|
||||||
.player {
|
.player {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
width: 80%;
|
width: 80%;
|
||||||
@@ -69,21 +69,86 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.shownotes {
|
.shownotes {
|
||||||
padding: 2em 1em 1em 1em;
|
padding: 2em 1em 0;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
width: 80%;
|
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 {
|
.bottom {
|
||||||
min-height: 100vh - $top-height;
|
min-height: 95vh - $top-height;
|
||||||
|
margin-bottom: 5vh;
|
||||||
.wrapper {
|
.wrapper {
|
||||||
min-height: 100vh - $top-height;
|
min-height: 95vh - $top-height;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-around;
|
justify-content: space-between;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding-top: 2em;
|
padding-top: 1em;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
.content {
|
.content {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -98,12 +163,27 @@ body {
|
|||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-radius: 1.5em;
|
border-radius: 1.5em;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
transition-duration: 0.4s;
|
||||||
}
|
}
|
||||||
.button:hover, .button:active {
|
.button:hover, .button:active {
|
||||||
|
transition-duration: 0.4s;
|
||||||
background-color: #000;
|
background-color: #000;
|
||||||
color: #FFF;
|
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 {
|
footer {
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
padding: 0 1em;
|
padding: 0 1em;
|
||||||
@@ -113,11 +193,13 @@ body {
|
|||||||
color: #292929;
|
color: #292929;
|
||||||
padding: 0.3em;
|
padding: 0.3em;
|
||||||
border-radius: 1.6em;
|
border-radius: 1.6em;
|
||||||
|
transition-duration: 0.2s;
|
||||||
}
|
}
|
||||||
a:hover {
|
a:hover {
|
||||||
color: #FFF;
|
color: #FFF;
|
||||||
background-color: #000;
|
background-color: #000;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
transition-duration: 0.2s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,10 +223,32 @@ h1 {
|
|||||||
@include title-font;
|
@include title-font;
|
||||||
font-size: 4em;
|
font-size: 4em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
a {
|
||||||
|
color: #000;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
body {
|
||||||
|
.top {
|
||||||
|
.wrapper {
|
||||||
|
.subscribe {
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
body {
|
body {
|
||||||
|
.top_bg {
|
||||||
|
height: $top-height-phone;
|
||||||
|
}
|
||||||
|
.bottom_bg {
|
||||||
|
height: 100vh - $top-height-phone;
|
||||||
|
top: $top-height-phone;
|
||||||
|
}
|
||||||
.top {
|
.top {
|
||||||
min-height: $top-height-phone;
|
min-height: $top-height-phone;
|
||||||
.wrapper {
|
.wrapper {
|
||||||
@@ -159,9 +263,9 @@ h1 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.bottom {
|
.bottom {
|
||||||
min-height: 100vh - $top-height-phone;
|
min-height: 95vh - $top-height-phone;
|
||||||
.wrapper {
|
.wrapper {
|
||||||
min-height: 100vh - $top-height-phone;
|
min-height: 95vh - $top-height-phone;
|
||||||
.content {
|
.content {
|
||||||
.button {
|
.button {
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
@@ -176,6 +280,17 @@ h1 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
body {
|
||||||
|
.top {
|
||||||
|
.wrapper {
|
||||||
|
.subscribe {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes move_right {
|
@keyframes move_right {
|
||||||
from {left: 0}
|
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