From 410d1173e1679db8b14713f959405a9f261db266 Mon Sep 17 00:00:00 2001 From: Simon Bruder Date: Tue, 11 Aug 2020 16:14:38 +0200 Subject: [PATCH] Initial commit --- .drone.yml | 74 +++++++++ .gitattributes | 3 + .gitignore | 6 + .netlify/state.json | 3 + Pipfile | 13 ++ Pipfile.lock | 29 ++++ README.md | 3 + build.sh | 8 + common.py | 63 ++++++++ config.toml | 62 ++++++++ content/_index.md | 4 + content/imprint-email.png | 3 + content/imprint.md | 35 ++++ content/subscribe.md | 22 +++ content/tpc001-aller-anfang-ist-schwer.jpg | 3 + content/tpc001-aller-anfang-ist-schwer.md | 22 +++ content/tpc001-aller-anfang-ist-schwer.opus | 3 + content/tpc001-aller-anfang-ist-schwer.txt | 8 + content/tpc002-noch-mehr-boilerplate.md | 21 +++ content/tpc002-noch-mehr-boilerplate.opus | 3 + content/tpc002-noch-mehr-boilerplate.txt | 11 ++ encode.py | 167 ++++++++++++++++++++ metadata.py | 77 +++++++++ sass/style.scss | 84 ++++++++++ static/assets/avatar.svg | 17 ++ static/assets/banner.jpg | 3 + static/assets/main.js | 9 ++ static/assets/poster.jpg | 3 + templates/base.html | 102 ++++++++++++ templates/formats/list.html | 1 + templates/formats/single.html | 1 + templates/index.html | 39 +++++ templates/macros.html | 25 +++ templates/page.html | 33 ++++ templates/podlove-player-script.html | 6 + templates/podlove-player.html | 80 ++++++++++ templates/post.html | 14 ++ templates/robots.txt | 3 + templates/rss.xml | 56 +++++++ templates/sitemap.xml | 13 ++ vendor.sh | 31 ++++ 41 files changed, 1163 insertions(+) create mode 100644 .drone.yml create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .netlify/state.json create mode 100644 Pipfile create mode 100644 Pipfile.lock create mode 100644 README.md create mode 100755 build.sh create mode 100644 common.py create mode 100644 config.toml create mode 100644 content/_index.md create mode 100644 content/imprint-email.png create mode 100644 content/imprint.md create mode 100644 content/subscribe.md create mode 100644 content/tpc001-aller-anfang-ist-schwer.jpg create mode 100644 content/tpc001-aller-anfang-ist-schwer.md create mode 100644 content/tpc001-aller-anfang-ist-schwer.opus create mode 100644 content/tpc001-aller-anfang-ist-schwer.txt create mode 100644 content/tpc002-noch-mehr-boilerplate.md create mode 100644 content/tpc002-noch-mehr-boilerplate.opus create mode 100644 content/tpc002-noch-mehr-boilerplate.txt create mode 100755 encode.py create mode 100755 metadata.py create mode 100644 sass/style.scss create mode 100644 static/assets/avatar.svg create mode 100644 static/assets/banner.jpg create mode 100644 static/assets/main.js create mode 100644 static/assets/poster.jpg create mode 100644 templates/base.html create mode 100644 templates/formats/list.html create mode 100644 templates/formats/single.html create mode 100644 templates/index.html create mode 100644 templates/macros.html create mode 100644 templates/page.html create mode 100644 templates/podlove-player-script.html create mode 100644 templates/podlove-player.html create mode 100644 templates/post.html create mode 100644 templates/robots.txt create mode 100644 templates/rss.xml create mode 100644 templates/sitemap.xml create mode 100755 vendor.sh diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..f02895c --- /dev/null +++ b/.drone.yml @@ -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 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..a25f6f1 --- /dev/null +++ b/.gitattributes @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b3c0b4f --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/__pycache__/ +/public/ +/static/episodes.json +/static/episodes/* +/static/vendor/ +/vendor/ diff --git a/.netlify/state.json b/.netlify/state.json new file mode 100644 index 0000000..1f47c70 --- /dev/null +++ b/.netlify/state.json @@ -0,0 +1,3 @@ +{ + "siteId": "b4207c7e-720e-4ea7-9a99-00b013da6370" +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..d78e65a --- /dev/null +++ b/Pipfile @@ -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" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..74f9971 --- /dev/null +++ b/Pipfile.lock @@ -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": {} +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..40a6bae --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# schulpodcast (prototype) + +Prototype for the site of the podcast for the practical seminar podcast. diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..abd6216 --- /dev/null +++ b/build.sh @@ -0,0 +1,8 @@ +#!/bin/sh +set -e +./metadata.py +zola build +echo "Encoding…" +pipenv run ./encode.py +./metadata.py +zola build diff --git a/common.py b/common.py new file mode 100644 index 0000000..53e8b52 --- /dev/null +++ b/common.py @@ -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:] diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..638953e --- /dev/null +++ b/config.toml @@ -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" diff --git a/content/_index.md b/content/_index.md new file mode 100644 index 0000000..cbca776 --- /dev/null +++ b/content/_index.md @@ -0,0 +1,4 @@ ++++ +paginate_by = 5 +sort_by = "date" ++++ diff --git a/content/imprint-email.png b/content/imprint-email.png new file mode 100644 index 0000000..08af08b --- /dev/null +++ b/content/imprint-email.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e677f7bb1438abaa9875942442da4ca8e37671b7f58e23f8bfee2bf7838772e3 +size 2024 diff --git a/content/imprint.md b/content/imprint.md new file mode 100644 index 0000000..da0c0ad --- /dev/null +++ b/content/imprint.md @@ -0,0 +1,35 @@ ++++ +title = "Impressum und Datenschutz" ++++ + +## Impressum + +### Angaben gemäß § 5 TMG + +Simon Bruder +Wallmersbach 42 +97215 Uffenheim + +#### Kontakt + +Telefon: +4915256561414 +E-Mail: (Aus Spamschutzgründen nicht angezeigt) + +#### Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV + +Simon Bruder +Wallmersbach 42 +97215 Uffenheim + +Quelle: + +## 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 diff --git a/content/subscribe.md b/content/subscribe.md new file mode 100644 index 0000000..338dccd --- /dev/null +++ b/content/subscribe.md @@ -0,0 +1,22 @@ ++++ +title = "Abonnieren" ++++ + +FIXME: was das die leute auch verstehen + + +

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 @@ + + + + + + image/svg+xml + + + + + + + + + + + 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 -%} + + + + + + + {% block title %}{% if page.title %}{{ macros::title(page=page) }} – {% endif %}{{ config.title }}{% endblock title %} + {%- for format in config.extra.formats %} + + {%- endfor %} + {%- block styles %} + + {%- endblock styles %} + {%- block preload %} + + + + + + + + + {%- endblock prefetch %} + {%- block metadata %} + + {%- endblock metadata %} + + +
+ {%- block banner %} + + {%- endblock %} + {%- block navbar %} + + {%- endblock navbar %} +
+
+
+ {% block content %}{% endblock content %} +
+ +
+
+ +
+ {% block scripts -%} + + + + {%- endblock scripts %} + + diff --git a/templates/formats/list.html b/templates/formats/list.html new file mode 100644 index 0000000..698055f --- /dev/null +++ b/templates/formats/list.html @@ -0,0 +1 @@ + diff --git a/templates/formats/single.html b/templates/formats/single.html new file mode 100644 index 0000000..6cce629 --- /dev/null +++ b/templates/formats/single.html @@ -0,0 +1 @@ + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..548e5f4 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,39 @@ +{% extends "base.html" %} +{% block content -%} +{%- for page in paginator.pages %} +{% include "post.html" %} +
+{%- 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

+ +

Kapitel

+

+ {%- for chapter in episodes[page.slug].chapters %} + {{ chapter.start }}: {{ chapter.title }}
+ {%- endfor %} +

+

Mitwirkende

+
    + {%- for contributor in page.extra.contributors %} +
  • {{ contributor | json_encode | safe }}
  • + {%- endfor %} +
+
+ +{%- endif %} diff --git a/templates/post.html b/templates/post.html new file mode 100644 index 0000000..0788676 --- /dev/null +++ b/templates/post.html @@ -0,0 +1,14 @@ +
+

{{ macros::title(page=page) }}

+ + +
+ {% if page.extra.subtitle %}

{{ page.extra.subtitle }}

{% endif %} + + {% if page.description %}

{{ page.description | safe }}

{% endif %} + + {% include "podlove-player.html" %} + + {{ page.content | safe }} +
+
diff --git a/templates/robots.txt b/templates/robots.txt new file mode 100644 index 0000000..479cfbc --- /dev/null +++ b/templates/robots.txt @@ -0,0 +1,3 @@ +User-agent: * +Sitemap: {{ get_url(path="sitemap.xml") }} +Disallow: /formats/* diff --git a/templates/rss.xml b/templates/rss.xml new file mode 100644 index 0000000..1d87793 --- /dev/null +++ b/templates/rss.xml @@ -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") -%} + + + + {{ config.title }} + {{ config.base_url | escape_xml | safe }} + {{ config.description }} + {{ config.default_language }} + {{ last_updated | date(format="%a, %d %b %Y %H:%M:%S %z") }} + {%- if taxonomy -%} + {% for format in config.extra.formats %} + + {%- endfor %} + {%- endif %} + + {{ get_url(path="assets/poster.jpg") | escape_xml | safe }} + {{ config.title }} + {{ config.base_url | escape_xml | safe }} + + {{ config.extra.author }} + {{ config.description }} + + + {{ config.extra.subtitle }} + {%- for page in pages %} + {%- set episode_meta = episodes_meta[page.slug] -%} + + {{ macros::title(page=page) }} + {{ page.permalink | escape_xml | safe }} + {{ page.permalink | escape_xml | safe }} + {{ page.date | date(format="%a, %d %b %Y %H:%M:%S %z") }} + + + + {{ episode_meta.duration }} + {{ config.extra.author }} + {{ page.extra.subtitle }} + {{ page.extra.episode }} + {{ page.description }} + {{ page.extra.subtitle }}

{{ page.description}}

{{ page.content | safe }}]]>
+ + {%- for chapter in episode_meta.chapters %} + + {%- endfor %} + + {%- for contributor in page.extra.contributors %} + + {{ contributor }} + + {%- endfor %} +
+ {%- endfor %} +
+
diff --git a/templates/sitemap.xml b/templates/sitemap.xml new file mode 100644 index 0000000..713d1d6 --- /dev/null +++ b/templates/sitemap.xml @@ -0,0 +1,13 @@ + + + {%- for sitemap_entry in entries %} + {%- if sitemap_entry.permalink is not starting_with(get_url(path="formats")) %} + + {{ sitemap_entry.permalink | escape_xml | safe }} + {%- if sitemap_entry.updated %} + {{ sitemap_entry.updated }} + {%- endif %} + + {%- endif %} + {%- endfor %} + diff --git a/vendor.sh b/vendor.sh new file mode 100755 index 0000000..841e142 --- /dev/null +++ b/vendor.sh @@ -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