Browse Source

[YouTube] Implement player JS override for player `0004de42`

* based on yt-dlp/yt-dlp#14398, thx seproDev
* adds --youtube-player-js-variant option
* adds --youtube-player-js-version option
* sets defaults to main variant of player `0004de42`
* fixes #33187, for now
dirkf 2 weeks ago
parent
commit
0739f58f90
3 changed files with 61 additions and 5 deletions
  1. 2 0
      youtube_dl/__init__.py
  2. 48 5
      youtube_dl/extractor/youtube.py
  3. 11 0
      youtube_dl/options.py

+ 2 - 0
youtube_dl/__init__.py

@@ -409,6 +409,8 @@ def _real_main(argv=None):
         'include_ads': opts.include_ads,
         'default_search': opts.default_search,
         'youtube_include_dash_manifest': opts.youtube_include_dash_manifest,
+        'youtube_player_js_version': opts.youtube_player_js_version,
+        'youtube_player_js_variant': opts.youtube_player_js_variant,
         'encoding': opts.encoding,
         'extract_flat': opts.extract_flat,
         'mark_watched': opts.mark_watched,

+ 48 - 5
youtube_dl/extractor/youtube.py

@@ -1625,6 +1625,19 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
         self._code_cache = {}
         self._player_cache = {}
 
+    def _get_player_js_version(self):
+        player_js_version = self.get_param('youtube_player_js_version') or '20348@0004de42'
+        sts_hash = self._search_regex(
+            ('^actual$(^)?(^)?', r'^([0-9]{5,})@([0-9a-f]{8,})$'),
+            player_js_version, 'player_js_version', group=(1, 2), default=None)
+        if sts_hash:
+            return sts_hash
+        self.report_warning(
+            'Invalid player JS version "{0}" specified. '
+            'It should be "{1}" or in the format of {2}'.format(
+                player_js_version, 'actual', 'SignatureTimeStamp@Hash'), only_once=True)
+        return None, None
+
     # *ytcfgs, webpage=None
     def _extract_player_url(self, *ytcfgs, **kw_webpage):
         if ytcfgs and not isinstance(ytcfgs[0], dict):
@@ -1635,19 +1648,43 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
                 webpage or '', 'player URL', fatal=False)
             if player_url:
                 ytcfgs = ytcfgs + ({'PLAYER_JS_URL': player_url},)
-        return traverse_obj(
+        player_url = traverse_obj(
             ytcfgs, (Ellipsis, 'PLAYER_JS_URL'), (Ellipsis, 'WEB_PLAYER_CONTEXT_CONFIGS', Ellipsis, 'jsUrl'),
             get_all=False, expected_type=lambda u: urljoin('https://www.youtube.com', u))
 
+        player_id_override = self._get_player_js_version()[1]
+
+        requested_js_variant = self.get_param('youtube_player_js_variant') or 'main'
+        variant_js = next(
+            (v for k, v in self._PLAYER_JS_VARIANT_MAP if k == requested_js_variant),
+            None)
+        if variant_js:
+            player_id = player_id_override or self._extract_player_info(player_url)
+            original_url = player_url
+            player_url = '/s/player/{0}/{1}'.format(player_id, variant_js)
+            if original_url != player_url:
+                self.write_debug(
+                    'Forcing "{0}" player JS variant for player {1}\n'
+                    '        original url = {2}'.format(
+                        requested_js_variant, player_id, original_url),
+                    only_once=True)
+        elif requested_js_variant != 'actual':
+            self.report_warning(
+                'Invalid player JS variant name "{0}" requested. '
+                'Valid choices are: {1}'.format(
+                    requested_js_variant, ','.join(k for k, _ in self._PLAYER_JS_VARIANT_MAP)),
+                only_once=True)
+
+        return urljoin('https://www.youtube.com', player_url)
+
     def _download_player_url(self, video_id, fatal=False):
         res = self._download_webpage(
             'https://www.youtube.com/iframe_api',
             note='Downloading iframe API JS', video_id=video_id, fatal=fatal)
         player_version = self._search_regex(
             r'player\\?/([0-9a-fA-F]{8})\\?/', res or '', 'player version', fatal=fatal,
-            default=NO_DEFAULT if res else None)
-        if player_version:
-            return 'https://www.youtube.com/s/player/{0}/player_ias.vflset/en_US/base.js'.format(player_version)
+            default=NO_DEFAULT if res else None) or None
+        return player_version and 'https://www.youtube.com/s/player/{0}/player_ias.vflset/en_US/base.js'.format(player_version)
 
     def _signature_cache_id(self, example_sig):
         """ Return a string representation of a signature """
@@ -2034,9 +2071,15 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
     def _extract_signature_timestamp(self, video_id, player_url, ytcfg=None, fatal=False):
         """
         Extract signatureTimestamp (sts)
+
         Required to tell API what sig/player version is in use.
         """
-        sts = traverse_obj(ytcfg, 'STS', expected_type=int)
+        sts = traverse_obj(
+            (self._get_player_js_version(), ytcfg),
+            (0, 0),
+            (1, 'STS'),
+            expected_type=int_or_none)
+
         if sts:
             return sts
 

+ 11 - 0
youtube_dl/options.py

@@ -412,6 +412,17 @@ def parseOpts(overrideArguments=None):
         '--youtube-skip-dash-manifest',
         action='store_false', dest='youtube_include_dash_manifest',
         help='Do not download the DASH manifests and related data on YouTube videos')
+    video_format.add_option(
+        '--youtube-player-js-variant',
+        action='store', dest='youtube_player_js_variant',
+        help='For YouTube, the player javascript variant to use for n/sig deciphering; `actual` to follow the site; default `%default`.',
+        choices=('actual', 'main', 'tcc', 'tce', 'es5', 'es6', 'tv', 'tv_es6', 'phone', 'tablet'),
+        default='main', metavar='VARIANT')
+    video_format.add_option(
+        '--youtube-player-js-version',
+        action='store', dest='youtube_player_js_version',
+        help='For YouTube, the player javascript version to use for n/sig deciphering, specified as `signature_timestamp@hash`, or `actual` to follow the site; default `%default`',
+        default='20348@0004de42', metavar='STS@HASH')
     video_format.add_option(
         '--merge-output-format',
         action='store', dest='merge_output_format', metavar='FORMAT', default=None,