diff options
| author | Matthew Fennell <matthew@fennell.dev> | 2026-08-28 17:06:25 +0100 |
|---|---|---|
| committer | Matthew Fennell <matthew@fennell.dev> | 2026-08-29 01:06:29 +0100 |
| commit | 7dd5c2186e2706a75ba719db88f5f1493184e7d6 (patch) | |
| tree | b6dbd5da8043b0f9c28d70b65bc3c89eaa9d8cd7 /podcast.py | |
| parent | 4982ef0e700193cbe9a30fd032ed2e385a701159 (diff) | |
Add podcast handlermain
Diffstat (limited to 'podcast.py')
| -rw-r--r-- | podcast.py | 118 |
1 files changed, 118 insertions, 0 deletions
diff --git a/podcast.py b/podcast.py new file mode 100644 index 0000000..9ee23d3 --- /dev/null +++ b/podcast.py @@ -0,0 +1,118 @@ +# SPDX-FileCopyrightText: 2026 Matthew Fennell <matthew@fennell.dev> +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +from feed2exec.utils import slug + +from filelock import FileLock +from pathlib import PosixPath +from random import choice +from shutil import which +from string import ascii_lowercase +from subprocess import PIPE, Popen +import logging + +def output(*args, feed=None, item=None, session=None, **kwargs): + """The podcast plugin saves the audio file at the enclosure's url to the + directory specified by the mailbox and folder, followed by a folder named + after the feed's title. The intention is for the mailbox to be the root + under which podcasts are stored, the folder to be used for categories, and + the title to separate multiple podcasts of the same category - like so: + + podcasts + ├── entertainment + │ ├── friday-night-comedy + │ │ └── friday-night-comedy-from-bbc-radio-4-yeft.opus + │ └── just-a-minute + │ ├── series-97-5-how-to-win-at-rock-paper-scissors-bmbg.opus + │ └── series-97-6-has-paul-joined-tiktok-txky.opus + └── news + └── bbc-news + └── 28-08-2026-22-01-gmt-hqmw.opus + + In the process, it severely compresses the audio using ffmpeg to save + space. It also appends the filepath to an m3u playlist at the root of the + mailbox, using a lockfile to avoid contention when run in parallel. + Finally, it appends short random strings to the filenames, to prevent + overwriting podcasts that use identical across episodes. + + Example:: + + [Friday Night Comedy] + url = https://podcasts.files.bbci.co.uk/p02pc9pj.rss + output = podcast + mailbox = /home/matthew/podcasts + folder = entertainment + + The above will save compressed .opus files into + /home/matthew/podcasts/entertainment/friday-night-comedy. + """ + + if not feed.get("mailbox") or not feed.get("folder") or not feed.get("name"): + logging.error(f"mailbox, folder or name not present on {feed}") + return False + + mailbox_dir = PosixPath(feed["mailbox"]) + feed_dir = PosixPath(mailbox_dir, feed["folder"], slug(feed["name"])) + + if not item.get("title"): + logging.error(f"name not present on {item}") + return False + + unique_suffix = "".join(choice(ascii_lowercase) for i in range(4)) + podcast_filename = slug(item["title"] + " " + unique_suffix) + podcast_filepath = PosixPath(feed_dir, podcast_filename).with_suffix(".opus") + + if len(item.enclosures) != 1: + logging.error(f"!= 1 enclosure on {item.enclosures}") + + enclosure = item.enclosures[0] + + if not enclosure.get("url") and not enclosure.get("href"): + logging.error(f"url/href not present on {enclosure}") + + url = enclosure.get("url", enclosure.href) + + playlist_filepath = PosixPath(mailbox_dir, "playlist").with_suffix(".m3u") + playlist_lock = FileLock(playlist_filepath.with_suffix(".lock")) + + if feed.get("catchup"): + return True + + feed_dir.mkdir(parents=True, exist_ok=True) + logging.debug(f"Ensured feed dir {feed_dir} exists") + + logging.debug(f"Fetching from {url}") + + wget = Popen([ + which("wget"), + "--quiet", + "--output-document", "-", + url, + ], stdout=PIPE) + + ffmpeg = Popen([ + which("ffmpeg"), + "-loglevel", "fatal", + "-i", "-", + "-c:a", "libopus", + "-ac", "1", + "-b:a", "12K", + "-vbr", "constrained", + "-y", + str(podcast_filepath), + ], stdin=wget.stdout) + + wget.stdout.close() + ffmpeg.communicate() + + logging.debug(f"Wrote to {podcast_filepath}") + + with playlist_lock: + with open(playlist_filepath, "a") as playlist_file: + relative_path = str(podcast_filepath.relative_to(feed["mailbox"])) + playlist_file.write(relative_path + "\n") + + logging.debug(f"Wrote to {playlist_filepath}") + + return True |
