#!/usr/bin/env python3 """ytdlp-worker.py - one long-lived yt-dlp process answering many requests. Spawning yt-dlp per call pays Python start-up + extractor import every time. This worker imports yt_dlp ONCE and runs each request's argv in-process through the CLI's own entry point (yt_dlp._real_main), so config files such as /etc/yt-dlp.conf and everything else the command line sets up apply. Protocol (newline-delimited JSON): stdin : {"id": , "args": [...]} stdout: {"id": , "code": , "out": "", "err": ""} Only for calls whose result is printed to stdout (-J, --dump-json, --version). Usage: python3 ytdlp-worker.py """ import io, json, sys, contextlib if len(sys.argv) > 1 and sys.argv[1]: sys.path.insert(0, sys.argv[1]) # the yt-dlp release binary is a zipapp import yt_dlp # noqa: E402 proto_out = sys.stdout sys.stdout = io.StringIO() # nothing may leak onto the protocol pipe def run(args): out, err = io.StringIO(), io.StringIO() code = 0 with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err): try: # The CLI's own entry point (not parse_options + YoutubeDL): it also # sets up what the command line does (JS challenge solving, plugins, # post-processing defaults). A bare YoutubeDL got "Sign in to # confirm you're not a bot" where the CLI succeeded. ret = yt_dlp._real_main(args) code = ret[0] if isinstance(ret, tuple) else (ret or 0) except SystemExit as e: code = e.code if isinstance(e.code, int) else 1 except Exception as e: # report, never die err.write(f"ERROR: {e}\n") code = 1 return code, out.getvalue(), err.getvalue()[-8000:] for line in sys.stdin: line = line.strip() if not line: continue try: req = json.loads(line) code, out, err = run([str(a) for a in req.get("args", [])]) resp = {"id": req.get("id"), "code": code, "out": out, "err": err} except Exception as e: resp = {"id": None, "code": 1, "out": "", "err": f"worker: {e}"} proto_out.write(json.dumps(resp) + "\n") proto_out.flush()