commit
410d1173e1
@ -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 |
@ -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 |
@ -0,0 +1,6 @@ |
||||
/__pycache__/ |
||||
/public/ |
||||
/static/episodes.json |
||||
/static/episodes/* |
||||
/static/vendor/ |
||||
/vendor/ |
@ -0,0 +1,3 @@ |
||||
{ |
||||
"siteId": "b4207c7e-720e-4ea7-9a99-00b013da6370" |
||||
} |
@ -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" |
@ -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": {} |
||||
} |
@ -0,0 +1,3 @@ |
||||
# schulpodcast (prototype) |
||||
|
||||
Prototype for the site of the podcast for the practical seminar podcast. |
@ -0,0 +1,8 @@ |
||||
#!/bin/sh |
||||
set -e |
||||
./metadata.py |
||||
zola build |
||||
echo "Encoding…" |
||||
pipenv run ./encode.py |
||||
./metadata.py |
||||
zola build |
@ -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:] |
@ -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" |
@ -0,0 +1,4 @@ |
||||
+++ |
||||
paginate_by = 5 |
||||
sort_by = "date" |
||||
+++ |
Binary file not shown.
@ -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 |
@ -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) |
Binary file not shown.
@ -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) |
Binary file not shown.
@ -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 |
@ -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) |
Binary file not shown.
@ -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 |
@ -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() |
@ -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)) |
@ -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; |
||||
} |
||||
} |
After Width: | Height: | Size: 870 B |
Binary file not shown.
@ -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') |
||||
}) |
||||
}) |
||||
}) |
Binary file not shown.
@ -0,0 +1 @@ |
||||
<meta http-equiv="refresh" content="0; url={{ get_url(path="/") | safe }}"> |
@ -0,0 +1 @@ |
||||
<meta http-equiv="refresh" content="0; url={{ get_url(path="/formats/" ~ term.name ~ "/rss.xml" ) | safe }}"> |
@ -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">…</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">…</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 %} |
@ -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 %} |
@ -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 %} |
@ -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> |
@ -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 %} |
@ -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> |
@ -0,0 +1,3 @@ |
||||
User-agent: * |
||||
Sitemap: {{ get_url(path="sitemap.xml") }} |
||||
Disallow: /formats/* |
@ -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> |
@ -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> |
@ -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 |
Loading…
Reference in new issue