mirror of
https://github.com/yt-dlp/yt-dlp.git
synced 2026-04-29 12:05:55 +00:00
Compare commits
4 Commits
e2ea6bd6ab
...
9ab4777b97
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ab4777b97 | ||
|
|
dde5eab3b3 | ||
|
|
23b8465063 | ||
|
|
d20f58d721 |
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@ -335,7 +335,7 @@ jobs:
|
|||||||
# We need to fuse our own universal2 wheels for curl_cffi
|
# We need to fuse our own universal2 wheels for curl_cffi
|
||||||
python3 -m pip install -U 'delocate==0.11.0'
|
python3 -m pip install -U 'delocate==0.11.0'
|
||||||
mkdir curl_cffi_whls curl_cffi_universal2
|
mkdir curl_cffi_whls curl_cffi_universal2
|
||||||
python3 devscripts/install_deps.py --print --omit-default --include-extra curl-cffi > requirements.txt
|
python3 devscripts/install_deps.py --print --omit-default --include-extra build-curl-cffi > requirements.txt
|
||||||
for platform in "macosx_11_0_arm64" "macosx_11_0_x86_64"; do
|
for platform in "macosx_11_0_arm64" "macosx_11_0_x86_64"; do
|
||||||
python3 -m pip download \
|
python3 -m pip download \
|
||||||
--only-binary=:all: \
|
--only-binary=:all: \
|
||||||
@ -464,7 +464,7 @@ jobs:
|
|||||||
if ("${Env:ARCH}" -eq "x86") {
|
if ("${Env:ARCH}" -eq "x86") {
|
||||||
python devscripts/install_deps.py
|
python devscripts/install_deps.py
|
||||||
} else {
|
} else {
|
||||||
python devscripts/install_deps.py --include-extra curl-cffi
|
python devscripts/install_deps.py --include-extra build-curl-cffi
|
||||||
}
|
}
|
||||||
|
|
||||||
- name: Prepare
|
- name: Prepare
|
||||||
|
|||||||
@ -1856,7 +1856,7 @@ The following extractors use this feature:
|
|||||||
#### youtube
|
#### 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
|
* `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
|
* `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_sdkless`, `android_vr`, `tv`, `tv_simply`, `tv_downgraded`, and `tv_embedded`. By default, `tv,android_sdkless,web` is used. If no JavaScript runtime/engine is available, then `android_sdkless,web_safari,web` is used. If logged-in cookies are passed to yt-dlp, then `tv_downgraded,web_safari,web` is used for free accounts and `tv_downgraded,web_creator,web` is used for 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_sdkless`, `android_vr`, `tv`, `tv_simply`, `tv_downgraded`, and `tv_embedded`. By default, `android_sdkless,web,web_safari` is used. If no JavaScript runtime/engine is available, then only `android_sdkless` is used. If logged-in cookies are passed to yt-dlp, then `tv_downgraded,web,web_safari` is used for free accounts and `tv_downgraded,web_creator,web` is used for 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
|
* `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
|
* `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.
|
* `player_params`: YouTube player parameters to use for player requests. Will overwrite any default ones set by yt-dlp.
|
||||||
|
|||||||
@ -20,7 +20,7 @@ INCLUDES=(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if [[ -z "${EXCLUDE_CURL_CFFI:-}" ]]; then
|
if [[ -z "${EXCLUDE_CURL_CFFI:-}" ]]; then
|
||||||
INCLUDES+=(--include-extra curl-cffi)
|
INCLUDES+=(--include-extra build-curl-cffi)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
runpy -m venv /yt-dlp-build-venv
|
runpy -m venv /yt-dlp-build-venv
|
||||||
|
|||||||
@ -59,12 +59,19 @@ default = [
|
|||||||
"yt-dlp-ejs==0.3.2",
|
"yt-dlp-ejs==0.3.2",
|
||||||
]
|
]
|
||||||
curl-cffi = [
|
curl-cffi = [
|
||||||
"curl-cffi>=0.5.10,!=0.6.*,!=0.7.*,!=0.8.*,!=0.9.*,<0.14; implementation_name=='cpython'",
|
"curl-cffi>=0.5.10,!=0.6.*,!=0.7.*,!=0.8.*,!=0.9.*,<0.15; implementation_name=='cpython'",
|
||||||
|
]
|
||||||
|
build-curl-cffi = [
|
||||||
|
"curl-cffi==0.13.0; sys_platform=='darwin' or (sys_platform=='linux' and platform_machine!='armv7l')",
|
||||||
|
"curl-cffi==0.14.0; sys_platform=='win32' or (sys_platform=='linux' and platform_machine=='armv7l')",
|
||||||
]
|
]
|
||||||
secretstorage = [
|
secretstorage = [
|
||||||
"cffi",
|
"cffi",
|
||||||
"secretstorage",
|
"secretstorage",
|
||||||
]
|
]
|
||||||
|
deno = [
|
||||||
|
"deno>=2.6.5", # v2.6.5 fixes installation of incompatible binaries
|
||||||
|
]
|
||||||
build = [
|
build = [
|
||||||
"build",
|
"build",
|
||||||
"hatchling>=1.27.0",
|
"hatchling>=1.27.0",
|
||||||
|
|||||||
@ -99,7 +99,7 @@ INNERTUBE_CLIENTS = {
|
|||||||
'INNERTUBE_CONTEXT': {
|
'INNERTUBE_CONTEXT': {
|
||||||
'client': {
|
'client': {
|
||||||
'clientName': 'WEB',
|
'clientName': 'WEB',
|
||||||
'clientVersion': '2.20250925.01.00',
|
'clientVersion': '2.20260114.08.00',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'INNERTUBE_CONTEXT_CLIENT_NAME': 1,
|
'INNERTUBE_CONTEXT_CLIENT_NAME': 1,
|
||||||
@ -112,7 +112,7 @@ INNERTUBE_CLIENTS = {
|
|||||||
'INNERTUBE_CONTEXT': {
|
'INNERTUBE_CONTEXT': {
|
||||||
'client': {
|
'client': {
|
||||||
'clientName': 'WEB',
|
'clientName': 'WEB',
|
||||||
'clientVersion': '2.20250925.01.00',
|
'clientVersion': '2.20260114.08.00',
|
||||||
'userAgent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Safari/605.1.15,gzip(gfe)',
|
'userAgent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Safari/605.1.15,gzip(gfe)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -125,7 +125,7 @@ INNERTUBE_CLIENTS = {
|
|||||||
'INNERTUBE_CONTEXT': {
|
'INNERTUBE_CONTEXT': {
|
||||||
'client': {
|
'client': {
|
||||||
'clientName': 'WEB_EMBEDDED_PLAYER',
|
'clientName': 'WEB_EMBEDDED_PLAYER',
|
||||||
'clientVersion': '1.20250923.21.00',
|
'clientVersion': '1.20260115.01.00',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'INNERTUBE_CONTEXT_CLIENT_NAME': 56,
|
'INNERTUBE_CONTEXT_CLIENT_NAME': 56,
|
||||||
@ -136,7 +136,7 @@ INNERTUBE_CLIENTS = {
|
|||||||
'INNERTUBE_CONTEXT': {
|
'INNERTUBE_CONTEXT': {
|
||||||
'client': {
|
'client': {
|
||||||
'clientName': 'WEB_REMIX',
|
'clientName': 'WEB_REMIX',
|
||||||
'clientVersion': '1.20250922.03.00',
|
'clientVersion': '1.20260114.03.00',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'INNERTUBE_CONTEXT_CLIENT_NAME': 67,
|
'INNERTUBE_CONTEXT_CLIENT_NAME': 67,
|
||||||
@ -166,7 +166,7 @@ INNERTUBE_CLIENTS = {
|
|||||||
'INNERTUBE_CONTEXT': {
|
'INNERTUBE_CONTEXT': {
|
||||||
'client': {
|
'client': {
|
||||||
'clientName': 'WEB_CREATOR',
|
'clientName': 'WEB_CREATOR',
|
||||||
'clientVersion': '1.20250922.03.00',
|
'clientVersion': '1.20260114.05.00',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'INNERTUBE_CONTEXT_CLIENT_NAME': 62,
|
'INNERTUBE_CONTEXT_CLIENT_NAME': 62,
|
||||||
@ -195,9 +195,9 @@ INNERTUBE_CLIENTS = {
|
|||||||
'INNERTUBE_CONTEXT': {
|
'INNERTUBE_CONTEXT': {
|
||||||
'client': {
|
'client': {
|
||||||
'clientName': 'ANDROID',
|
'clientName': 'ANDROID',
|
||||||
'clientVersion': '20.10.38',
|
'clientVersion': '21.02.35',
|
||||||
'androidSdkVersion': 30,
|
'androidSdkVersion': 30,
|
||||||
'userAgent': 'com.google.android.youtube/20.10.38 (Linux; U; Android 11) gzip',
|
'userAgent': 'com.google.android.youtube/21.02.35 (Linux; U; Android 11) gzip',
|
||||||
'osName': 'Android',
|
'osName': 'Android',
|
||||||
'osVersion': '11',
|
'osVersion': '11',
|
||||||
},
|
},
|
||||||
@ -228,8 +228,8 @@ INNERTUBE_CLIENTS = {
|
|||||||
'INNERTUBE_CONTEXT': {
|
'INNERTUBE_CONTEXT': {
|
||||||
'client': {
|
'client': {
|
||||||
'clientName': 'ANDROID',
|
'clientName': 'ANDROID',
|
||||||
'clientVersion': '20.10.38',
|
'clientVersion': '21.02.35',
|
||||||
'userAgent': 'com.google.android.youtube/20.10.38 (Linux; U; Android 11) gzip',
|
'userAgent': 'com.google.android.youtube/21.02.35 (Linux; U; Android 11) gzip',
|
||||||
'osName': 'Android',
|
'osName': 'Android',
|
||||||
'osVersion': '11',
|
'osVersion': '11',
|
||||||
},
|
},
|
||||||
@ -242,11 +242,11 @@ INNERTUBE_CLIENTS = {
|
|||||||
'INNERTUBE_CONTEXT': {
|
'INNERTUBE_CONTEXT': {
|
||||||
'client': {
|
'client': {
|
||||||
'clientName': 'ANDROID_VR',
|
'clientName': 'ANDROID_VR',
|
||||||
'clientVersion': '1.65.10',
|
'clientVersion': '1.71.26',
|
||||||
'deviceMake': 'Oculus',
|
'deviceMake': 'Oculus',
|
||||||
'deviceModel': 'Quest 3',
|
'deviceModel': 'Quest 3',
|
||||||
'androidSdkVersion': 32,
|
'androidSdkVersion': 32,
|
||||||
'userAgent': 'com.google.android.apps.youtube.vr.oculus/1.65.10 (Linux; U; Android 12L; eureka-user Build/SQ3A.220605.009.A1) gzip',
|
'userAgent': 'com.google.android.apps.youtube.vr.oculus/1.71.26 (Linux; U; Android 12L; eureka-user Build/SQ3A.220605.009.A1) gzip',
|
||||||
'osName': 'Android',
|
'osName': 'Android',
|
||||||
'osVersion': '12L',
|
'osVersion': '12L',
|
||||||
},
|
},
|
||||||
@ -260,10 +260,10 @@ INNERTUBE_CLIENTS = {
|
|||||||
'INNERTUBE_CONTEXT': {
|
'INNERTUBE_CONTEXT': {
|
||||||
'client': {
|
'client': {
|
||||||
'clientName': 'IOS',
|
'clientName': 'IOS',
|
||||||
'clientVersion': '20.10.4',
|
'clientVersion': '21.02.3',
|
||||||
'deviceMake': 'Apple',
|
'deviceMake': 'Apple',
|
||||||
'deviceModel': 'iPhone16,2',
|
'deviceModel': 'iPhone16,2',
|
||||||
'userAgent': 'com.google.ios.youtube/20.10.4 (iPhone16,2; U; CPU iOS 18_3_2 like Mac OS X;)',
|
'userAgent': 'com.google.ios.youtube/21.02.3 (iPhone16,2; U; CPU iOS 18_3_2 like Mac OS X;)',
|
||||||
'osName': 'iPhone',
|
'osName': 'iPhone',
|
||||||
'osVersion': '18.3.2.22D82',
|
'osVersion': '18.3.2.22D82',
|
||||||
},
|
},
|
||||||
@ -291,7 +291,7 @@ INNERTUBE_CLIENTS = {
|
|||||||
'INNERTUBE_CONTEXT': {
|
'INNERTUBE_CONTEXT': {
|
||||||
'client': {
|
'client': {
|
||||||
'clientName': 'MWEB',
|
'clientName': 'MWEB',
|
||||||
'clientVersion': '2.20250925.01.00',
|
'clientVersion': '2.20260115.01.00',
|
||||||
# mweb previously did not require PO Token with this UA
|
# mweb previously did not require PO Token with this UA
|
||||||
'userAgent': 'Mozilla/5.0 (iPad; CPU OS 16_7_10 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1,gzip(gfe)',
|
'userAgent': 'Mozilla/5.0 (iPad; CPU OS 16_7_10 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1,gzip(gfe)',
|
||||||
},
|
},
|
||||||
@ -322,7 +322,7 @@ INNERTUBE_CLIENTS = {
|
|||||||
'INNERTUBE_CONTEXT': {
|
'INNERTUBE_CONTEXT': {
|
||||||
'client': {
|
'client': {
|
||||||
'clientName': 'TVHTML5',
|
'clientName': 'TVHTML5',
|
||||||
'clientVersion': '7.20250923.13.00',
|
'clientVersion': '7.20260114.12.00',
|
||||||
'userAgent': 'Mozilla/5.0 (ChromiumStylePlatform) Cobalt/Version',
|
'userAgent': 'Mozilla/5.0 (ChromiumStylePlatform) Cobalt/Version',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -335,7 +335,7 @@ INNERTUBE_CLIENTS = {
|
|||||||
'INNERTUBE_CONTEXT': {
|
'INNERTUBE_CONTEXT': {
|
||||||
'client': {
|
'client': {
|
||||||
'clientName': 'TVHTML5',
|
'clientName': 'TVHTML5',
|
||||||
'clientVersion': '5.20251105',
|
'clientVersion': '5.20260114',
|
||||||
'userAgent': 'Mozilla/5.0 (ChromiumStylePlatform) Cobalt/Version',
|
'userAgent': 'Mozilla/5.0 (ChromiumStylePlatform) Cobalt/Version',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import re
|
|||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import traceback
|
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
from ._base import (
|
from ._base import (
|
||||||
@ -63,6 +62,7 @@ from ...utils import (
|
|||||||
unescapeHTML,
|
unescapeHTML,
|
||||||
unified_strdate,
|
unified_strdate,
|
||||||
unsmuggle_url,
|
unsmuggle_url,
|
||||||
|
update_url,
|
||||||
update_url_query,
|
update_url_query,
|
||||||
url_or_none,
|
url_or_none,
|
||||||
urljoin,
|
urljoin,
|
||||||
@ -145,9 +145,9 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|||||||
r'\b(?P<id>vfl[a-zA-Z0-9_-]+)\b.*?\.js$',
|
r'\b(?P<id>vfl[a-zA-Z0-9_-]+)\b.*?\.js$',
|
||||||
)
|
)
|
||||||
_SUBTITLE_FORMATS = ('json3', 'srv1', 'srv2', 'srv3', 'ttml', 'srt', 'vtt')
|
_SUBTITLE_FORMATS = ('json3', 'srv1', 'srv2', 'srv3', 'ttml', 'srt', 'vtt')
|
||||||
_DEFAULT_CLIENTS = ('tv', 'android_sdkless', 'web')
|
_DEFAULT_CLIENTS = ('android_sdkless', 'web', 'web_safari')
|
||||||
_DEFAULT_JSLESS_CLIENTS = ('android_sdkless', 'web_safari', 'web')
|
_DEFAULT_JSLESS_CLIENTS = ('android_sdkless',)
|
||||||
_DEFAULT_AUTHED_CLIENTS = ('tv_downgraded', 'web_safari', 'web')
|
_DEFAULT_AUTHED_CLIENTS = ('tv_downgraded', 'web', 'web_safari')
|
||||||
# Premium does not require POT (except for subtitles)
|
# Premium does not require POT (except for subtitles)
|
||||||
_DEFAULT_PREMIUM_CLIENTS = ('tv_downgraded', 'web_creator', 'web')
|
_DEFAULT_PREMIUM_CLIENTS = ('tv_downgraded', 'web_creator', 'web')
|
||||||
|
|
||||||
@ -2193,64 +2193,32 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|||||||
self._code_cache[player_js_key] = code
|
self._code_cache[player_js_key] = code
|
||||||
return self._code_cache.get(player_js_key)
|
return self._code_cache.get(player_js_key)
|
||||||
|
|
||||||
def _sig_spec_cache_id(self, player_url, spec_id):
|
def _load_player_data_from_cache(self, name, player_url, *cache_keys, use_disk_cache=False):
|
||||||
return join_nonempty(self._player_js_cache_key(player_url), str(spec_id))
|
cache_id = (f'youtube-{name}', self._player_js_cache_key(player_url), *map(str_or_none, cache_keys))
|
||||||
|
if cache_id in self._player_cache:
|
||||||
|
return self._player_cache[cache_id]
|
||||||
|
|
||||||
def _load_sig_spec_from_cache(self, spec_cache_id):
|
if not use_disk_cache:
|
||||||
# This is almost identical to _load_player_data_from_cache
|
return None
|
||||||
# I hate it
|
|
||||||
if spec_cache_id in self._player_cache:
|
|
||||||
return self._player_cache[spec_cache_id]
|
|
||||||
spec = self.cache.load('youtube-sigfuncs', spec_cache_id, min_ver='2025.07.21')
|
|
||||||
if spec:
|
|
||||||
self._player_cache[spec_cache_id] = spec
|
|
||||||
return spec
|
|
||||||
|
|
||||||
def _store_sig_spec_to_cache(self, spec_cache_id, spec):
|
data = self.cache.load(cache_id[0], join_nonempty(*cache_id[1:]), min_ver='2025.07.21')
|
||||||
if spec_cache_id not in self._player_cache:
|
|
||||||
self._player_cache[spec_cache_id] = spec
|
|
||||||
self.cache.store('youtube-sigfuncs', spec_cache_id, spec)
|
|
||||||
|
|
||||||
def _load_player_data_from_cache(self, name, player_url):
|
|
||||||
cache_id = (f'youtube-{name}', self._player_js_cache_key(player_url))
|
|
||||||
|
|
||||||
if data := self._player_cache.get(cache_id):
|
|
||||||
return data
|
|
||||||
|
|
||||||
data = self.cache.load(*cache_id, min_ver='2025.07.21')
|
|
||||||
if data:
|
if data:
|
||||||
self._player_cache[cache_id] = data
|
self._player_cache[cache_id] = data
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def _cached(self, func, *cache_id):
|
def _store_player_data_to_cache(self, data, name, player_url, *cache_keys, use_disk_cache=False):
|
||||||
def inner(*args, **kwargs):
|
cache_id = (f'youtube-{name}', self._player_js_cache_key(player_url), *map(str_or_none, cache_keys))
|
||||||
if cache_id not in self._player_cache:
|
if cache_id not in self._player_cache:
|
||||||
try:
|
|
||||||
self._player_cache[cache_id] = func(*args, **kwargs)
|
|
||||||
except ExtractorError as e:
|
|
||||||
self._player_cache[cache_id] = e
|
|
||||||
except Exception as e:
|
|
||||||
self._player_cache[cache_id] = ExtractorError(traceback.format_exc(), cause=e)
|
|
||||||
|
|
||||||
ret = self._player_cache[cache_id]
|
|
||||||
if isinstance(ret, Exception):
|
|
||||||
raise ret
|
|
||||||
return ret
|
|
||||||
return inner
|
|
||||||
|
|
||||||
def _store_player_data_to_cache(self, name, player_url, data):
|
|
||||||
cache_id = (f'youtube-{name}', self._player_js_cache_key(player_url))
|
|
||||||
if cache_id not in self._player_cache:
|
|
||||||
self.cache.store(*cache_id, data)
|
|
||||||
self._player_cache[cache_id] = data
|
self._player_cache[cache_id] = data
|
||||||
|
if use_disk_cache:
|
||||||
|
self.cache.store(cache_id[0], join_nonempty(*cache_id[1:]), data)
|
||||||
|
|
||||||
def _extract_signature_timestamp(self, video_id, player_url, ytcfg=None, fatal=False):
|
def _extract_signature_timestamp(self, video_id, player_url, ytcfg=None, fatal=False):
|
||||||
"""
|
"""
|
||||||
Extract signatureTimestamp (sts)
|
Extract signatureTimestamp (sts)
|
||||||
Required to tell API what sig/player version is in use.
|
Required to tell API what sig/player version is in use.
|
||||||
"""
|
"""
|
||||||
CACHE_ENABLED = False # TODO: enable when preprocessed player JS cache is solved/enabled
|
|
||||||
|
|
||||||
player_sts_override = self._get_player_js_version()[0]
|
player_sts_override = self._get_player_js_version()[0]
|
||||||
if player_sts_override:
|
if player_sts_override:
|
||||||
@ -2267,15 +2235,17 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|||||||
self.report_warning(error_msg)
|
self.report_warning(error_msg)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if CACHE_ENABLED and (sts := self._load_player_data_from_cache('sts', player_url)):
|
# TODO: Pass `use_disk_cache=True` when preprocessed player JS cache is solved
|
||||||
|
if sts := self._load_player_data_from_cache('sts', player_url):
|
||||||
return sts
|
return sts
|
||||||
|
|
||||||
if code := self._load_player(video_id, player_url, fatal=fatal):
|
if code := self._load_player(video_id, player_url, fatal=fatal):
|
||||||
sts = int_or_none(self._search_regex(
|
sts = int_or_none(self._search_regex(
|
||||||
r'(?:signatureTimestamp|sts)\s*:\s*(?P<sts>[0-9]{5})', code,
|
r'(?:signatureTimestamp|sts)\s*:\s*(?P<sts>[0-9]{5})', code,
|
||||||
'JS player signature timestamp', group='sts', fatal=fatal))
|
'JS player signature timestamp', group='sts', fatal=fatal))
|
||||||
if CACHE_ENABLED and sts:
|
if sts:
|
||||||
self._store_player_data_to_cache('sts', player_url, sts)
|
# TODO: Pass `use_disk_cache=True` when preprocessed player JS cache is solved
|
||||||
|
self._store_player_data_to_cache(sts, 'sts', player_url)
|
||||||
|
|
||||||
return sts
|
return sts
|
||||||
|
|
||||||
@ -2793,7 +2763,8 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|||||||
'WEB_PLAYER_CONTEXT_CONFIGS', ..., 'serializedExperimentFlags', {urllib.parse.parse_qs}))
|
'WEB_PLAYER_CONTEXT_CONFIGS', ..., 'serializedExperimentFlags', {urllib.parse.parse_qs}))
|
||||||
if 'true' in traverse_obj(experiments, (..., 'html5_generate_content_po_token', -1)):
|
if 'true' in traverse_obj(experiments, (..., 'html5_generate_content_po_token', -1)):
|
||||||
self.write_debug(
|
self.write_debug(
|
||||||
f'{video_id}: Detected experiment to bind GVS PO Token to video id.', only_once=True)
|
f'{video_id}: Detected experiment to bind GVS PO Token '
|
||||||
|
f'to video ID for {client} client', only_once=True)
|
||||||
gvs_bind_to_video_id = True
|
gvs_bind_to_video_id = True
|
||||||
|
|
||||||
# GVS WebPO Token is bound to visitor_data / Visitor ID when logged out.
|
# GVS WebPO Token is bound to visitor_data / Visitor ID when logged out.
|
||||||
@ -3233,6 +3204,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|||||||
'audio_quality_ultralow', 'audio_quality_low', 'audio_quality_medium', 'audio_quality_high', # Audio only formats
|
'audio_quality_ultralow', 'audio_quality_low', 'audio_quality_medium', 'audio_quality_high', # Audio only formats
|
||||||
'small', 'medium', 'large', 'hd720', 'hd1080', 'hd1440', 'hd2160', 'hd2880', 'highres',
|
'small', 'medium', 'large', 'hd720', 'hd1080', 'hd1440', 'hd2160', 'hd2880', 'highres',
|
||||||
])
|
])
|
||||||
|
skip_player_js = 'js' in self._configuration_arg('player_skip')
|
||||||
format_types = self._configuration_arg('formats')
|
format_types = self._configuration_arg('formats')
|
||||||
all_formats = 'duplicate' in format_types
|
all_formats = 'duplicate' in format_types
|
||||||
if self._configuration_arg('include_duplicate_formats'):
|
if self._configuration_arg('include_duplicate_formats'):
|
||||||
@ -3278,6 +3250,98 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|||||||
return language_code, DEFAULT_LANG_VALUE
|
return language_code, DEFAULT_LANG_VALUE
|
||||||
return language_code, -1
|
return language_code, -1
|
||||||
|
|
||||||
|
def get_manifest_n_challenge(manifest_url):
|
||||||
|
if not url_or_none(manifest_url):
|
||||||
|
return None
|
||||||
|
# Same pattern that the player JS uses to read/replace the n challenge value
|
||||||
|
return self._search_regex(
|
||||||
|
r'/n/([^/]+)/', urllib.parse.urlparse(manifest_url).path,
|
||||||
|
'n challenge', default=None)
|
||||||
|
|
||||||
|
n_challenges = set()
|
||||||
|
s_challenges = set()
|
||||||
|
|
||||||
|
def solve_js_challenges():
|
||||||
|
# Solve all n/sig challenges in bulk and store the results in self._player_cache
|
||||||
|
challenge_requests = []
|
||||||
|
if n_challenges:
|
||||||
|
challenge_requests.append(JsChallengeRequest(
|
||||||
|
type=JsChallengeType.N,
|
||||||
|
video_id=video_id,
|
||||||
|
input=NChallengeInput(challenges=list(n_challenges), player_url=player_url)))
|
||||||
|
if s_challenges:
|
||||||
|
cached_sigfuncs = set()
|
||||||
|
for spec_id in s_challenges:
|
||||||
|
if self._load_player_data_from_cache('sigfuncs', player_url, spec_id, use_disk_cache=True):
|
||||||
|
cached_sigfuncs.add(spec_id)
|
||||||
|
s_challenges.difference_update(cached_sigfuncs)
|
||||||
|
|
||||||
|
challenge_requests.append(JsChallengeRequest(
|
||||||
|
type=JsChallengeType.SIG,
|
||||||
|
video_id=video_id,
|
||||||
|
input=SigChallengeInput(
|
||||||
|
challenges=[''.join(map(chr, range(spec_id))) for spec_id in s_challenges],
|
||||||
|
player_url=player_url)))
|
||||||
|
|
||||||
|
if challenge_requests:
|
||||||
|
for _challenge_request, challenge_response in self._jsc_director.bulk_solve(challenge_requests):
|
||||||
|
if challenge_response.type == JsChallengeType.SIG:
|
||||||
|
for challenge, result in challenge_response.output.results.items():
|
||||||
|
spec_id = len(challenge)
|
||||||
|
self._store_player_data_to_cache(
|
||||||
|
[ord(c) for c in result], 'sigfuncs',
|
||||||
|
player_url, spec_id, use_disk_cache=True)
|
||||||
|
if spec_id in s_challenges:
|
||||||
|
s_challenges.remove(spec_id)
|
||||||
|
|
||||||
|
elif challenge_response.type == JsChallengeType.N:
|
||||||
|
for challenge, result in challenge_response.output.results.items():
|
||||||
|
self._store_player_data_to_cache(result, 'n', player_url, challenge)
|
||||||
|
if challenge in n_challenges:
|
||||||
|
n_challenges.remove(challenge)
|
||||||
|
|
||||||
|
# Raise warning if any challenge requests remain
|
||||||
|
# Depending on type of challenge request
|
||||||
|
help_message = (
|
||||||
|
'Ensure you have a supported JavaScript runtime and '
|
||||||
|
'challenge solver script distribution installed. '
|
||||||
|
'Review any warnings presented before this message. '
|
||||||
|
f'For more details, refer to {_EJS_WIKI_URL}')
|
||||||
|
if s_challenges:
|
||||||
|
self.report_warning(
|
||||||
|
f'Signature solving failed: Some formats may be missing. {help_message}',
|
||||||
|
video_id=video_id, only_once=True)
|
||||||
|
if n_challenges:
|
||||||
|
self.report_warning(
|
||||||
|
f'n challenge solving failed: Some formats may be missing. {help_message}',
|
||||||
|
video_id=video_id, only_once=True)
|
||||||
|
|
||||||
|
# Clear challenge sets so that any subsequent call of this function is a no-op
|
||||||
|
s_challenges.clear()
|
||||||
|
n_challenges.clear()
|
||||||
|
|
||||||
|
# 1st pass to collect all n/sig challenges so they can later be solved at once in bulk
|
||||||
|
for streaming_data in traverse_obj(player_responses, (..., 'streamingData', {dict})):
|
||||||
|
# HTTPS formats
|
||||||
|
for fmt_stream in traverse_obj(streaming_data, (('formats', 'adaptiveFormats'), ..., {dict})):
|
||||||
|
fmt_url = fmt_stream.get('url')
|
||||||
|
s_challenge = None
|
||||||
|
if not fmt_url:
|
||||||
|
sc = urllib.parse.parse_qs(fmt_stream.get('signatureCipher'))
|
||||||
|
fmt_url = traverse_obj(sc, ('url', 0, {url_or_none}))
|
||||||
|
s_challenge = traverse_obj(sc, ('s', 0))
|
||||||
|
|
||||||
|
if s_challenge:
|
||||||
|
s_challenges.add(len(s_challenge))
|
||||||
|
|
||||||
|
if n_challenge := traverse_obj(fmt_url, ({parse_qs}, 'n', 0)):
|
||||||
|
n_challenges.add(n_challenge)
|
||||||
|
|
||||||
|
# Manifest formats
|
||||||
|
n_challenges.update(traverse_obj(
|
||||||
|
streaming_data, (('hlsManifestUrl', 'dashManifestUrl'), {get_manifest_n_challenge})))
|
||||||
|
|
||||||
|
# Final pass to extract formats and solve n/sig challenges as needed
|
||||||
for pr in player_responses:
|
for pr in player_responses:
|
||||||
streaming_data = traverse_obj(pr, 'streamingData')
|
streaming_data = traverse_obj(pr, 'streamingData')
|
||||||
if not streaming_data:
|
if not streaming_data:
|
||||||
@ -3385,7 +3449,6 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|||||||
def process_https_formats():
|
def process_https_formats():
|
||||||
proto = 'https'
|
proto = 'https'
|
||||||
https_fmts = []
|
https_fmts = []
|
||||||
skip_player_js = 'js' in self._configuration_arg('player_skip')
|
|
||||||
|
|
||||||
for fmt_stream in streaming_formats:
|
for fmt_stream in streaming_formats:
|
||||||
if fmt_stream.get('targetDurationSec'):
|
if fmt_stream.get('targetDurationSec'):
|
||||||
@ -3422,19 +3485,21 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|||||||
# See: https://github.com/yt-dlp/yt-dlp/issues/14883
|
# See: https://github.com/yt-dlp/yt-dlp/issues/14883
|
||||||
get_language_code_and_preference(fmt_stream)
|
get_language_code_and_preference(fmt_stream)
|
||||||
sc = urllib.parse.parse_qs(fmt_stream.get('signatureCipher'))
|
sc = urllib.parse.parse_qs(fmt_stream.get('signatureCipher'))
|
||||||
fmt_url = url_or_none(try_get(sc, lambda x: x['url'][0]))
|
fmt_url = traverse_obj(sc, ('url', 0, {url_or_none}))
|
||||||
encrypted_sig = try_get(sc, lambda x: x['s'][0])
|
encrypted_sig = traverse_obj(sc, ('s', 0))
|
||||||
if not all((sc, fmt_url, skip_player_js or player_url, encrypted_sig)):
|
if not all((sc, fmt_url, skip_player_js or player_url, encrypted_sig)):
|
||||||
msg = f'Some {client_name} client https formats have been skipped as they are missing a URL. '
|
msg_tmpl = (
|
||||||
|
'{}Some {} client https formats have been skipped as they are missing a URL. '
|
||||||
|
'{}. See https://github.com/yt-dlp/yt-dlp/issues/12482 for more details')
|
||||||
if client_name in ('web', 'web_safari'):
|
if client_name in ('web', 'web_safari'):
|
||||||
msg += 'YouTube is forcing SABR streaming for this client. '
|
self.write_debug(msg_tmpl.format(
|
||||||
|
f'{video_id}: ', client_name,
|
||||||
|
'YouTube is forcing SABR streaming for this client'), only_once=True)
|
||||||
else:
|
else:
|
||||||
msg += (
|
msg = (
|
||||||
f'YouTube may have enabled the SABR-only streaming experiment for '
|
f'YouTube may have enabled the SABR-only streaming experiment for '
|
||||||
f'{"your account" if self.is_authenticated else "the current session"}. '
|
f'{"your account" if self.is_authenticated else "the current session"}')
|
||||||
)
|
self.report_warning(msg_tmpl.format('', client_name, msg), video_id, only_once=True)
|
||||||
msg += 'See https://github.com/yt-dlp/yt-dlp/issues/12482 for more details'
|
|
||||||
self.report_warning(msg, video_id, only_once=True)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
fmt = process_format_stream(
|
fmt = process_format_stream(
|
||||||
@ -3444,19 +3509,17 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# signature
|
# signature
|
||||||
# Attempt to load sig spec from cache
|
|
||||||
if encrypted_sig:
|
if encrypted_sig:
|
||||||
if skip_player_js:
|
if skip_player_js:
|
||||||
continue
|
continue
|
||||||
spec_cache_id = self._sig_spec_cache_id(player_url, len(encrypted_sig))
|
solve_js_challenges()
|
||||||
spec = self._load_sig_spec_from_cache(spec_cache_id)
|
spec = self._load_player_data_from_cache(
|
||||||
if spec:
|
'sigfuncs', player_url, len(encrypted_sig), use_disk_cache=True)
|
||||||
self.write_debug(f'Using cached signature function {spec_cache_id}', only_once=True)
|
if not spec:
|
||||||
fmt_url += '&{}={}'.format(traverse_obj(sc, ('sp', -1)) or 'signature',
|
continue
|
||||||
|
fmt_url += '&{}={}'.format(
|
||||||
|
traverse_obj(sc, ('sp', -1)) or 'signature',
|
||||||
solve_sig(encrypted_sig, spec))
|
solve_sig(encrypted_sig, spec))
|
||||||
else:
|
|
||||||
fmt['_jsc_s_challenge'] = encrypted_sig
|
|
||||||
fmt['_jsc_s_sc'] = sc
|
|
||||||
|
|
||||||
# n challenge
|
# n challenge
|
||||||
query = parse_qs(fmt_url)
|
query = parse_qs(fmt_url)
|
||||||
@ -3464,10 +3527,11 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|||||||
if skip_player_js:
|
if skip_player_js:
|
||||||
continue
|
continue
|
||||||
n_challenge = query['n'][0]
|
n_challenge = query['n'][0]
|
||||||
if n_challenge in self._player_cache:
|
solve_js_challenges()
|
||||||
fmt_url = update_url_query(fmt_url, {'n': self._player_cache[n_challenge]})
|
n_result = self._load_player_data_from_cache('n', player_url, n_challenge)
|
||||||
else:
|
if not n_result:
|
||||||
fmt['_jsc_n_challenge'] = n_challenge
|
continue
|
||||||
|
fmt_url = update_url_query(fmt_url, {'n': n_result})
|
||||||
|
|
||||||
if po_token:
|
if po_token:
|
||||||
fmt_url = update_url_query(fmt_url, {'pot': po_token})
|
fmt_url = update_url_query(fmt_url, {'pot': po_token})
|
||||||
@ -3484,80 +3548,6 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|||||||
|
|
||||||
https_fmts.append(fmt)
|
https_fmts.append(fmt)
|
||||||
|
|
||||||
# Bulk process sig/n handling
|
|
||||||
# Retrieve all JSC Sig and n requests for this player response in one go
|
|
||||||
n_challenges = {}
|
|
||||||
s_challenges = {}
|
|
||||||
for fmt in https_fmts:
|
|
||||||
# This will de-duplicate requests
|
|
||||||
n_challenge = fmt.pop('_jsc_n_challenge', None)
|
|
||||||
if n_challenge is not None:
|
|
||||||
n_challenges.setdefault(n_challenge, []).append(fmt)
|
|
||||||
|
|
||||||
s_challenge = fmt.pop('_jsc_s_challenge', None)
|
|
||||||
if s_challenge is not None:
|
|
||||||
s_challenges.setdefault(len(s_challenge), {}).setdefault(s_challenge, []).append(fmt)
|
|
||||||
|
|
||||||
challenge_requests = []
|
|
||||||
if n_challenges:
|
|
||||||
challenge_requests.append(JsChallengeRequest(
|
|
||||||
type=JsChallengeType.N,
|
|
||||||
video_id=video_id,
|
|
||||||
input=NChallengeInput(challenges=list(n_challenges.keys()), player_url=player_url)))
|
|
||||||
if s_challenges:
|
|
||||||
challenge_requests.append(JsChallengeRequest(
|
|
||||||
type=JsChallengeType.SIG,
|
|
||||||
video_id=video_id,
|
|
||||||
input=SigChallengeInput(challenges=[''.join(map(chr, range(spec_id))) for spec_id in s_challenges], player_url=player_url)))
|
|
||||||
|
|
||||||
if challenge_requests:
|
|
||||||
for _challenge_request, challenge_response in self._jsc_director.bulk_solve(challenge_requests):
|
|
||||||
if challenge_response.type == JsChallengeType.SIG:
|
|
||||||
for challenge, result in challenge_response.output.results.items():
|
|
||||||
spec_id = len(challenge)
|
|
||||||
spec = [ord(c) for c in result]
|
|
||||||
self._store_sig_spec_to_cache(self._sig_spec_cache_id(player_url, spec_id), spec)
|
|
||||||
s_challenge_data = s_challenges.pop(spec_id, {})
|
|
||||||
if not s_challenge_data:
|
|
||||||
continue
|
|
||||||
for s_challenge, fmts in s_challenge_data.items():
|
|
||||||
solved_challenge = solve_sig(s_challenge, spec)
|
|
||||||
for fmt in fmts:
|
|
||||||
sc = fmt.pop('_jsc_s_sc')
|
|
||||||
fmt['url'] += '&{}={}'.format(
|
|
||||||
traverse_obj(sc, ('sp', -1)) or 'signature',
|
|
||||||
solved_challenge)
|
|
||||||
|
|
||||||
elif challenge_response.type == JsChallengeType.N:
|
|
||||||
for challenge, result in challenge_response.output.results.items():
|
|
||||||
fmts = n_challenges.pop(challenge, [])
|
|
||||||
for fmt in fmts:
|
|
||||||
self._player_cache[challenge] = result
|
|
||||||
fmt['url'] = update_url_query(fmt['url'], {'n': result})
|
|
||||||
|
|
||||||
# Raise warning if any challenge requests remain
|
|
||||||
# Depending on type of challenge request
|
|
||||||
|
|
||||||
help_message = (
|
|
||||||
'Ensure you have a supported JavaScript runtime and '
|
|
||||||
'challenge solver script distribution installed. '
|
|
||||||
'Review any warnings presented before this message. '
|
|
||||||
f'For more details, refer to {_EJS_WIKI_URL}')
|
|
||||||
|
|
||||||
if s_challenges:
|
|
||||||
self.report_warning(
|
|
||||||
f'Signature solving failed: Some formats may be missing. {help_message}',
|
|
||||||
video_id=video_id, only_once=True)
|
|
||||||
if n_challenges:
|
|
||||||
self.report_warning(
|
|
||||||
f'n challenge solving failed: Some formats may be missing. {help_message}',
|
|
||||||
video_id=video_id, only_once=True)
|
|
||||||
|
|
||||||
for cfmts in list(s_challenges.values()) + list(n_challenges.values()):
|
|
||||||
for fmt in cfmts:
|
|
||||||
if fmt in https_fmts:
|
|
||||||
https_fmts.remove(fmt)
|
|
||||||
|
|
||||||
for fmt in https_fmts:
|
for fmt in https_fmts:
|
||||||
if (all_formats or 'dashy' in format_types) and fmt['filesize']:
|
if (all_formats or 'dashy' in format_types) and fmt['filesize']:
|
||||||
yield {
|
yield {
|
||||||
@ -3640,17 +3630,34 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|||||||
|
|
||||||
hls_manifest_url = 'hls' not in skip_manifests and streaming_data.get('hlsManifestUrl')
|
hls_manifest_url = 'hls' not in skip_manifests and streaming_data.get('hlsManifestUrl')
|
||||||
if hls_manifest_url:
|
if hls_manifest_url:
|
||||||
|
manifest_path = urllib.parse.urlparse(hls_manifest_url).path
|
||||||
|
if m := re.fullmatch(r'(?P<path>.+)(?P<suffix>/(?:file|playlist)/index\.m3u8)', manifest_path):
|
||||||
|
manifest_path, manifest_suffix = m.group('path', 'suffix')
|
||||||
|
else:
|
||||||
|
manifest_suffix = ''
|
||||||
|
|
||||||
|
solved_n = False
|
||||||
|
n_challenge = get_manifest_n_challenge(hls_manifest_url)
|
||||||
|
if n_challenge and not skip_player_js:
|
||||||
|
solve_js_challenges()
|
||||||
|
n_result = self._load_player_data_from_cache('n', player_url, n_challenge)
|
||||||
|
if n_result:
|
||||||
|
manifest_path = manifest_path.replace(f'/n/{n_challenge}', f'/n/{n_result}')
|
||||||
|
solved_n = n_result in manifest_path
|
||||||
|
|
||||||
pot_policy: GvsPoTokenPolicy = self._get_default_ytcfg(
|
pot_policy: GvsPoTokenPolicy = self._get_default_ytcfg(
|
||||||
client_name)['GVS_PO_TOKEN_POLICY'][StreamingProtocol.HLS]
|
client_name)['GVS_PO_TOKEN_POLICY'][StreamingProtocol.HLS]
|
||||||
require_po_token = gvs_pot_required(pot_policy, is_premium_subscriber, player_token_provided)
|
require_po_token = gvs_pot_required(pot_policy, is_premium_subscriber, player_token_provided)
|
||||||
po_token = gvs_pots.get(client_name, fetch_po_token_func(required=require_po_token or pot_policy.recommended))
|
po_token = gvs_pots.get(client_name, fetch_po_token_func(required=require_po_token or pot_policy.recommended))
|
||||||
if po_token:
|
if po_token:
|
||||||
hls_manifest_url = hls_manifest_url.rstrip('/') + f'/pot/{po_token}'
|
manifest_path = manifest_path.rstrip('/') + f'/pot/{po_token}'
|
||||||
if client_name not in gvs_pots:
|
if client_name not in gvs_pots:
|
||||||
gvs_pots[client_name] = po_token
|
gvs_pots[client_name] = po_token
|
||||||
|
|
||||||
if require_po_token and not po_token and 'missing_pot' not in self._configuration_arg('formats'):
|
if require_po_token and not po_token and 'missing_pot' not in self._configuration_arg('formats'):
|
||||||
self._report_pot_format_skipped(video_id, client_name, 'hls')
|
self._report_pot_format_skipped(video_id, client_name, 'hls')
|
||||||
else:
|
elif solved_n or not n_challenge:
|
||||||
|
hls_manifest_url = update_url(hls_manifest_url, path=f'{manifest_path}{manifest_suffix}')
|
||||||
fmts, subs = self._extract_m3u8_formats_and_subtitles(
|
fmts, subs = self._extract_m3u8_formats_and_subtitles(
|
||||||
hls_manifest_url, video_id, 'mp4', fatal=False, live=live_status == 'is_live')
|
hls_manifest_url, video_id, 'mp4', fatal=False, live=live_status == 'is_live')
|
||||||
for sub in traverse_obj(subs, (..., ..., {dict})):
|
for sub in traverse_obj(subs, (..., ..., {dict})):
|
||||||
@ -3665,17 +3672,30 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|||||||
|
|
||||||
dash_manifest_url = 'dash' not in skip_manifests and streaming_data.get('dashManifestUrl')
|
dash_manifest_url = 'dash' not in skip_manifests and streaming_data.get('dashManifestUrl')
|
||||||
if dash_manifest_url:
|
if dash_manifest_url:
|
||||||
|
manifest_path = urllib.parse.urlparse(dash_manifest_url).path
|
||||||
|
|
||||||
|
solved_n = False
|
||||||
|
n_challenge = get_manifest_n_challenge(dash_manifest_url)
|
||||||
|
if n_challenge and not skip_player_js:
|
||||||
|
solve_js_challenges()
|
||||||
|
n_result = self._load_player_data_from_cache('n', player_url, n_challenge)
|
||||||
|
if n_result:
|
||||||
|
manifest_path = manifest_path.replace(f'/n/{n_challenge}', f'/n/{n_result}')
|
||||||
|
solved_n = n_result in manifest_path
|
||||||
|
|
||||||
pot_policy: GvsPoTokenPolicy = self._get_default_ytcfg(
|
pot_policy: GvsPoTokenPolicy = self._get_default_ytcfg(
|
||||||
client_name)['GVS_PO_TOKEN_POLICY'][StreamingProtocol.DASH]
|
client_name)['GVS_PO_TOKEN_POLICY'][StreamingProtocol.DASH]
|
||||||
require_po_token = gvs_pot_required(pot_policy, is_premium_subscriber, player_token_provided)
|
require_po_token = gvs_pot_required(pot_policy, is_premium_subscriber, player_token_provided)
|
||||||
po_token = gvs_pots.get(client_name, fetch_po_token_func(required=require_po_token or pot_policy.recommended))
|
po_token = gvs_pots.get(client_name, fetch_po_token_func(required=require_po_token or pot_policy.recommended))
|
||||||
if po_token:
|
if po_token:
|
||||||
dash_manifest_url = dash_manifest_url.rstrip('/') + f'/pot/{po_token}'
|
manifest_path = manifest_path.rstrip('/') + f'/pot/{po_token}'
|
||||||
if client_name not in gvs_pots:
|
if client_name not in gvs_pots:
|
||||||
gvs_pots[client_name] = po_token
|
gvs_pots[client_name] = po_token
|
||||||
|
|
||||||
if require_po_token and not po_token and 'missing_pot' not in self._configuration_arg('formats'):
|
if require_po_token and not po_token and 'missing_pot' not in self._configuration_arg('formats'):
|
||||||
self._report_pot_format_skipped(video_id, client_name, 'dash')
|
self._report_pot_format_skipped(video_id, client_name, 'dash')
|
||||||
else:
|
elif solved_n or not n_challenge:
|
||||||
|
dash_manifest_url = update_url(dash_manifest_url, path=manifest_path)
|
||||||
formats, subs = self._extract_mpd_formats_and_subtitles(dash_manifest_url, video_id, fatal=False)
|
formats, subs = self._extract_mpd_formats_and_subtitles(dash_manifest_url, video_id, fatal=False)
|
||||||
for sub in traverse_obj(subs, (..., ..., {dict})):
|
for sub in traverse_obj(subs, (..., ..., {dict})):
|
||||||
# TODO: If DASH video requires a PO Token, do the subs also require pot?
|
# TODO: If DASH video requires a PO Token, do the subs also require pot?
|
||||||
|
|||||||
@ -33,9 +33,9 @@ if curl_cffi is None:
|
|||||||
|
|
||||||
curl_cffi_version = tuple(map(int, re.split(r'[^\d]+', curl_cffi.__version__)[:3]))
|
curl_cffi_version = tuple(map(int, re.split(r'[^\d]+', curl_cffi.__version__)[:3]))
|
||||||
|
|
||||||
if curl_cffi_version != (0, 5, 10) and not (0, 10) <= curl_cffi_version < (0, 14):
|
if curl_cffi_version != (0, 5, 10) and not (0, 10) <= curl_cffi_version < (0, 15):
|
||||||
curl_cffi._yt_dlp__version = f'{curl_cffi.__version__} (unsupported)'
|
curl_cffi._yt_dlp__version = f'{curl_cffi.__version__} (unsupported)'
|
||||||
raise ImportError('Only curl_cffi versions 0.5.10, 0.10.x, 0.11.x, 0.12.x, 0.13.x are supported')
|
raise ImportError('Only curl_cffi versions 0.5.10 and 0.10.x through 0.14.x are supported')
|
||||||
|
|
||||||
import curl_cffi.requests
|
import curl_cffi.requests
|
||||||
from curl_cffi.const import CurlECode, CurlOpt
|
from curl_cffi.const import CurlECode, CurlOpt
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import dataclasses
|
|||||||
import functools
|
import functools
|
||||||
import os.path
|
import os.path
|
||||||
import sys
|
import sys
|
||||||
|
import sysconfig
|
||||||
|
|
||||||
from ._utils import _get_exe_version_output, detect_exe_version, version_tuple
|
from ._utils import _get_exe_version_output, detect_exe_version, version_tuple
|
||||||
|
|
||||||
@ -13,6 +14,13 @@ _FALLBACK_PATHEXT = ('.COM', '.EXE', '.BAT', '.CMD')
|
|||||||
|
|
||||||
|
|
||||||
def _find_exe(basename: str) -> str:
|
def _find_exe(basename: str) -> str:
|
||||||
|
# Check in Python "scripts" path, e.g. for pipx-installed binaries
|
||||||
|
binary = os.path.join(
|
||||||
|
sysconfig.get_path('scripts'),
|
||||||
|
basename + sysconfig.get_config_var('EXE'))
|
||||||
|
if os.access(binary, os.F_OK | os.X_OK) and not os.path.isdir(binary):
|
||||||
|
return binary
|
||||||
|
|
||||||
if os.name != 'nt':
|
if os.name != 'nt':
|
||||||
return basename
|
return basename
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user