diff --git a/test/test_pot/test_pot_builtin_utils.py b/test/test_pot/test_pot_builtin_utils.py index 7645ba601f..15a25cff2f 100644 --- a/test/test_pot/test_pot_builtin_utils.py +++ b/test/test_pot/test_pot_builtin_utils.py @@ -45,3 +45,8 @@ def test_no_visitor_id(self, pot_request): def test_invalid_base64(self, pot_request): pot_request.visitor_data = 'invalid-base64' assert get_webpo_content_binding(pot_request, bind_to_visitor_id=True) == (pot_request.visitor_data, ContentBindingType.VISITOR_DATA) + + def test_gvs_video_id_binding_experiment(self, pot_request): + pot_request.context = PoTokenContext.GVS + pot_request._gvs_bind_to_video_id = True + assert get_webpo_content_binding(pot_request) == ('example-video-id', ContentBindingType.VIDEO_ID) diff --git a/yt_dlp/extractor/youtube/_video.py b/yt_dlp/extractor/youtube/_video.py index 79c183c6a5..9ef7f14dfa 100644 --- a/yt_dlp/extractor/youtube/_video.py +++ b/yt_dlp/extractor/youtube/_video.py @@ -2955,9 +2955,20 @@ def fetch_po_token(self, client='web', context: _PoTokenContext = _PoTokenContex # TODO(future): This validation should be moved into pot framework. # Some sort of middleware or validation provider perhaps? + gvs_bind_to_video_id = False + experiments = traverse_obj(ytcfg, ( + 'WEB_PLAYER_CONTEXT_CONFIGS', ..., 'serializedExperimentFlags', {urllib.parse.parse_qs})) + if 'true' in traverse_obj(experiments, (..., 'html5_generate_content_po_token', -1)): + self.write_debug( + f'{video_id}: Detected experiment to bind GVS PO Token to video id.', only_once=True) + gvs_bind_to_video_id = True + # GVS WebPO Token is bound to visitor_data / Visitor ID when logged out. # Must have visitor_data for it to function. - if player_url and context == _PoTokenContext.GVS and not visitor_data and not self.is_authenticated: + if ( + player_url and context == _PoTokenContext.GVS + and not visitor_data and not self.is_authenticated and not gvs_bind_to_video_id + ): self.report_warning( f'Unable to fetch GVS PO Token for {client} client: Missing required Visitor Data. ' f'You may need to pass Visitor Data with --extractor-args "youtube:visitor_data=XXX"', only_once=True) @@ -2971,7 +2982,10 @@ def fetch_po_token(self, client='web', context: _PoTokenContext = _PoTokenContex config_po_token = self._get_config_po_token(client, context) if config_po_token: # GVS WebPO token is bound to data_sync_id / account Session ID when logged in. - if player_url and context == _PoTokenContext.GVS and not data_sync_id and self.is_authenticated: + if ( + player_url and context == _PoTokenContext.GVS + and not data_sync_id and self.is_authenticated and not gvs_bind_to_video_id + ): self.report_warning( f'Got a GVS PO Token for {client} client, but missing Data Sync ID for account. Formats may not work.' f'You may need to pass a Data Sync ID with --extractor-args "youtube:data_sync_id=XXX"') @@ -2997,6 +3011,7 @@ def fetch_po_token(self, client='web', context: _PoTokenContext = _PoTokenContex video_id=video_id, video_webpage=webpage, required=required, + _gvs_bind_to_video_id=gvs_bind_to_video_id, **kwargs, ) @@ -3040,6 +3055,7 @@ def _fetch_po_token(self, client, **kwargs): data_sync_id=kwargs.get('data_sync_id'), video_id=kwargs.get('video_id'), request_cookiejar=self._downloader.cookiejar, + _gvs_bind_to_video_id=kwargs.get('_gvs_bind_to_video_id', False), # All requests that would need to be proxied should be in the # context of www.youtube.com or the innertube host diff --git a/yt_dlp/extractor/youtube/pot/provider.py b/yt_dlp/extractor/youtube/pot/provider.py index 13b3b1f9bb..2511edf015 100644 --- a/yt_dlp/extractor/youtube/pot/provider.py +++ b/yt_dlp/extractor/youtube/pot/provider.py @@ -58,6 +58,8 @@ class PoTokenRequest: visitor_data: str | None = None data_sync_id: str | None = None video_id: str | None = None + # Internal, YouTube experiment on whether to bind GVS PO Token to video_id. + _gvs_bind_to_video_id: bool = False # Networking parameters request_cookiejar: YoutubeDLCookieJar = dataclasses.field(default_factory=YoutubeDLCookieJar) diff --git a/yt_dlp/extractor/youtube/pot/utils.py b/yt_dlp/extractor/youtube/pot/utils.py index a27921d4af..7f9ca078d6 100644 --- a/yt_dlp/extractor/youtube/pot/utils.py +++ b/yt_dlp/extractor/youtube/pot/utils.py @@ -42,6 +42,9 @@ def get_webpo_content_binding( if not client_name or client_name not in webpo_clients: return None, None + if request.context == PoTokenContext.GVS and request._gvs_bind_to_video_id: + return request.video_id, ContentBindingType.VIDEO_ID + if request.context == PoTokenContext.GVS or client_name in ('WEB_REMIX', ): if request.is_authenticated: return request.data_sync_id, ContentBindingType.DATASYNC_ID