2020-08-11 16:14:38 +02:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
import base64
|
2021-12-01 15:48:29 +01:00
|
|
|
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
|
|
|
|
|
2021-12-17 15:30:25 +01:00
|
|
|
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)
|
2021-11-30 21:31:44 +01:00
|
|
|
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"],
|
|
|
|
}
|
|
|
|
|
2021-11-30 19:17:20 +01:00
|
|
|
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"]:
|
2021-11-30 21:31:44 +01:00
|
|
|
command.extend(["-i", podcast["poster"]])
|
2021-11-30 20:16:26 +01:00
|
|
|
# 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"])
|
2022-01-28 14:50:56 +01:00
|
|
|
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)
|
2021-11-30 19:39:46 +01:00
|
|
|
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()
|
2021-11-30 21:31:44 +01:00
|
|
|
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()
|
|
|
|
|
2021-12-01 15:48:29 +01:00
|
|
|
print(f"[✔️] {episode['file_base']}.{format}")
|
2020-08-11 16:14:38 +02:00
|
|
|
else:
|
2021-12-01 15:48:29 +01:00
|
|
|
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,
|
|
|
|
}
|
|
|
|
|
2021-12-01 15:48:29 +01:00
|
|
|
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():
|
2021-12-01 15:48:29 +01:00
|
|
|
pool.submit(encode_episode, podcast, episode, format)
|
2020-08-11 16:14:38 +02:00
|
|
|
|
2021-12-01 15:48:29 +01:00
|
|
|
pool.shutdown(wait=True)
|