From 33e9472f1774e6540731c3dc64a5622c5eb47b64 Mon Sep 17 00:00:00 2001 From: Mattias Tall Date: Wed, 27 May 2026 08:47:02 +0200 Subject: [PATCH] Pin all yt-dlp calls to tv_embedded client to stop IP rate limiting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without a player_client, yt-dlp probes web+mweb+android+tv in sequence for every request, multiplying API calls ~4x and triggering YouTube IP blocks. tv_embedded exposes the full format list in one shot and is the least restricted client for authenticated sessions. Applies to: search, trending, video/channel metadata, playlists, subs, comments, and downloads — every yt-dlp invocation site. Co-Authored-By: Claude Sonnet 4.6 --- backend/services/ytdlp.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/backend/services/ytdlp.py b/backend/services/ytdlp.py index f6ae795..9304ec4 100644 --- a/backend/services/ytdlp.py +++ b/backend/services/ytdlp.py @@ -55,6 +55,12 @@ _meta_lock = threading.Lock() _meta_last_call: float = 0.0 _META_MIN_GAP = 12.0 # seconds between any two metadata requests +# Pin to tv_embedded client — it exposes full format list without SABR +# restrictions and makes exactly ONE set of API requests per operation. +# Without this yt-dlp probes web + mweb + android + tv in sequence, +# multiplying requests and triggering YouTube IP rate limits. +_YT_CLIENT = ["--extractor-args", "youtube:player_client=tv_embedded"] + # Active download counter — _meta_run pauses while any download is running so # background discovery and a concurrent download never share the YouTube session # at the same time. Downloads set/clear this; _meta_run reads it. @@ -211,6 +217,7 @@ def search_youtube(query: str, max_results: int = 40, polite: bool = False) -> l "--dump-json", "--flat-playlist", "--quiet", + *_YT_CLIENT, *_cookie_args(), ], timeout=60) @@ -259,6 +266,7 @@ def fetch_trending(region: str = "US", max_results: int = 50) -> list[dict]: "--flat-playlist", "--quiet", "--playlist-end", str(max_results), + *_YT_CLIENT, *_cookie_args(), ], timeout=60) @@ -309,6 +317,7 @@ def fetch_video_metadata(video_id: str, polite: bool = False) -> dict | None: base_cmd = [ "yt-dlp", url, "--dump-json", "--no-download", "--no-playlist", + *_YT_CLIENT, ] runner = _meta_run if polite else _run stdout, stderr, code = runner([*base_cmd, *cookie_args], timeout=30) @@ -379,6 +388,7 @@ def fetch_channel_metadata(channel_id: str, max_videos: int = 30, start_video: i "--dump-single-json", "--flat-playlist", "--quiet", + *_YT_CLIENT, *_cookie_args(), ] if start_video > 1: @@ -442,6 +452,7 @@ def fetch_channel_playlists(channel_id: str, max_results: int = 100) -> list[dic "--dump-json", "--flat-playlist", "--playlist-end", str(max_results), "--quiet", + *_YT_CLIENT, *_cookie_args(), ], timeout=60) @@ -484,6 +495,7 @@ def fetch_playlist_videos(playlist_id: str, max_videos: int = 200) -> list[dict] "yt-dlp", url, "--dump-json", "--flat-playlist", "--quiet", + *_YT_CLIENT, *_cookie_args(), ] if max_videos > 0: @@ -532,6 +544,7 @@ def fetch_featured_channels(channel_id: str) -> list[str]: "--dump-json", "--flat-playlist", "--quiet", + *_YT_CLIENT, *_cookie_args(), ], timeout=30) @@ -564,6 +577,7 @@ def fetch_channel_links(channel_id: str) -> list[str]: "--flat-playlist", "--playlist-end", "1", "--quiet", + *_YT_CLIENT, *_cookie_args(), ], timeout=30) @@ -614,6 +628,7 @@ def download_subs_only(video_id: str, subtitle_langs: str) -> bool: "--sub-langs", subtitle_langs, "--convert-subs", "vtt", "-o", output_template, + *_YT_CLIENT, *_cookie_args(), ], timeout=60) if code == 0: @@ -628,7 +643,7 @@ def fetch_available_subs(video_id: str) -> dict: BCP-47 lang codes. Manual = human-made; auto = auto-generated captions. """ url = f"https://www.youtube.com/watch?v={video_id}" - base_cmd = ["yt-dlp", url, "--dump-json", "--no-download", "--no-playlist"] + base_cmd = ["yt-dlp", url, "--dump-json", "--no-download", "--no-playlist", *_YT_CLIENT] cookie_args = _cookie_args() stdout, _, code = _meta_run([*base_cmd, *cookie_args], timeout=30) if code != 0 and cookie_args: @@ -674,6 +689,7 @@ def fetch_video_comments(youtube_video_id: str, max_comments: int = 20) -> list[ "--skip-download", "--no-playlist", "--output", out_tmpl, + *_YT_CLIENT, *_cookie_args(), ] _run(args, timeout=90) @@ -928,6 +944,7 @@ def start_download( "-o", output_template, "--newline", "--progress", "--no-colors", *subtitle_args, + *_YT_CLIENT, *cookie_args, ] cmd, tmp_cookie_path = _make_private_cookie_copy(cmd)