From cd883205c65f66c7a610e5a826a0dacbe71bd510 Mon Sep 17 00:00:00 2001 From: xpltd Date: Sat, 21 Mar 2026 22:46:21 -0500 Subject: [PATCH] Enable yt-dlp remote JS challenge solver, consolidate base opts - Add remote_components={'ejs:github'} to all yt-dlp invocations, fixing YouTube signature/n-parameter challenge failures that caused missing formats - Extract _base_opts() method to consolidate quiet, no_warnings, remote_components, and extractor_args across all three call sites - Removes PASSWORD_HASH from user-facing config comment --- backend/app/services/download.py | 41 ++++++++++++++++---------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/backend/app/services/download.py b/backend/app/services/download.py index 07fc492..444c333 100644 --- a/backend/app/services/download.py +++ b/backend/app/services/download.py @@ -72,6 +72,18 @@ class DownloadService: # Per-job throttle state for DB writes (only used inside worker threads) self._last_db_percent: dict[str, float] = {} + def _base_opts(self) -> dict: + """Return yt-dlp options common to all invocations.""" + opts: dict = { + "quiet": True, + "no_warnings": True, + # Enable remote JS challenge solver for YouTube signature/n-parameter + "remote_components": {"ejs:github"}, + } + if self._config.ytdlp.extractor_args: + opts["extractor_args"] = self._config.ytdlp.extractor_args + return opts + def update_max_concurrent(self, max_workers: int) -> None: """Update the thread pool size for concurrent downloads. @@ -165,14 +177,13 @@ class DownloadService: os.makedirs(output_dir, exist_ok=True) outtmpl = os.path.join(output_dir, template) - opts: dict = { + opts = self._base_opts() + opts.update({ "outtmpl": outtmpl, - "quiet": True, - "no_warnings": True, "noprogress": True, "noplaylist": True, # Individual jobs — don't re-expand playlists "overwrites": True, # Allow re-downloading same URL with different format - } + }) if job_create.format_id: opts["format"] = job_create.format_id elif job_create.quality: @@ -204,10 +215,6 @@ class DownloadService: if cookie_path: opts["cookiefile"] = cookie_path - # Operator-configured extractor_args (e.g. YouTube player_client) - if self._config.ytdlp.extractor_args: - opts["extractor_args"] = self._config.ytdlp.extractor_args - self._loop.run_in_executor( self._executor, self._run_download, @@ -499,13 +506,8 @@ class DownloadService: def _extract_info(self, url: str) -> dict | None: """Run yt-dlp extract_info synchronously (called from thread pool).""" - opts: dict = { - "quiet": True, - "no_warnings": True, - "skip_download": True, - } - if self._config.ytdlp.extractor_args: - opts["extractor_args"] = self._config.ytdlp.extractor_args + opts = self._base_opts() + opts["skip_download"] = True try: with yt_dlp.YoutubeDL(opts) as ydl: return ydl.extract_info(url, download=False) @@ -515,15 +517,12 @@ class DownloadService: def _extract_url_info(self, url: str) -> dict | None: """Extract URL metadata including playlist detection.""" - opts: dict = { - "quiet": True, - "no_warnings": True, + opts = self._base_opts() + opts.update({ "skip_download": True, "extract_flat": "in_playlist", "noplaylist": False, - } - if self._config.ytdlp.extractor_args: - opts["extractor_args"] = self._config.ytdlp.extractor_args + }) try: with yt_dlp.YoutubeDL(opts) as ydl: return ydl.extract_info(url, download=False)