mirror of
https://github.com/yt-dlp/yt-dlp.git
synced 2025-10-03 08:42:42 +00:00
Compare commits
3 Commits
50e452fd7d
...
88e2a2de8e
Author | SHA1 | Date | |
---|---|---|---|
|
88e2a2de8e | ||
|
12b57d2858 | ||
|
b7b7910d96 |
10
Changelog.md
10
Changelog.md
@@ -4,6 +4,16 @@
|
||||
# To create a release, dispatch the https://github.com/yt-dlp/yt-dlp/actions/workflows/release.yml workflow on master
|
||||
-->
|
||||
|
||||
### 2025.09.26
|
||||
|
||||
#### Extractor changes
|
||||
- **twitch**: vod: [Fix `live_status` detection](https://github.com/yt-dlp/yt-dlp/commit/50e452fd7dfb8a648bd3b9aaabc8f94f37ce2051) ([#14457](https://github.com/yt-dlp/yt-dlp/issues/14457)) by [bashonly](https://github.com/bashonly)
|
||||
- **youtube**
|
||||
- [Fix player JS overrides](https://github.com/yt-dlp/yt-dlp/commit/b7b7910d96359a539b7997890342ab4a59dd685d) ([#14430](https://github.com/yt-dlp/yt-dlp/issues/14430)) by [bashonly](https://github.com/bashonly), [seproDev](https://github.com/seproDev)
|
||||
- [Improve PO token logging](https://github.com/yt-dlp/yt-dlp/commit/7df5acc546dccd32213c3a125d721e32b06d71b0) ([#14447](https://github.com/yt-dlp/yt-dlp/issues/14447)) by [seproDev](https://github.com/seproDev)
|
||||
- [Player client maintenance](https://github.com/yt-dlp/yt-dlp/commit/94c5622be96474ca3c637e52898c4daee4d8fb69) ([#14448](https://github.com/yt-dlp/yt-dlp/issues/14448)) by [seproDev](https://github.com/seproDev)
|
||||
- [Replace `tv_simply` with `web_safari` in default clients](https://github.com/yt-dlp/yt-dlp/commit/12b57d2858845c0c7fb33bf9aa8ed7be6905535d) ([#14465](https://github.com/yt-dlp/yt-dlp/issues/14465)) by [bashonly](https://github.com/bashonly)
|
||||
|
||||
### 2025.09.23
|
||||
|
||||
#### Important changes
|
||||
|
@@ -1814,7 +1814,7 @@ The following extractors use this feature:
|
||||
#### youtube
|
||||
* `lang`: Prefer translated metadata (`title`, `description` etc) of this language code (case-sensitive). By default, the video primary language metadata is preferred, with a fallback to `en` translated. See [youtube/_base.py](https://github.com/yt-dlp/yt-dlp/blob/415b4c9f955b1a0391204bd24a7132590e7b3bdb/yt_dlp/extractor/youtube/_base.py#L402-L409) for the list of supported content language codes
|
||||
* `skip`: One or more of `hls`, `dash` or `translated_subs` to skip extraction of the m3u8 manifests, dash manifests and [auto-translated subtitles](https://github.com/yt-dlp/yt-dlp/issues/4090#issuecomment-1158102032) respectively
|
||||
* `player_client`: Clients to extract video data from. The currently available clients are `web`, `web_safari`, `web_embedded`, `web_music`, `web_creator`, `mweb`, `ios`, `android`, `android_vr`, `tv`, `tv_simply` and `tv_embedded`. By default, `tv_simply,tv,web` is used, but `tv,web_safari,web` is used when authenticating with cookies and `tv,web_creator,web` is used with premium accounts. The `web_music` client is added for `music.youtube.com` URLs when logged-in cookies are used. The `web_embedded` client is added for age-restricted videos but only works if the video is embeddable. The `tv_embedded` and `web_creator` clients are added for age-restricted videos if account age-verification is required. Some clients, such as `web` and `web_music`, require a `po_token` for their formats to be downloadable. Some clients, such as `web_creator`, will only work with authentication. Not all clients support authentication via cookies. You can use `default` for the default clients, or you can use `all` for all clients (not recommended). You can prefix a client with `-` to exclude it, e.g. `youtube:player_client=default,-ios`
|
||||
* `player_client`: Clients to extract video data from. The currently available clients are `web`, `web_safari`, `web_embedded`, `web_music`, `web_creator`, `mweb`, `ios`, `android`, `android_vr`, `tv`, `tv_simply` and `tv_embedded`. By default, `tv,web_safari,web` is used, and `tv,web_creator,web` is used with premium accounts. The `web_music` client is added for `music.youtube.com` URLs when logged-in cookies are used. The `web_embedded` client is added for age-restricted videos but only works if the video is embeddable. The `tv_embedded` and `web_creator` clients are added for age-restricted videos if account age-verification is required. Some clients, such as `web` and `web_music`, require a `po_token` for their formats to be downloadable. Some clients, such as `web_creator`, will only work with authentication. Not all clients support authentication via cookies. You can use `default` for the default clients, or you can use `all` for all clients (not recommended). You can prefix a client with `-` to exclude it, e.g. `youtube:player_client=default,-ios`
|
||||
* `player_skip`: Skip some network requests that are generally needed for robust extraction. One or more of `configs` (skip client configs), `webpage` (skip initial webpage), `js` (skip js player), `initial_data` (skip initial data/next ep request). While these options can help reduce the number of requests needed or avoid some rate-limiting, they could cause issues such as missing formats or metadata. See [#860](https://github.com/yt-dlp/yt-dlp/pull/860) and [#12826](https://github.com/yt-dlp/yt-dlp/issues/12826) for more details
|
||||
* `webpage_skip`: Skip extraction of embedded webpage data. One or both of `player_response`, `initial_data`. These options are for testing purposes and don't skip any network requests
|
||||
* `player_params`: YouTube player parameters to use for player requests. Will overwrite any default ones set by yt-dlp.
|
||||
|
@@ -320,6 +320,20 @@ INNERTUBE_CLIENTS = {
|
||||
'clientVersion': '1.0',
|
||||
},
|
||||
},
|
||||
'GVS_PO_TOKEN_POLICY': {
|
||||
StreamingProtocol.HTTPS: GvsPoTokenPolicy(
|
||||
required=True,
|
||||
recommended=True,
|
||||
),
|
||||
StreamingProtocol.DASH: GvsPoTokenPolicy(
|
||||
required=True,
|
||||
recommended=True,
|
||||
),
|
||||
StreamingProtocol.HLS: GvsPoTokenPolicy(
|
||||
required=False,
|
||||
recommended=True,
|
||||
),
|
||||
},
|
||||
'INNERTUBE_CONTEXT_CLIENT_NAME': 75,
|
||||
},
|
||||
# This client now requires sign-in for every video
|
||||
|
@@ -257,7 +257,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'401': {'ext': 'mp4', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'av01.0.12M.08'},
|
||||
}
|
||||
_SUBTITLE_FORMATS = ('json3', 'srv1', 'srv2', 'srv3', 'ttml', 'srt', 'vtt')
|
||||
_DEFAULT_CLIENTS = ('tv_simply', 'tv', 'web')
|
||||
_DEFAULT_CLIENTS = ('tv', 'web_safari', 'web')
|
||||
_DEFAULT_AUTHED_CLIENTS = ('tv', 'web_safari', 'web')
|
||||
# Premium does not require POT (except for subtitles)
|
||||
_DEFAULT_PREMIUM_CLIENTS = ('tv', 'web_creator', 'web')
|
||||
@@ -1815,6 +1815,8 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'params': {'skip_download': True},
|
||||
}]
|
||||
|
||||
_DEFAULT_PLAYER_JS_VERSION = '20348@0004de42' # TODO: revert to 'actual' when n/sig is fixed
|
||||
_DEFAULT_PLAYER_JS_VARIANT = 'main'
|
||||
_PLAYER_JS_VARIANT_MAP = {
|
||||
'main': 'player_ias.vflset/en_US/base.js',
|
||||
'tcc': 'player_ias_tcc.vflset/en_US/base.js',
|
||||
@@ -2016,7 +2018,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
time.sleep(max(0, FETCH_SPAN + fetch_time - time.time()))
|
||||
|
||||
def _get_player_js_version(self):
|
||||
player_js_version = self._configuration_arg('player_js_version', [''])[0] or '20348@0004de42'
|
||||
player_js_version = self._configuration_arg('player_js_version', [''])[0] or self._DEFAULT_PLAYER_JS_VERSION
|
||||
if player_js_version == 'actual':
|
||||
return None, None
|
||||
if not re.fullmatch(r'[0-9]{5,}@[0-9a-f]{8,}', player_js_version):
|
||||
@@ -2026,31 +2028,64 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
return None, None
|
||||
return player_js_version.split('@')
|
||||
|
||||
def _construct_player_url(self, *, player_id=None, player_url=None):
|
||||
assert player_id or player_url, '_construct_player_url must take one of player_id or player_url'
|
||||
if not player_id:
|
||||
player_id = self._extract_player_info(player_url)
|
||||
|
||||
force_player_id = False
|
||||
player_id_override = self._get_player_js_version()[1]
|
||||
if player_id_override and player_id_override != player_id:
|
||||
force_player_id = f'Forcing player {player_id_override} in place of player {player_id}'
|
||||
player_id = player_id_override
|
||||
|
||||
variant = self._configuration_arg('player_js_variant', [''])[0] or self._DEFAULT_PLAYER_JS_VARIANT
|
||||
if variant not in (*self._PLAYER_JS_VARIANT_MAP, 'actual'):
|
||||
self.report_warning(
|
||||
f'Invalid player JS variant name "{variant}" requested. '
|
||||
f'Valid choices are: {", ".join(self._PLAYER_JS_VARIANT_MAP)}', only_once=True)
|
||||
variant = self._DEFAULT_PLAYER_JS_VARIANT
|
||||
|
||||
if not player_url:
|
||||
if force_player_id:
|
||||
self.write_debug(force_player_id, only_once=True)
|
||||
if variant == 'actual':
|
||||
# We don't have an actual variant so we always use 'main' & don't need to write debug
|
||||
variant = 'main'
|
||||
return urljoin('https://www.youtube.com', f'/s/player/{player_id}/{self._PLAYER_JS_VARIANT_MAP[variant]}')
|
||||
|
||||
actual_variant = self._get_player_id_variant_and_path(player_url)[1]
|
||||
if not force_player_id and (variant == 'actual' or variant == actual_variant):
|
||||
return urljoin('https://www.youtube.com', player_url)
|
||||
|
||||
if variant == 'actual':
|
||||
if actual_variant:
|
||||
variant = actual_variant
|
||||
else:
|
||||
# We need to force player_id but can't determine variant; fall back to 'main' variant
|
||||
variant = 'main'
|
||||
|
||||
self.write_debug(join_nonempty(
|
||||
force_player_id,
|
||||
variant != actual_variant and f'Forcing "{variant}" player JS variant for player {player_id}',
|
||||
f'original url = {player_url}',
|
||||
delim='\n '), only_once=True)
|
||||
|
||||
return urljoin('https://www.youtube.com', f'/s/player/{player_id}/{self._PLAYER_JS_VARIANT_MAP[variant]}')
|
||||
|
||||
def _extract_player_url(self, *ytcfgs, webpage=None):
|
||||
player_url = traverse_obj(
|
||||
ytcfgs, (..., 'PLAYER_JS_URL'), (..., 'WEB_PLAYER_CONTEXT_CONFIGS', ..., 'jsUrl'),
|
||||
get_all=False, expected_type=str)
|
||||
if not player_url:
|
||||
return
|
||||
player_id_override = self._get_player_js_version()[1]
|
||||
|
||||
requested_js_variant = self._configuration_arg('player_js_variant', [''])[0] or 'main'
|
||||
if requested_js_variant in self._PLAYER_JS_VARIANT_MAP:
|
||||
player_id = player_id_override or self._extract_player_info(player_url)
|
||||
original_url = player_url
|
||||
player_url = f'/s/player/{player_id}/{self._PLAYER_JS_VARIANT_MAP[requested_js_variant]}'
|
||||
if original_url != player_url:
|
||||
self.write_debug(
|
||||
f'Forcing "{requested_js_variant}" player JS variant for player {player_id}\n'
|
||||
f' original url = {original_url}', only_once=True)
|
||||
elif requested_js_variant != 'actual':
|
||||
self.report_warning(
|
||||
f'Invalid player JS variant name "{requested_js_variant}" requested. '
|
||||
f'Valid choices are: {", ".join(self._PLAYER_JS_VARIANT_MAP)}', only_once=True)
|
||||
|
||||
return urljoin('https://www.youtube.com', player_url)
|
||||
return self._construct_player_url(player_url=player_url)
|
||||
|
||||
def _download_player_url(self, video_id, fatal=False):
|
||||
if player_id_override := self._get_player_js_version()[1]:
|
||||
self.write_debug(f'Forcing player {player_id_override}', only_once=True)
|
||||
return self._construct_player_url(player_id=player_id_override)
|
||||
|
||||
iframe_webpage = self._download_webpage_with_retries(
|
||||
'https://www.youtube.com/iframe_api',
|
||||
note='Downloading iframe API JS',
|
||||
@@ -2060,9 +2095,9 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
player_version = self._search_regex(
|
||||
r'player\\?/([0-9a-fA-F]{8})\\?/', iframe_webpage, 'player version', fatal=fatal)
|
||||
if player_version:
|
||||
return f'https://www.youtube.com/s/player/{player_version}/player_ias.vflset/en_US/base.js'
|
||||
return self._construct_player_url(player_id=player_version)
|
||||
|
||||
def _player_js_cache_key(self, player_url):
|
||||
def _get_player_id_variant_and_path(self, player_url):
|
||||
player_id = self._extract_player_info(player_url)
|
||||
player_path = remove_start(urllib.parse.urlparse(player_url).path, f'/s/player/{player_id}/')
|
||||
variant = self._INVERSE_PLAYER_JS_VARIANT_MAP.get(player_path) or next((
|
||||
@@ -2072,8 +2107,13 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
self.write_debug(
|
||||
f'Unable to determine player JS variant\n'
|
||||
f' player = {player_url}', only_once=True)
|
||||
return player_id, variant, player_path
|
||||
|
||||
def _player_js_cache_key(self, player_url):
|
||||
player_id, variant, player_path = self._get_player_id_variant_and_path(player_url)
|
||||
if not variant:
|
||||
variant = re.sub(r'[^a-zA-Z0-9]', '_', remove_end(player_path, '.js'))
|
||||
return join_nonempty(player_id, variant)
|
||||
return f'{player_id}-{variant}'
|
||||
|
||||
def _signature_cache_id(self, example_sig):
|
||||
""" Return a string representation of a signature """
|
||||
|
@@ -1,8 +1,8 @@
|
||||
# Autogenerated by devscripts/update-version.py
|
||||
|
||||
__version__ = '2025.09.23'
|
||||
__version__ = '2025.09.26'
|
||||
|
||||
RELEASE_GIT_HEAD = '2e81e298cdce23afadb06a95836284acb38f7018'
|
||||
RELEASE_GIT_HEAD = '12b57d2858845c0c7fb33bf9aa8ed7be6905535d'
|
||||
|
||||
VARIANT = None
|
||||
|
||||
@@ -12,4 +12,4 @@ CHANNEL = 'stable'
|
||||
|
||||
ORIGIN = 'yt-dlp/yt-dlp'
|
||||
|
||||
_pkg_version = '2025.09.23'
|
||||
_pkg_version = '2025.09.26'
|
||||
|
Reference in New Issue
Block a user