From a06c5eb04803452d21e5734b3df78890cd631dee Mon Sep 17 00:00:00 2001 From: ed Date: Wed, 9 Apr 2025 19:44:13 +0000 Subject: [PATCH] new xau hook: podcast-normalizer.py --- bin/hooks/README.md | 3 + bin/hooks/podcast-normalizer.py | 121 ++++++++++++++++++++++++++++++++ docs/devnotes.md | 3 + 3 files changed, 127 insertions(+) create mode 100755 bin/hooks/podcast-normalizer.py diff --git a/bin/hooks/README.md b/bin/hooks/README.md index 31ee3bff..d4481523 100644 --- a/bin/hooks/README.md +++ b/bin/hooks/README.md @@ -14,6 +14,8 @@ run copyparty with `--help-hooks` for usage details / hook type explanations (xm * [discord-announce.py](discord-announce.py) announces new uploads on discord using webhooks ([example](https://user-images.githubusercontent.com/241032/215304439-1c1cb3c8-ec6f-4c17-9f27-81f969b1811a.png)) * [reject-mimetype.py](reject-mimetype.py) rejects uploads unless the mimetype is acceptable * [into-the-cache-it-goes.py](into-the-cache-it-goes.py) avoids bugs in caching proxies by immediately downloading each file that is uploaded +* [podcast-normalizer.py](podcast-normalizer.py) creates a second file with dynamic-range-compression whenever an audio file is uploaded + * good example of the `idx` [hook effect](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#hook-effects) to tell copyparty about additional files to scan/index # upload batches @@ -25,6 +27,7 @@ these are `--xiu` hooks; unlike `xbu` and `xau` (which get executed on every sin # before upload * [reject-extension.py](reject-extension.py) rejects uploads if they match a list of file extensions * [reloc-by-ext.py](reloc-by-ext.py) redirects an upload to another destination based on the file extension + * good example of the `reloc` [hook effect](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#hook-effects) # on message diff --git a/bin/hooks/podcast-normalizer.py b/bin/hooks/podcast-normalizer.py new file mode 100755 index 00000000..7061ef64 --- /dev/null +++ b/bin/hooks/podcast-normalizer.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 + +import json +import os +import sys +import subprocess as sp + + +_ = r""" +sends all uploaded audio files through an aggressive +dynamic-range-compressor to even out the volume levels + +dependencies: + ffmpeg + +being an xau hook, this gets eXecuted After Upload completion + but before copyparty has started hashing/indexing the file, so + we'll create a second normalized copy in a subfolder and tell + copyparty to hash/index that additional file as well + +example usage as global config: + -e2d -e2t --xau j,c1,bin/hooks/podcast-normalizer.py + +parameters explained, + e2d/e2t = enable database and metadata indexing + xau = execute after upload + j = this hook needs upload information as json (not just the filename) + c1 = this hook returns json on stdout, so tell copyparty to read that + +example usage as a volflag (per-volume config): + -v srv/inc/pods:inc/pods:r:rw,ed:c,xau=j,c1,bin/hooks/podcast-normalizer.py + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + (share fs-path srv/inc/pods at URL /inc/pods, + readable by all, read-write for user ed, + running this xau (exec-after-upload) plugin for all uploaded files) + +example usage as a volflag in a copyparty config file: + [/inc/pods] + srv/inc/pods + accs: + r: * + rw: ed + flags: + e2d # enables file indexing + e2t # metadata tags too + xau: j,c1,bin/hooks/podcast-normalizer.py + +""" + +######################################################################## +### CONFIG + +# filetypes to process; ignores everything else +EXTS = "mp3 flac ogg opus m4a aac wav wma" + +# the name of the subdir to put the normalized files in +SUBDIR = "normalized" + +######################################################################## + + +# try to enable support for crazy filenames +try: + from copyparty.util import fsenc +except: + + def fsenc(p): + return p.encode("utf-8") + + +def main(): + # read info from copyparty + inf = json.loads(sys.argv[1]) + vpath = inf["vp"] + abspath = inf["ap"] + + # check if the file-extension is on the to-be-processed list + ext = abspath.lower().split(".")[-1] + if ext not in EXTS.split(): + return + + # jump into the folder where the file was uploaded + # and create the subfolder to place the normalized copy inside + dirpath, filename = os.path.split(abspath) + os.chdir(fsenc(dirpath)) + os.makedirs(SUBDIR, exist_ok=True) + + # the input and output filenames to give ffmpeg + fname_in = fsenc(f"./{filename}") + fname_out = fsenc(f"{SUBDIR}/{filename}.opus") + + # fmt: off + # create and run the ffmpeg command + cmd = [ + b"ffmpeg", + b"-nostdin", + b"-hide_banner", + b"-i", fname_in, + b"-af", b"dynaudnorm=f=100:g=9", # the normalizer config + b"-c:a", b"libopus", + b"-b:a", b"128k", + fname_out, + ] + # fmt: on + sp.check_output(cmd) + + # and finally, tell copyparty about the new file + # so it appears in the database and rss-feed: + vpath = f"{SUBDIR}/{filename}.opus" + print(json.dumps({"idx": {"vp": [vpath]}})) + + # (it's fine to give it a relative path like that; it gets + # resolved relative to the folder the file was uploaded into) + + +if __name__ == "__main__": + try: + main() + except Exception as ex: + print("podcast-normalizer failed; %r" % (ex,)) diff --git a/docs/devnotes.md b/docs/devnotes.md index 1f126916..e90a4c23 100644 --- a/docs/devnotes.md +++ b/docs/devnotes.md @@ -281,8 +281,11 @@ on writing your own [hooks](../README.md#event-hooks) hooks can cause intentional side-effects, such as redirecting an upload into another location, or creating+indexing additional files, or deleting existing files, by returning json on stdout * `reloc` can redirect uploads before/after uploading has finished, based on filename, extension, file contents, uploader ip/name etc. + * example: [reloc-by-ext](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/reloc-by-ext.py) * `idx` informs copyparty about a new file to index as a consequence of this upload + * example: [podcast-normalizer.py](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/podcast-normalizer.py) * `del` tells copyparty to delete an unrelated file by vpath + * example: ( ´・ω・) nyoro~n for these to take effect, the hook must be defined with the `c1` flag; see example [reloc-by-ext](https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/reloc-by-ext.py)