1
0
mirror of https://github.com/yt-dlp/yt-dlp.git synced 2026-03-23 18:22:09 +01:00

Compare commits

...

9 Commits

Author SHA1 Message Date
bashonly
f01e1a1ced [ie/rtp] Support multi-part episodes and --no-playlist (#16299)
Closes #16286
Authored by: bashonly
2026-03-21 19:52:25 +00:00
github-actions[bot]
7fd74d1009 Release 2026.03.17
Created by: bashonly

:ci skip all
2026-03-17 23:25:11 +00:00
bashonly
04d6974f50 [ie/youtube] Update ejs to 0.8.0 (#16269)
* Also default to `main` for `player_js_variant` extractor-arg

Closes #16256
Authored by: bashonly, Grub4K

Co-authored-by: Simon Sawicki <contact@grub4k.dev>
2026-03-17 23:17:34 +00:00
bashonly
18656b2f2a [test:networking] Mark all CurlCFFIRH tests as flaky for any OS (#16266)
Authored by: bashonly
2026-03-17 22:45:03 +00:00
bashonly
1b6ec8fc25 [ie/youtube] Fix --live-from-start support (#16254)
Closes #16237
Authored by: bashonly
2026-03-17 22:44:30 +00:00
bashonly
7fab4c2b23 [build] Use PyInstaller v6.19.0 for Windows (#16265)
Authored by: bashonly
2026-03-17 19:48:07 +00:00
bashonly
66c4947e9c [ie/youtube] Always respect webpage_client extractor-arg (#16250)
Authored by: bashonly
2026-03-17 19:47:34 +00:00
bashonly
4fc768b7f7 [ci] Bump actions pins (#16252)
* Bump actions/cache v5.0.2 → v5.0.3
* Bump actions/download-artifact v7.0.0 → v8.0.1
* Bump actions/setup-node v6.2.0 → v6.3.0
* Bump actions/upload-artifact v6.0.0 → v7.0.0
* Bump docker/setup-qemu-action v3.7.0 → v4.0.0
* Bump github/codeql-action v4.31.9 → v4.33.0
* Bump oven-sh/setup-bun v2.1.2 → v2.2.0
* Bump zizmorcore/zizmor-action v0.4.1 → v0.5.2
* Bump actionlint v1.7.9 → v1.7.11
* Bump zizmor v1.22.0 → v1.23.1
* Adapt zizmor configuration to new version

Authored by: bashonly
2026-03-17 18:04:32 +00:00
bashonly
e68afb2827 [docs] Fix player_client extractor-arg documentation (#16235)
Authored by: bashonly
2026-03-13 17:35:36 +00:00
19 changed files with 296 additions and 118 deletions

View File

@@ -231,7 +231,7 @@ jobs:
[[ "${version}" != "${downgraded_version}" ]] [[ "${version}" != "${downgraded_version}" ]]
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with: with:
name: build-bin-${{ github.job }} name: build-bin-${{ github.job }}
path: | path: |
@@ -267,7 +267,7 @@ jobs:
- name: Set up QEMU - name: Set up QEMU
if: matrix.qemu_platform if: matrix.qemu_platform
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
with: with:
image: tonistiigi/binfmt:qemu-v10.0.4-56@sha256:30cc9a4d03765acac9be2ed0afc23af1ad018aed2c28ea4be8c2eb9afe03fbd1 image: tonistiigi/binfmt:qemu-v10.0.4-56@sha256:30cc9a4d03765acac9be2ed0afc23af1ad018aed2c28ea4be8c2eb9afe03fbd1
cache-image: false cache-image: false
@@ -294,7 +294,7 @@ jobs:
docker compose up --build --exit-code-from "${SERVICE}" "${SERVICE}" docker compose up --build --exit-code-from "${SERVICE}" "${SERVICE}"
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with: with:
name: build-bin-${{ matrix.os }}_${{ matrix.arch }} name: build-bin-${{ matrix.os }}_${{ matrix.arch }}
path: | path: |
@@ -384,7 +384,7 @@ jobs:
[[ "$version" != "$downgraded_version" ]] [[ "$version" != "$downgraded_version" ]]
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with: with:
name: build-bin-${{ github.job }} name: build-bin-${{ github.job }}
path: | path: |
@@ -407,23 +407,23 @@ jobs:
runner: windows-2025 runner: windows-2025
python_version: '3.10' python_version: '3.10'
platform_tag: win_amd64 platform_tag: win_amd64
pyi_version: '6.18.0' pyi_version: '6.19.0'
pyi_tag: '2026.01.29.160356' pyi_tag: '2026.03.17.175201'
pyi_hash: bb9cd0b0b233e4d031a295211cb8aa7c7f8b3c12ff33f1d57a40849ab4d3cf42 pyi_hash: '1a5f4b844abd02bd758ae6b64c5243fed1a2fa641dbcab2f79480c6a7b957e2d'
- arch: 'x86' - arch: 'x86'
runner: windows-2025 runner: windows-2025
python_version: '3.10' python_version: '3.10'
platform_tag: win32 platform_tag: win32
pyi_version: '6.18.0' pyi_version: '6.19.0'
pyi_tag: '2026.01.29.160356' pyi_tag: '2026.03.17.175201'
pyi_hash: aa8f260e735d94f1e2e1aac42e322f508eb54d0433de803c2998c337f72045e4 pyi_hash: '9b3c791d7e5cc23f5b48dffc3c367dac10a516b86904db48b6096c2b5d1ffb41'
- arch: 'arm64' - arch: 'arm64'
runner: windows-11-arm runner: windows-11-arm
python_version: '3.13' # arm64 only has Python >= 3.11 available python_version: '3.13' # arm64 only has Python >= 3.11 available
platform_tag: win_arm64 platform_tag: win_arm64
pyi_version: '6.18.0' pyi_version: '6.19.0'
pyi_tag: '2026.01.29.160356' pyi_tag: '2026.03.17.175201'
pyi_hash: 4bbca67d0cdfa860d92ac9cc7e4c2586fd393d1e814e3f1375b8c62d5cfb6771 pyi_hash: 'd008e5c8bb2143f7c05c8b5fcc15dab5f079d79425f78af1936c6768f8e87504'
env: env:
CHANNEL: ${{ inputs.channel }} CHANNEL: ${{ inputs.channel }}
ORIGIN: ${{ needs.process.outputs.origin }} ORIGIN: ${{ needs.process.outputs.origin }}
@@ -501,7 +501,7 @@ jobs:
} }
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with: with:
name: build-bin-${{ github.job }}-${{ matrix.arch }} name: build-bin-${{ github.job }}-${{ matrix.arch }}
path: | path: |
@@ -521,7 +521,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with: with:
path: artifact path: artifact
pattern: build-bin-* pattern: build-bin-*
@@ -590,7 +590,7 @@ jobs:
done done
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with: with:
name: build-${{ github.job }} name: build-${{ github.job }}
path: | path: |

View File

@@ -50,13 +50,13 @@ jobs:
with: with:
deno-version: '2.0.0' # minimum supported version deno-version: '2.0.0' # minimum supported version
- name: Install Bun - name: Install Bun
uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2 uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with: with:
# minimum supported version is 1.0.31 but earliest available Windows version is 1.1.0 # minimum supported version is 1.0.31 but earliest available Windows version is 1.1.0
bun-version: ${{ (matrix.os == 'windows-latest' && '1.1.0') || '1.0.31' }} bun-version: ${{ (matrix.os == 'windows-latest' && '1.1.0') || '1.0.31' }}
no-cache: true no-cache: true
- name: Install Node - name: Install Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with: with:
node-version: '20.0' # minimum supported version node-version: '20.0' # minimum supported version
- name: Install QuickJS (Linux) - name: Install QuickJS (Linux)

View File

@@ -36,12 +36,12 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 uses: github/codeql-action/init@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
build-mode: none build-mode: none
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 uses: github/codeql-action/analyze@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
with: with:
category: "/language:${{matrix.language}}" category: "/language:${{matrix.language}}"

View File

@@ -42,7 +42,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with: with:
path: dist path: dist
name: build-pypi name: build-pypi

View File

@@ -27,7 +27,7 @@ jobs:
run: echo "head=$(git rev-parse HEAD)" | tee -a "${GITHUB_OUTPUT}" run: echo "head=$(git rev-parse HEAD)" | tee -a "${GITHUB_OUTPUT}"
- name: Cache nightly commit hash - name: Cache nightly commit hash
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
env: env:
SEGMENT_DOWNLOAD_TIMEOUT_MINS: 1 SEGMENT_DOWNLOAD_TIMEOUT_MINS: 1
with: with:
@@ -94,7 +94,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with: with:
path: dist path: dist
name: build-pypi name: build-pypi

View File

@@ -214,7 +214,7 @@ jobs:
- name: Upload artifacts - name: Upload artifacts
if: github.event.workflow != '.github/workflows/release.yml' # Reusable workflow_call if: github.event.workflow != '.github/workflows/release.yml' # Reusable workflow_call
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with: with:
name: build-pypi name: build-pypi
path: | path: |
@@ -243,7 +243,7 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
persist-credentials: false persist-credentials: false
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with: with:
path: artifact path: artifact
pattern: build-* pattern: build-*

View File

@@ -26,8 +26,8 @@ concurrency:
cancel-in-progress: ${{ github.event_name == 'pull_request' }} cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env: env:
ACTIONLINT_VERSION: "1.7.9" ACTIONLINT_VERSION: "1.7.11"
ACTIONLINT_SHA256SUM: 233b280d05e100837f4af1433c7b40a5dcb306e3aa68fb4f17f8a7f45a7df7b4 ACTIONLINT_SHA256SUM: 900919a84f2229bac68ca9cd4103ea297abc35e9689ebb842c6e34a3d1b01b0a
ACTIONLINT_REPO: https://github.com/rhysd/actionlint ACTIONLINT_REPO: https://github.com/rhysd/actionlint
jobs: jobs:
@@ -76,8 +76,8 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Run zizmor - name: Run zizmor
uses: zizmorcore/zizmor-action@135698455da5c3b3e55f73f4419e481ab68cdd95 # v0.4.1 uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2
with: with:
advanced-security: false advanced-security: false
persona: pedantic persona: pedantic
version: v1.22.0 version: v1.23.1

4
.github/zizmor.yml vendored
View File

@@ -9,6 +9,10 @@ rules:
obfuscation: obfuscation:
ignore: ignore:
- release.yml # Not actual obfuscation - release.yml # Not actual obfuscation
secrets-outside-env:
ignore:
- build.yml
- release.yml
unpinned-uses: unpinned-uses:
config: config:
policies: policies:

View File

@@ -4,6 +4,20 @@
# To create a release, dispatch the https://github.com/yt-dlp/yt-dlp/actions/workflows/release.yml workflow on master # To create a release, dispatch the https://github.com/yt-dlp/yt-dlp/actions/workflows/release.yml workflow on master
--> -->
### 2026.03.17
#### Extractor changes
- **youtube**
- [Always respect `webpage_client` extractor-arg](https://github.com/yt-dlp/yt-dlp/commit/66c4947e9cb70c9de96f7da75f9acbe4192d6c9d) ([#16250](https://github.com/yt-dlp/yt-dlp/issues/16250)) by [bashonly](https://github.com/bashonly)
- [Fix `--live-from-start` support](https://github.com/yt-dlp/yt-dlp/commit/1b6ec8fc2589a1733a0937270faa4230ce6b1ca5) ([#16254](https://github.com/yt-dlp/yt-dlp/issues/16254)) by [bashonly](https://github.com/bashonly)
- [Update ejs to 0.8.0](https://github.com/yt-dlp/yt-dlp/commit/04d6974f502bbdfaed72c624344f262e30ad9708) ([#16269](https://github.com/yt-dlp/yt-dlp/issues/16269)) by [bashonly](https://github.com/bashonly), [Grub4K](https://github.com/Grub4K)
#### Misc. changes
- **build**: [Use PyInstaller v6.19.0 for Windows](https://github.com/yt-dlp/yt-dlp/commit/7fab4c2b23e16c4a4f94020a37a6bdf8d502be37) ([#16265](https://github.com/yt-dlp/yt-dlp/issues/16265)) by [bashonly](https://github.com/bashonly)
- **ci**: [Bump actions pins](https://github.com/yt-dlp/yt-dlp/commit/4fc768b7f7194a05b13ad3e7bc5bfde84ed9ede7) ([#16252](https://github.com/yt-dlp/yt-dlp/issues/16252)) by [bashonly](https://github.com/bashonly)
- **docs**: [Fix `player_client` extractor-arg documentation](https://github.com/yt-dlp/yt-dlp/commit/e68afb28277b4bee39726dbcbb06801edde9f659) ([#16235](https://github.com/yt-dlp/yt-dlp/issues/16235)) by [bashonly](https://github.com/bashonly)
- **test**: networking: [Mark all CurlCFFIRH tests as flaky for any OS](https://github.com/yt-dlp/yt-dlp/commit/18656b2f2af41a138793c7012a88f467c0d90274) ([#16266](https://github.com/yt-dlp/yt-dlp/issues/16266)) by [bashonly](https://github.com/bashonly)
### 2026.03.13 ### 2026.03.13
#### Extractor changes #### Extractor changes

View File

@@ -202,9 +202,9 @@ CONTRIBUTORS: Changelog.md
# The following EJS_-prefixed variables are auto-generated by devscripts/update_ejs.py # The following EJS_-prefixed variables are auto-generated by devscripts/update_ejs.py
# DO NOT EDIT! # DO NOT EDIT!
EJS_VERSION = 0.7.0 EJS_VERSION = 0.8.0
EJS_WHEEL_NAME = yt_dlp_ejs-0.7.0-py3-none-any.whl EJS_WHEEL_NAME = yt_dlp_ejs-0.8.0-py3-none-any.whl
EJS_WHEEL_HASH = sha256:967e9cbe114ddfd046ff4668af18b1827b4597e2e47a83deea668a355828c798 EJS_WHEEL_HASH = sha256:79300e5fca7f937a1eeede11f0456862c1b41107ce1d726871e0207424f4bdb4
EJS_PY_FOLDERS = yt_dlp_ejs yt_dlp_ejs/yt yt_dlp_ejs/yt/solver 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_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 EJS_JS_FOLDERS = yt_dlp_ejs/yt/solver

View File

@@ -1860,12 +1860,12 @@ 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_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_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_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_safari` is used for free accounts and `tv_downgraded,web_creator` 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_safari`
* `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. 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_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) * `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_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_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 `main`, 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`. Using any other value will imply `webpage_skip=player_response` * `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) * `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` * `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`

View File

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

View File

@@ -53,50 +53,66 @@ class Challenge:
CHALLENGES: list[Challenge] = [ CHALLENGES: list[Challenge] = [
# 20518
Challenge('edc3ba07', Variant.tv, JsChallengeType.N, {
'BQoJvGBkC2nj1ZZLK-': '-m-se9fQVnvEofLx',
}),
Challenge('edc3ba07', Variant.tv, JsChallengeType.SIG, {
'NJAJEij0EwRgIhAI0KExTgjfPk-MPM9MAdzyyPRt=BM8-XO5tm5hlMCSVpAiEAv7eP3CURqZNSPow8BXXAoazVoXgeMP7gH9BdylHCwgw=gwzz':
'zwg=wgwCHlydB9zg7PMegXoVzaoAXXB8woPSNZqRUC3Pe7vAEiApVSCMlh5mt5OX-8MB=tRPyyEdAM9MPM-kPfjgTxEK0IAhIgRwE0jiz',
}),
# 20521
Challenge('316b61b4', Variant.tv, JsChallengeType.N, {
'IlLiA21ny7gqA2m4p37': 'GchRcsUC_WmnhOUVGV',
}),
Challenge('316b61b4', Variant.tv, JsChallengeType.SIG, {
'NJAJEij0EwRgIhAI0KExTgjfPk-MPM9MAdzyyPRt=BM8-XO5tm5hlMCSVpAiEAv7eP3CURqZNSPow8BXXAoazVoXgeMP7gH9BdylHCwgw=gwzz':
'tJAJEij0EwRgIhAI0KExTgjfPk-MPM9MAdzyyPRN=BM8-XO5tm5hlMCSVpAiEAv7eP3CURqZNSPow8BXXAoazVoXgeMP7gH9BdylHCwgw=gwz',
}),
# 20522 # 20522
Challenge('74edf1a3', Variant.tv, JsChallengeType.N, { Challenge('74edf1a3', Variant.main, JsChallengeType.N, {
'IlLiA21ny7gqA2m4p37': '9nRTxrbM1f0yHg', 'IlLiA21ny7gqA2m4p37': '9nRTxrbM1f0yHg',
'eabGFpsUKuWHXGh6FR4': 'izmYqDEY6kl7Sg', 'eabGFpsUKuWHXGh6FR4': 'izmYqDEY6kl7Sg',
'eabGF/ps%UK=uWHXGh6FR4': 'LACmqlhaBpiPlgE-a', 'eabGF/ps%UK=uWHXGh6FR4': 'LACmqlhaBpiPlgE-a',
}), }),
Challenge('74edf1a3', Variant.tv, JsChallengeType.SIG, { Challenge('74edf1a3', Variant.main, JsChallengeType.SIG, {
'NJAJEij0EwRgIhAI0KExTgjfPk-MPM9MAdzyyPRt=BM8-XO5tm5hlMCSVpAiEAv7eP3CURqZNSPow8BXXAoazVoXgeMP7gH9BdylHCwgw=gwzz': 'NJAJEij0EwRgIhAI0KExTgjfPk-MPM9MAdzyyPRt=BM8-XO5tm5hlMCSVpAiEAv7eP3CURqZNSPow8BXXAoazVoXgeMP7gH9BdylHCwgw=gwzz':
'NJAJEij0EwRgIhAI0KExTgjfPk-MPM9MAdzyyPRt=BM8-XO5tm5hzMCSVpAiEAv7eP3CURqZNSPow8BXXAoazVoXgeMP7gH9BdylHCwgw=gwzl', '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\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', '\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',
}), }),
# 20523 # 20523
Challenge('901741ab', Variant.tv, JsChallengeType.N, { Challenge('901741ab', Variant.main, JsChallengeType.N, {
'BQoJvGBkC2nj1ZZLK-': 'UMPovvBZRh-sjb', 'BQoJvGBkC2nj1ZZLK-': 'UMPovvBZRh-sjb',
}), }),
Challenge('901741ab', Variant.tv, JsChallengeType.SIG, { Challenge('901741ab', Variant.main, JsChallengeType.SIG, {
'NJAJEij0EwRgIhAI0KExTgjfPk-MPM9MAdzyyPRt=BM8-XO5tm5hlMCSVpAiEAv7eP3CURqZNSPow8BXXAoazVoXgeMP7gH9BdylHCwgw=gwzz': 'NJAJEij0EwRgIhAI0KExTgjfPk-MPM9MAdzyyPRt=BM8-XO5tm5hlMCSVpAiEAv7eP3CURqZNSPow8BXXAoazVoXgeMP7gH9BdylHCwgw=gwzz':
'wgwCHlydB9Hg7PMegXoVzaoAXXB8woPSNZqRUC3Pe7vAEiApVSCMlhwmt5ON-8MB=5RPyyzdAM9MPM-kPfjgTxEK0IAhIgRwE0jiEJA', 'wgwCHlydB9Hg7PMegXoVzaoAXXB8woPSNZqRUC3Pe7vAEiApVSCMlhwmt5ON-8MB=5RPyyzdAM9MPM-kPfjgTxEK0IAhIgRwE0jiEJA',
}), }),
# 20524 # 20524
Challenge('e7573094', Variant.tv, JsChallengeType.N, { Challenge('e7573094', Variant.main, JsChallengeType.N, {
'IlLiA21ny7gqA2m4p37': '3KuQ3235dojTSjo4', 'IlLiA21ny7gqA2m4p37': '3KuQ3235dojTSjo4',
}), }),
Challenge('e7573094', Variant.tv, JsChallengeType.SIG, { Challenge('e7573094', Variant.main, JsChallengeType.SIG, {
'NJAJEij0EwRgIhAI0KExTgjfPk-MPM9MAdzyyPRt=BM8-XO5tm5hlMCSVpAiEAv7eP3CURqZNSPow8BXXAoazVoXgeMP7gH9BdylHCwgw=gwzz': 'NJAJEij0EwRgIhAI0KExTgjfPk-MPM9MAdzyyPRt=BM8-XO5tm5hlMCSVpAiEAv7eP3CURqZNSPow8BXXAoazVoXgeMP7gH9BdylHCwgw=gwzz':
'yEij0EwRgIhAI0KExTgjfPk-MPM9MAdzyNPRt=BM8-XO5tm5hlMCSVNAiEAvpeP3CURqZJSPow8BXXAoazVoXgeMP7gH9BdylHCwgw=g', 'yEij0EwRgIhAI0KExTgjfPk-MPM9MAdzyNPRt=BM8-XO5tm5hlMCSVNAiEAvpeP3CURqZJSPow8BXXAoazVoXgeMP7gH9BdylHCwgw=g',
}), }),
# 20525
Challenge('9fcf08e8', Variant.main, JsChallengeType.N, {
'Dyc5ALyWiO0VqwCiT': 'H2PLmmAmJsYjKA',
}),
Challenge('9fcf08e8', Variant.main, JsChallengeType.SIG, {
'\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a':
'\x6a\x69\x68\x67\x66\x65\x64\x63\x62\x61\x60\x5f\x5e\x5d\x5c\x5b\x5a\x59\x58\x57\x56\x55\x54\x53\x52\x51\x50\x4f\x4e\x4d\x4c\x4b\x4a\x49\x48\x47\x46\x45\x44\x43\x42\x41\x40\x3f\x3e\x3d\x3c\x3b\x3a\x39\x38\x37\x36\x35\x34\x33\x32\x31\x30\x2f\x2e\x2d\x2c\x2b\x2a\x29\x28\x27\x26\x25\x24\x23\x22\x21\x20\x1f\x1e\x1d\x1c\x1b\x1a\x19\x18\x17\x16\x15\x14\x13\x12\x11\x10\x0f\x0e\x0d\x0c\x0b\x03\x09\x08\x07\x06\x05\x04\x0a',
}),
# 20527
Challenge('21cd2156', Variant.main, JsChallengeType.N, {
'CiOxDbW1WEE8Ti4w': 'ZcBE4klItiC4rQ',
}),
Challenge('21cd2156', Variant.main, JsChallengeType.SIG, {
'\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a':
'\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x00\x46\x47\x48\x49\x4a\x4b\x6a\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x4c',
}),
# 20528
Challenge('5e55da5a', Variant.tv, JsChallengeType.N, {
'FgTvzyq4jKv482R7': 'l26nyYSotkzDxg',
}),
Challenge('5e55da5a', Variant.tv, JsChallengeType.SIG, {
'\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a':
'\x46\x66\x65\x64\x63\x62\x61\x60\x5f\x5e\x67\x6a\x5b\x5a\x59\x58\x57\x56\x55\x54\x53\x52\x51\x50\x4f\x4e\x4d\x4c\x4b\x4a\x49\x48\x47\x2c\x45\x44\x43\x42\x41\x40\x3f\x3e\x3d\x3c\x3b\x3a\x39\x38\x13\x36\x35\x34\x33\x32\x31\x30\x2f\x2e\x2d\x5d\x2b\x2a\x29\x28\x27\x26\x25\x24\x23\x22\x21\x20\x1f\x1e\x1d\x1c\x1b\x1a\x19\x18\x17\x16\x15\x14\x0c\x12\x11\x10\x0f\x0e\x0d\x00\x0b\x0a\x09\x08\x07\x06\x05\x04\x03\x02\x01\x37',
}),
# 20529
Challenge('631d3938', Variant.main, JsChallengeType.N, {
'KBx1qz7jMhxELa8c': 'ttPvh7WIptsgSw',
}),
Challenge('631d3938', Variant.main, JsChallengeType.SIG, {
'\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60\x61\x62\x63\x64\x65\x66':
'\x19\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x00\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60\x61\x62\x63',
}),
] ]
requests: list[JsChallengeRequest] = [] requests: list[JsChallengeRequest] = []

View File

@@ -312,7 +312,7 @@ class TestRequestHandlerBase:
@pytest.mark.parametrize('handler', ['Urllib', 'Requests', 'CurlCFFI'], indirect=True) @pytest.mark.parametrize('handler', ['Urllib', 'Requests', 'CurlCFFI'], indirect=True)
@pytest.mark.handler_flaky('CurlCFFI', os.name == 'nt', reason='segfaults') @pytest.mark.handler_flaky('CurlCFFI', reason='segfaults')
class TestHTTPRequestHandler(TestRequestHandlerBase): class TestHTTPRequestHandler(TestRequestHandlerBase):
def test_verify_cert(self, handler): def test_verify_cert(self, handler):
@@ -1100,7 +1100,7 @@ class TestRequestsRequestHandler(TestRequestHandlerBase):
@pytest.mark.parametrize('handler', ['CurlCFFI'], indirect=True) @pytest.mark.parametrize('handler', ['CurlCFFI'], indirect=True)
@pytest.mark.handler_flaky('CurlCFFI', os.name == 'nt', reason='segfaults') @pytest.mark.handler_flaky('CurlCFFI', reason='segfaults')
class TestCurlCFFIRequestHandler(TestRequestHandlerBase): class TestCurlCFFIRequestHandler(TestRequestHandlerBase):
@pytest.mark.parametrize('params,extensions', [ @pytest.mark.parametrize('params,extensions', [

View File

@@ -8,6 +8,7 @@ from ..utils import (
determine_ext, determine_ext,
int_or_none, int_or_none,
js_to_json, js_to_json,
make_archive_id,
parse_duration, parse_duration,
parse_iso8601, parse_iso8601,
url_or_none, url_or_none,
@@ -16,12 +17,12 @@ from ..utils.traversal import traverse_obj
class RTPIE(InfoExtractor): class RTPIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?rtp\.pt/play/(?:[^/#?]+/)?p(?P<program_id>\d+)/(?P<id>e\d+)' _VALID_URL = r'https?://(?:www\.)?rtp\.pt/play/(?:[^/#?]+/)?(?P<program_id>p\d+)/(?P<episode_id>e\d+)(?:/[^/#?]+/(?P<asset_id>\d+))?'
_TESTS = [{ _TESTS = [{
'url': 'http://www.rtp.pt/play/p405/e174042/paixoes-cruzadas', 'url': 'http://www.rtp.pt/play/p405/e174042/paixoes-cruzadas',
'md5': 'e736ce0c665e459ddb818546220b4ef8', 'md5': 'e736ce0c665e459ddb818546220b4ef8',
'info_dict': { 'info_dict': {
'id': 'e174042', 'id': '395769',
'ext': 'mp3', 'ext': 'mp3',
'title': 'Paixões Cruzadas', 'title': 'Paixões Cruzadas',
'description': 'md5:af979e58ba0ab73f78435fc943fdb070', 'description': 'md5:af979e58ba0ab73f78435fc943fdb070',
@@ -32,12 +33,15 @@ class RTPIE(InfoExtractor):
'modified_date': '20190327', 'modified_date': '20190327',
'timestamp': 1417219200, 'timestamp': 1417219200,
'upload_date': '20141129', 'upload_date': '20141129',
'episode_id': 'e174042',
'series_id': 'p405',
'_old_archive_ids': ['rtp e174042'],
}, },
}, { }, {
'url': 'https://www.rtp.pt/play/zigzag/p13166/e757904/25-curiosidades-25-de-abril', 'url': 'https://www.rtp.pt/play/zigzag/p13166/e757904/25-curiosidades-25-de-abril',
'md5': '5b4859940e3adef61247a77dfb76046a', 'md5': '5b4859940e3adef61247a77dfb76046a',
'info_dict': { 'info_dict': {
'id': 'e757904', 'id': '1226642',
'ext': 'mp4', 'ext': 'mp4',
'title': 'Estudar ou não estudar', 'title': 'Estudar ou não estudar',
'description': 'md5:3bfd7eb8bebfd5711a08df69c9c14c35', 'description': 'md5:3bfd7eb8bebfd5711a08df69c9c14c35',
@@ -50,13 +54,16 @@ class RTPIE(InfoExtractor):
'episode_number': 2, 'episode_number': 2,
'episode': 'Estudar ou não estudar', 'episode': 'Estudar ou não estudar',
'modified_date': '20240404', 'modified_date': '20240404',
'episode_id': 'e757904',
'series_id': 'p13166',
'_old_archive_ids': ['rtp e757904'],
}, },
}, { }, {
# Episode not accessible through API # Episode not accessible through API
'url': 'https://www.rtp.pt/play/estudoemcasa/p7776/e500050/portugues-1-ano', 'url': 'https://www.rtp.pt/play/estudoemcasa/p7776/e500050/portugues-1-ano',
'md5': '57660c0b46db9f22118c52cbd65975e4', 'md5': '57660c0b46db9f22118c52cbd65975e4',
'info_dict': { 'info_dict': {
'id': 'e500050', 'id': '871639',
'ext': 'mp4', 'ext': 'mp4',
'title': 'Português - 1.º ano', 'title': 'Português - 1.º ano',
'duration': 1669.0, 'duration': 1669.0,
@@ -64,6 +71,67 @@ class RTPIE(InfoExtractor):
'upload_date': '20201020', 'upload_date': '20201020',
'timestamp': 1603180799, 'timestamp': 1603180799,
'thumbnail': 'https://cdn-images.rtp.pt/EPG/imagens/39482_59449_64850.png?v=3&w=860', 'thumbnail': 'https://cdn-images.rtp.pt/EPG/imagens/39482_59449_64850.png?v=3&w=860',
'episode_id': 'e500050',
'series_id': 'p7776',
'_old_archive_ids': ['rtp e500050'],
},
'expected_warnings': ['Episode data not found in API response; falling back to web extraction'],
}, {
# Ambiguous URL for 1st part of a multi-part episode without --no-playlist
'url': 'https://www.rtp.pt/play/p14335/e877072/a-nossa-tarde',
'info_dict': {
'id': 'e877072',
'title': 'A Nossa Tarde',
'duration': 6545.0,
'series': 'A Nossa Tarde',
'series_id': 'p14335',
'season': '2025',
'episode_id': 'e877072',
'timestamp': 1758560188,
'upload_date': '20250922',
'modified_timestamp': 1758563110,
'modified_date': '20250922',
},
'playlist_count': 3,
}, {
# Ambiguous URL for 1st part of a multi-part episode with --no-playlist
'url': 'https://www.rtp.pt/play/p14335/e877072/a-nossa-tarde',
'md5': '2aa3c89c95e852d6f04168b95d0d0632',
'info_dict': {
'id': '1364711',
'ext': 'mp4',
'title': 'A Nossa Tarde',
'duration': 1292.0,
'thumbnail': r're:https://cdn-images\.rtp\.pt/multimedia/screenshots/p14335/p14335_1_20250922155118e161t0312\.jpg',
'series': 'A Nossa Tarde',
'series_id': 'p14335',
'season': '2025',
'episode_id': 'e877072',
'timestamp': 1758560188,
'upload_date': '20250922',
'modified_timestamp': 1758563110,
'modified_date': '20250922',
'_old_archive_ids': ['rtp e877072'],
},
'params': {'noplaylist': True},
}, {
# Unambiguous URL for 2nd part of a multi-part episode
'url': 'https://www.rtp.pt/play/p14335/e877072/a-nossa-tarde/1364744',
'md5': 'b624767af558a557372a6fcd1dcdfa17',
'info_dict': {
'id': '1364744',
'ext': 'mp4',
'title': 'A Nossa Tarde',
'duration': 3270.0,
'thumbnail': r're:https://cdn-images\.rtp\.pt/multimedia/screenshots/p14335/p14335_2_20250922165718e161t0412\.jpg',
'series': 'A Nossa Tarde',
'series_id': 'p14335',
'season': '2025',
'episode_id': 'e877072',
'timestamp': 1758560188,
'upload_date': '20250922',
'modified_timestamp': 1758563110,
'modified_date': '20250922',
}, },
}] }]
@@ -92,19 +160,19 @@ class RTPIE(InfoExtractor):
return None return None
return url.replace('/drm-fps/', '/hls/').replace('/drm-dash/', '/dash/') return url.replace('/drm-fps/', '/hls/').replace('/drm-dash/', '/dash/')
def _extract_formats(self, media_urls, episode_id): def _extract_formats(self, media_urls, display_id):
formats = [] formats = []
subtitles = {} subtitles = {}
for media_url in set(traverse_obj(media_urls, (..., {url_or_none}, {self._cleanup_media_url}))): for media_url in set(traverse_obj(media_urls, (..., {url_or_none}, {self._cleanup_media_url}))):
ext = determine_ext(media_url) ext = determine_ext(media_url)
if ext == 'm3u8': if ext == 'm3u8':
fmts, subs = self._extract_m3u8_formats_and_subtitles( fmts, subs = self._extract_m3u8_formats_and_subtitles(
media_url, episode_id, m3u8_id='hls', fatal=False) media_url, display_id, m3u8_id='hls', fatal=False)
formats.extend(fmts) formats.extend(fmts)
self._merge_subtitles(subs, target=subtitles) self._merge_subtitles(subs, target=subtitles)
elif ext == 'mpd': elif ext == 'mpd':
fmts, subs = self._extract_mpd_formats_and_subtitles( fmts, subs = self._extract_mpd_formats_and_subtitles(
media_url, episode_id, mpd_id='dash', fatal=False) media_url, display_id, mpd_id='dash', fatal=False)
formats.extend(fmts) formats.extend(fmts)
self._merge_subtitles(subs, target=subtitles) self._merge_subtitles(subs, target=subtitles)
else: else:
@@ -114,24 +182,12 @@ class RTPIE(InfoExtractor):
}) })
return formats, subtitles return formats, subtitles
def _extract_from_api(self, program_id, episode_id): def _extract_asset(self, asset_data, episode_id, episode_info, archive_compat=False):
auth_token = self._fetch_auth_token() asset_id = asset_data['asset_id']
if not auth_token: asset_urls = traverse_obj(asset_data, ('asset_url', {dict}))
return
episode_data = traverse_obj(self._download_json(
f'https://www.rtp.pt/play/api/1/get-episode/{program_id}/{episode_id[1:]}', episode_id,
query={'include_assets': 'true', 'include_webparams': 'true'},
headers={
'Accept': '*/*',
'Authorization': f'Bearer {auth_token}',
'User-Agent': self._USER_AGENT,
}, fatal=False), 'result', {dict})
if not episode_data:
return
asset_urls = traverse_obj(episode_data, ('assets', 0, 'asset_url', {dict}))
media_urls = traverse_obj(asset_urls, ( media_urls = traverse_obj(asset_urls, (
((('hls', 'dash'), 'stream_url'), ('multibitrate', ('url_hls', 'url_dash'))),)) ((('hls', 'dash'), 'stream_url'), ('multibitrate', ('url_hls', 'url_dash'))),))
formats, subtitles = self._extract_formats(media_urls, episode_id) formats, subtitles = self._extract_formats(media_urls, asset_id)
for sub_data in traverse_obj(asset_urls, ('subtitles', 'vtt_list', lambda _, v: url_or_none(v['file']))): for sub_data in traverse_obj(asset_urls, ('subtitles', 'vtt_list', lambda _, v: url_or_none(v['file']))):
subtitles.setdefault(sub_data.get('code') or 'pt', []).append({ subtitles.setdefault(sub_data.get('code') or 'pt', []).append({
@@ -140,17 +196,63 @@ class RTPIE(InfoExtractor):
}) })
return { return {
'id': episode_id, **episode_info,
'id': asset_id,
'episode_id': episode_id,
# asset_id is a unique identifier for all RTP videos, while episode_id is duplicated
# across all parts of a multi-part episode. Older versions of this IE returned
# episode_id as the video id and would only download the first part of multi-part eps.
# For download archive compat, we should return the episode_id as the old archive id
# *only* when extracting single-part episodes OR the *first* part of a multi-part ep.
'_old_archive_ids': [make_archive_id(self, episode_id)] if archive_compat else None,
'formats': formats, 'formats': formats,
'subtitles': subtitles, 'subtitles': subtitles,
'thumbnail': traverse_obj(episode_data, ('assets', 0, 'asset_thumbnail', {url_or_none})), **traverse_obj(asset_data, {
'thumbnail': ('asset_thumbnail', {url_or_none}),
'duration': ('asset_duration', {parse_duration}),
'webpage_url': ('web', 'url', {url_or_none}),
}),
}
def _report_fallback_warning(self, missing_info_name='required info', display_id=None):
self.report_warning(
f'{missing_info_name.capitalize()} not found in API response; falling back to web extraction',
video_id=display_id)
def _entries(self, assets, episode_id, episode_info):
# Only pass archive_compat=True for the first entry without an asset_id in its webpage_url
for idx, asset_data in enumerate(assets):
yield self._extract_asset(asset_data, episode_id, episode_info, archive_compat=not idx)
def _extract_from_api(self, program_id, episode_id, asset_id):
auth_token = self._fetch_auth_token()
if not auth_token:
self._report_fallback_warning('auth token', episode_id)
return None
episode_data = traverse_obj(self._download_json(
f'https://www.rtp.pt/play/api/1/get-episode/{program_id[1:]}/{episode_id[1:]}',
asset_id or episode_id, query={'include_assets': 'true', 'include_webparams': 'true'},
headers={
'Accept': '*/*',
'Authorization': f'Bearer {auth_token}',
'User-Agent': self._USER_AGENT,
}, fatal=False), 'result', {dict})
if not episode_data:
self._report_fallback_warning('episode data', episode_id)
return None
episode_info = {
'id': episode_id, # playlist id
'episode_id': episode_id,
'series_id': program_id,
**traverse_obj(episode_data, ('episode', { **traverse_obj(episode_data, ('episode', {
'title': (('episode_title', 'program_title'), {str}, filter, any), 'title': (('episode_title', 'program_title'), {str}, filter, any),
'alt_title': ('episode_subtitle', {str}, filter), 'alt_title': ('episode_subtitle', {str}, filter),
'description': (('episode_description', 'episode_summary'), {str}, filter, any), 'description': (('episode_description', 'episode_summary'), {str}, filter, any),
'timestamp': ('episode_air_date', {parse_iso8601(delimiter=' ')}), 'timestamp': ('episode_air_date', {parse_iso8601(delimiter=' ')}),
'modified_timestamp': ('episode_lastchanged', {parse_iso8601(delimiter=' ')}), 'modified_timestamp': ('episode_lastchanged', {parse_iso8601(delimiter=' ')}),
'duration': ('episode_duration_complete', {parse_duration}), 'duration': ('episode_duration_complete', {parse_duration}), # playlist duration
'episode': ('episode_title', {str}, filter), 'episode': ('episode_title', {str}, filter),
'episode_number': ('episode_number', {int_or_none}), 'episode_number': ('episode_number', {int_or_none}),
'season': ('program_season', {str}, filter), 'season': ('program_season', {str}, filter),
@@ -158,6 +260,30 @@ class RTPIE(InfoExtractor):
})), })),
} }
assets = traverse_obj(episode_data, ('assets', lambda _, v: v['asset_id']))
if not assets:
self._report_fallback_warning('asset IDs', episode_id)
return None
if asset_id:
asset_data = traverse_obj(assets, (lambda _, v: v['asset_id'] == asset_id, any))
if not asset_data:
self._report_fallback_warning(f'asset {asset_id}', episode_id)
return None
return self._extract_asset(asset_data, episode_id, episode_info)
asset_data = assets[0]
if self._yes_playlist(
len(assets) > 1 and episode_id, asset_data['asset_id'],
playlist_label='multi-part episode', video_label='individual part',
):
return self.playlist_result(
self._entries(assets, episode_id, episode_info), **episode_info)
# Pass archive_compat=True so we return _old_archive_ids for URLs without an asset_id
return self._extract_asset(asset_data, episode_id, episode_info, archive_compat=True)
_RX_OBFUSCATION = re.compile(r'''(?xs) _RX_OBFUSCATION = re.compile(r'''(?xs)
atob\s*\(\s*decodeURIComponent\s*\(\s* atob\s*\(\s*decodeURIComponent\s*\(\s*
(\[[0-9A-Za-z%,'"]*\]) (\[[0-9A-Za-z%,'"]*\])
@@ -172,25 +298,35 @@ class RTPIE(InfoExtractor):
)).decode('iso-8859-1')), )).decode('iso-8859-1')),
data) data)
def _extract_from_html(self, url, episode_id): def _extract_from_html(self, url, program_id, episode_id, asset_id):
webpage = self._download_webpage(url, episode_id) webpage = self._download_webpage(url, asset_id or episode_id)
if not asset_id:
asset_id = self._search_regex(r'\basset_id\s*:\s*"(\d+)"', webpage, 'asset ID')
old_archive_ids = [make_archive_id(self, episode_id)]
else:
old_archive_ids = None
formats = [] formats = []
subtitles = {} subtitles = {}
media_urls = traverse_obj(re.findall(r'(?:var\s+f\s*=|RTPPlayer\({[^}]+file:)\s*({[^}]+}|"[^"]+")', webpage), ( media_urls = traverse_obj(re.findall(r'(?:var\s+f\s*=|RTPPlayer\({[^}]+file:)\s*({[^}]+}|"[^"]+")', webpage), (
-1, (({self.__unobfuscate}, {js_to_json}, {json.loads}, {dict.values}, ...), {json.loads}))) -1, (({self.__unobfuscate}, {js_to_json}, {json.loads}, {dict.values}, ...), {json.loads})))
formats, subtitles = self._extract_formats(media_urls, episode_id) formats, subtitles = self._extract_formats(media_urls, asset_id)
return { return {
'id': episode_id, 'id': asset_id,
'episode_id': episode_id,
'series_id': program_id,
'formats': formats, 'formats': formats,
'subtitles': subtitles, 'subtitles': subtitles,
'description': self._html_search_meta(['og:description', 'twitter:description'], webpage, default=None), 'description': self._html_search_meta(['og:description', 'twitter:description'], webpage, default=None),
'thumbnail': self._html_search_meta(['og:image', 'twitter:image'], webpage, default=None), 'thumbnail': self._html_search_meta(['og:image', 'twitter:image'], webpage, default=None),
**self._search_json_ld(webpage, episode_id, default={}), **self._search_json_ld(webpage, asset_id, default={}),
'title': self._html_search_meta(['og:title', 'twitter:title'], webpage, default=None), 'title': self._html_search_meta(['og:title', 'twitter:title'], webpage, default=None),
'_old_archive_ids': old_archive_ids,
} }
def _real_extract(self, url): def _real_extract(self, url):
program_id, episode_id = self._match_valid_url(url).group('program_id', 'id') program_id, episode_id, asset_id = self._match_valid_url(url).group('program_id', 'episode_id', 'asset_id')
return self._extract_from_api(program_id, episode_id) or self._extract_from_html(url, episode_id) return (
self._extract_from_api(program_id, episode_id, asset_id)
or self._extract_from_html(url, program_id, episode_id, asset_id))

View File

@@ -1876,7 +1876,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
}] }]
_DEFAULT_PLAYER_JS_VERSION = 'actual' _DEFAULT_PLAYER_JS_VERSION = 'actual'
_DEFAULT_PLAYER_JS_VARIANT = 'tv' _DEFAULT_PLAYER_JS_VARIANT = 'main'
_PLAYER_JS_VARIANT_MAP = { _PLAYER_JS_VARIANT_MAP = {
'main': 'player_ias.vflset/en_US/base.js', 'main': 'player_ias.vflset/en_US/base.js',
'tcc': 'player_ias_tcc.vflset/en_US/base.js', 'tcc': 'player_ias_tcc.vflset/en_US/base.js',
@@ -1896,6 +1896,16 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
def _player_js_version(self): def _player_js_version(self):
return self._configuration_arg('player_js_version', [None])[0] or self._DEFAULT_PLAYER_JS_VERSION return self._configuration_arg('player_js_version', [None])[0] or self._DEFAULT_PLAYER_JS_VERSION
@functools.cached_property
def _webpage_client(self):
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
return webpage_client
@functools.cached_property @functools.cached_property
def _skipped_webpage_data(self): def _skipped_webpage_data(self):
skipped = set(self._configuration_arg('webpage_skip')) skipped = set(self._configuration_arg('webpage_skip'))
@@ -1929,13 +1939,13 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
start_time = time.time() start_time = time.time()
formats = [f for f in formats if f.get('is_from_start')] formats = [f for f in formats if f.get('is_from_start')]
def refetch_manifest(format_id, delay): def refetch_manifest(itag, client_name, delay):
nonlocal formats, start_time, is_live nonlocal formats, start_time, is_live
if time.time() <= start_time + delay: if time.time() <= start_time + delay:
return return
_, _, _, _, prs, player_url = self._initial_extract( _, _, _, _, prs, player_url = self._initial_extract(
url, smuggled_data, webpage_url, 'web', video_id) url, smuggled_data, webpage_url, self._webpage_client, video_id)
video_details = traverse_obj(prs, (..., 'videoDetails'), expected_type=dict) video_details = traverse_obj(prs, (..., 'videoDetails'), expected_type=dict)
microformats = traverse_obj( microformats = traverse_obj(
prs, (..., 'microformat', 'playerMicroformatRenderer'), prs, (..., 'microformat', 'playerMicroformatRenderer'),
@@ -1944,20 +1954,20 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
is_live = live_status == 'is_live' is_live = live_status == 'is_live'
start_time = time.time() start_time = time.time()
def mpd_feed(format_id, delay): def mpd_feed(itag, client_name, delay):
""" """
@returns (manifest_url, manifest_stream_number, is_live) or None @returns (manifest_url, manifest_stream_number, is_live) or None
""" """
for retry in self.RetryManager(fatal=False): for retry in self.RetryManager(fatal=False):
with lock: with lock:
refetch_manifest(format_id, delay) refetch_manifest(itag, client_name, delay)
f = next((f for f in formats if f['format_id'] == format_id), None) f = next((f for f in formats if f.get('_itag') == itag and f.get('_client') == client_name), None)
if not f: if not f:
if not is_live: if not is_live:
retry.error = f'{video_id}: Video is no longer live' retry.error = f'{video_id}: Video is no longer live'
else: else:
retry.error = f'Cannot find refreshed manifest for format {format_id}{bug_reports_message()}' retry.error = f'Cannot find refreshed manifest for format {itag}{bug_reports_message()}'
continue continue
# Formats from ended premieres will be missing a manifest_url # Formats from ended premieres will be missing a manifest_url
@@ -1970,7 +1980,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
for f in formats: for f in formats:
f['is_live'] = is_live f['is_live'] = is_live
gen = functools.partial(self._live_dash_fragments, video_id, f['format_id'], gen = functools.partial(self._live_dash_fragments, video_id, f['_itag'], f['_client'],
live_start_time, mpd_feed, not is_live and f.copy()) live_start_time, mpd_feed, not is_live and f.copy())
if is_live: if is_live:
f['fragments'] = gen f['fragments'] = gen
@@ -1979,7 +1989,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
f['fragments'] = LazyList(gen({})) f['fragments'] = LazyList(gen({}))
del f['is_from_start'] del f['is_from_start']
def _live_dash_fragments(self, video_id, format_id, live_start_time, mpd_feed, manifestless_orig_fmt, ctx): def _live_dash_fragments(self, video_id, itag, client_name, live_start_time, mpd_feed, manifestless_orig_fmt, ctx):
FETCH_SPAN, MAX_DURATION = 5, 432000 FETCH_SPAN, MAX_DURATION = 5, 432000
mpd_url, stream_number, is_live = None, None, True mpd_url, stream_number, is_live = None, None, True
@@ -2003,7 +2013,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
old_mpd_url = mpd_url old_mpd_url = mpd_url
last_error = ctx.pop('last_error', None) last_error = ctx.pop('last_error', None)
expire_fast = immediate or (last_error and isinstance(last_error, HTTPError) and last_error.status == 403) expire_fast = immediate or (last_error and isinstance(last_error, HTTPError) and last_error.status == 403)
mpd_url, stream_number, is_live = (mpd_feed(format_id, 5 if expire_fast else 18000) mpd_url, stream_number, is_live = (mpd_feed(itag, client_name, 5 if expire_fast else 18000)
or (mpd_url, stream_number, False)) or (mpd_url, stream_number, False))
if not refresh_sequence: if not refresh_sequence:
if expire_fast and not is_live: if expire_fast and not is_live:
@@ -2029,7 +2039,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
_last_seq = int(re.search(r'(?:/|^)sq/(\d+)', fragments[-1]['path']).group(1)) _last_seq = int(re.search(r'(?:/|^)sq/(\d+)', fragments[-1]['path']).group(1))
return True, _last_seq return True, _last_seq
self.write_debug(f'[{video_id}] Generating fragments for format {format_id}') self.write_debug(f'[{video_id}] Generating fragments for format {itag}')
while is_live: while is_live:
fetch_time = time.time() fetch_time = time.time()
if no_fragment_score > 30: if no_fragment_score > 30:
@@ -3737,11 +3747,15 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
sub[STREAMING_DATA_CLIENT_NAME] = client_name sub[STREAMING_DATA_CLIENT_NAME] = client_name
subtitles = self._merge_subtitles(subs, subtitles) # Prioritize HLS subs over DASH subtitles = self._merge_subtitles(subs, subtitles) # Prioritize HLS subs over DASH
for f in formats: for f in formats:
if process_manifest_format(f, 'dash', client_name, f['format_id'], require_po_token and not po_token): # Save original itag value as format_id because process_manifest_format mutates f
format_id = f['format_id']
if process_manifest_format(f, 'dash', client_name, format_id, require_po_token and not po_token):
f['filesize'] = int_or_none(self._search_regex( f['filesize'] = int_or_none(self._search_regex(
r'/clen/(\d+)', f.get('fragment_base_url') or f['url'], 'file size', default=None)) r'/clen/(\d+)', f.get('fragment_base_url') or f['url'], 'file size', default=None))
if needs_live_processing: if needs_live_processing:
f['is_from_start'] = True f['is_from_start'] = True
f['_itag'] = format_id
f['_client'] = client_name
yield f yield f
yield subtitles yield subtitles
@@ -3902,15 +3916,9 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
base_url = self.http_scheme() + '//www.youtube.com/' base_url = self.http_scheme() + '//www.youtube.com/'
webpage_url = base_url + 'watch?v=' + video_id webpage_url = base_url + 'watch?v=' + video_id
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( webpage, webpage_ytcfg, initial_data, is_premium_subscriber, player_responses, player_url = self._initial_extract(
url, smuggled_data, webpage_url, webpage_client, video_id) url, smuggled_data, webpage_url, self._webpage_client, video_id)
playability_statuses = traverse_obj( playability_statuses = traverse_obj(
player_responses, (..., 'playabilityStatus'), expected_type=dict) player_responses, (..., 'playabilityStatus'), expected_type=dict)

View File

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

View File

@@ -175,7 +175,7 @@ var jsc = (function (meriyah, astring) {
); );
} }
const setupNodes = meriyah.parse( 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`, `\nif (typeof globalThis.XMLHttpRequest === "undefined") {\n globalThis.XMLHttpRequest = { prototype: {} };\n}\nif (typeof URL === "undefined") {\n globalThis.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 globalThis.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}\nif (typeof globalThis.window === "undefined") {\n globalThis.window = globalThis;\n}\n`,
).body; ).body;
function _optionalChain(ops) { function _optionalChain(ops) {
let lastAccessLHS = undefined; let lastAccessLHS = undefined;

View File

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