Initial commit

master
Simon Bruder 2020-08-11 16:14:38 +02:00
commit 410d1173e1
No known key found for this signature in database
GPG Key ID: 6F03E0000CC5B62F
41 changed files with 1163 additions and 0 deletions

74
.drone.yml Normal file
View File

@ -0,0 +1,74 @@
kind: pipeline
name: default
steps:
- name: cache-restore
image: r.sbruder.de/drone-s3-cache
settings:
pull: true
endpoint: https://s3.sbruder.de
access_key:
from_secret: aws_access_key_id
secret_key:
from_secret: aws_secret_access_key
restore: true
- name: build
image: alpine
commands:
- set -e
- echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories
- apk add --no-cache git git-lfs jq py3-pip py3-virtualenv unzip wget zola
- pip3 install --no-cache-dir pipenv
- pipenv install
- wget -qO - https://raw.githubusercontent.com/MestreLion/git-tools/master/git-restore-mtime | python3 -
- ./vendor.sh
- ./build.sh
- '[ "$DRONE_COMMIT_BRANCH" = "master" ] || zola build -u "https://${DRONE_COMMIT}--schulpodcast.netlify.app"'
- name: publish-prod
image: r.sbruder.de/drone-netlify
settings:
auth_token:
from_secret: netlify_auth_token
dir: public
prod: 1
when:
branch:
- master
- name: publish-preview
image: r.sbruder.de/drone-netlify
settings:
auth_token:
from_secret: netlify_auth_token
dir: public
when:
branch:
exclude:
- master
- name: cache-rebuild
image: r.sbruder.de/drone-s3-cache
settings:
pull: true
endpoint: https://s3.sbruder.de
access_key:
from_secret: aws_access_key_id
secret_key:
from_secret: aws_secret_access_key
rebuild: true
mount:
- static/episodes
- name: cache-flush
image: r.sbruder.de/drone-s3-cache
settings:
pull: true
endpoint: https://s3.sbruder.de
access_key:
from_secret: aws_access_key_id
secret_key:
from_secret: aws_secret_access_key
flush: true
flush_age: 14

3
.gitattributes vendored Normal file
View File

@ -0,0 +1,3 @@
*.jpg filter=lfs diff=lfs merge=lfs -text
content/*.opus filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
/__pycache__/
/public/
/static/episodes.json
/static/episodes/*
/static/vendor/
/vendor/

3
.netlify/state.json Normal file
View File

@ -0,0 +1,3 @@
{
"siteId": "b4207c7e-720e-4ea7-9a99-00b013da6370"
}

13
Pipfile Normal file
View File

@ -0,0 +1,13 @@
# vim: set ft=toml:
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
[packages]
mutagen = "*"
[requires]
python_version = "3.8"

29
Pipfile.lock generated Normal file
View File

@ -0,0 +1,29 @@
{
"_meta": {
"hash": {
"sha256": "127455b11ccc0f3968d05e2c6ef0b3532f68d20ac459d08f1aa4550c22ebb866"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.8"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"mutagen": {
"hashes": [
"sha256:6397602efb3c2d7baebd2166ed85731ae1c1d475abca22090b7141ff5034b3e1",
"sha256:9c9f243fcec7f410f138cb12c21c84c64fde4195481a30c9bfb05b5f003adfed"
],
"index": "pypi",
"version": "==1.45.1"
}
},
"develop": {}
}

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# schulpodcast (prototype)
Prototype for the site of the podcast for the practical seminar podcast.

8
build.sh Executable file
View File

@ -0,0 +1,8 @@
#!/bin/sh
set -e
./metadata.py
zola build
echo "Encoding…"
pipenv run ./encode.py
./metadata.py
zola build

63
common.py Normal file
View File

@ -0,0 +1,63 @@
from subprocess import run
import csv
import json
import time
ACRONYM = "tpc"
FORMATS = {
"opus": ["-b:a", "48k"],
"m4a": [
"-b:a",
"48k",
"-f",
"ipod",
"-profile:a",
"aac_he",
"-disposition:v:0",
"attached_pic",
],
"oga": ["-b:a", "64k", "-c:a", "libvorbis"],
"mp3": ["-b:a", "72k", "-map", "0:0", "-map", "1:0"],
}
def path_to_episode(name, format):
if format in ["original", "txt", "md"]:
base = "content"
else:
base = "static/episodes"
if format == "original":
format = "opus"
return f"{base}/{name}.{format}"
def get_chapters(episode):
with open(path_to_episode(episode, "txt")) as f:
chapter_reader = csv.reader(f, delimiter="\t")
for chapter in chapter_reader:
yield {"start": float(chapter[0]), "title": chapter[2]}
def get_episode_info(name, format):
path = path_to_episode(name, format)
return json.loads(
run(
[
"./vendor/ffprobe",
"-loglevel",
"error",
"-show_format",
"-print_format",
"json",
path,
],
capture_output=True,
).stdout
)["format"]
def sexagesimal(ts):
return time.strftime("%H:%M:%S", time.gmtime(ts)) + str(round(ts % 1, 3))[1:]

62
config.toml Normal file
View File

@ -0,0 +1,62 @@
base_url = "https://schulpodcast.netlify.app"
compile_sass = true
default_language = "de"
feed_filename = "rss.xml"
# source opus files and chapter data are not published
ignored_content = ["tpc???-*.opus", "tpc???-*.txt"]
title = "Test-Podcast"
description = "Ein Podcast mit dem Test als einziges Ziel."
taxonomies = [
{ name = "formats", feed = true },
]
[extra]
author = "Test-Podcast-Team"
subtitle = "Neues aus der Testwelt."
acronym = "TPC"
# to what length the episode number should be padded
pad_to = 3
[extra.itunes]
category = "News"
[extra.theme]
main = "#5f8806"
[[extra.navbar.links]]
title = "Startseite"
target = "/"
[[extra.navbar.links]]
title = "Abonnieren"
target = "subscribe"
[[extra.navbar.links]]
title = "Impressum & Datenschutz"
target = "imprint"
[[extra.formats]]
codec = "opus"
ext = "opus"
name = "Opus Audio"
mime_type = "audio/ogg"
[[extra.formats]]
codec = "aac"
ext = "m4a"
name = "MPEG-4 AAC Audio"
mime_type = "audio/mp4"
[[extra.formats]]
codec = "vorbis"
ext = "oga"
name = "Ogg Vorbis Audio"
mime_type = "audio/ogg"
[[extra.formats]]
codec = "mp3"
ext = "mp3"
name = "MP3 Audio"
mime_type = "audio/mpeg"

4
content/_index.md Normal file
View File

@ -0,0 +1,4 @@
+++
paginate_by = 5
sort_by = "date"
+++

BIN
content/imprint-email.png (Stored with Git LFS) Normal file

Binary file not shown.

35
content/imprint.md Normal file
View File

@ -0,0 +1,35 @@
+++
title = "Impressum und Datenschutz"
+++
## Impressum
### Angaben gemäß § 5 TMG
Simon Bruder
Wallmersbach 42
97215 Uffenheim
#### Kontakt
Telefon: +4915256561414
E-Mail: <img style="margin-bottom: -5px;" src="/imprint-email.png" alt="(Aus Spamschutzgründen nicht angezeigt)">
#### Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV
Simon Bruder
Wallmersbach 42
97215 Uffenheim
Quelle: <https://www.e-recht24.de>
## Datenschutz
Diese Internetseite wird bei [Netlify](https://www.netlify.com/) gehostet.
Dieser Anbieter sammelt Zugriffsdaten inklusive der IP-Adresse der Besuchenden
und speichert diese für weniger als 30 Tage ([Quelle][netlify-pii]). Auf diese
Daten haben die oben genannten Personen keinen Zugriff.
Darüber hinaus werden Besuchsdaten nicht gespeichert oder verarbeitet
[netlify-pii]: https://www.netlify.com/gdpr/#types-of-personally-identifiable-information-pii-we-collect

22
content/subscribe.md Normal file
View File

@ -0,0 +1,22 @@
+++
title = "Abonnieren"
+++
FIXME: was das die leute auch verstehen
<!-- broken when id is “podlove-subscribe-button” -->
<h2 id="podlove-subscribe-button-">Podlove Subscribe Button</h2>
Der Podlove Subscribe Button, den ihr auf jeder Seite am rechten Rand bzw. am
Handy ganz unten auf der Seite findet schlägt euch automatisch Podcast Clients
vor, mit denen ihr den Podcast abonnieren könnt.
## Manuell
Wer einen anderen Podcast Client benutzt oder ein anderes Audioformat benutzen
möchte kann aus einem der folgenden Feeds auswählen:
* [Ogg Opus](/formats/opus/rss.xml)
* [AAC (FDK)](/formats/m4a/rss.xml)
* [Ogg Vorbis](/formats/oga/rss.xml)
* [MP3](/formats/mp3/rss.xml)

BIN
content/tpc001-aller-anfang-ist-schwer.jpg (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,22 @@
+++
title = "Aller Anfang ist schwer"
description = "Weil wir noch keine Inhalte haben liest jemand einen Wikipedia-Artikel vor."
date = 2006-02-21
[extra]
episode = 1
subtitle = "Über die Philosophie des Geistes (1/2)"
contributors = [
"Cartaphilus",
"Autoren des Artikels",
]
poster = true
[taxonomies]
formats = ["opus", "m4a", "oga", "mp3"]
+++
Quelle:
[Audio-Version](https://de.wikipedia.org/wiki/Datei:De-Philosophie_des_Geistes_01-article.ogg)
von [Wikipedia (de): Philosophie des
Geistes](https://de.wikipedia.org/wiki/Philosophie_des_Geistes)

BIN
content/tpc001-aller-anfang-ist-schwer.opus (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,8 @@
0.000000 0.000000 Intro
19.343339 19.343339 Übersicht
49.177982 49.177982 Das Leib-Seele-Problem
251.672047 251.672047 Dualistische Antworten auf das Leib-Seele Problem
666.674603 666.674603 noch mehr
1030.055181 1030.055181 kapitel
1250.372539 1250.372539 aber
1542.221506 1542.221506 kein bock

View File

@ -0,0 +1,21 @@
+++
title = "Noch mehr Boilerplate"
description = "Weil wir immer noch keine Inhalte haben liest jemand den Wikipedia-Artikel vom letzten Mal zu Ende."
date = 2006-02-21T00:00:01Z
[extra]
episode = 2
subtitle = "Über die Philosophie des Geistes (2/2)"
contributors = [
"Cartaphilus",
"Autoren des Artikels",
]
[taxonomies]
formats = ["opus", "m4a", "oga", "mp3"]
+++
Quelle:
[Audio-Version](https://de.wikipedia.org/wiki/Datei:De-Philosophie_des_Geistes_02-article.ogg)
von [Wikipedia (de): Philosophie des
Geistes](https://de.wikipedia.org/wiki/Philosophie_des_Geistes)

BIN
content/tpc002-noch-mehr-boilerplate.opus (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,11 @@
0.000000 0.000000 auch
84.910579 84.910579 für
156.622622 156.622622 diesen
222.175348 222.175348 test
294.767294 294.767294 will
343.601876 343.601876 ich
402.995286 402.995286 nicht
468.108061 468.108061 so
512.983082 512.983082 viel
597.013758 597.013758 Zeit
674.885118 674.885118 verschwenden

167
encode.py Executable file
View File

@ -0,0 +1,167 @@
#!/usr/bin/env python3
from datetime import datetime
from mutagen.flac import Picture
from mutagen.oggopus import OggOpus
from mutagen.oggvorbis import OggVorbis
from subprocess import run
from urllib.parse import urlparse
import base64
import os
import threading
import xml.etree.ElementTree as ET
import common
def encode_episode(podcast, episode, format):
format, options = format
infile = common.path_to_episode(episode["file_base"], "original")
outfile = common.path_to_episode(episode["file_base"], format)
content_file = common.path_to_episode(episode["file_base"], "md")
try:
changed = any(
os.path.getmtime(file) > os.path.getmtime(outfile)
for file in [infile, content_file, episode["poster"]]
)
except FileNotFoundError:
changed = True
if changed:
tags = {
"TITLE": episode["title"],
"ARTIST": ", ".join(episode["contributors"]),
"ALBUM": podcast["title"],
"TRACK": episode["number"],
"GENRE": "podcast",
"DATE": datetime.strftime(episode["date"], "%Y-%m-%d"),
"URL": podcast["link"],
"COMMENT": episode["summary"],
}
chapters = list(common.get_chapters(episode["file_base"]))
duration = float(
common.get_episode_info(episode["file_base"], "original")["duration"]
)
ffmpeg_chapters = b";FFMETADATA1\n\n"
for idx, chapter in enumerate(chapters):
ffmpeg_chapters += b"[CHAPTER]\nTIMEBASE=1/1000\n"
ffmpeg_chapters += f"START={int(chapter['start'] * 1000)}\n".encode("utf-8")
try:
ffmpeg_chapters += f"END={int(chapters[idx+1]['start'] * 1000)}\n".encode(
"utf-8"
)
except:
ffmpeg_chapters += f"END={int(duration * 1000)}\n".encode("utf-8")
escaped_title = (
chapter["title"]
.replace("=", "\=")
.replace(";", "\;")
.replace("#", "\#")
.replace("\\", "\\\\")
.replace("\n", "\\\n")
)
ffmpeg_chapters += f"title={escaped_title}\n\n".encode("utf-8")
command = ["./vendor/ffmpeg", "-y", "-loglevel", "error"]
command.extend(["-i", infile])
if not format in ["oga", "opus"]:
command.extend(["-i", episode["poster"]])
command.extend(["-i", "-"])
command.extend(["-c:v", "copy"])
command.extend(["-map_metadata", "1"])
command.extend(options)
for k, v in tags.items():
command.extend(["-metadata", f"{k}={v}"])
command.append(outfile)
run(command, input=ffmpeg_chapters, check=True)
if format in ["oga", "opus"]:
if format == "oga":
audio = OggVorbis(outfile)
else:
audio = OggOpus(outfile)
# poster
picture = Picture()
with open(episode["poster"], "rb") as f:
picture.data = f.read()
picture.type = 17
picture.desc = ""
picture.mime = "image/jpeg"
picture.width = 500
picture.height = 500
picture.depth = 24
audio["metadata_block_picture"] = [
base64.b64encode(picture.write()).decode("ascii")
]
# chapters for vorbis
if format == "oga":
for idx, chapter in enumerate(chapters):
audio[f"CHAPTER{idx:03}"] = common.sexagesimal(chapter["start"])
audio[f"CHAPTER{idx:03}NAME"] = chapter["title"]
audio.save()
print(f" {format}", end="", flush=True)
else:
print(f" ({format})", end="", flush=True)
os.makedirs("static/episodes", exist_ok=True)
tree = ET.parse("public/formats/opus/rss.xml")
root = tree.getroot()
channel = root.find("channel")
podcast = {
"title": channel.find("title").text,
"link": channel.find("link").text,
"poster": "static" + urlparse(channel.find("image").find("url").text).path,
}
for item in channel.findall("item"):
episode = {
"title": item.find("title").text,
"number": item.find("{http://www.itunes.com/dtds/podcast-1.0.dtd}episode").text,
"date": datetime.strptime(
item.find("pubDate").text, "%a, %d %b %Y %H:%M:%S %z"
),
"contributors": [
contributor.find("{http://www.w3.org/2005/Atom}name").text
for contributor in item.findall("{http://www.w3.org/2005/Atom}contributor")
],
"summary": item.find(
"{http://www.itunes.com/dtds/podcast-1.0.dtd}summary"
).text,
"file_base": os.path.splitext(
os.path.basename(item.find("enclosure").attrib["url"])
)[0],
}
episode_poster = f"content/{episode['file_base']}.jpg"
if os.path.isfile(episode_poster):
episode["poster"] = episode_poster
else:
episode["poster"] = podcast["poster"]
print(episode["file_base"], end="", flush=True)
threads = []
for format in common.FORMATS.items():
thread = threading.Thread(
target=encode_episode, args=(podcast, episode, format), daemon=True
)
thread.start()
threads.append(thread)
for thread in threads:
thread.join()
print()

77
metadata.py Executable file
View File

@ -0,0 +1,77 @@
#!/usr/bin/env python3
from glob import glob
from html.parser import HTMLParser
import json
import os.path
import common
class EpisodeHtmlParser(HTMLParser):
current_tag_is_episode_json = False
data = {}
def __init__(self, episode):
super().__init__()
self.episode = episode
def handle_starttag(self, tag, attrs):
attrs = dict(attrs)
if (
tag == "script"
and attrs.get("type") == "application/json"
and attrs.get("id") == f"config-episode-{episode}"
):
self.current_tag_is_episode_json = True
def handle_endtag(self, tag):
if self.current_tag_is_episode_json:
self.current_tag_is_episode_json = False
def handle_data(self, data):
if self.current_tag_is_episode_json:
self.data = json.loads(data)
def chapter_fix_timestamp(chapter):
chapter["start"] = common.sexagesimal(chapter["start"])
return chapter
metadata = {}
for file in sorted(glob(f"content/{common.ACRONYM}*.md")):
episode = os.path.splitext(os.path.basename(file))[0]
metadata[episode] = {}
metadata[episode]["duration"] = common.sexagesimal(
float(common.get_episode_info(episode, "original")["duration"])
)
metadata[episode]["formats"] = {}
for format in common.FORMATS.keys():
try:
size = os.path.getsize(common.path_to_episode(episode, format))
except FileNotFoundError:
# when bootstrapping for the first time the encoded files do not exist
size = 0
metadata[episode]["formats"][format] = {"size": size}
metadata[episode]["chapters"] = list(
map(chapter_fix_timestamp, common.get_chapters(episode))
)
with open("static/episodes.json", "w") as f:
f.write(json.dumps(metadata))
# extract podlove episode json
for file in sorted(glob(f"public/{common.ACRONYM}*/index.html")):
episode = os.path.basename(os.path.dirname(file))
parser = EpisodeHtmlParser(episode)
with open(file) as f:
parser.feed(f.read())
metadata = parser.data
os.makedirs("static/podlove", exist_ok=True)
with open(f"static/episodes/{episode}.podlove.json", "w") as f:
f.write(json.dumps(metadata))

84
sass/style.scss Normal file
View File

@ -0,0 +1,84 @@
$navbar-breakpoint: 640px;
@import "../vendor/bulma/sass/utilities/_all";
@import "../vendor/bulma/sass/base/_all";
@import "../vendor/bulma/sass/components/navbar";
@import "../vendor/bulma/sass/components/pagination";
@import "../vendor/bulma/sass/elements/button";
@import "../vendor/bulma/sass/elements/container";
@import "../vendor/bulma/sass/elements/content";
@import "../vendor/bulma/sass/elements/title";
@import "../vendor/bulma/sass/grid/columns";
@import "../vendor/bulma/sass/helpers/color";
@import "../vendor/bulma/sass/helpers/spacing";
@import "../vendor/bulma/sass/layout/footer";
#page-wrapper {
@include desktop {
margin-top: 2rem;
margin-bottom: 2rem;
}
max-width: 1000px;
}
#banner {
display: block;
}
#main-wrapper {
padding: 2rem 7.6%;
}
nav {
padding: 0 7.6%;
}
aside {
h3 {
margin-bottom: .5rem;
text-transform: uppercase;
font-size: .75rem;
}
}
h2 a {
color: black;
&:hover {
color: $link;
}
}
.post-meta {
font-size: 0.75rem;
}
// noscript fallbacks
.podlove-player > audio {
width: 100%;
}
.noscript-subscribe-button {
width: 100%;
border: none;
font-size: 16px;
height: 54px;
margin-bottom: 4px;
cursor: pointer;
font-family: "Roboto", sans-serif;
font-weight: 500;
letter-spacing: 0.5px;
text-transform: uppercase;
// default colours from podlove, not customisable
background-color: #2b8ac6;
color: #dfeef8;
&:hover {
background-color: #226d9c;
color: #b5d8ef;
}
}

17
static/assets/avatar.svg Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1024" height="1024" version="1.1" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<metadata>
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<g stroke-linecap="round">
<rect width="1024" height="1024" fill="#d8d8d8" stroke-width="9.7733"/>
<ellipse cx="512" cy="340.04" rx="244.68" ry="264.88" fill="#adadad" stroke-width="21.166"/>
<path d="m329.32 648.1c-132.57 0-239.3 106.73-239.3 239.3v136.61h843.95v-136.61c0-132.57-106.73-239.3-239.3-239.3z" fill="#adadad" stroke-width="9.7522"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 870 B

BIN
static/assets/banner.jpg (Stored with Git LFS) Normal file

Binary file not shown.

9
static/assets/main.js Normal file
View File

@ -0,0 +1,9 @@
document.addEventListener('DOMContentLoaded', () => {
Array.from(document.querySelectorAll('.navbar-burger')).forEach(el => {
el.addEventListener('click', () => {
const target = document.getElementById(el.dataset.target)
el.classList.toggle('is-active')
target.classList.toggle('is-active')
})
})
})

BIN
static/assets/poster.jpg (Stored with Git LFS) Normal file

Binary file not shown.

102
templates/base.html Normal file
View File

@ -0,0 +1,102 @@
{%- import "macros.html" as macros -%}
<!DOCTYPE html>
<html lang="{{ config.default_language }}" class="has-background-light">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}{% if page.title %}{{ macros::title(page=page) }} {% endif %}{{ config.title }}{% endblock title %}</title>
{%- for format in config.extra.formats %}
<link rel="alternate" type="application/rss+xml" title="Podcast Feed: {{ config.title }} ({{ format.name }})" href="{{ get_url(path="formats/" ~ format.ext ~ "/rss.xml") | safe }}">
{%- endfor %}
{%- block styles %}
<link rel="stylesheet" href="{{ get_url(path="style.css") | safe }}">
{%- endblock styles %}
{%- block preload %}
<link rel="preload" href="{{ get_url(path="vendor/podlove-subscribe-button/stylesheets/app.css") | safe }}" as="style">
<link rel="preload" href="{{ get_url(path="vendor/podlove-subscribe-button/javascripts/app.js") | safe }}" as="script">
<link rel="preload" href="{{ get_url(path="vendor/podlove-subscribe-button/fonts/podlove/Podlove.woff") | safe }}" as="font" crossorigin="anonymous">
<link rel="preload" href="{{ get_url(path="vendor/podlove-subscribe-button/fonts/podlove/Podlove.ttf") | safe }}" as="font" crossorigin="anonymous">
<link rel="preload" href="{{ get_url(path="vendor/podlove-subscribe-button/fonts/roboto_light/Roboto-Light-webfont.woff") | safe }}" as="font" crossorigin="anonymous">
<link rel="preload" href="{{ get_url(path="vendor/podlove-subscribe-button/fonts/roboto_light/Roboto-Light-webfont.ttf") | safe }}" as="font" crossorigin="anonymous">
<link rel="preload" href="{{ get_url(path="vendor/podlove-subscribe-button/fonts/roboto_medium/Roboto-Medium-webfont.woff") | safe }}" as="font" crossorigin="anonymous">
<link rel="preload" href="{{ get_url(path="vendor/podlove-subscribe-button/fonts/roboto_medium/Roboto-Medium-webfont.ttf") | safe }}" as="font" crossorigin="anonymous">
{%- endblock prefetch %}
{%- block metadata %}
<meta name="description" content="{% if page.description %}{{ page.description }}{% else %}{{ config.description }}{% endif %}">
{%- endblock metadata %}
</head>
<body>
<div class="container has-background-white" id="page-wrapper">
{%- block banner %}
<a href="{{ get_url(path="/") }}"><img id="banner" src="{{ get_url(path="assets/banner.jpg") | safe }}" alt="Banner von {{ config.title }}"></a>
{%- endblock %}
{%- block navbar %}
<nav class="navbar is-dark">
<div class="navbar-brand">
<a role="button" class="navbar-burger burger" aria-label="Menü öffnen" aria-expanded="false" data-target="navbar">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div id="navbar" class="navbar-menu">
<div class="navbar-start">
{%- for link in config.extra.navbar.links %}
<a class="navbar-item{% if current_path == link.target %} is-active{% endif %}" href="{{ get_url(path=link.target) | safe }}"{% if current_path == link.target %} aria-current="page"{% endif %}>
{{ link.title }}
</a>
{%- endfor %}
</div>
</div>
</nav>
{%- endblock navbar %}
<div id="main-wrapper">
<div class="columns is-variable is-8">
<main class="column is-three-quarters" aria-label="Hauptinhalt">
{% block content %}{% endblock content %}
</main>
<aside class="column" aria-label="Sekundärer Inhalt">
<h3>{{ config.title }}</h3>
{{ macros::podlove_subscribe_button(size="big") }}
<p>{{ config.description }}</p>
</aside>
</div>
</div>
<footer class="footer">
{%- if get_env(name="DRONE", default="") == "true" -%}
<a href="{{ get_env(name="DRONE_SYSTEM_PROTO") ~ "://" ~ get_env(name="DRONE_SYSTEM_HOSTNAME") ~ "/" ~ get_env(name="DRONE_REPO") ~ "/" ~ get_env(name="DRONE_BUILD_NUMBER") }}">Automatisch generiert</a>
aus Commit <a href="{{ get_env(name="DRONE_COMMIT_LINK") }}">{{ get_env(name="DRONE_COMMIT") | truncate(length=7, end="") }}</a>
{%- else %}
Manuell generiert
{%- endif %}
{{ now() | date(format="am %d.%m.%Y um %T") }}.
</footer>
</div>
{% block scripts -%}
<script src="{{ get_url(path="assets/main.js") | safe }}"></script>
<script id="config-subscribe-button" type="application/json">
{
"title": {{ config.title | json_encode | safe }},
"subtitle": {{ config.extra.subtitle | json_encode | safe }},
"description": {{ config.description | json_encode | safe }},
"cover": {{ get_url(path="assets/poster.jpg") | json_encode | safe }},
"feeds": [
{%- for format in config.extra.formats %}
{
"type": "audio",
"format": {{ format.codec | json_encode | safe }},
"url": {{ get_url(path="formats/" ~ format.ext ~ "/rss.xml") | json_encode | safe }},
"variant": "high"
}{% if not loop.last %},{% endif %}
{%- endfor %}
]
}
</script>
<script>
window.podcastData = JSON.parse(document.getElementById("config-subscribe-button").innerText)
</script>
{%- endblock scripts %}
</body>
</html>

View File

@ -0,0 +1 @@
<meta http-equiv="refresh" content="0; url={{ get_url(path="/") | safe }}">

View File

@ -0,0 +1 @@
<meta http-equiv="refresh" content="0; url={{ get_url(path="/formats/" ~ term.name ~ "/rss.xml" ) | safe }}">

39
templates/index.html Normal file
View File

@ -0,0 +1,39 @@
{% extends "base.html" %}
{% block content -%}
{%- for page in paginator.pages %}
{% include "post.html" %}
<hr class="hr">
{%- endfor %}
{%- if paginator.number_pagers > 1 %}
<nav class="pagination is-centered" role="navigation" aria-label="pagination">
{% if paginator.next %}<a class="pagination-previous" href="{{ paginator.next | safe }}">Ältere Beiträge</a>{% endif %}
{% if paginator.previous %}<a class="pagination-next" href="{{ paginator.previous | safe }}">Neuere Beiträge</a>{% endif %}
<ul class="pagination-list">
{%- if paginator.current_index != paginator.number_pagers %}
<li><a class="pagination-link" aria-label="Zu Seite {{ paginator.number_pagers }}" href="{{ paginator.last | safe }}">{{ paginator.number_pagers }}</a></li>
{%- endif %}
{%- if paginator.current_index + 3 <= paginator.number_pagers %}
<li><span class="pagination-ellipsis">&hellip;</span></li>
{%- endif %}
{%- if paginator.current_index + 2 <= paginator.number_pagers %}
<li><a class="pagination-link" aria-label="Zu Seite {{ paginator.current_index + 1 }}" href="{{ paginator.next | safe }}">{{ paginator.current_index + 1 }}</a></li>
{%- endif %}
<li><a class="pagination-link is-current" aria-label="Seite 46" aria-current="page">{{ paginator.current_index }}</a></li>
{%- if paginator.current_index > 2 %}
<li><a class="pagination-link" aria-label="Zu Seite {{ paginator.current_index - 1 }}" href="{{ paginator.previous | safe }}">{{ paginator.current_index - 1 }}</a></li>
{%- endif %}
{%- if paginator.current_index > 3 %}
<li><span class="pagination-ellipsis">&hellip;</span></li>
{%- endif %}
{%- if paginator.current_index != 1 %}
<li><a class="pagination-link" aria-label="Zu Seite 1" href="{{ paginator.first | safe }}">1</a></li>
{%- endif %}
</ul>
</nav>
{%- endif %}
{%- endblock content %}
{% block scripts -%}
{{ super() }}
{% include "podlove-player-script.html" %}
{%- endblock scripts %}

25
templates/macros.html Normal file
View File

@ -0,0 +1,25 @@
{% macro podlove_subscribe_button(size="medium") %}
<noscript>
<img src="{{ get_url(path="assets/poster.jpg") | safe }}" alt="Logo von {{ config.title }}">
<a href="{{ get_url(path="subscribe") }}"><button class="noscript-subscribe-button">Abonnieren</button></a>
</noscript>
<script
class="podlove-subscribe-button"
src="{{ get_url(path="vendor/podlove-subscribe-button/javascripts/app.js") | safe }}"
data-language="{{ config.default_language }}"
data-size="{{ size }}"
data-format="cover"
data-json-data="podcastData"
data-colors="{{ config.extra.theme.main | default(value="#2B8AC6") }}"
></script>
{% endmacro podlove_subscribe_button %}
{% macro poster(page) -%}
{% if page.extra.poster %}{{ get_url(path=page.slug ~ ".jpg") | safe }}{% else %}{{ get_url(path="assets/poster.jpg") | safe }}{% endif %}
{%- endmacro poster %}
{% macro title(page) -%}
{#- tera lacks zero padding and also log operator -#}
{%- if page.extra.episode -%}
{%- set len = page.extra.episode | as_str | length -%}
{{ config.extra.acronym }}{% for i in range(end=config.extra.pad_to - len) %}0{% endfor %}{{ page.extra.episode }} {% endif -%}
{{ page.title }}
{%- endmacro poster %}

33
templates/page.html Normal file
View File

@ -0,0 +1,33 @@
{% extends "base.html" %}
{% block metadata -%}
{%- if page.extra.episode -%}
{{ super() }}
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="{{ page.title }}">
<meta name="twitter:description" content="{{ page.description }}">
<meta name="twitter:image" content="{{ macros::poster(page=page) }}">
<meta property="og:type" content="website">
<meta property="og:site_name" content="{{ config.title }}">
<meta property="og:title" content="{{ page.title }}">
<meta property="og:url" content="{{ page.permalink }}">
<meta property="og:description" content="{{ page.description }}">
<meta property="og:image" content="{{ macros::poster(page=page) }}">
{%- for format in config.extra.formats %}
<meta property="og:audio" content="{{ get_url(path="episodes/" ~ page.slug ~ "." ~ format.ext) | safe }}">
<meta property="og:audio:type" content="{{ format.mime_type }}">
{%- endfor %}
{%- endif %}
{%- endblock metadata %}
{% block content -%}
{% include "post.html" %}
{%- endblock content %}
{% block scripts -%}
{{ super() }}
{%- if page.extra.episode %}
{% include "podlove-player-script.html" %}
{%- endif %}
{%- endblock scripts %}

View File

@ -0,0 +1,6 @@
<script src="{{ get_url(path="vendor/podlove-web-player/embed.js") | safe }}"></script>
<script>
document.querySelectorAll('.podlove-player').forEach(el => {
podlovePlayer( `#${el.id}`, JSON.parse(document.getElementById(`config-episode-${el.dataset.id}`).innerText))
})
</script>

View File

@ -0,0 +1,80 @@
{%- if page.extra.episode %}
{%- set episodes = load_data(path="static/episodes.json") -%}
<div class="podlove-player" id="podlove-player-{{ page.slug }}" data-id="{{ page.slug }}">
<audio controls preload="none">
{%- for format in config.extra.formats %}
<source src="{{ get_url(path="episodes/" ~ page.slug ~ "." ~ format.ext) | safe }}" type="{{ format.mime_type | safe }}">
{%- endfor %}
</audio>
<h4>Download</h4>
<ul>
{%- for format in config.extra.formats %}
<li><a href="{{ get_url(path="episodes/" ~ page.slug ~ "." ~ format.ext) | safe }}">{{ format.name }} ({{ episodes[page.slug].formats[format.ext].size | filesizeformat }})</a></li>
{%- endfor %}
</ul>
<h4>Kapitel</h4>
<p>
{%- for chapter in episodes[page.slug].chapters %}
<strong>{{ chapter.start }}:</strong> {{ chapter.title }}<br/>
{%- endfor %}
</p>
<h4>Mitwirkende</h4>
<ul>
{%- for contributor in page.extra.contributors %}
<li>{{ contributor | json_encode | safe }}</li>
{%- endfor %}
</ul>
</div>
<script id="config-episode-{{ page.slug }}" type="application/json">
{
"title": {{ macros::title(page=page) | json_encode | safe }},
"subtitle": {{ page.extra.subtitle | default(value="") | json_encode | safe }},
"summary": {{ page.description | json_encode | safe }},
"publicationDate": {{ page.date | date(format="%+") | json_encode | safe }},
"poster": {{ macros::poster(page=page) | json_encode | safe }},
"duration": {{ episodes[page.slug].duration | json_encode | safe }},
"link": {{ page.permalink | json_encode | safe }},
"show": {
"title": {{ config.title | json_encode | safe }},
"subtitle": {{ config.extra.subtitle | json_encode | safe }},
"summary": {{ config.description | json_encode | safe }},
"poster": {{ get_url(path="assets/poster.jpg") | json_encode | safe }},
"link": {{ config.base_url | json_encode | safe }}
},
"audio": [
{%- for format in config.extra.formats %}
{
"url": {{ get_url(path="episodes/" ~ page.slug ~ "." ~ format.ext) | json_encode | safe }},
"mimeType": {{ format.mime_type | json_encode | safe }},
"title": {{ format.name | json_encode | safe }},
"size": {{ episodes[page.slug].formats[format.ext].size }}
}{% if not loop.last %},{% endif %}
{%- endfor %}
],
"chapters": {{ episodes[page.slug].chapters | json_encode | safe }},
"contributors": [
{%- for contributor in page.extra.contributors %}
{
"name": {{ contributor | json_encode | safe }},
"avatar": {{ get_url(path="assets/avatar.svg") | json_encode | safe }}
}{% if not loop.last %},{% endif %}
{%- endfor %}
],
"reference": {
"base": {{ get_url(path="vendor/podlove-web-player/", trailing_slash=true) | json_encode | safe }},
"config": {{ get_url(path="episodes/" ~ page.slug ~ ".podlove.json") | json_encode | safe }},
"share": {{ get_url(path="vendor/podlove-web-player/share.html") | json_encode | safe }}
},
"theme": {
"main": {{ config.extra.theme.main | default(value="#2B8AC6") | json_encode | safe }},
"highlight": {{ config.extra.theme.highlight | default(value="") | json_encode | safe }}
}
}
</script>
{%- endif %}

14
templates/post.html Normal file
View File

@ -0,0 +1,14 @@
<article>
<h2 class="title mb-2"><a href="{{ page.permalink | safe }}">{{ macros::title(page=page) }}</a></h2>
<div class="post-meta"><span>{% if page.date %}Veröffentlicht am {{ page.date | date(format="%d.%m.%Y") }}{% endif %}</span></div>
<div class="content mt-5">
{% if page.extra.subtitle %}<p><strong>{{ page.extra.subtitle }}</strong></p>{% endif %}
{% if page.description %}<p>{{ page.description | safe }}</p>{% endif %}
{% include "podlove-player.html" %}
{{ page.content | safe }}
</div>
</article>

3
templates/robots.txt Normal file
View File

@ -0,0 +1,3 @@
User-agent: *
Sitemap: {{ get_url(path="sitemap.xml") }}
Disallow: /formats/*

56
templates/rss.xml Normal file
View File

@ -0,0 +1,56 @@
{%- import "macros.html" as macros -%}
{%- set format = config.extra.formats | filter(attribute="ext", value=term.name | default(value="opus")) | first -%}
{%- set episodes_meta = load_data(path="static/episodes.json") -%}
<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:psc="http://podlove.org/simple-chapters" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">
<channel>
<title>{{ config.title }}</title>
<link>{{ config.base_url | escape_xml | safe }}</link>
<description>{{ config.description }}</description>
<language>{{ config.default_language }}</language>
<lastBuildDate>{{ last_updated | date(format="%a, %d %b %Y %H:%M:%S %z") }}</lastBuildDate>
{%- if taxonomy -%}
{% for format in config.extra.formats %}
<atom:link rel="{% if format.ext == term.name %}self{% else %}alternate{% endif %}" type="application/rss+xml" title="{{ config.title }} ({{ format.name }})" href="{{ get_url(path="formats/" ~ format.ext ~ "/rss.xml") | escape_xml | safe }}"/>
{%- endfor %}
{%- endif %}
<image>
<url>{{ get_url(path="assets/poster.jpg") | escape_xml | safe }}</url>
<title>{{ config.title }}</title>
<link>{{ config.base_url | escape_xml | safe }}</link>
</image>
<itunes:author>{{ config.extra.author }}</itunes:author>
<itunes:summary>{{ config.description }}</itunes:summary>
<itunes:category text="{{ config.extra.itunes.category }}"/>
<itunes:image href="{{ get_url(path="assets/poster.jpg") | safe }}"/>
<itunes:subtitle>{{ config.extra.subtitle }}</itunes:subtitle>
{%- for page in pages %}
{%- set episode_meta = episodes_meta[page.slug] -%}
<item>
<title>{{ macros::title(page=page) }}</title>
<link>{{ page.permalink | escape_xml | safe }}</link>
<guid>{{ page.permalink | escape_xml | safe }}</guid>
<pubDate>{{ page.date | date(format="%a, %d %b %Y %H:%M:%S %z") }}</pubDate>
<description><![CDATA[{{ page.description }}]]></description>
<atom:link rel="http://podlove.org/deep-link" href="{{ page.permalink | escape_xml | safe }}#"/>
<enclosure url="{{ get_url(path="episodes/" ~ page.slug ~ "." ~ format.ext) | escape_xml | safe }}" length="{{ episode_meta.formats[format.ext].size }}" type="{{ format.mime_type | escape_xml | safe }}"/>
<itunes:duration>{{ episode_meta.duration }}</itunes:duration>
<itunes:author>{{ config.extra.author }}</itunes:author>
<itunes:subtitle>{{ page.extra.subtitle }}</itunes:subtitle>
<itunes:episode>{{ page.extra.episode }}</itunes:episode>
<itunes:summary>{{ page.description }}</itunes:summary>
<content:encoded><![CDATA[<p><strong>{{ page.extra.subtitle }}</strong></p> <p>{{ page.description}}</p> {{ page.content | safe }}]]></content:encoded>
<psc:chapters xmlns:psc="http://podlove.org/simple-chapters" version="1.2">
{%- for chapter in episode_meta.chapters %}
<psc:chapter start="{{ chapter.start }}" title="{{ chapter.title }}"/>
{%- endfor %}
</psc:chapters>
{%- for contributor in page.extra.contributors %}
<atom:contributor>
<atom:name>{{ contributor }}</atom:name>
</atom:contributor>
{%- endfor %}
</item>
{%- endfor %}
</channel>
</rss>

13
templates/sitemap.xml Normal file
View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
{%- for sitemap_entry in entries %}
{%- if sitemap_entry.permalink is not starting_with(get_url(path="formats")) %}
<url>
<loc>{{ sitemap_entry.permalink | escape_xml | safe }}</loc>
{%- if sitemap_entry.updated %}
<lastmod>{{ sitemap_entry.updated }}</lastmod>
{%- endif %}
</url>
{%- endif %}
{%- endfor %}
</urlset>

31
vendor.sh Executable file
View File

@ -0,0 +1,31 @@
#!/bin/sh
set -e
mkdir -p vendor
cd vendor
rm -rf bulma
wget -nv -O bulma.zip "https://github.com/jgthms/bulma/releases/download/0.9.0/bulma-0.9.0.zip"
unzip -q bulma.zip
rm bulma.zip
mv bulma-* bulma
wget -nv \
"https://sbruder-cdn.s3.eu-central-1.wasabisys.com/ffmpeg-fdk/ffmpeg" \
"https://sbruder-cdn.s3.eu-central-1.wasabisys.com/ffmpeg-fdk/ffprobe"
sha512sum -c << EOF
29cc5a88d508781fc34ccafa8292c8505395c8a3a2231f71a49f5553176994f2118217f248ba03593464ee6e63c72e8bb1c630779fdac4a3e97925a4aca27836 ffmpeg
bbe17d8b9489c0b5155c57fdc4fe4d7635d57ae98da9767229070210ab41099c4f2af50b42ad5a3283a75605ac9972c93bf8b6cdb00cf2333cc2adfd943ce5f0 ffprobe
EOF
chmod +x ffmpeg ffprobe
mkdir -p ../static/vendor/
cd ../static/vendor/
rm -rf podlove-web-player
mkdir podlove-web-player
wget -nv -O- "https://registry.npmjs.org/@podlove/web-player/-/web-player-4.5.13.tgz" | tar xzf - --strip-components=1 -C podlove-web-player
rm -rf podlove-subscribe-button
wget -nv -O podlove-subscribe-button.zip "https://github.com/podlove/podlove-subscribe-button/releases/download/v17/podlove-subscribe-button-v17.zip"
unzip -q -d podlove-subscribe-button podlove-subscribe-button.zip
rm podlove-subscribe-button.zip