site/encode.py

125 lines
3.9 KiB
Python
Raw Normal View History

2020-08-11 16:14:38 +02:00
#!/usr/bin/env python3
import base64
import concurrent.futures
import multiprocessing
2020-08-11 16:14:38 +02:00
import os
import xml.etree.ElementTree as ET
2021-11-30 19:57:59 +01:00
from datetime import datetime
from subprocess import run
from urllib.parse import urlparse
from mutagen.flac import Picture
from mutagen.oggopus import OggOpus
from mutagen.oggvorbis import OggVorbis
2020-08-11 16:14:38 +02:00
import common
def encode_episode(podcast, episode, format):
format, options = format
infile = common.path_to_episode(episode["file_base"], "flac")
2020-08-11 16:14:38 +02:00
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, podcast["poster"]]
2020-08-11 16:14:38 +02:00
)
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"],
}
command = ["ffmpeg", "-y", "-loglevel", "error"]
2020-08-11 16:14:38 +02:00
command.extend(["-i", infile])
2021-11-30 19:57:59 +01:00
if format not in ["oga", "opus"]:
command.extend(["-i", podcast["poster"]])
# For AAC, the default codec choice (ffmpeg native) is not the best choice
if format == "m4a":
command.extend(["-c:a", "libfdk_aac"])
2020-08-11 16:14:38 +02:00
command.extend(["-c:v", "copy"])
command.extend(["-bitexact"]) # deterministic output
2020-08-11 16:14:38 +02:00
command.extend(options)
for k, v in tags.items():
command.extend(["-metadata", f"{k}={v}"])
command.append(outfile)
run(command, check=True)
2020-08-11 16:14:38 +02:00
if format in ["oga", "opus"]:
if format == "oga":
audio = OggVorbis(outfile)
else:
audio = OggOpus(outfile)
# poster
picture = Picture()
with open(podcast["poster"], "rb") as f:
2020-08-11 16:14:38 +02:00
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")
]
audio.save()
print(f"[✔️] {episode['file_base']}.{format}")
2020-08-11 16:14:38 +02:00
else:
print(f"[⏭️] {episode['file_base']}.{format}")
2020-08-11 16:14:38 +02:00
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,
}
pool = concurrent.futures.ThreadPoolExecutor(max_workers=multiprocessing.cpu_count())
2020-08-11 16:14:38 +02:00
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],
}
for format in common.FORMATS.items():
pool.submit(encode_episode, podcast, episode, format)
2020-08-11 16:14:38 +02:00
pool.shutdown(wait=True)