{{ macros::title(page=page) }}
+ + +{{ page.extra.subtitle }}
{% endif %} + + {% if page.description %}{{ page.description | safe }}
{% endif %} + + {% include "podlove-player.html" %} + + {{ page.content | safe }} +From 410d1173e1679db8b14713f959405a9f261db266 Mon Sep 17 00:00:00 2001
From: Simon Bruder
+ {%- for chapter in episodes[page.slug].chapters %}
+ {{ chapter.start }}: {{ chapter.title }} {{ page.extra.subtitle }} {{ page.description | safe }}Podlove Subscribe Button
+
+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)
diff --git a/content/tpc001-aller-anfang-ist-schwer.jpg b/content/tpc001-aller-anfang-ist-schwer.jpg
new file mode 100644
index 0000000..796e4b6
--- /dev/null
+++ b/content/tpc001-aller-anfang-ist-schwer.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e1c6bfddd276b52a18c1950dbc904d9d8aabb88864151ceb0a1b54489281035d
+size 28024
diff --git a/content/tpc001-aller-anfang-ist-schwer.md b/content/tpc001-aller-anfang-ist-schwer.md
new file mode 100644
index 0000000..53c22ff
--- /dev/null
+++ b/content/tpc001-aller-anfang-ist-schwer.md
@@ -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)
diff --git a/content/tpc001-aller-anfang-ist-schwer.opus b/content/tpc001-aller-anfang-ist-schwer.opus
new file mode 100644
index 0000000..d1368e0
--- /dev/null
+++ b/content/tpc001-aller-anfang-ist-schwer.opus
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ce2c169dca6a2f05f86e2fc985c936995bd512fadd5cc311fe8e3e98b9cc5ec4
+size 28838954
diff --git a/content/tpc001-aller-anfang-ist-schwer.txt b/content/tpc001-aller-anfang-ist-schwer.txt
new file mode 100644
index 0000000..82e6223
--- /dev/null
+++ b/content/tpc001-aller-anfang-ist-schwer.txt
@@ -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
diff --git a/content/tpc002-noch-mehr-boilerplate.md b/content/tpc002-noch-mehr-boilerplate.md
new file mode 100644
index 0000000..2029a46
--- /dev/null
+++ b/content/tpc002-noch-mehr-boilerplate.md
@@ -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)
diff --git a/content/tpc002-noch-mehr-boilerplate.opus b/content/tpc002-noch-mehr-boilerplate.opus
new file mode 100644
index 0000000..e01336e
--- /dev/null
+++ b/content/tpc002-noch-mehr-boilerplate.opus
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:441d488044d98882756c340c010236427b2ece8c478379752026308feb106b8f
+size 13379005
diff --git a/content/tpc002-noch-mehr-boilerplate.txt b/content/tpc002-noch-mehr-boilerplate.txt
new file mode 100644
index 0000000..dfb76a7
--- /dev/null
+++ b/content/tpc002-noch-mehr-boilerplate.txt
@@ -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
diff --git a/encode.py b/encode.py
new file mode 100755
index 0000000..065a807
--- /dev/null
+++ b/encode.py
@@ -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()
diff --git a/metadata.py b/metadata.py
new file mode 100755
index 0000000..47a6fe8
--- /dev/null
+++ b/metadata.py
@@ -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))
diff --git a/sass/style.scss b/sass/style.scss
new file mode 100644
index 0000000..2a658d4
--- /dev/null
+++ b/sass/style.scss
@@ -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;
+ }
+}
diff --git a/static/assets/avatar.svg b/static/assets/avatar.svg
new file mode 100644
index 0000000..51a2a83
--- /dev/null
+++ b/static/assets/avatar.svg
@@ -0,0 +1,17 @@
+
+
diff --git a/static/assets/banner.jpg b/static/assets/banner.jpg
new file mode 100644
index 0000000..a40295c
--- /dev/null
+++ b/static/assets/banner.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f97c04468e45ddaaa0801c6f6823e395af3a5f13bf16160d97202dd4a6a08a74
+size 58736
diff --git a/static/assets/main.js b/static/assets/main.js
new file mode 100644
index 0000000..c0c8cd7
--- /dev/null
+++ b/static/assets/main.js
@@ -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')
+ })
+ })
+})
diff --git a/static/assets/poster.jpg b/static/assets/poster.jpg
new file mode 100644
index 0000000..dbc42ff
--- /dev/null
+++ b/static/assets/poster.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bb5729151ac9ec084e287b4ad7f87136602a50be491f60c75411151774acc26c
+size 229262
diff --git a/templates/base.html b/templates/base.html
new file mode 100644
index 0000000..cc27c0d
--- /dev/null
+++ b/templates/base.html
@@ -0,0 +1,102 @@
+{%- import "macros.html" as macros -%}
+
+
+
+
+
+
+
+{%- endfor %}
+
+{%- if paginator.number_pagers > 1 %}
+
+{%- endif %}
+{%- endblock content %}
+{% block scripts -%}
+{{ super() }}
+{% include "podlove-player-script.html" %}
+{%- endblock scripts %}
diff --git a/templates/macros.html b/templates/macros.html
new file mode 100644
index 0000000..a24c972
--- /dev/null
+++ b/templates/macros.html
@@ -0,0 +1,25 @@
+{% macro podlove_subscribe_button(size="medium") %}
+
+
+{% 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 %}
diff --git a/templates/page.html b/templates/page.html
new file mode 100644
index 0000000..b942c62
--- /dev/null
+++ b/templates/page.html
@@ -0,0 +1,33 @@
+{% extends "base.html" %}
+
+{% block metadata -%}
+{%- if page.extra.episode -%}
+{{ super() }}
+
+
+
+
+
+
+
+
+
+
+
+{%- for format in config.extra.formats %}
+
+
+{%- 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 %}
diff --git a/templates/podlove-player-script.html b/templates/podlove-player-script.html
new file mode 100644
index 0000000..64f9209
--- /dev/null
+++ b/templates/podlove-player-script.html
@@ -0,0 +1,6 @@
+
+
diff --git a/templates/podlove-player.html b/templates/podlove-player.html
new file mode 100644
index 0000000..ede09db
--- /dev/null
+++ b/templates/podlove-player.html
@@ -0,0 +1,80 @@
+{%- if page.extra.episode %}
+{%- set episodes = load_data(path="static/episodes.json") -%}
+Download
+
+ {%- for format in config.extra.formats %}
+
+ Kapitel
+
+ {%- endfor %}
+ Mitwirkende
+
+ {%- for contributor in page.extra.contributors %}
+
+{{ macros::title(page=page) }}
+
+
+
{{ page.description}}
{{ page.content | safe }}]]> +