From 883fda2169537d14e378ded4a59690334c996260 Mon Sep 17 00:00:00 2001 From: David Ching Date: Mon, 9 Feb 2026 09:42:37 -0800 Subject: [PATCH 1/2] [YouTube] Fix HTTP 403 on DASH fragments by adding android_vr client - Add android_vr (Oculus Quest 3) as first-priority InnerTube client, replacing android_sdkless which is now blocked by YouTube's CDN - Remove break on n parameter that prevented all formats from being added - Set client User-Agent on format download headers to avoid CDN mismatch Ref: yt-dlp/yt-dlp#15726 Co-Authored-By: Claude Opus 4.6 --- youtube_dl/extractor/youtube.py | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/youtube_dl/extractor/youtube.py b/youtube_dl/extractor/youtube.py index 81a019143..1d38504eb 100644 --- a/youtube_dl/extractor/youtube.py +++ b/youtube_dl/extractor/youtube.py @@ -90,6 +90,24 @@ class YoutubeBaseInfoExtractor(InfoExtractor): # priority order for now _INNERTUBE_CLIENTS = o_dict(( + # thx yt-dlp/yt-dlp#15726 + ('android_vr', { + 'INNERTUBE_CONTEXT': { + 'client': { + 'clientName': 'ANDROID_VR', + 'clientVersion': '1.62.27', + 'deviceMake': 'Oculus', + 'deviceModel': 'Quest 3', + 'androidSdkVersion': 32, + 'userAgent': 'com.google.android.apps.youtube.vr.oculus/1.62.27 (Linux; U; Android 12L; eureka-user Build/SQ3A.220605.009.A1) gzip', + 'osName': 'Android', + 'osVersion': '12L', + }, + }, + 'INNERTUBE_CONTEXT_CLIENT_NAME': 28, + 'REQUIRE_JS_PLAYER': False, + 'WITH_COOKIES': False, + }), # Doesn't require a PoToken for some reason: thx yt-dlp/yt-dlp#14693 ('android_sdkless', { 'INNERTUBE_CONTEXT': { @@ -2364,6 +2382,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor): is_live = traverse_obj(player_response, ('videoDetails', 'isLive')) fetched_timestamp = None + client_user_agent = None if False and not player_response: player_response = self._call_api( 'player', {'videoId': video_id}, video_id) @@ -2455,6 +2474,10 @@ class YoutubeIE(YoutubeBaseInfoExtractor): 'streamingData', 'hlsManifestUrl', T(url_or_none))): player_response['streamingData']['hlsManifestUrl'] = hls + # Save client User-Agent for use in format download headers + client_user_agent = traverse_obj(client, ( + 'INNERTUBE_CONTEXT', 'client', 'userAgent')) + def is_agegated(playability): # playability: dict if not playability: @@ -2654,10 +2677,6 @@ class YoutubeIE(YoutubeBaseInfoExtractor): self.write_debug(error_to_compat_str(e), only_once=True) continue - if parse_qs(fmt_url).get('n'): - # this and (we assume) all the formats here are n-scrambled - break - language_preference = ( 10 if audio_track.get('audioIsDefault') else -10 if 'descriptive' in (traverse_obj(audio_track, ('displayName', T(lower))) or '') @@ -2678,6 +2697,8 @@ class YoutubeIE(YoutubeBaseInfoExtractor): # Strictly de-prioritize 3gp formats 'preference': -2 if itag == '17' else None, } + if client_user_agent: + dct['http_headers'] = {'User-Agent': client_user_agent} if itag: itags[itag].add(('https', dct.get('language'))) self._unthrottle_format_urls(video_id, player_url, dct) From 56b404fd7f483f0a158369795ab96c4642dd7c83 Mon Sep 17 00:00:00 2001 From: David Ching Date: Thu, 12 Feb 2026 16:49:49 -0800 Subject: [PATCH 2/2] Restore break on n parameter since we can't solve the challenge. --- youtube_dl/extractor/youtube.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/youtube_dl/extractor/youtube.py b/youtube_dl/extractor/youtube.py index 1d38504eb..247283e19 100644 --- a/youtube_dl/extractor/youtube.py +++ b/youtube_dl/extractor/youtube.py @@ -2677,6 +2677,10 @@ class YoutubeIE(YoutubeBaseInfoExtractor): self.write_debug(error_to_compat_str(e), only_once=True) continue + if parse_qs(fmt_url).get('n'): + # this and (we assume) all the formats here are n-scrambled + break + language_preference = ( 10 if audio_track.get('audioIsDefault') else -10 if 'descriptive' in (traverse_obj(audio_track, ('displayName', T(lower))) or '')