Compare commits

..

16 Commits

Author SHA1 Message Date
github-actions[bot]
990fdf36dd Release 2026.03.13
Created by: bashonly

:ci skip all
2026-03-13 08:45:00 +00:00
bashonly
92f1d99dbe [ie/youtube] Update ejs to 0.7.0 (#16231)
Closes #16118, Closes #16212
Authored by: bashonly, Grub4K

Co-authored-by: Simon Sawicki <contact@grub4k.dev>
2026-03-13 08:29:40 +00:00
bashonly
db62e438a1 [ie/tiktok] Fix challenge solving (#16223)
Fix e3f0d8b731

Closes #16199
Authored by: bashonly
2026-03-13 05:12:36 +00:00
Peter Devine
3e36cf9cdb [ie/youtube:tab] Improve description extraction (#16057)
Closes #16056
Authored by: Peter-Devine
2026-03-11 06:19:50 +00:00
Frieder Hannenheim
ae025da023 [ie/youtube:tab] Fix album extraction (#16041)
Closes #16016
Authored by: FriederHannenheim
2026-03-11 06:48:20 +01:00
bashonly
48a61d0f38 [ie/youtube] Request web_safari & web_creator client configs (#16198)
Closes #16144
Authored by: bashonly
2026-03-11 05:32:30 +00:00
SparseOrnament15
f2bd3202c0 [ie/youtube] Fix web_embedded player client (#16177)
Closes #16077
Authored by: SparseOrnament15, bashonly

Co-authored-by: bashonly <88596187+bashonly@users.noreply.github.com>
2026-03-10 23:25:13 +00:00
bashonly
7e145ac1ca [ie/youtube] Fix use_ad_playback_context extractor-arg (#16196)
Authored by: bashonly
2026-03-10 23:24:03 +00:00
gamer191
ff459e5fc0 [ie/youtube] Fix android_vr player client (#16168)
Closes #16150
Authored by: gamer191
2026-03-10 22:08:13 +00:00
github-actions[bot]
b8058cdf37 Release 2026.03.03
Created by: bashonly

:ci skip all
2026-03-03 16:36:58 +00:00
bashonly
2ecc4c3bc3 [ie/youtube] Skip webpage player response by default (#16126)
* Needed for d3165e83ff
  to be effective with the `web` player client

Authored by: bashonly
2026-03-03 16:20:22 +00:00
bashonly
d3165e83ff [ie/youtube] Force player 9f4cc5e4 (#16123)
Closes #16118
Authored by: bashonly
2026-03-03 14:48:45 +00:00
bashonly
bf4dfffe01 [ie/patreon] Fix extractors (#16112)
Closes #15218, Closes #16111
Authored by: bashonly
2026-03-02 22:55:43 +00:00
doe1080
6f796a2bff [ie/zapiks] Improve extraction (#16030)
Authored by: doe1080
2026-02-26 16:26:17 +00:00
0x∅
e3118604aa [ie/thechosen] Rework extractor (#16021)
Closes #16008
Authored by: 0xvd
2026-02-22 23:12:53 +00:00
bashonly
338dbebdb8 [ie/aenetworks] Fix extraction (#16036)
Fix 2485653859

Authored by: bashonly
2026-02-22 21:53:36 +00:00
20 changed files with 589 additions and 916 deletions

View File

@@ -874,3 +874,6 @@ LordMZTE
regulad
stastix
syphyr
FriederHannenheim
Peter-Devine
SparseOrnament15

View File

@@ -4,6 +4,31 @@
# To create a release, dispatch the https://github.com/yt-dlp/yt-dlp/actions/workflows/release.yml workflow on master
-->
### 2026.03.13
#### Extractor changes
- **tiktok**: [Fix challenge solving](https://github.com/yt-dlp/yt-dlp/commit/db62e438a15743b156ca5ebfc6dbe160e9bc1662) ([#16223](https://github.com/yt-dlp/yt-dlp/issues/16223)) by [bashonly](https://github.com/bashonly)
- **youtube**
- [Fix `android_vr` player client](https://github.com/yt-dlp/yt-dlp/commit/ff459e5fc04b1a061212672626b7bfa23ff3cdcd) ([#16168](https://github.com/yt-dlp/yt-dlp/issues/16168)) by [gamer191](https://github.com/gamer191)
- [Fix `use_ad_playback_context` extractor-arg](https://github.com/yt-dlp/yt-dlp/commit/7e145ac1cae8f891e18c9375fa23097f1dfa0b19) ([#16196](https://github.com/yt-dlp/yt-dlp/issues/16196)) by [bashonly](https://github.com/bashonly)
- [Fix `web_embedded` player client](https://github.com/yt-dlp/yt-dlp/commit/f2bd3202c0ffa3f0c0069c44ca53b625dca568bc) ([#16177](https://github.com/yt-dlp/yt-dlp/issues/16177)) by [bashonly](https://github.com/bashonly), [SparseOrnament15](https://github.com/SparseOrnament15)
- [Request `web_safari` & `web_creator` client configs](https://github.com/yt-dlp/yt-dlp/commit/48a61d0f38b156785d24df628d42892441e008c4) ([#16198](https://github.com/yt-dlp/yt-dlp/issues/16198)) by [bashonly](https://github.com/bashonly)
- [Update ejs to 0.7.0](https://github.com/yt-dlp/yt-dlp/commit/92f1d99dbe1e10d942ef0963f625dbc5bc0768aa) ([#16231](https://github.com/yt-dlp/yt-dlp/issues/16231)) by [bashonly](https://github.com/bashonly), [Grub4K](https://github.com/Grub4K)
- tab
- [Fix album extraction](https://github.com/yt-dlp/yt-dlp/commit/ae025da02364f4d085953f41fd0d32ade3c4afb9) ([#16041](https://github.com/yt-dlp/yt-dlp/issues/16041)) by [FriederHannenheim](https://github.com/FriederHannenheim)
- [Improve description extraction](https://github.com/yt-dlp/yt-dlp/commit/3e36cf9cdb12ef566416c5620a1a95b5a0221017) ([#16057](https://github.com/yt-dlp/yt-dlp/issues/16057)) by [Peter-Devine](https://github.com/Peter-Devine)
### 2026.03.03
#### Extractor changes
- **aenetworks**: [Fix extraction](https://github.com/yt-dlp/yt-dlp/commit/338dbebdb8627a95bd8f72ed86fdc2d50c8e2d14) ([#16036](https://github.com/yt-dlp/yt-dlp/issues/16036)) by [bashonly](https://github.com/bashonly)
- **patreon**: [Fix extractors](https://github.com/yt-dlp/yt-dlp/commit/bf4dfffe0164385c29a2dcb0367110babe4d4f27) ([#16112](https://github.com/yt-dlp/yt-dlp/issues/16112)) by [bashonly](https://github.com/bashonly)
- **thechosen**: [Rework extractor](https://github.com/yt-dlp/yt-dlp/commit/e3118604aa99a5514342d6a002c9b4a3fe1235b4) ([#16021](https://github.com/yt-dlp/yt-dlp/issues/16021)) by [0xvd](https://github.com/0xvd)
- **youtube**
- [Force player `9f4cc5e4`](https://github.com/yt-dlp/yt-dlp/commit/d3165e83ffc0088eef5e594927ea9ac99a6e2ce6) ([#16123](https://github.com/yt-dlp/yt-dlp/issues/16123)) by [bashonly](https://github.com/bashonly)
- [Skip webpage player response by default](https://github.com/yt-dlp/yt-dlp/commit/2ecc4c3bc300701d85e2cbaeb2b28a921a68f0f0) ([#16126](https://github.com/yt-dlp/yt-dlp/issues/16126)) by [bashonly](https://github.com/bashonly)
- **zapiks**: [Improve extraction](https://github.com/yt-dlp/yt-dlp/commit/6f796a2bff332f72c3f250207cdf10db852f6016) ([#16030](https://github.com/yt-dlp/yt-dlp/issues/16030)) by [doe1080](https://github.com/doe1080)
### 2026.02.21
#### Important changes

View File

@@ -202,9 +202,9 @@ CONTRIBUTORS: Changelog.md
# The following EJS_-prefixed variables are auto-generated by devscripts/update_ejs.py
# DO NOT EDIT!
EJS_VERSION = 0.5.0
EJS_WHEEL_NAME = yt_dlp_ejs-0.5.0-py3-none-any.whl
EJS_WHEEL_HASH = sha256:674fc0efea741d3100cdf3f0f9e123150715ee41edf47ea7a62fbdeda204bdec
EJS_VERSION = 0.7.0
EJS_WHEEL_NAME = yt_dlp_ejs-0.7.0-py3-none-any.whl
EJS_WHEEL_HASH = sha256:967e9cbe114ddfd046ff4668af18b1827b4597e2e47a83deea668a355828c798
EJS_PY_FOLDERS = yt_dlp_ejs yt_dlp_ejs/yt yt_dlp_ejs/yt/solver
EJS_PY_FILES = yt_dlp_ejs/__init__.py yt_dlp_ejs/_version.py yt_dlp_ejs/yt/__init__.py yt_dlp_ejs/yt/solver/__init__.py
EJS_JS_FOLDERS = yt_dlp_ejs/yt/solver

View File

@@ -1862,10 +1862,11 @@ The following extractors use this feature:
* `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_downgraded`, and `tv_simply`. By default, `android_vr,web,web_safari` is used. If no JavaScript runtime/engine is available, then only `android_vr` 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 successfully works around the age-restriction sometimes (e.g. if the video is embeddable), and may be added as a fallback if `android_vr` is unable to access a video. The `web_creator` client is added for age-restricted videos if account age-verification is required. Some clients, such as `web_creator` 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,-web`
* `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. Neither is skipped by default; however, if a `player_js_version` value other than `actual` is used, then `webpage_skip=player_response` is implied
* `webpage_client`: Client to use for the video webpage request. One of `web` or `web_safari` (default)
* `player_params`: YouTube player parameters to use for player requests. Will overwrite any default ones set by yt-dlp.
* `player_js_variant`: The player javascript variant to use for n/sig deciphering. The known variants are: `main`, `tcc`, `tce`, `es5`, `es6`, `es6_tcc`, `es6_tce`, `tv`, `tv_es6`, `phone`, `house`. The default is `tv`, and the others are for debugging purposes. You can use `actual` to go with what is prescribed by the site
* `player_js_version`: The player javascript version to use for n/sig deciphering, in the format of `signature_timestamp@hash` (e.g. `20348@0004de42`). The default is to use what is prescribed by the site, and can be selected with `actual`
* `player_js_version`: The player javascript version to use for n/sig deciphering, in the format of `signature_timestamp@hash` (e.g. `20348@0004de42`). The default is to use what is prescribed by the site, and can be selected with `actual`. Using any other value will imply `webpage_skip=player_response`
* `comment_sort`: `top` or `new` (default) - choose comment sorting mode (on YouTube's side)
* `max_comments`: Limit the amount of comments to gather. Comma-separated list of integers representing `max-comments,max-parents,max-replies,max-replies-per-thread,max-depth`. Default is `all,all,all,all,all`
* A `max-depth` value of `1` will discard all replies, regardless of the `max-replies` or `max-replies-per-thread` values given
@@ -1880,7 +1881,7 @@ The following extractors use this feature:
* `pot_trace`: Enable debug logging for PO Token fetching. Either `true` or `false` (default)
* `fetch_pot`: Policy to use for fetching a PO Token from providers. One of `always` (always try fetch a PO Token regardless if the client requires one for the given context), `never` (never fetch a PO Token), or `auto` (default; only fetch a PO Token if the client requires one for the given context)
* `jsc_trace`: Enable debug logging for JS Challenge fetching. Either `true` or `false` (default)
* `use_ad_playback_context`: Skip preroll ads to eliminate the mandatory wait period before download. Do NOT use this when passing premium account cookies to yt-dlp, as it will result in a loss of premium formats. Only effective with the `web`, `web_safari`, `web_music` and `mweb` player clients. Either `true` or `false` (default)
* `use_ad_playback_context`: Skip preroll ads to eliminate the mandatory wait period before download. Do NOT use this when passing premium account cookies to yt-dlp, as it will result in a loss of premium formats. Only effective with the `mweb` and `web_music` player clients. Either `true` or `false` (default)
#### youtube-ejs
* `jitless`: Run supported Javascript engines in JIT-less mode. Supported runtimes are `deno`, `node` and `bun`. Provides better security at the cost of performance/speed. Do note that `node` and `bun` are still considered insecure. Either `true` or `false` (default)

View File

@@ -55,7 +55,7 @@ default = [
"requests>=2.32.2,<3",
"urllib3>=2.0.2,<3",
"websockets>=13.0",
"yt-dlp-ejs==0.5.0",
"yt-dlp-ejs==0.7.0",
]
curl-cffi = [
"curl-cffi>=0.5.10,!=0.6.*,!=0.7.*,!=0.8.*,!=0.9.*,<0.15; implementation_name=='cpython'",

View File

@@ -1472,7 +1472,7 @@ The only reliable way to check if a site is supported is to try it.
- **theatercomplextown:ppv**: [*theatercomplextown*](## "netrc machine")
- **theatercomplextown:vod**: [*theatercomplextown*](## "netrc machine")
- **TheChosen**
- **TheChosenGroup**
- **TheChosenGroup**: (**Currently broken**)
- **TheGuardianPodcast**
- **TheGuardianPodcastPlaylist**
- **TheHighWire**

View File

@@ -53,117 +53,49 @@ class Challenge:
CHALLENGES: list[Challenge] = [
Challenge('3d3ba064', Variant.tce, JsChallengeType.N, {
'ZdZIqFPQK-Ty8wId': 'qmtUsIz04xxiNW',
'4GMrWHyKI5cEvhDO': 'N9gmEX7YhKTSmw',
# 20518
Challenge('edc3ba07', Variant.tv, JsChallengeType.N, {
'BQoJvGBkC2nj1ZZLK-': '-m-se9fQVnvEofLx',
}),
Challenge('3d3ba064', Variant.tce, JsChallengeType.SIG, {
'gN7a-hudCuAuPH6fByOk1_GNXN0yNMHShjZXS2VOgsEItAJz0tipeavEOmNdYN-wUtcEqD3bCXjc0iyKfAyZxCBGgIARwsSdQfJ2CJtt':
'ttJC2JfQdSswRAIgGBCxZyAfKyi0cjXCb3gqEctUw-NYdNmOEvaepit0zJAtIEsgOV2SXZjhSHMNy0NXNG_1kNyBf6HPuAuCduh-a7O',
}),
Challenge('5ec65609', Variant.tce, JsChallengeType.N, {
'0eRGgQWJGfT5rFHFj': '4SvMpDQH-vBJCw',
}),
Challenge('5ec65609', Variant.tce, JsChallengeType.SIG, {
'AAJAJfQdSswRQIhAMG5SN7-cAFChdrE7tLA6grH0rTMICA1mmDc0HoXgW3CAiAQQ4=CspfaF_vt82XH5yewvqcuEkvzeTsbRuHssRMyJQ=I':
'AJfQdSswRQIhAMG5SN7-cAFChdrE7tLA6grI0rTMICA1mmDc0HoXgW3CAiAQQ4HCspfaF_vt82XH5yewvqcuEkvzeTsbRuHssRMyJQ==',
}),
Challenge('6742b2b9', Variant.tce, JsChallengeType.N, {
'_HPB-7GFg1VTkn9u': 'qUAsPryAO_ByYg',
'K1t_fcB6phzuq2SF': 'Y7PcOt3VE62mog',
}),
Challenge('6742b2b9', Variant.tce, JsChallengeType.SIG, {
'MMGZJMUucirzS_SnrSPYsc85CJNnTUi6GgR5NKn-znQEICACojE8MHS6S7uYq4TGjQX_D4aPk99hNU6wbTvorvVVMgIARwsSdQfJAA':
'AJfQdSswRAIgMVVvrovTbw6UNh99kPa4D_XQjGT4qYu7S6SHM8EjoCACIEQnz-nKN5RgG6iUTnNJC58csYPSrnS_SzricuUMJZGM',
}),
Challenge('2b83d2e0', Variant.main, JsChallengeType.N, {
'0eRGgQWJGfT5rFHFj': 'euHbygrCMLksxd',
}),
Challenge('2b83d2e0', Variant.main, JsChallengeType.SIG, {
'MMGZJMUucirzS_SnrSPYsc85CJNnTUi6GgR5NKn-znQEICACojE8MHS6S7uYq4TGjQX_D4aPk99hNU6wbTvorvVVMgIARwsSdQfJA':
'-MGZJMUucirzS_SnrSPYsc85CJNnTUi6GgR5NKnMznQEICACojE8MHS6S7uYq4TGjQX_D4aPk99hNU6wbTvorvVVMgIARwsSdQfJ',
}),
Challenge('638ec5c6', Variant.main, JsChallengeType.N, {
'ZdZIqFPQK-Ty8wId': '1qov8-KM-yH',
}),
Challenge('638ec5c6', Variant.main, JsChallengeType.SIG, {
'gN7a-hudCuAuPH6fByOk1_GNXN0yNMHShjZXS2VOgsEItAJz0tipeavEOmNdYN-wUtcEqD3bCXjc0iyKfAyZxCBGgIARwsSdQfJ2CJtt':
'MhudCuAuP-6fByOk1_GNXN7gNHHShjyXS2VOgsEItAJz0tipeav0OmNdYN-wUtcEqD3bCXjc0iyKfAyZxCBGgIARwsSdQfJ2CJtt',
}),
# c1c87fb0: tce variant broke sig solving; n and main variant are added only for regression testing
Challenge('c1c87fb0', Variant.main, JsChallengeType.N, {
'ZdZIqFPQK-Ty8wId': 'jCHBK5GuAFNa2',
}),
Challenge('c1c87fb0', Variant.main, JsChallengeType.SIG, {
'gN7a-hudCuAuPH6fByOk1_GNXN0yNMHShjZXS2VOgsEItAJz0tipeavEOmNdYN-wUtcEqD3bCXjc0iyKfAyZxCBGgIARwsSdQfJ2CJtt':
'ttJC2JfQdSswRAIgGBCxZyAfKyi0cjXCb3DqEctUw-NYdNmOEvaepit0zJAtIEsgOV2SXZjhSHMNy0NXNGa1kOyBf6HPuAuCduh-_',
}),
Challenge('c1c87fb0', Variant.tce, JsChallengeType.N, {
'ZdZIqFPQK-Ty8wId': 'jCHBK5GuAFNa2',
}),
Challenge('c1c87fb0', Variant.tce, JsChallengeType.SIG, {
'gN7a-hudCuAuPH6fByOk1_GNXN0yNMHShjZXS2VOgsEItAJz0tipeavEOmNdYN-wUtcEqD3bCXjc0iyKfAyZxCBGgIARwsSdQfJ2CJtt':
'ttJC2JfQdSswRAIgGBCxZyAfKyi0cjXCb3DqEctUw-NYdNmOEvaepit0zJAtIEsgOV2SXZjhSHMNy0NXNGa1kOyBf6HPuAuCduh-_',
}),
# 4e51e895: main variant broke sig solving; n challenge is added only for regression testing
Challenge('4e51e895', Variant.main, JsChallengeType.N, {
'0eRGgQWJGfT5rFHFj': 't5kO23_msekBur',
}),
Challenge('4e51e895', Variant.main, JsChallengeType.SIG, {
'AL6p_8AwdY9yAhRzK8rYA_9n97Kizf7_9n97Kizf7_9n97Kizf7_9n97Kizf7_9n97Kizf7_9n97Kizf7':
'AwdY9yAhRzK8rYA_9n97Kizf7_9n97Kizf7_9n9pKizf7_9n97Kizf7_9n97Kizf7_9n97Kizf7',
}),
# 42c5570b: tce variant broke sig solving; n challenge is added only for regression testing
Challenge('42c5570b', Variant.tce, JsChallengeType.N, {
'ZdZIqFPQK-Ty8wId': 'CRoXjB-R-R',
}),
Challenge('42c5570b', Variant.tce, JsChallengeType.SIG, {
'gN7a-hudCuAuPH6fByOk1_GNXN0yNMHShjZXS2VOgsEItAJz0tipeavEOmNdYN-wUtcEqD3bCXjc0iyKfAyZxCBGgIARwsSdQfJ2CJtt':
'EN7a-hudCuAuPH6fByOk1_GNXN0yNMHShjZXS2VOgsEItAJz0tipeavcOmNdYN-wUtgEqD3bCXjc0iyKfAyZxCBGgIARwsSdQfJ2CJtt',
}),
# 54bd1de4: tce variant broke sig solving; n challenge is added only for regression testing
Challenge('54bd1de4', Variant.tce, JsChallengeType.N, {
'ZdZIqFPQK-Ty8wId': 'ka-slAQ31sijFN',
}),
Challenge('54bd1de4', Variant.tce, JsChallengeType.SIG, {
'gN7a-hudCuAuPH6fByOk1_GNXN0yNMHShjZXS2VOgsEItAJz0tipeavEOmNdYN-wUtcEqD3bCXjc0iyKfAyZxCBGgIARwsSdQfJ2CJtt':
'gN7a-hudCuAuPH6fByOk1_GNXN0yNMHShjZXS2VOgsEItAJz0titeavEOmNdYN-wUtcEqD3bCXjc0iyKfAyZxCBGgIARwsSdQfJ2CJtp',
}),
# 94667337: tce and es6 variants broke sig solving; n and main/tv variants are added only for regression testing
Challenge('94667337', Variant.main, JsChallengeType.N, {
'BQoJvGBkC2nj1ZZLK-': 'ib1ShEOGoFXIIw',
}),
Challenge('94667337', Variant.main, JsChallengeType.SIG, {
Challenge('edc3ba07', Variant.tv, JsChallengeType.SIG, {
'NJAJEij0EwRgIhAI0KExTgjfPk-MPM9MAdzyyPRt=BM8-XO5tm5hlMCSVpAiEAv7eP3CURqZNSPow8BXXAoazVoXgeMP7gH9BdylHCwgw=gwzz':
'AJEij0EwRgIhAI0KExTgjfPk-MPM9MNdzyyPRtzBM8-XO5tm5hlMCSVpAiEAv7eP3CURqZNSPow8BXXAoazVoXgeMP7gH9BdylHCwgw=',
'zwg=wgwCHlydB9zg7PMegXoVzaoAXXB8woPSNZqRUC3Pe7vAEiApVSCMlh5mt5OX-8MB=tRPyyEdAM9MPM-kPfjgTxEK0IAhIgRwE0jiz',
}),
Challenge('94667337', Variant.tv, JsChallengeType.N, {
'BQoJvGBkC2nj1ZZLK-': 'ib1ShEOGoFXIIw',
# 20521
Challenge('316b61b4', Variant.tv, JsChallengeType.N, {
'IlLiA21ny7gqA2m4p37': 'GchRcsUC_WmnhOUVGV',
}),
Challenge('94667337', Variant.tv, JsChallengeType.SIG, {
Challenge('316b61b4', Variant.tv, JsChallengeType.SIG, {
'NJAJEij0EwRgIhAI0KExTgjfPk-MPM9MAdzyyPRt=BM8-XO5tm5hlMCSVpAiEAv7eP3CURqZNSPow8BXXAoazVoXgeMP7gH9BdylHCwgw=gwzz':
'AJEij0EwRgIhAI0KExTgjfPk-MPM9MNdzyyPRtzBM8-XO5tm5hlMCSVpAiEAv7eP3CURqZNSPow8BXXAoazVoXgeMP7gH9BdylHCwgw=',
'tJAJEij0EwRgIhAI0KExTgjfPk-MPM9MAdzyyPRN=BM8-XO5tm5hlMCSVpAiEAv7eP3CURqZNSPow8BXXAoazVoXgeMP7gH9BdylHCwgw=gwz',
}),
Challenge('94667337', Variant.es6, JsChallengeType.N, {
'BQoJvGBkC2nj1ZZLK-': 'ib1ShEOGoFXIIw',
# 20522
Challenge('74edf1a3', Variant.tv, JsChallengeType.N, {
'IlLiA21ny7gqA2m4p37': '9nRTxrbM1f0yHg',
'eabGFpsUKuWHXGh6FR4': 'izmYqDEY6kl7Sg',
'eabGF/ps%UK=uWHXGh6FR4': 'LACmqlhaBpiPlgE-a',
}),
Challenge('94667337', Variant.es6, JsChallengeType.SIG, {
Challenge('74edf1a3', Variant.tv, JsChallengeType.SIG, {
'NJAJEij0EwRgIhAI0KExTgjfPk-MPM9MAdzyyPRt=BM8-XO5tm5hlMCSVpAiEAv7eP3CURqZNSPow8BXXAoazVoXgeMP7gH9BdylHCwgw=gwzz':
'AJEij0EwRgIhAI0KExTgjfPk-MPM9MNdzyyPRtzBM8-XO5tm5hlMCSVpAiEAv7eP3CURqZNSPow8BXXAoazVoXgeMP7gH9BdylHCwgw=',
'NJAJEij0EwRgIhAI0KExTgjfPk-MPM9MAdzyyPRt=BM8-XO5tm5hzMCSVpAiEAv7eP3CURqZNSPow8BXXAoazVoXgeMP7gH9BdylHCwgw=gwzl',
'\x00\x01\x02%\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49':
'\x00\x01\x02%\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x40\x41\x42\x49\x44\x45\x46\x47\x48\x43',
}),
Challenge('94667337', Variant.tce, JsChallengeType.N, {
'BQoJvGBkC2nj1ZZLK-': 'ib1ShEOGoFXIIw',
# 20523
Challenge('901741ab', Variant.tv, JsChallengeType.N, {
'BQoJvGBkC2nj1ZZLK-': 'UMPovvBZRh-sjb',
}),
Challenge('94667337', Variant.tce, JsChallengeType.SIG, {
Challenge('901741ab', Variant.tv, JsChallengeType.SIG, {
'NJAJEij0EwRgIhAI0KExTgjfPk-MPM9MAdzyyPRt=BM8-XO5tm5hlMCSVpAiEAv7eP3CURqZNSPow8BXXAoazVoXgeMP7gH9BdylHCwgw=gwzz':
'AJEij0EwRgIhAI0KExTgjfPk-MPM9MNdzyyPRtzBM8-XO5tm5hlMCSVpAiEAv7eP3CURqZNSPow8BXXAoazVoXgeMP7gH9BdylHCwgw=',
'wgwCHlydB9Hg7PMegXoVzaoAXXB8woPSNZqRUC3Pe7vAEiApVSCMlhwmt5ON-8MB=5RPyyzdAM9MPM-kPfjgTxEK0IAhIgRwE0jiEJA',
}),
Challenge('94667337', Variant.es6_tce, JsChallengeType.N, {
'BQoJvGBkC2nj1ZZLK-': 'ib1ShEOGoFXIIw',
# 20524
Challenge('e7573094', Variant.tv, JsChallengeType.N, {
'IlLiA21ny7gqA2m4p37': '3KuQ3235dojTSjo4',
}),
Challenge('94667337', Variant.es6_tce, JsChallengeType.SIG, {
Challenge('e7573094', Variant.tv, JsChallengeType.SIG, {
'NJAJEij0EwRgIhAI0KExTgjfPk-MPM9MAdzyyPRt=BM8-XO5tm5hlMCSVpAiEAv7eP3CURqZNSPow8BXXAoazVoXgeMP7gH9BdylHCwgw=gwzz':
'AJEij0EwRgIhAI0KExTgjfPk-MPM9MNdzyyPRtzBM8-XO5tm5hlMCSVpAiEAv7eP3CURqZNSPow8BXXAoazVoXgeMP7gH9BdylHCwgw=',
'yEij0EwRgIhAI0KExTgjfPk-MPM9MAdzyNPRt=BM8-XO5tm5hlMCSVNAiEAvpeP3CURqZJSPow8BXXAoazVoXgeMP7gH9BdylHCwgw=g',
}),
]

View File

@@ -672,10 +672,6 @@ from .frontendmasters import (
FrontendMastersIE,
FrontendMastersLessonIE,
)
from .frontro import (
TheChosenGroupIE,
TheChosenIE,
)
from .fujitv import FujiTVFODPlus7IE
from .funk import FunkIE
from .funker530 import Funker530IE
@@ -2063,6 +2059,10 @@ from .tenplay import (
from .testurl import TestURLIE
from .tf1 import TF1IE
from .tfo import TFOIE
from .thechosen import (
TheChosenGroupIE,
TheChosenIE,
)
from .theguardian import (
TheGuardianPodcastIE,
TheGuardianPodcastPlaylistIE,

View File

@@ -91,8 +91,8 @@ class AENetworksBaseIE(ThePlatformIE): # XXX: Do not subclass from concrete IE
if filter_key == 'canonical':
webpage = self._download_webpage(url, filter_value)
graphql_video_id = self._search_regex(
r'<meta\b[^>]+\bcontent="[^"]*\btpid/(\d+)"', webpage,
'id') or self._html_search_meta('videoId', webpage, 'GraphQL video ID', fatal=True)
r'<meta\b[^>]+\bcontent="[^"]*\btpid/(\d+)"', webpage, 'id',
default=None) or self._html_search_meta('videoId', webpage, 'GraphQL video ID', fatal=True)
else:
graphql_video_id = filter_value

View File

@@ -99,66 +99,3 @@ class FrontroGroupBaseIE(FrontoBaseIE):
'modified_timestamp': ('updatedAt', {parse_iso8601}),
}),
}
class TheChosenIE(FrontroVideoBaseIE):
_CHANNEL_ID = '12884901895'
_VALID_URL = r'https?://(?:www\.)?watch\.thechosen\.tv/watch/(?P<id>[0-9]+)'
_TESTS = [{
'url': 'https://watch.thechosen.tv/watch/184683594325',
'md5': '3f878b689588c71b38ec9943c54ff5b0',
'info_dict': {
'id': '184683594325',
'ext': 'mp4',
'title': 'Season 3 Episode 2: Two by Two',
'description': 'md5:174c373756ecc8df46b403f4fcfbaf8c',
'comment_count': int,
'view_count': int,
'like_count': int,
'duration': 4212,
'thumbnail': r're:https://fastly\.frontrowcdn\.com/channels/12884901895/VIDEO_THUMBNAIL/184683594325/',
'timestamp': 1698954546,
'upload_date': '20231102',
'modified_timestamp': int,
'modified_date': str,
},
}, {
'url': 'https://watch.thechosen.tv/watch/184683596189',
'md5': 'd581562f9d29ce82f5b7770415334151',
'info_dict': {
'id': '184683596189',
'ext': 'mp4',
'title': 'Season 4 Episode 8: Humble',
'description': 'md5:20a57bead43da1cf77cd5b0fe29bbc76',
'comment_count': int,
'view_count': int,
'like_count': int,
'duration': 5092,
'thumbnail': r're:https://fastly\.frontrowcdn\.com/channels/12884901895/VIDEO_THUMBNAIL/184683596189/',
'timestamp': 1715019474,
'upload_date': '20240506',
'modified_timestamp': int,
'modified_date': str,
},
}]
class TheChosenGroupIE(FrontroGroupBaseIE):
_CHANNEL_ID = '12884901895'
_VIDEO_EXTRACTOR = TheChosenIE
_VIDEO_URL_TMPL = 'https://watch.thechosen.tv/watch/%s'
_VALID_URL = r'https?://(?:www\.)?watch\.thechosen\.tv/group/(?P<id>[0-9]+)'
_TESTS = [{
'url': 'https://watch.thechosen.tv/group/309237658592',
'info_dict': {
'id': '309237658592',
'title': 'Season 3',
'timestamp': 1746203969,
'upload_date': '20250502',
'modified_timestamp': int,
'modified_date': str,
},
'playlist_count': 8,
}]

View File

@@ -25,6 +25,7 @@ from ..utils.traversal import (
find_elements,
require,
traverse_obj,
trim_str,
value,
)
@@ -32,16 +33,15 @@ from ..utils.traversal import (
class PatreonBaseIE(InfoExtractor):
@functools.cached_property
def patreon_user_agent(self):
# Patreon mobile UA is needed to avoid triggering Cloudflare anti-bot protection.
# Newer UA yields higher res m3u8 formats for locked posts, but gives 401 if not logged-in
# Patreon mobile UA yields higher res m3u8 for locked posts, but gives 401 if not logged-in
if self._get_cookies('https://www.patreon.com/').get('session_id'):
return 'Patreon/72.2.28 (Android; Android 14; Scale/2.10)'
return 'Patreon/7.6.28 (Android; Android 11; Scale/2.10)'
return 'Patreon/126.9.0.15 (Android; Android 14; Scale/2.10)'
return None
def _call_api(self, ep, item_id, query=None, headers=None, fatal=True, note=None):
if headers is None:
headers = {}
if 'User-Agent' not in headers:
if 'User-Agent' not in headers and self.patreon_user_agent:
headers['User-Agent'] = self.patreon_user_agent
if query:
query.update({'json-api-version': 1.0})
@@ -50,7 +50,9 @@ class PatreonBaseIE(InfoExtractor):
return self._download_json(
f'https://www.patreon.com/api/{ep}',
item_id, note=note if note else 'Downloading API JSON',
query=query, fatal=fatal, headers=headers)
query=query, fatal=fatal, headers=headers,
# If not using Patreon mobile UA, we need impersonation due to Cloudflare
impersonate=not self.patreon_user_agent)
except ExtractorError as e:
if not isinstance(e.cause, HTTPError) or mimetype2ext(e.cause.response.headers.get('Content-Type')) != 'json':
raise
@@ -623,14 +625,13 @@ class PatreonCampaignIE(PatreonBaseIE):
'info_dict': {
'id': '9631148',
'title': 'Anything Else?',
'description': 'md5:2ee1db4aed2f9460c2b295825a24aa08',
'description': 'md5:b2f20eec4cb5520d9a4be4971f28add5',
'uploader': 'dan ',
'uploader_id': '13852412',
'uploader_url': 'https://www.patreon.com/anythingelse',
'channel': 'Anything Else?',
'channel_id': '9631148',
'channel_url': 'https://www.patreon.com/anythingelse',
'channel_follower_count': int,
'age_limit': 0,
'thumbnail': r're:https?://.+/.+',
},
@@ -675,16 +676,15 @@ class PatreonCampaignIE(PatreonBaseIE):
break
def _real_extract(self, url):
campaign_id, vanity = self._match_valid_url(url).group('campaign_id', 'vanity')
if campaign_id is None:
webpage = self._download_webpage(url, vanity, headers={'User-Agent': self.patreon_user_agent})
campaign_id = traverse_obj(self._search_nextjs_data(webpage, vanity, default=None), (
'props', 'pageProps', 'bootstrapEnvelope', 'pageBootstrap', 'campaign', 'data', 'id', {str}))
if not campaign_id:
campaign_id = traverse_obj(self._search_nextjs_v13_data(webpage, vanity), (
((..., 'value', 'campaign', 'data'), lambda _, v: v['type'] == 'campaign'),
'id', {str}, any, {require('campaign ID')}))
results = self._call_api('search', vanity, query={
'q': vanity,
'page[size]': '5',
})['data']
campaign_id = traverse_obj(results, (
lambda _, v: v['type'] == 'campaign-document' and v['attributes']['url'].lower().endswith(f'/{vanity.lower()}'),
'id', {trim_str(start='campaign_')}, filter, any, {require('campaign ID')}))
params = {
'json-api-use-default-includes': 'false',

View File

@@ -0,0 +1,118 @@
from .common import InfoExtractor
from .frontro import FrontroGroupBaseIE
from ..utils import (
determine_ext,
int_or_none,
url_or_none,
)
from ..utils.traversal import traverse_obj
class TheChosenIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?watch\.thechosen\.tv/(?:video|watch)/(?P<id>[0-9]+)'
_TESTS = [{
'url': 'https://watch.thechosen.tv/video/184683594325',
'md5': '3f878b689588c71b38ec9943c54ff5b0',
'info_dict': {
'id': '184683594325',
'ext': 'mp4',
'title': 'Season 3 Episode 2: Two by Two',
'description': 'md5:174c373756ecc8df46b403f4fcfbaf8c',
'duration': 4212,
'thumbnail': 'https://cas.global.ssl.fastly.net/hls-10-4/184683594325/thumbnail.png',
},
'params': {'skip_download': 'm3u8'},
}, {
'url': 'https://watch.thechosen.tv/video/184683596189',
'md5': 'd581562f9d29ce82f5b7770415334151',
'info_dict': {
'id': '184683596189',
'ext': 'mp4',
'title': 'Season 4 Episode 8: Humble',
'description': 'md5:20a57bead43da1cf77cd5b0fe29bbc76',
'duration': 5092,
'thumbnail': 'https://cdn.thechosen.media/videos/cmkvu7nn500nhfm0wpgmm6180/thumbnail.jpg',
},
'params': {'skip_download': 'm3u8'},
}, {
'url': 'https://watch.thechosen.tv/video/184683621748',
'info_dict': {
'id': '184683621748',
'ext': 'mp4',
'title': 'Season 5 Episode 2: House of Cards',
'description': 'md5:55b389cbb4b7a01d8c2d837102905617',
'duration': 3086,
'thumbnail': 'https://cdn.thechosen.media/videos/cmkolt4el000afd5zd6x0aeph/thumbnail.jpg',
},
'params': {'skip_download': 'm3u8'},
}, {
'url': 'https://watch.thechosen.tv/video/184683621750',
'info_dict': {
'id': '184683621750',
'ext': 'mp4',
'title': 'Season 5 Episode 3: Woes',
'description': 'md5:90ca3cc41316a965fd1cd3d5b3458784',
'duration': 3519,
'thumbnail': 'https://cdn.thechosen.media/videos/cmkoltsl8000dfd5z3luid3mg/thumbnail.jpg',
},
'params': {'skip_download': 'm3u8'},
}]
def _real_extract(self, url):
video_id = self._match_id(url)
metadata = self._download_json(f'https://api.watch.thechosen.tv/v1/videos/{video_id}', video_id)
formats, subtitles = [], {}
for fmt_url in traverse_obj(metadata, ('details', 'video', ..., 'url', {url_or_none})):
ext = determine_ext(fmt_url)
if ext == 'm3u8':
fmts, subs = self._extract_m3u8_formats_and_subtitles(fmt_url, video_id, 'mp4', fatal=False)
elif ext == 'mpd':
fmts, subs = self._extract_mpd_formats_and_subtitles(fmt_url, video_id, fatal=False)
else:
self.report_warning(f'Skipping unsupported format extension "{ext}"', video_id=video_id)
continue
formats.extend(fmts)
self._merge_subtitles(subs, target=subtitles)
thumbnails = []
for thumb_id, thumb_url in traverse_obj(metadata, (
('thumbs', 'thumbnails'), {dict.items}, lambda _, v: url_or_none(v[1]),
)):
thumbnails.append({
'id': thumb_id,
'url': thumb_url,
})
return {
'id': video_id,
**traverse_obj(metadata, ({
'title': ('title', {str}),
'description': ('description', {str}),
'duration': ('duration', {int_or_none}),
})),
'thumbnails': thumbnails,
'formats': formats,
'subtitles': subtitles,
}
class TheChosenGroupIE(FrontroGroupBaseIE):
_WORKING = False
_CHANNEL_ID = '12884901895'
_VIDEO_EXTRACTOR = TheChosenIE
_VIDEO_URL_TMPL = 'https://watch.thechosen.tv/watch/%s'
_VALID_URL = r'https?://(?:www\.)?watch\.thechosen\.tv/group/(?P<id>[0-9]+)'
_TESTS = [{
'url': 'https://watch.thechosen.tv/group/309237658592',
'info_dict': {
'id': '309237658592',
'title': 'Season 3',
'timestamp': 1746203969,
'upload_date': '20250502',
'modified_timestamp': int,
'modified_date': str,
},
'playlist_count': 8,
}]

View File

@@ -220,7 +220,7 @@ class TikTokBaseIE(InfoExtractor):
raise ExtractorError('Unable to extract aweme detail info', video_id=aweme_id)
return self._parse_aweme_video_app(aweme_detail)
def _solve_challenge_and_set_cookie(self, webpage):
def _solve_challenge_and_set_cookies(self, webpage):
challenge_data = traverse_obj(webpage, (
{find_element(id='cs', html=True)}, {extract_attributes}, 'class',
filter, {lambda x: f'{x}==='}, {base64.b64decode}, {json.loads}))
@@ -250,17 +250,27 @@ class TikTokBaseIE(InfoExtractor):
else:
raise ExtractorError('Unable to solve JS challenge')
cookie_value = base64.b64encode(
wci_cookie_value = base64.b64encode(
json.dumps(challenge_data, separators=(',', ':')).encode()).decode()
# At time of writing, the cookie name was _wafchallengeid
cookie_name = traverse_obj(webpage, (
# At time of writing, the wci cookie name was `_wafchallengeid`
wci_cookie_name = traverse_obj(webpage, (
{find_element(id='wci', html=True)}, {extract_attributes},
'class', {require('challenge cookie name')}))
# Actual JS sets Max-Age=1, but we need to adjust for --sleep-requests and Python slowness
expire_time = int(time.time()) + (self.get_param('sleep_interval_requests') or 0) + 2
self._set_cookie('.tiktok.com', cookie_name, cookie_value, expire_time=expire_time)
# At time of writing, the **optional** rci cookie name was `waforiginalreid`
rci_cookie_name = traverse_obj(webpage, (
{find_element(id='rci', html=True)}, {extract_attributes}, 'class'))
rci_cookie_value = traverse_obj(webpage, (
{find_element(id='rs', html=True)}, {extract_attributes}, 'class'))
# Actual JS sets Max-Age=1 for the cookies, but we'll manually clear them later instead
expire_time = int(time.time()) + (self.get_param('sleep_interval_requests') or 0) + 120
self._set_cookie('.tiktok.com', wci_cookie_name, wci_cookie_value, expire_time=expire_time)
if rci_cookie_name and rci_cookie_value:
self._set_cookie('.tiktok.com', rci_cookie_name, rci_cookie_value, expire_time=expire_time)
return wci_cookie_name, rci_cookie_name
def _extract_web_data_and_status(self, url, video_id, fatal=True):
video_data, status = {}, -1
@@ -287,7 +297,7 @@ class TikTokBaseIE(InfoExtractor):
universal_data = self._get_universal_data(webpage, video_id)
if not universal_data:
try:
self._solve_challenge_and_set_cookie(webpage)
cookie_names = self._solve_challenge_and_set_cookies(webpage)
except ExtractorError as e:
if fatal:
raise
@@ -295,6 +305,9 @@ class TikTokBaseIE(InfoExtractor):
return video_data, status
webpage = get_webpage(note='Downloading webpage with challenge cookie')
# Manually clear challenge cookies that should expire immediately after webpage request
for cookie_name in filter(None, cookie_names):
self.cookiejar.clear(domain='.tiktok.com', path='/', name=cookie_name)
if webpage is False:
return video_data, status
universal_data = self._get_universal_data(webpage, video_id)

View File

@@ -104,7 +104,6 @@ INNERTUBE_CLIENTS = {
},
'INNERTUBE_CONTEXT_CLIENT_NAME': 1,
'SUPPORTS_COOKIES': True,
'SUPPORTS_AD_PLAYBACK_CONTEXT': True,
**WEB_PO_TOKEN_POLICIES,
},
# Safari UA returns pre-merged video+audio 144p/240p/360p/720p/1080p HLS formats
@@ -118,7 +117,6 @@ INNERTUBE_CLIENTS = {
},
'INNERTUBE_CONTEXT_CLIENT_NAME': 1,
'SUPPORTS_COOKIES': True,
'SUPPORTS_AD_PLAYBACK_CONTEXT': True,
**WEB_PO_TOKEN_POLICIES,
},
'web_embedded': {
@@ -223,16 +221,17 @@ INNERTUBE_CLIENTS = {
},
'PLAYER_PO_TOKEN_POLICY': PlayerPoTokenPolicy(required=False, recommended=True),
},
# YouTube Kids videos aren't returned on this client for some reason
# "Made for kids" videos aren't available with this client
# Using a clientVersion>1.65 may return SABR streams only
'android_vr': {
'INNERTUBE_CONTEXT': {
'client': {
'clientName': 'ANDROID_VR',
'clientVersion': '1.71.26',
'clientVersion': '1.65.10',
'deviceMake': 'Oculus',
'deviceModel': 'Quest 3',
'androidSdkVersion': 32,
'userAgent': 'com.google.android.apps.youtube.vr.oculus/1.71.26 (Linux; U; Android 12L; eureka-user Build/SQ3A.220605.009.A1) gzip',
'userAgent': 'com.google.android.apps.youtube.vr.oculus/1.65.10 (Linux; U; Android 12L; eureka-user Build/SQ3A.220605.009.A1) gzip',
'osName': 'Android',
'osVersion': '12L',
},
@@ -369,7 +368,7 @@ def short_client_name(client_name):
def _fix_embedded_ytcfg(ytcfg):
ytcfg['INNERTUBE_CONTEXT'].setdefault('thirdParty', {}).update({
'embedUrl': 'https://www.youtube.com/', # Can be any valid URL
'embedUrl': 'https://www.reddit.com/', # Can be any valid non-YouTube URL
})
@@ -958,16 +957,25 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
url = {
'mweb': 'https://m.youtube.com',
'web': 'https://www.youtube.com',
'web_safari': 'https://www.youtube.com',
'web_music': 'https://music.youtube.com',
'web_creator': 'https://studio.youtube.com',
'web_embedded': f'https://www.youtube.com/embed/{video_id}?html5=1',
'tv': 'https://www.youtube.com/tv',
}.get(client)
if not url:
return {}
default_ytcfg = self._get_default_ytcfg(client)
if default_ytcfg['REQUIRE_AUTH'] and not self.is_authenticated:
return {}
webpage = self._download_webpage_with_retries(
url, video_id, note=f'Downloading {client.replace("_", " ").strip()} client config',
headers=traverse_obj(self._get_default_ytcfg(client), {
headers=traverse_obj(default_ytcfg, {
'User-Agent': ('INNERTUBE_CONTEXT', 'client', 'userAgent', {str}),
'Referer': ('INNERTUBE_CONTEXT', 'thirdParty', 'embedUrl', {str}),
}))
ytcfg = self.extract_ytcfg(video_id, webpage) or {}

View File

@@ -81,7 +81,7 @@ class YoutubeTabBaseInfoExtractor(YoutubeBaseInfoExtractor):
'reelPlayerHeaderSupportedRenderers', 'reelPlayerHeaderRenderer'))
title = self._get_text(renderer, 'title', 'headline') or self._get_text(reel_header_renderer, 'reelTitleText')
description = self._get_text(renderer, 'descriptionSnippet')
description = self._get_text(renderer, 'descriptionSnippet', ('detailedMetadataSnippets', ..., 'snippetText'))
duration = int_or_none(renderer.get('lengthSeconds'))
if duration is None:
@@ -2148,7 +2148,7 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
f'https://music.youtube.com/playlist?list={item_id[2:]}', YoutubeTabIE, item_id[2:])
elif item_id[:2] == 'MP': # Resolve albums (/[channel/browse]/MP...) to their equivalent playlist
mdata = self._extract_tab_endpoint(
f'https://music.youtube.com/channel/{item_id}', item_id, default_client='web_music')
f'https://music.youtube.com/browse/{item_id}', item_id, default_client='web_music')
murl = traverse_obj(mdata, ('microformat', 'microformatDataRenderer', 'urlCanonical'),
get_all=False, expected_type=str)
if not murl:

View File

@@ -140,11 +140,13 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
_RETURN_TYPE = 'video' # XXX: How to handle multifeed?
_SUBTITLE_FORMATS = ('json3', 'srv1', 'srv2', 'srv3', 'ttml', 'srt', 'vtt')
_DEFAULT_CLIENTS = ('android_vr', 'web', 'web_safari')
_DEFAULT_CLIENTS = ('android_vr', 'web_safari')
_DEFAULT_JSLESS_CLIENTS = ('android_vr',)
_DEFAULT_AUTHED_CLIENTS = ('tv_downgraded', 'web', 'web_safari')
_DEFAULT_AUTHED_CLIENTS = ('tv_downgraded', 'web_safari')
# Premium does not require POT (except for subtitles)
_DEFAULT_PREMIUM_CLIENTS = ('tv_downgraded', 'web_creator', 'web')
_DEFAULT_PREMIUM_CLIENTS = ('tv_downgraded', 'web_creator')
_WEBPAGE_CLIENTS = ('web', 'web_safari')
_DEFAULT_WEBPAGE_CLIENT = 'web_safari'
_GEO_BYPASS = False
@@ -1890,6 +1892,18 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
}
_INVERSE_PLAYER_JS_VARIANT_MAP = {v: k for k, v in _PLAYER_JS_VARIANT_MAP.items()}
@functools.cached_property
def _player_js_version(self):
return self._configuration_arg('player_js_version', [None])[0] or self._DEFAULT_PLAYER_JS_VERSION
@functools.cached_property
def _skipped_webpage_data(self):
skipped = set(self._configuration_arg('webpage_skip'))
# If forcing a player version, the webpage player response must be skipped
if self._player_js_version != 'actual':
skipped.add('player_response')
return skipped
@classmethod
def suitable(cls, url):
from yt_dlp.utils import parse_qs
@@ -2077,15 +2091,14 @@ 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 self._DEFAULT_PLAYER_JS_VERSION
if player_js_version == 'actual':
if self._player_js_version == 'actual':
return None, None
if not re.fullmatch(r'[0-9]{5,}@[0-9a-f]{8,}', player_js_version):
if not re.fullmatch(r'[0-9]{5,}@[0-9a-f]{8,}', self._player_js_version):
self.report_warning(
f'Invalid player JS version "{player_js_version}" specified. '
f'Invalid player JS version "{self._player_js_version}" specified. '
f'It should be "actual" or in the format of STS@HASH', only_once=True)
return None, None
return player_js_version.split('@')
return self._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'
@@ -2675,12 +2688,14 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
return {'contentCheckOk': True, 'racyCheckOk': True}
@classmethod
def _generate_player_context(cls, sts=None, use_ad_playback_context=False):
def _generate_player_context(cls, sts=None, use_ad_playback_context=False, encrypted_context=None):
context = {
'html5Preference': 'HTML5_PREF_WANTS',
}
if sts is not None:
context['signatureTimestamp'] = sts
if encrypted_context:
context['encryptedHostFlags'] = encrypted_context
playback_context = {
'contentPlaybackContext': context,
@@ -2925,7 +2940,19 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
self._configuration_arg('use_ad_playback_context', ['false'])[0] != 'false'
and traverse_obj(INNERTUBE_CLIENTS, (client, 'SUPPORTS_AD_PLAYBACK_CONTEXT', {bool})))
yt_query.update(self._generate_player_context(sts, use_ad_playback_context))
# web_embedded player requests may need to include encryptedHostFlags in its contentPlaybackContext.
# This can be detected with the embeds_enable_encrypted_host_flags_enforcement experiemnt flag,
# but there is no harm in including encryptedHostFlags with all web_embedded player requests.
encrypted_context = None
if _split_innertube_client(client)[2] == 'embedded':
encrypted_context = traverse_obj(player_ytcfg, (
'WEB_PLAYER_CONTEXT_CONFIGS', 'WEB_PLAYER_CONTEXT_CONFIG_ID_EMBEDDED_PLAYER', 'encryptedHostFlags'))
yt_query.update(
self._generate_player_context(
sts=sts,
use_ad_playback_context=use_ad_playback_context,
encrypted_context=encrypted_context))
return self._extract_response(
item_id=video_id, ep='player', query=yt_query,
@@ -3044,7 +3071,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
tried_iframe_fallback = True
pr = None
if client == webpage_client and 'player_response' not in self._configuration_arg('webpage_skip'):
if client == webpage_client and 'player_response' not in self._skipped_webpage_data:
pr = initial_pr
visitor_data = visitor_data or self._extract_visitor_data(webpage_ytcfg, initial_pr, player_ytcfg)
@@ -3827,7 +3854,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
def _download_initial_data(self, video_id, webpage, webpage_client, webpage_ytcfg):
initial_data = None
if webpage and 'initial_data' not in self._configuration_arg('webpage_skip'):
if webpage and 'initial_data' not in self._skipped_webpage_data:
initial_data = self.extract_yt_initial_data(video_id, webpage, fatal=False)
if not traverse_obj(initial_data, 'contents'):
self.report_warning('Incomplete data received in embedded initial data; re-fetching using API.')
@@ -3875,7 +3902,12 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
base_url = self.http_scheme() + '//www.youtube.com/'
webpage_url = base_url + 'watch?v=' + video_id
webpage_client = 'web'
webpage_client = self._configuration_arg('webpage_client', [self._DEFAULT_WEBPAGE_CLIENT])[0]
if webpage_client not in self._WEBPAGE_CLIENTS:
self.report_warning(
f'Invalid webpage_client "{webpage_client}" requested; '
f'falling back to {self._DEFAULT_WEBPAGE_CLIENT}', only_once=True)
webpage_client = self._DEFAULT_WEBPAGE_CLIENT
webpage, webpage_ytcfg, initial_data, is_premium_subscriber, player_responses, player_url = self._initial_extract(
url, smuggled_data, webpage_url, webpage_client, video_id)

View File

@@ -1,10 +1,10 @@
# This file is generated by devscripts/update_ejs.py. DO NOT MODIFY!
VERSION = '0.5.0'
VERSION = '0.7.0'
HASHES = {
'yt.solver.bun.lib.js': '6ff45e94de9f0ea936a183c48173cfa9ce526ee4b7544cd556428427c1dd53c8073ef0174e79b320252bf0e7c64b0032cc1cf9c4358f3fda59033b7caa01c241',
'yt.solver.core.js': '9742868113d7b0c29e24a95c8eb2c2bec7cdf95513dc7f55f523ba053c0ecf2af7dcb0138b1d933578304f0dda633a6b3bfff64e912b4c547b99dad083428c4b',
'yt.solver.core.min.js': 'aee8c3354cfd535809c871c2a517d03231f89cd184e903af82ee274bcc2e90991ef19cb3f65f2ccc858c4963856ea87f8692fe16d71209f4fc7f41c44b828e36',
'yt.solver.core.js': '84e91a8ae91684272d11f1ef0970c757e9fec9ab277fb415b976c156163dde6ae2a9857c19c1ee21c9dcd01e2f89071098a1de2dc3072cf3ceeded84537db5e4',
'yt.solver.core.min.js': 'd965ec01dcf44a0a9dea43f5935141c788471de9e8def5bf70d0b88ca656b79ca983d3e595f84b788d921dc98b900b7bf7380e9775ccb3b70a87c865482c71e3',
'yt.solver.deno.lib.js': '9c8ee3ab6c23e443a5a951e3ac73c6b8c1c8fb34335e7058a07bf99d349be5573611de00536dcd03ecd3cf34014c4e9b536081de37af3637c5390c6a6fd6a0f0',
'yt.solver.lib.js': '1ee3753a8222fc855f5c39db30a9ccbb7967dbe1fb810e86dc9a89aa073a0907f294c720e9b65427d560a35aa1ce6af19ef854d9126a05ca00afe03f72047733',
'yt.solver.lib.min.js': '8420c259ad16e99ce004e4651ac1bcabb53b4457bf5668a97a9359be9a998a789fee8ab124ee17f91a2ea8fd84e0f2b2fc8eabcaf0b16a186ba734cf422ad053',

View File

@@ -39,284 +39,8 @@ var jsc = (function (meriyah, astring) {
function isOneOf(value, ...of) {
return of.includes(value);
}
function _optionalChain$2(ops) {
let lastAccessLHS = undefined;
let value = ops[0];
let i = 1;
while (i < ops.length) {
const op = ops[i];
const fn = ops[i + 1];
i += 2;
if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) {
return undefined;
}
if (op === 'access' || op === 'optionalAccess') {
lastAccessLHS = value;
value = fn(value);
} else if (op === 'call' || op === 'optionalCall') {
value = fn((...args) => value.call(lastAccessLHS, ...args));
lastAccessLHS = undefined;
}
}
return value;
}
const nsig = {
type: 'CallExpression',
callee: { or: [{ type: 'Identifier' }, { type: 'SequenceExpression' }] },
arguments: [
{},
{
type: 'CallExpression',
callee: { type: 'Identifier', name: 'decodeURIComponent' },
arguments: [{}],
},
],
};
const nsigAssignment = {
type: 'AssignmentExpression',
left: { type: 'Identifier' },
operator: '=',
right: nsig,
};
const nsigDeclarator = {
type: 'VariableDeclarator',
id: { type: 'Identifier' },
init: nsig,
};
const logicalExpression = {
type: 'ExpressionStatement',
expression: {
type: 'LogicalExpression',
left: { type: 'Identifier' },
right: {
type: 'SequenceExpression',
expressions: [
{
type: 'AssignmentExpression',
left: { type: 'Identifier' },
operator: '=',
right: {
type: 'CallExpression',
callee: { type: 'Identifier' },
arguments: {
or: [
[
{
type: 'CallExpression',
callee: {
type: 'Identifier',
name: 'decodeURIComponent',
},
arguments: [{ type: 'Identifier' }],
optional: false,
},
],
[
{ type: 'Literal' },
{
type: 'CallExpression',
callee: {
type: 'Identifier',
name: 'decodeURIComponent',
},
arguments: [{ type: 'Identifier' }],
optional: false,
},
],
[
{ type: 'Literal' },
{ type: 'Literal' },
{
type: 'CallExpression',
callee: {
type: 'Identifier',
name: 'decodeURIComponent',
},
arguments: [{ type: 'Identifier' }],
optional: false,
},
],
],
},
optional: false,
},
},
{ type: 'CallExpression' },
],
},
operator: '&&',
},
};
const identifier$1 = {
or: [
{
type: 'ExpressionStatement',
expression: {
type: 'AssignmentExpression',
operator: '=',
left: { or: [{ type: 'Identifier' }, { type: 'MemberExpression' }] },
right: { type: 'FunctionExpression' },
},
},
{ type: 'FunctionDeclaration' },
{
type: 'VariableDeclaration',
declarations: {
anykey: [
{
type: 'VariableDeclarator',
init: { type: 'FunctionExpression' },
},
],
},
},
],
};
function extract$1(node) {
const blocks = [];
if (matchesStructure(node, identifier$1)) {
if (
node.type === 'ExpressionStatement' &&
node.expression.type === 'AssignmentExpression' &&
node.expression.right.type === 'FunctionExpression' &&
node.expression.right.params.length >= 3
) {
blocks.push(node.expression.right.body);
} else if (node.type === 'VariableDeclaration') {
for (const decl of node.declarations) {
if (
_optionalChain$2([
decl,
'access',
(_) => _.init,
'optionalAccess',
(_2) => _2.type,
]) === 'FunctionExpression' &&
decl.init.params.length >= 3
) {
blocks.push(decl.init.body);
}
}
} else if (
node.type === 'FunctionDeclaration' &&
node.params.length >= 3
) {
blocks.push(node.body);
} else {
return null;
}
} else if (
node.type === 'ExpressionStatement' &&
node.expression.type === 'SequenceExpression'
) {
for (const expr of node.expression.expressions) {
if (
expr.type === 'AssignmentExpression' &&
expr.right.type === 'FunctionExpression' &&
expr.right.params.length === 3
) {
blocks.push(expr.right.body);
}
}
} else {
return null;
}
for (const block of blocks) {
let call = null;
for (const stmt of block.body) {
if (matchesStructure(stmt, logicalExpression)) {
if (
stmt.type === 'ExpressionStatement' &&
stmt.expression.type === 'LogicalExpression' &&
stmt.expression.right.type === 'SequenceExpression' &&
stmt.expression.right.expressions[0].type ===
'AssignmentExpression' &&
stmt.expression.right.expressions[0].right.type === 'CallExpression'
) {
call = stmt.expression.right.expressions[0].right;
}
} else if (stmt.type === 'IfStatement') {
let consequent = stmt.consequent;
while (consequent.type === 'LabeledStatement') {
consequent = consequent.body;
}
if (consequent.type !== 'BlockStatement') {
continue;
}
for (const n of consequent.body) {
if (n.type !== 'VariableDeclaration') {
continue;
}
for (const decl of n.declarations) {
if (
matchesStructure(decl, nsigDeclarator) &&
_optionalChain$2([
decl,
'access',
(_3) => _3.init,
'optionalAccess',
(_4) => _4.type,
]) === 'CallExpression'
) {
call = decl.init;
break;
}
}
if (call) {
break;
}
}
} else if (stmt.type === 'ExpressionStatement') {
if (
stmt.expression.type !== 'LogicalExpression' ||
stmt.expression.operator !== '&&' ||
stmt.expression.right.type !== 'SequenceExpression'
) {
continue;
}
for (const expr of stmt.expression.right.expressions) {
if (matchesStructure(expr, nsigAssignment) && expr.type) {
if (
expr.type === 'AssignmentExpression' &&
expr.right.type === 'CallExpression'
) {
call = expr.right;
break;
}
}
}
}
if (call) {
break;
}
}
if (!call) {
continue;
}
return {
type: 'ArrowFunctionExpression',
params: [{ type: 'Identifier', name: 'sig' }],
body: {
type: 'CallExpression',
callee: call.callee,
arguments: call.arguments.map((arg) => {
if (
arg.type === 'CallExpression' &&
arg.callee.type === 'Identifier' &&
arg.callee.name === 'decodeURIComponent'
) {
return { type: 'Identifier', name: 'sig' };
}
return arg;
}),
optional: false,
},
async: false,
expression: false,
generator: false,
};
}
return null;
function generateArrowFunction(data) {
return meriyah.parse(data).body[0].expression;
}
function _optionalChain$1(ops) {
let lastAccessLHS = undefined;
@@ -341,153 +65,114 @@ var jsc = (function (meriyah, astring) {
}
const identifier = {
or: [
{
type: 'ExpressionStatement',
expression: {
type: 'AssignmentExpression',
operator: '=',
left: { or: [{ type: 'Identifier' }, { type: 'MemberExpression' }] },
right: { type: 'FunctionExpression', async: false },
},
},
{ type: 'FunctionDeclaration', async: false, id: { type: 'Identifier' } },
{
type: 'VariableDeclaration',
kind: 'var',
declarations: {
anykey: [
{
type: 'VariableDeclarator',
id: { type: 'Identifier' },
init: {
type: 'ArrayExpression',
elements: [{ type: 'Identifier' }],
},
init: { type: 'FunctionExpression', async: false },
},
],
},
},
{
type: 'ExpressionStatement',
expression: {
type: 'AssignmentExpression',
left: { type: 'Identifier' },
operator: '=',
right: {
type: 'ArrayExpression',
elements: [{ type: 'Identifier' }],
},
},
},
],
};
const catchBlockBody = [
{
type: 'ReturnStatement',
argument: {
type: 'BinaryExpression',
left: {
type: 'MemberExpression',
object: { type: 'Identifier' },
computed: true,
property: { type: 'Literal' },
optional: false,
},
right: { type: 'Identifier' },
operator: '+',
const asdasd = {
type: 'ExpressionStatement',
expression: {
type: 'CallExpression',
callee: {
type: 'MemberExpression',
object: { type: 'Identifier' },
property: {},
optional: false,
},
arguments: [
{ type: 'Literal', value: 'alr' },
{ type: 'Literal', value: 'yes' },
],
optional: false,
},
];
};
function extract(node) {
if (!matchesStructure(node, identifier)) {
let name = null;
let block = null;
switch (node.type) {
case 'ExpressionStatement': {
if (
node.expression.type === 'AssignmentExpression' &&
node.expression.left.type === 'Identifier' &&
node.expression.right.type === 'FunctionExpression' &&
node.expression.right.params.length === 1
) {
name = node.expression.left.name;
block = node.expression.right.body;
}
break;
}
case 'FunctionDeclaration': {
if (node.params.length === 1) {
name = _optionalChain$1([
node,
'access',
(_) => _.id,
'optionalAccess',
(_2) => _2.name,
]);
block = node.body;
}
break;
}
}
if (!block || !name) {
return null;
}
const tryNode = block.body.at(-2);
if (
_optionalChain$1([tryNode, 'optionalAccess', (_3) => _3.type]) !==
'TryStatement' ||
_optionalChain$1([
tryNode,
'access',
(_4) => _4.handler,
'optionalAccess',
(_5) => _5.type,
]) !== 'CatchClause'
) {
return null;
}
const catchBody = tryNode.handler.body.body;
if (matchesStructure(catchBody, catchBlockBody)) {
return makeSolverFuncFromName(name);
}
return null;
}
if (node.type === 'VariableDeclaration') {
for (const declaration of node.declarations) {
if (
declaration.type !== 'VariableDeclarator' ||
!declaration.init ||
declaration.init.type !== 'ArrayExpression' ||
declaration.init.elements.length !== 1
) {
continue;
}
const [firstElement] = declaration.init.elements;
if (firstElement && firstElement.type === 'Identifier') {
return makeSolverFuncFromName(firstElement.name);
}
const options = [];
if (node.type === 'FunctionDeclaration') {
if (
node.id &&
_optionalChain$1([
node,
'access',
(_) => _.body,
'optionalAccess',
(_2) => _2.body,
])
) {
options.push({
name: node.id,
statements: _optionalChain$1([
node,
'access',
(_3) => _3.body,
'optionalAccess',
(_4) => _4.body,
]),
});
}
} else if (node.type === 'ExpressionStatement') {
const expr = node.expression;
if (
expr.type === 'AssignmentExpression' &&
expr.left.type === 'Identifier' &&
expr.operator === '=' &&
expr.right.type === 'ArrayExpression' &&
expr.right.elements.length === 1
) {
const [firstElement] = expr.right.elements;
if (firstElement && firstElement.type === 'Identifier') {
return makeSolverFuncFromName(firstElement.name);
if (node.expression.type !== 'AssignmentExpression') {
return null;
}
const name = node.expression.left;
const body = _optionalChain$1([
node.expression.right,
'optionalAccess',
(_5) => _5.body,
'optionalAccess',
(_6) => _6.body,
]);
if (name && body) {
options.push({ name: name, statements: body });
}
} else if (node.type === 'VariableDeclaration') {
for (const declaration of node.declarations) {
const name = declaration.id;
const body = _optionalChain$1([
declaration.init,
'optionalAccess',
(_7) => _7.body,
'optionalAccess',
(_8) => _8.body,
]);
if (name && body) {
options.push({ name: name, statements: body });
}
}
}
for (const { name: name, statements: statements } of options) {
if (matchesStructure(statements, { anykey: [asdasd] })) {
return createSolver(name);
}
}
return null;
}
function makeSolverFuncFromName(name) {
return {
type: 'ArrowFunctionExpression',
params: [{ type: 'Identifier', name: 'n' }],
body: {
type: 'CallExpression',
callee: { type: 'Identifier', name: name },
arguments: [{ type: 'Identifier', name: 'n' }],
optional: false,
},
async: false,
expression: false,
generator: false,
};
function createSolver(expression) {
return generateArrowFunction(
`\n({sig, n}) => {\n const url = (${astring.generate(expression)})("https://youtube.com/watch?v=yt-dlp-wins", "s", sig ? encodeURIComponent(sig) : undefined);\n url.set("n", n);\n const proto = Object.getPrototypeOf(url);\n const keys = Object.keys(proto).concat(Object.getOwnPropertyNames(proto));\n for (const key of keys) {\n if (!["constructor", "set", "get", "clone"].includes(key)) {\n url[key]();\n break;\n }\n }\n const s = url.get("s");\n return {\n sig: s ? decodeURIComponent(s) : null,\n n: url.get("n") ?? null,\n };\n}\n`,
);
}
const setupNodes = meriyah.parse(
`\nif (typeof globalThis.XMLHttpRequest === "undefined") {\n globalThis.XMLHttpRequest = { prototype: {} };\n}\nconst window = Object.create(null);\nif (typeof URL === "undefined") {\n window.location = {\n hash: "",\n host: "www.youtube.com",\n hostname: "www.youtube.com",\n href: "https://www.youtube.com/watch?v=yt-dlp-wins",\n origin: "https://www.youtube.com",\n password: "",\n pathname: "/watch",\n port: "",\n protocol: "https:",\n search: "?v=yt-dlp-wins",\n username: "",\n };\n} else {\n window.location = new URL("https://www.youtube.com/watch?v=yt-dlp-wins");\n}\nif (typeof globalThis.document === "undefined") {\n globalThis.document = Object.create(null);\n}\nif (typeof globalThis.navigator === "undefined") {\n globalThis.navigator = Object.create(null);\n}\nif (typeof globalThis.self === "undefined") {\n globalThis.self = globalThis;\n}\n`,
@@ -585,235 +270,59 @@ var jsc = (function (meriyah, astring) {
function getSolutions(statements) {
const found = { n: [], sig: [] };
for (const statement of statements) {
const n = extract(statement);
if (n) {
found.n.push(n);
}
const sig = extract$1(statement);
if (sig) {
found.sig.push(sig);
const result = extract(statement);
if (result) {
found.n.push(makeSolver(result, { type: 'Identifier', name: 'n' }));
found.sig.push(makeSolver(result, { type: 'Identifier', name: 'sig' }));
}
}
return found;
}
function makeSolver(result, ident) {
return {
type: 'ArrowFunctionExpression',
params: [ident],
body: {
type: 'MemberExpression',
object: {
type: 'CallExpression',
callee: result,
arguments: [
{
type: 'ObjectExpression',
properties: [
{
type: 'Property',
key: ident,
value: ident,
kind: 'init',
computed: false,
method: false,
shorthand: true,
},
],
},
],
optional: false,
},
computed: false,
property: ident,
optional: false,
},
async: false,
expression: true,
generator: false,
};
}
function getFromPrepared(code) {
const resultObj = { n: null, sig: null };
Function('_result', code)(resultObj);
return resultObj;
}
function multiTry(generators) {
return {
type: 'ArrowFunctionExpression',
params: [{ type: 'Identifier', name: '_input' }],
body: {
type: 'BlockStatement',
body: [
{
type: 'VariableDeclaration',
kind: 'const',
declarations: [
{
type: 'VariableDeclarator',
id: { type: 'Identifier', name: '_results' },
init: {
type: 'NewExpression',
callee: { type: 'Identifier', name: 'Set' },
arguments: [],
},
},
],
},
{
type: 'ForOfStatement',
left: {
type: 'VariableDeclaration',
kind: 'const',
declarations: [
{
type: 'VariableDeclarator',
id: { type: 'Identifier', name: '_generator' },
init: null,
},
],
},
right: { type: 'ArrayExpression', elements: generators },
body: {
type: 'BlockStatement',
body: [
{
type: 'TryStatement',
block: {
type: 'BlockStatement',
body: [
{
type: 'ExpressionStatement',
expression: {
type: 'CallExpression',
callee: {
type: 'MemberExpression',
object: { type: 'Identifier', name: '_results' },
computed: false,
property: { type: 'Identifier', name: 'add' },
optional: false,
},
arguments: [
{
type: 'CallExpression',
callee: {
type: 'Identifier',
name: '_generator',
},
arguments: [
{ type: 'Identifier', name: '_input' },
],
optional: false,
},
],
optional: false,
},
},
],
},
handler: {
type: 'CatchClause',
param: null,
body: { type: 'BlockStatement', body: [] },
},
finalizer: null,
},
],
},
await: false,
},
{
type: 'IfStatement',
test: {
type: 'UnaryExpression',
operator: '!',
argument: {
type: 'MemberExpression',
object: { type: 'Identifier', name: '_results' },
computed: false,
property: { type: 'Identifier', name: 'size' },
optional: false,
},
prefix: true,
},
consequent: {
type: 'BlockStatement',
body: [
{
type: 'ThrowStatement',
argument: {
type: 'TemplateLiteral',
expressions: [],
quasis: [
{
type: 'TemplateElement',
value: { cooked: 'no solutions', raw: 'no solutions' },
tail: true,
},
],
},
},
],
},
alternate: null,
},
{
type: 'IfStatement',
test: {
type: 'BinaryExpression',
left: {
type: 'MemberExpression',
object: { type: 'Identifier', name: '_results' },
computed: false,
property: { type: 'Identifier', name: 'size' },
optional: false,
},
right: { type: 'Literal', value: 1 },
operator: '!==',
},
consequent: {
type: 'BlockStatement',
body: [
{
type: 'ThrowStatement',
argument: {
type: 'TemplateLiteral',
expressions: [
{
type: 'CallExpression',
callee: {
type: 'MemberExpression',
object: { type: 'Identifier', name: '_results' },
computed: false,
property: { type: 'Identifier', name: 'join' },
optional: false,
},
arguments: [{ type: 'Literal', value: ', ' }],
optional: false,
},
],
quasis: [
{
type: 'TemplateElement',
value: {
cooked: 'invalid solutions: ',
raw: 'invalid solutions: ',
},
tail: false,
},
{
type: 'TemplateElement',
value: { cooked: '', raw: '' },
tail: true,
},
],
},
},
],
},
alternate: null,
},
{
type: 'ReturnStatement',
argument: {
type: 'MemberExpression',
object: {
type: 'CallExpression',
callee: {
type: 'MemberExpression',
object: {
type: 'CallExpression',
callee: {
type: 'MemberExpression',
object: { type: 'Identifier', name: '_results' },
computed: false,
property: { type: 'Identifier', name: 'values' },
optional: false,
},
arguments: [],
optional: false,
},
computed: false,
property: { type: 'Identifier', name: 'next' },
optional: false,
},
arguments: [],
optional: false,
},
computed: false,
property: { type: 'Identifier', name: 'value' },
optional: false,
},
},
],
},
async: false,
expression: false,
generator: false,
};
return generateArrowFunction(
`\n(_input) => {\n const _results = new Set();\n const errors = [];\n for (const _generator of ${astring.generate({ type: 'ArrayExpression', elements: generators })}) {\n try {\n _results.add(_generator(_input));\n } catch (e) {\n errors.push(e);\n }\n }\n if (!_results.size) {\n throw \`no solutions: \${errors.join(", ")}\`;\n }\n if (_results.size !== 1) {\n throw \`invalid solutions: \${[..._results].map(x => JSON.stringify(x)).join(", ")}\`;\n }\n return _results.values().next().value;\n}\n`,
);
}
function main(input) {
const preprocessedPlayer =

View File

@@ -1,110 +1,205 @@
import json
import re
import urllib.parse
from .common import InfoExtractor
from ..utils import (
clean_html,
extract_attributes,
int_or_none,
parse_duration,
parse_iso8601,
xpath_text,
xpath_with_ns,
parse_resolution,
str_or_none,
unified_timestamp,
url_or_none,
)
from ..utils.traversal import (
find_element,
find_elements,
traverse_obj,
)
class ZapiksIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?zapiks\.(?:fr|com)/(?:(?:[a-z]{2}/)?(?P<display_id>.+?)\.html|index\.php\?.*\bmedia_id=(?P<id>\d+))'
_EMBED_REGEX = [r'<iframe[^>]+src="(?P<url>https?://(?:www\.)?zapiks\.fr/index\.php\?.+?)"']
_VALID_URL = [
r'https?://(?:www\.)?zapiks\.(?:com|fr)/(?P<id>[\w-]+)\.html',
r'https?://(?:www\.)?zapiks\.fr/index\.php\?(?:[^#]+&)?media_id=(?P<id>\d+)',
]
_EMBED_REGEX = [r'<iframe\b[^>]+\bsrc=["\'](?P<url>(?:https?:)?//(?:www\.)?zapiks\.fr/index\.php\?(?:[^#"\']+&(?:amp;)?)?media_id=\d+)']
_TESTS = [{
'url': 'http://www.zapiks.fr/ep2s3-bon-appetit-eh-be-viva.html',
'url': 'https://www.zapiks.fr/ep2s3-bon-appetit-eh-be-viva.html',
'md5': 'aeb3c473b2d564b2d46d664d28d5f050',
'info_dict': {
'id': '80798',
'ext': 'mp4',
'title': 'EP2S3 - Bon Appétit - Eh bé viva les pyrénées con!',
'description': 'md5:7054d6f6f620c6519be1fe710d4da847',
'thumbnail': r're:https?://zpks\.com/.+\.jpg',
'description': 'md5:db07a553c1550e2905bceafa923000fd',
'display_id': 'ep2s3-bon-appetit-eh-be-viva',
'duration': 528,
'tags': 'count:5',
'thumbnail': r're:https?://zpks\.com/.+',
'timestamp': 1359044972,
'upload_date': '20130124',
'uploader': 'BonAppetit',
'uploader_id': 'bonappetit',
'view_count': int,
},
}, {
'url': 'http://www.zapiks.com/ep3s5-bon-appetit-baqueira-m-1.html',
'only_matching': True,
}, {
'url': 'http://www.zapiks.com/nl/ep3s5-bon-appetit-baqueira-m-1.html',
'only_matching': True,
}, {
'url': 'http://www.zapiks.fr/index.php?action=playerIframe&amp;media_id=118046&amp;width=640&amp;height=360&amp;autoStart=false&amp;language=fr',
'only_matching': True,
}]
_WEBPAGE_TESTS = [{
'url': 'https://www.skipass.com/news/116090-bon-appetit-s5ep3-baqueira-mi-cor.html',
'url': 'https://www.zapiks.com/ep3s5-bon-appetit-baqueira-m-1.html',
'md5': '196fe42901639d868956b1dcaa48de15',
'info_dict': {
'id': '118046',
'ext': 'mp4',
'title': 'EP3S5 - Bon Appétit - Baqueira Mi Corazon !',
'thumbnail': r're:https?://zpks\.com/.+\.jpg',
'display_id': 'ep3s5-bon-appetit-baqueira-m-1',
'duration': 642,
'tags': 'count:8',
'thumbnail': r're:https?://zpks\.com/.+',
'timestamp': 1424370543,
'upload_date': '20150219',
'uploader': 'BonAppetit',
'uploader_id': 'bonappetit',
'view_count': int,
},
}, {
'url': 'https://www.zapiks.fr/index.php?action=playerIframe&media_id=164049',
'md5': 'fb81a7c9b7b84c00ba111028aee593b8',
'info_dict': {
'id': '164049',
'ext': 'mp4',
'title': 'Courchevel Hiver 2025/2026',
'display_id': 'courchevel-hiver-2025-2026',
'duration': 38,
'tags': 'count:1',
'thumbnail': r're:https?://zpks\.com/.+',
'timestamp': 1769019147,
'upload_date': '20260121',
'uploader': 'jamrek',
'uploader_id': 'jamrek',
'view_count': int,
},
}, {
# https://www.youtube.com/watch?v=UBAABvegu2M
'url': 'https://www.zapiks.com/live-fwt18-vallnord-arcalis-.html',
'info_dict': {
'id': 'UBAABvegu2M',
'ext': 'mp4',
'title': 'Replay Live - FWT18 Vallnord-Arcalís Andorra - Freeride World Tour 2018',
'age_limit': 0,
'availability': 'public',
'categories': ['Sports'],
'channel': 'FIS Freeride World Tour by Peak Performance',
'channel_follower_count': int,
'channel_id': 'UCraJ3GNFfw6LXFuCV6McByg',
'channel_url': 'https://www.youtube.com/channel/UCraJ3GNFfw6LXFuCV6McByg',
'comment_count': int,
'description': 'md5:2d9fefef758d5ad0d5a987d46aff7572',
'duration': 11328,
'heatmap': 'count:100',
'like_count': int,
'live_status': 'was_live',
'media_type': 'livestream',
'playable_in_embed': True,
'release_date': '20180306',
'release_timestamp': 1520321809,
'tags': 'count:27',
'thumbnail': r're:https?://i\.ytimg\.com/.+',
'timestamp': 1520336958,
'upload_date': '20180306',
'uploader': 'FIS Freeride World Tour by Peak Performance',
'uploader_id': '@FISFreerideWorldTour',
'uploader_url': 'https://www.youtube.com/@FISFreerideWorldTour',
'view_count': int,
},
'add_ie': ['Youtube'],
}, {
# https://vimeo.com/235746460
'url': 'https://www.zapiks.fr/waking-dream-2017-full-movie.html',
'info_dict': {
'id': '235746460',
'ext': 'mp4',
'title': '"WAKING DREAM" (2017) Full Movie by Sam Favret & Julien Herry',
'duration': 1649,
'thumbnail': r're:https?://i\.vimeocdn\.com/video/.+',
'uploader': 'Favret Sam',
'uploader_id': 'samfavret',
'uploader_url': 'https://vimeo.com/samfavret',
},
'add_ie': ['Vimeo'],
'expected_warnings': ['Failed to parse XML: not well-formed'],
}]
_WEBPAGE_TESTS = [{
# https://www.zapiks.fr/ep3s5-bon-appetit-baqueira-m-1.html
# https://www.zapiks.fr/index.php?action=playerIframe&media_id=118046
'url': 'https://www.skipass.com/news/116090-bon-appetit-s5ep3-baqueira-mi-cor.html',
'md5': '196fe42901639d868956b1dcaa48de15',
'info_dict': {
'id': '118046',
'ext': 'mp4',
'title': 'EP3S5 - Bon Appétit - Baqueira Mi Corazon !',
'description': 'md5:b45295c3897c4c01d7c04e8484c26aaf',
'display_id': 'ep3s5-bon-appetit-baqueira-m-1',
'duration': 642,
'tags': 'count:8',
'thumbnail': r're:https?://zpks\.com/.+',
'timestamp': 1424370543,
'upload_date': '20150219',
'uploader': 'BonAppetit',
'uploader_id': 'bonappetit',
'view_count': int,
},
}]
_UPLOADER_ID_RE = re.compile(r'/pro(?:fil)?/(?P<id>[^/?#]+)/?')
def _real_extract(self, url):
mobj = self._match_valid_url(url)
video_id = mobj.group('id')
display_id = mobj.group('display_id') or video_id
display_id = self._match_id(url)
webpage = self._download_webpage(url, display_id)
if embed_url := traverse_obj(webpage, (
{find_element(cls='embed-container')}, {find_element(tag='iframe', html=True)},
{extract_attributes}, 'src', {self._proto_relative_url}, {url_or_none},
)):
if not self.suitable(embed_url):
return self.url_result(embed_url)
if not video_id:
video_id = self._search_regex(
r'data-media-id="(\d+)"', webpage, 'video id')
playlist = self._download_xml(
f'http://www.zapiks.fr/view/index.php?action=playlist&media_id={video_id}&lang=en',
display_id)
NS_MAP = {
'jwplayer': 'http://rss.jwpcdn.com/',
}
def ns(path):
return xpath_with_ns(path, NS_MAP)
item = playlist.find('./channel/item')
title = xpath_text(item, 'title', 'title') or self._og_search_title(webpage)
description = self._og_search_description(webpage, default=None)
thumbnail = xpath_text(
item, ns('./jwplayer:image'), 'thumbnail') or self._og_search_thumbnail(webpage, default=None)
duration = parse_duration(self._html_search_meta(
'duration', webpage, 'duration', default=None))
timestamp = parse_iso8601(self._html_search_meta(
'uploadDate', webpage, 'upload date', default=None), ' ')
view_count = int_or_none(self._search_regex(
r'UserPlays:(\d+)', webpage, 'view count', default=None))
comment_count = int_or_none(self._search_regex(
r'UserComments:(\d+)', webpage, 'comment count', default=None))
video_responsive = traverse_obj(webpage, (
{find_element(cls='video-responsive', html=True)}, {extract_attributes}, {dict}))
data_media_url = traverse_obj(video_responsive, ('data-media-url', {url_or_none}))
if data_media_url and urllib.parse.urlparse(url).path == '/index.php':
return self.url_result(data_media_url, ZapiksIE)
data_playlist = traverse_obj(video_responsive, ('data-playlist', {json.loads}, ..., any))
formats = []
for source in item.findall(ns('./jwplayer:source')):
format_id = source.attrib['label']
f = {
'url': source.attrib['file'],
for source in traverse_obj(data_playlist, (
'sources', lambda _, v: url_or_none(v['file']),
)):
format_id = traverse_obj(source, ('label', {str_or_none}))
formats.append({
'format_id': format_id,
}
m = re.search(r'^(?P<height>\d+)[pP]', format_id)
if m:
f['height'] = int(m.group('height'))
formats.append(f)
'url': source['file'],
**parse_resolution(format_id),
})
return {
'id': video_id,
'title': title,
'description': description,
'thumbnail': thumbnail,
'duration': duration,
'timestamp': timestamp,
'view_count': view_count,
'comment_count': comment_count,
'display_id': display_id,
'duration': parse_duration(self._html_search_meta('duration', webpage, default=None)),
'formats': formats,
'timestamp': unified_timestamp(self._html_search_meta('uploadDate', webpage, default=None)),
**traverse_obj(webpage, {
'description': ({find_element(cls='description-text')}, {clean_html}, filter),
'tags': (
{find_elements(cls='bs-label', html=True)},
..., {extract_attributes}, 'title', {clean_html}, filter),
'view_count': (
{find_element(cls='video-content-view-counter')}, {clean_html},
{lambda x: re.sub(r'(?:vues|views|\s+)', '', x)}, {int_or_none}),
}),
**traverse_obj(webpage, ({find_element(cls='video-content-user-link', html=True)}, {
'uploader': ({clean_html}, filter),
'uploader_id': ({extract_attributes}, 'href', {self._UPLOADER_ID_RE.fullmatch}, 'id'),
})),
**traverse_obj(data_playlist, {
'id': ('mediaid', {str_or_none}),
'title': ('title', {clean_html}, filter),
'thumbnail': ('image', {url_or_none}),
}),
}

View File

@@ -1,8 +1,8 @@
# Autogenerated by devscripts/update-version.py
__version__ = '2026.02.21'
__version__ = '2026.03.13'
RELEASE_GIT_HEAD = '646bb31f39614e6c2f7ba687c53e7496394cbadb'
RELEASE_GIT_HEAD = '92f1d99dbe1e10d942ef0963f625dbc5bc0768aa'
VARIANT = None
@@ -12,4 +12,4 @@ CHANNEL = 'stable'
ORIGIN = 'yt-dlp/yt-dlp'
_pkg_version = '2026.02.21'
_pkg_version = '2026.03.13'