Initial commit
This commit is contained in:
commit
410d1173e1
74
.drone.yml
Normal file
74
.drone.yml
Normal 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
3
.gitattributes
vendored
Normal 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
6
.gitignore
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
/__pycache__/
|
||||||
|
/public/
|
||||||
|
/static/episodes.json
|
||||||
|
/static/episodes/*
|
||||||
|
/static/vendor/
|
||||||
|
/vendor/
|
3
.netlify/state.json
Normal file
3
.netlify/state.json
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"siteId": "b4207c7e-720e-4ea7-9a99-00b013da6370"
|
||||||
|
}
|
13
Pipfile
Normal file
13
Pipfile
Normal 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
29
Pipfile.lock
generated
Normal 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
3
README.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# schulpodcast (prototype)
|
||||||
|
|
||||||
|
Prototype for the site of the podcast for the practical seminar podcast.
|
8
build.sh
Executable file
8
build.sh
Executable 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
63
common.py
Normal 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
62
config.toml
Normal 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
4
content/_index.md
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
+++
|
||||||
|
paginate_by = 5
|
||||||
|
sort_by = "date"
|
||||||
|
+++
|
BIN
content/imprint-email.png
(Stored with Git LFS)
Normal file
BIN
content/imprint-email.png
(Stored with Git LFS)
Normal file
Binary file not shown.
35
content/imprint.md
Normal file
35
content/imprint.md
Normal 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
22
content/subscribe.md
Normal 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
BIN
content/tpc001-aller-anfang-ist-schwer.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
22
content/tpc001-aller-anfang-ist-schwer.md
Normal file
22
content/tpc001-aller-anfang-ist-schwer.md
Normal 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
BIN
content/tpc001-aller-anfang-ist-schwer.opus
(Stored with Git LFS)
Normal file
Binary file not shown.
8
content/tpc001-aller-anfang-ist-schwer.txt
Normal file
8
content/tpc001-aller-anfang-ist-schwer.txt
Normal 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
|
21
content/tpc002-noch-mehr-boilerplate.md
Normal file
21
content/tpc002-noch-mehr-boilerplate.md
Normal 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
BIN
content/tpc002-noch-mehr-boilerplate.opus
(Stored with Git LFS)
Normal file
Binary file not shown.
11
content/tpc002-noch-mehr-boilerplate.txt
Normal file
11
content/tpc002-noch-mehr-boilerplate.txt
Normal 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
167
encode.py
Executable 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
77
metadata.py
Executable 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
84
sass/style.scss
Normal 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
17
static/assets/avatar.svg
Normal 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
BIN
static/assets/banner.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
9
static/assets/main.js
Normal file
9
static/assets/main.js
Normal 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
BIN
static/assets/poster.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
102
templates/base.html
Normal file
102
templates/base.html
Normal 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>
|
1
templates/formats/list.html
Normal file
1
templates/formats/list.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<meta http-equiv="refresh" content="0; url={{ get_url(path="/") | safe }}">
|
1
templates/formats/single.html
Normal file
1
templates/formats/single.html
Normal 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
39
templates/index.html
Normal 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">…</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 %}
|
25
templates/macros.html
Normal file
25
templates/macros.html
Normal 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
33
templates/page.html
Normal 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 %}
|
6
templates/podlove-player-script.html
Normal file
6
templates/podlove-player-script.html
Normal 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>
|
80
templates/podlove-player.html
Normal file
80
templates/podlove-player.html
Normal 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
14
templates/post.html
Normal 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
3
templates/robots.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
User-agent: *
|
||||||
|
Sitemap: {{ get_url(path="sitemap.xml") }}
|
||||||
|
Disallow: /formats/*
|
56
templates/rss.xml
Normal file
56
templates/rss.xml
Normal 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
13
templates/sitemap.xml
Normal 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
31
vendor.sh
Executable 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
|
Loading…
Reference in a new issue