mirror of
https://github.com/yt-dlp/yt-dlp.git
synced 2026-03-23 18:22:09 +01:00
Compare commits
18 Commits
2026.03.03
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f01e1a1ced | ||
|
|
7fd74d1009 | ||
|
|
04d6974f50 | ||
|
|
18656b2f2a | ||
|
|
1b6ec8fc25 | ||
|
|
7fab4c2b23 | ||
|
|
66c4947e9c | ||
|
|
4fc768b7f7 | ||
|
|
e68afb2827 | ||
|
|
990fdf36dd | ||
|
|
92f1d99dbe | ||
|
|
db62e438a1 | ||
|
|
3e36cf9cdb | ||
|
|
ae025da023 | ||
|
|
48a61d0f38 | ||
|
|
f2bd3202c0 | ||
|
|
7e145ac1ca | ||
|
|
ff459e5fc0 |
32
.github/workflows/build.yml
vendored
32
.github/workflows/build.yml
vendored
@@ -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: |
|
||||||
|
|||||||
4
.github/workflows/challenge-tests.yml
vendored
4
.github/workflows/challenge-tests.yml
vendored
@@ -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)
|
||||||
|
|||||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -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}}"
|
||||||
|
|||||||
2
.github/workflows/release-master.yml
vendored
2
.github/workflows/release-master.yml
vendored
@@ -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
|
||||||
|
|||||||
4
.github/workflows/release-nightly.yml
vendored
4
.github/workflows/release-nightly.yml
vendored
@@ -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
|
||||||
|
|||||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -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-*
|
||||||
|
|||||||
8
.github/workflows/test-workflows.yml
vendored
8
.github/workflows/test-workflows.yml
vendored
@@ -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
4
.github/zizmor.yml
vendored
@@ -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:
|
||||||
|
|||||||
@@ -874,3 +874,6 @@ LordMZTE
|
|||||||
regulad
|
regulad
|
||||||
stastix
|
stastix
|
||||||
syphyr
|
syphyr
|
||||||
|
FriederHannenheim
|
||||||
|
Peter-Devine
|
||||||
|
SparseOrnament15
|
||||||
|
|||||||
28
Changelog.md
28
Changelog.md
@@ -4,6 +4,34 @@
|
|||||||
# 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
|
||||||
|
|
||||||
|
#### 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
|
### 2026.03.03
|
||||||
|
|
||||||
#### Extractor changes
|
#### Extractor changes
|
||||||
|
|||||||
6
Makefile
6
Makefile
@@ -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.5.0
|
EJS_VERSION = 0.8.0
|
||||||
EJS_WHEEL_NAME = yt_dlp_ejs-0.5.0-py3-none-any.whl
|
EJS_WHEEL_NAME = yt_dlp_ejs-0.8.0-py3-none-any.whl
|
||||||
EJS_WHEEL_HASH = sha256:674fc0efea741d3100cdf3f0f9e123150715ee41edf47ea7a62fbdeda204bdec
|
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
|
||||||
|
|||||||
11
README.md
11
README.md
@@ -1860,12 +1860,13 @@ 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`. Using these will not skip any network requests, and in some cases will result in additional network requests. Currently, the default is `player_response`; however, typically these are for testing purposes only
|
* `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_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`). Currently, the default is to force `20514@9f4cc5e4`. 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`
|
||||||
* `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`
|
||||||
* A `max-depth` value of `1` will discard all replies, regardless of the `max-replies` or `max-replies-per-thread` values given
|
* 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)
|
* `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)
|
* `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)
|
* `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
|
#### 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)
|
* `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)
|
||||||
|
|||||||
@@ -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.5.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'",
|
||||||
|
|||||||
@@ -53,117 +53,65 @@ class Challenge:
|
|||||||
|
|
||||||
|
|
||||||
CHALLENGES: list[Challenge] = [
|
CHALLENGES: list[Challenge] = [
|
||||||
Challenge('3d3ba064', Variant.tce, JsChallengeType.N, {
|
# 20522
|
||||||
'ZdZIqFPQK-Ty8wId': 'qmtUsIz04xxiNW',
|
Challenge('74edf1a3', Variant.main, JsChallengeType.N, {
|
||||||
'4GMrWHyKI5cEvhDO': 'N9gmEX7YhKTSmw',
|
'IlLiA21ny7gqA2m4p37': '9nRTxrbM1f0yHg',
|
||||||
|
'eabGFpsUKuWHXGh6FR4': 'izmYqDEY6kl7Sg',
|
||||||
|
'eabGF/ps%UK=uWHXGh6FR4': 'LACmqlhaBpiPlgE-a',
|
||||||
}),
|
}),
|
||||||
Challenge('3d3ba064', Variant.tce, JsChallengeType.SIG, {
|
Challenge('74edf1a3', Variant.main, 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, {
|
|
||||||
'NJAJEij0EwRgIhAI0KExTgjfPk-MPM9MAdzyyPRt=BM8-XO5tm5hlMCSVpAiEAv7eP3CURqZNSPow8BXXAoazVoXgeMP7gH9BdylHCwgw=gwzz':
|
'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.tv, JsChallengeType.N, {
|
# 20523
|
||||||
'BQoJvGBkC2nj1ZZLK-': 'ib1ShEOGoFXIIw',
|
Challenge('901741ab', Variant.main, JsChallengeType.N, {
|
||||||
|
'BQoJvGBkC2nj1ZZLK-': 'UMPovvBZRh-sjb',
|
||||||
}),
|
}),
|
||||||
Challenge('94667337', Variant.tv, JsChallengeType.SIG, {
|
Challenge('901741ab', Variant.main, JsChallengeType.SIG, {
|
||||||
'NJAJEij0EwRgIhAI0KExTgjfPk-MPM9MAdzyyPRt=BM8-XO5tm5hlMCSVpAiEAv7eP3CURqZNSPow8BXXAoazVoXgeMP7gH9BdylHCwgw=gwzz':
|
'NJAJEij0EwRgIhAI0KExTgjfPk-MPM9MAdzyyPRt=BM8-XO5tm5hlMCSVpAiEAv7eP3CURqZNSPow8BXXAoazVoXgeMP7gH9BdylHCwgw=gwzz':
|
||||||
'AJEij0EwRgIhAI0KExTgjfPk-MPM9MNdzyyPRtzBM8-XO5tm5hlMCSVpAiEAv7eP3CURqZNSPow8BXXAoazVoXgeMP7gH9BdylHCwgw=',
|
'wgwCHlydB9Hg7PMegXoVzaoAXXB8woPSNZqRUC3Pe7vAEiApVSCMlhwmt5ON-8MB=5RPyyzdAM9MPM-kPfjgTxEK0IAhIgRwE0jiEJA',
|
||||||
}),
|
}),
|
||||||
Challenge('94667337', Variant.es6, JsChallengeType.N, {
|
# 20524
|
||||||
'BQoJvGBkC2nj1ZZLK-': 'ib1ShEOGoFXIIw',
|
Challenge('e7573094', Variant.main, JsChallengeType.N, {
|
||||||
|
'IlLiA21ny7gqA2m4p37': '3KuQ3235dojTSjo4',
|
||||||
}),
|
}),
|
||||||
Challenge('94667337', Variant.es6, JsChallengeType.SIG, {
|
Challenge('e7573094', Variant.main, JsChallengeType.SIG, {
|
||||||
'NJAJEij0EwRgIhAI0KExTgjfPk-MPM9MAdzyyPRt=BM8-XO5tm5hlMCSVpAiEAv7eP3CURqZNSPow8BXXAoazVoXgeMP7gH9BdylHCwgw=gwzz':
|
'NJAJEij0EwRgIhAI0KExTgjfPk-MPM9MAdzyyPRt=BM8-XO5tm5hlMCSVpAiEAv7eP3CURqZNSPow8BXXAoazVoXgeMP7gH9BdylHCwgw=gwzz':
|
||||||
'AJEij0EwRgIhAI0KExTgjfPk-MPM9MNdzyyPRtzBM8-XO5tm5hlMCSVpAiEAv7eP3CURqZNSPow8BXXAoazVoXgeMP7gH9BdylHCwgw=',
|
'yEij0EwRgIhAI0KExTgjfPk-MPM9MAdzyNPRt=BM8-XO5tm5hlMCSVNAiEAvpeP3CURqZJSPow8BXXAoazVoXgeMP7gH9BdylHCwgw=g',
|
||||||
}),
|
}),
|
||||||
Challenge('94667337', Variant.tce, JsChallengeType.N, {
|
# 20525
|
||||||
'BQoJvGBkC2nj1ZZLK-': 'ib1ShEOGoFXIIw',
|
Challenge('9fcf08e8', Variant.main, JsChallengeType.N, {
|
||||||
|
'Dyc5ALyWiO0VqwCiT': 'H2PLmmAmJsYjKA',
|
||||||
}),
|
}),
|
||||||
Challenge('94667337', Variant.tce, JsChallengeType.SIG, {
|
Challenge('9fcf08e8', Variant.main, JsChallengeType.SIG, {
|
||||||
'NJAJEij0EwRgIhAI0KExTgjfPk-MPM9MAdzyyPRt=BM8-XO5tm5hlMCSVpAiEAv7eP3CURqZNSPow8BXXAoazVoXgeMP7gH9BdylHCwgw=gwzz':
|
'\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':
|
||||||
'AJEij0EwRgIhAI0KExTgjfPk-MPM9MNdzyyPRtzBM8-XO5tm5hlMCSVpAiEAv7eP3CURqZNSPow8BXXAoazVoXgeMP7gH9BdylHCwgw=',
|
'\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',
|
||||||
}),
|
}),
|
||||||
Challenge('94667337', Variant.es6_tce, JsChallengeType.N, {
|
# 20527
|
||||||
'BQoJvGBkC2nj1ZZLK-': 'ib1ShEOGoFXIIw',
|
Challenge('21cd2156', Variant.main, JsChallengeType.N, {
|
||||||
|
'CiOxDbW1WEE8Ti4w': 'ZcBE4klItiC4rQ',
|
||||||
}),
|
}),
|
||||||
Challenge('94667337', Variant.es6_tce, JsChallengeType.SIG, {
|
Challenge('21cd2156', Variant.main, JsChallengeType.SIG, {
|
||||||
'NJAJEij0EwRgIhAI0KExTgjfPk-MPM9MAdzyyPRt=BM8-XO5tm5hlMCSVpAiEAv7eP3CURqZNSPow8BXXAoazVoXgeMP7gH9BdylHCwgw=gwzz':
|
'\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':
|
||||||
'AJEij0EwRgIhAI0KExTgjfPk-MPM9MNdzyyPRtzBM8-XO5tm5hlMCSVpAiEAv7eP3CURqZNSPow8BXXAoazVoXgeMP7gH9BdylHCwgw=',
|
'\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',
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -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', [
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ class TikTokBaseIE(InfoExtractor):
|
|||||||
raise ExtractorError('Unable to extract aweme detail info', video_id=aweme_id)
|
raise ExtractorError('Unable to extract aweme detail info', video_id=aweme_id)
|
||||||
return self._parse_aweme_video_app(aweme_detail)
|
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, (
|
challenge_data = traverse_obj(webpage, (
|
||||||
{find_element(id='cs', html=True)}, {extract_attributes}, 'class',
|
{find_element(id='cs', html=True)}, {extract_attributes}, 'class',
|
||||||
filter, {lambda x: f'{x}==='}, {base64.b64decode}, {json.loads}))
|
filter, {lambda x: f'{x}==='}, {base64.b64decode}, {json.loads}))
|
||||||
@@ -250,17 +250,27 @@ class TikTokBaseIE(InfoExtractor):
|
|||||||
else:
|
else:
|
||||||
raise ExtractorError('Unable to solve JS challenge')
|
raise ExtractorError('Unable to solve JS challenge')
|
||||||
|
|
||||||
cookie_value = base64.b64encode(
|
wci_cookie_value = base64.b64encode(
|
||||||
json.dumps(challenge_data, separators=(',', ':')).encode()).decode()
|
json.dumps(challenge_data, separators=(',', ':')).encode()).decode()
|
||||||
|
|
||||||
# At time of writing, the cookie name was _wafchallengeid
|
# At time of writing, the wci cookie name was `_wafchallengeid`
|
||||||
cookie_name = traverse_obj(webpage, (
|
wci_cookie_name = traverse_obj(webpage, (
|
||||||
{find_element(id='wci', html=True)}, {extract_attributes},
|
{find_element(id='wci', html=True)}, {extract_attributes},
|
||||||
'class', {require('challenge cookie name')}))
|
'class', {require('challenge cookie name')}))
|
||||||
|
|
||||||
# Actual JS sets Max-Age=1, but we need to adjust for --sleep-requests and Python slowness
|
# At time of writing, the **optional** rci cookie name was `waforiginalreid`
|
||||||
expire_time = int(time.time()) + (self.get_param('sleep_interval_requests') or 0) + 2
|
rci_cookie_name = traverse_obj(webpage, (
|
||||||
self._set_cookie('.tiktok.com', cookie_name, cookie_value, expire_time=expire_time)
|
{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):
|
def _extract_web_data_and_status(self, url, video_id, fatal=True):
|
||||||
video_data, status = {}, -1
|
video_data, status = {}, -1
|
||||||
@@ -287,7 +297,7 @@ class TikTokBaseIE(InfoExtractor):
|
|||||||
universal_data = self._get_universal_data(webpage, video_id)
|
universal_data = self._get_universal_data(webpage, video_id)
|
||||||
if not universal_data:
|
if not universal_data:
|
||||||
try:
|
try:
|
||||||
self._solve_challenge_and_set_cookie(webpage)
|
cookie_names = self._solve_challenge_and_set_cookies(webpage)
|
||||||
except ExtractorError as e:
|
except ExtractorError as e:
|
||||||
if fatal:
|
if fatal:
|
||||||
raise
|
raise
|
||||||
@@ -295,6 +305,9 @@ class TikTokBaseIE(InfoExtractor):
|
|||||||
return video_data, status
|
return video_data, status
|
||||||
|
|
||||||
webpage = get_webpage(note='Downloading webpage with challenge cookie')
|
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:
|
if webpage is False:
|
||||||
return video_data, status
|
return video_data, status
|
||||||
universal_data = self._get_universal_data(webpage, video_id)
|
universal_data = self._get_universal_data(webpage, video_id)
|
||||||
|
|||||||
@@ -104,7 +104,6 @@ INNERTUBE_CLIENTS = {
|
|||||||
},
|
},
|
||||||
'INNERTUBE_CONTEXT_CLIENT_NAME': 1,
|
'INNERTUBE_CONTEXT_CLIENT_NAME': 1,
|
||||||
'SUPPORTS_COOKIES': True,
|
'SUPPORTS_COOKIES': True,
|
||||||
'SUPPORTS_AD_PLAYBACK_CONTEXT': True,
|
|
||||||
**WEB_PO_TOKEN_POLICIES,
|
**WEB_PO_TOKEN_POLICIES,
|
||||||
},
|
},
|
||||||
# Safari UA returns pre-merged video+audio 144p/240p/360p/720p/1080p HLS formats
|
# 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,
|
'INNERTUBE_CONTEXT_CLIENT_NAME': 1,
|
||||||
'SUPPORTS_COOKIES': True,
|
'SUPPORTS_COOKIES': True,
|
||||||
'SUPPORTS_AD_PLAYBACK_CONTEXT': True,
|
|
||||||
**WEB_PO_TOKEN_POLICIES,
|
**WEB_PO_TOKEN_POLICIES,
|
||||||
},
|
},
|
||||||
'web_embedded': {
|
'web_embedded': {
|
||||||
@@ -223,16 +221,17 @@ INNERTUBE_CLIENTS = {
|
|||||||
},
|
},
|
||||||
'PLAYER_PO_TOKEN_POLICY': PlayerPoTokenPolicy(required=False, recommended=True),
|
'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': {
|
'android_vr': {
|
||||||
'INNERTUBE_CONTEXT': {
|
'INNERTUBE_CONTEXT': {
|
||||||
'client': {
|
'client': {
|
||||||
'clientName': 'ANDROID_VR',
|
'clientName': 'ANDROID_VR',
|
||||||
'clientVersion': '1.71.26',
|
'clientVersion': '1.65.10',
|
||||||
'deviceMake': 'Oculus',
|
'deviceMake': 'Oculus',
|
||||||
'deviceModel': 'Quest 3',
|
'deviceModel': 'Quest 3',
|
||||||
'androidSdkVersion': 32,
|
'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',
|
'osName': 'Android',
|
||||||
'osVersion': '12L',
|
'osVersion': '12L',
|
||||||
},
|
},
|
||||||
@@ -369,7 +368,7 @@ def short_client_name(client_name):
|
|||||||
|
|
||||||
def _fix_embedded_ytcfg(ytcfg):
|
def _fix_embedded_ytcfg(ytcfg):
|
||||||
ytcfg['INNERTUBE_CONTEXT'].setdefault('thirdParty', {}).update({
|
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 = {
|
url = {
|
||||||
'mweb': 'https://m.youtube.com',
|
'mweb': 'https://m.youtube.com',
|
||||||
'web': 'https://www.youtube.com',
|
'web': 'https://www.youtube.com',
|
||||||
|
'web_safari': 'https://www.youtube.com',
|
||||||
'web_music': 'https://music.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',
|
'web_embedded': f'https://www.youtube.com/embed/{video_id}?html5=1',
|
||||||
'tv': 'https://www.youtube.com/tv',
|
'tv': 'https://www.youtube.com/tv',
|
||||||
}.get(client)
|
}.get(client)
|
||||||
if not url:
|
if not url:
|
||||||
return {}
|
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(
|
webpage = self._download_webpage_with_retries(
|
||||||
url, video_id, note=f'Downloading {client.replace("_", " ").strip()} client config',
|
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}),
|
'User-Agent': ('INNERTUBE_CONTEXT', 'client', 'userAgent', {str}),
|
||||||
|
'Referer': ('INNERTUBE_CONTEXT', 'thirdParty', 'embedUrl', {str}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
ytcfg = self.extract_ytcfg(video_id, webpage) or {}
|
ytcfg = self.extract_ytcfg(video_id, webpage) or {}
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ class YoutubeTabBaseInfoExtractor(YoutubeBaseInfoExtractor):
|
|||||||
'reelPlayerHeaderSupportedRenderers', 'reelPlayerHeaderRenderer'))
|
'reelPlayerHeaderSupportedRenderers', 'reelPlayerHeaderRenderer'))
|
||||||
|
|
||||||
title = self._get_text(renderer, 'title', 'headline') or self._get_text(reel_header_renderer, 'reelTitleText')
|
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'))
|
duration = int_or_none(renderer.get('lengthSeconds'))
|
||||||
if duration is None:
|
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:])
|
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
|
elif item_id[:2] == 'MP': # Resolve albums (/[channel/browse]/MP...) to their equivalent playlist
|
||||||
mdata = self._extract_tab_endpoint(
|
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'),
|
murl = traverse_obj(mdata, ('microformat', 'microformatDataRenderer', 'urlCanonical'),
|
||||||
get_all=False, expected_type=str)
|
get_all=False, expected_type=str)
|
||||||
if not murl:
|
if not murl:
|
||||||
|
|||||||
@@ -140,11 +140,13 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|||||||
_RETURN_TYPE = 'video' # XXX: How to handle multifeed?
|
_RETURN_TYPE = 'video' # XXX: How to handle multifeed?
|
||||||
|
|
||||||
_SUBTITLE_FORMATS = ('json3', 'srv1', 'srv2', 'srv3', 'ttml', 'srt', 'vtt')
|
_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_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)
|
# 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
|
_GEO_BYPASS = False
|
||||||
|
|
||||||
@@ -1873,13 +1875,8 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|||||||
'params': {'skip_download': True},
|
'params': {'skip_download': True},
|
||||||
}]
|
}]
|
||||||
|
|
||||||
@property
|
_DEFAULT_PLAYER_JS_VERSION = 'actual'
|
||||||
def _skipped_webpage_data(self):
|
_DEFAULT_PLAYER_JS_VARIANT = 'main'
|
||||||
# XXX: player_response as a default is a TEMPORARY workaround for pinning _DEFAULT_PLAYER_JS_VERSION
|
|
||||||
return self._configuration_arg('webpage_skip', default=['player_response'])
|
|
||||||
|
|
||||||
_DEFAULT_PLAYER_JS_VERSION = '20514@9f4cc5e4'
|
|
||||||
_DEFAULT_PLAYER_JS_VARIANT = 'tv'
|
|
||||||
_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',
|
||||||
@@ -1895,6 +1892,28 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|||||||
}
|
}
|
||||||
_INVERSE_PLAYER_JS_VARIANT_MAP = {v: k for k, v in _PLAYER_JS_VARIANT_MAP.items()}
|
_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 _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
|
||||||
|
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
|
@classmethod
|
||||||
def suitable(cls, url):
|
def suitable(cls, url):
|
||||||
from yt_dlp.utils import parse_qs
|
from yt_dlp.utils import parse_qs
|
||||||
@@ -1920,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'),
|
||||||
@@ -1935,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
|
||||||
@@ -1961,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
|
||||||
@@ -1970,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
|
||||||
@@ -1994,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:
|
||||||
@@ -2020,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:
|
||||||
@@ -2082,15 +2101,14 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|||||||
time.sleep(max(0, FETCH_SPAN + fetch_time - time.time()))
|
time.sleep(max(0, FETCH_SPAN + fetch_time - time.time()))
|
||||||
|
|
||||||
def _get_player_js_version(self):
|
def _get_player_js_version(self):
|
||||||
player_js_version = self._configuration_arg('player_js_version', [''])[0] or self._DEFAULT_PLAYER_JS_VERSION
|
if self._player_js_version == 'actual':
|
||||||
if player_js_version == 'actual':
|
|
||||||
return None, None
|
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(
|
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)
|
f'It should be "actual" or in the format of STS@HASH', only_once=True)
|
||||||
return None, None
|
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):
|
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'
|
assert player_id or player_url, '_construct_player_url must take one of player_id or player_url'
|
||||||
@@ -2680,12 +2698,14 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|||||||
return {'contentCheckOk': True, 'racyCheckOk': True}
|
return {'contentCheckOk': True, 'racyCheckOk': True}
|
||||||
|
|
||||||
@classmethod
|
@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 = {
|
context = {
|
||||||
'html5Preference': 'HTML5_PREF_WANTS',
|
'html5Preference': 'HTML5_PREF_WANTS',
|
||||||
}
|
}
|
||||||
if sts is not None:
|
if sts is not None:
|
||||||
context['signatureTimestamp'] = sts
|
context['signatureTimestamp'] = sts
|
||||||
|
if encrypted_context:
|
||||||
|
context['encryptedHostFlags'] = encrypted_context
|
||||||
|
|
||||||
playback_context = {
|
playback_context = {
|
||||||
'contentPlaybackContext': context,
|
'contentPlaybackContext': context,
|
||||||
@@ -2930,7 +2950,19 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|||||||
self._configuration_arg('use_ad_playback_context', ['false'])[0] != 'false'
|
self._configuration_arg('use_ad_playback_context', ['false'])[0] != 'false'
|
||||||
and traverse_obj(INNERTUBE_CLIENTS, (client, 'SUPPORTS_AD_PLAYBACK_CONTEXT', {bool})))
|
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(
|
return self._extract_response(
|
||||||
item_id=video_id, ep='player', query=yt_query,
|
item_id=video_id, ep='player', query=yt_query,
|
||||||
@@ -3715,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
|
||||||
|
|
||||||
@@ -3880,10 +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 = 'web'
|
|
||||||
|
|
||||||
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)
|
||||||
|
|||||||
@@ -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.5.0'
|
VERSION = '0.8.0'
|
||||||
HASHES = {
|
HASHES = {
|
||||||
'yt.solver.bun.lib.js': '6ff45e94de9f0ea936a183c48173cfa9ce526ee4b7544cd556428427c1dd53c8073ef0174e79b320252bf0e7c64b0032cc1cf9c4358f3fda59033b7caa01c241',
|
'yt.solver.bun.lib.js': '6ff45e94de9f0ea936a183c48173cfa9ce526ee4b7544cd556428427c1dd53c8073ef0174e79b320252bf0e7c64b0032cc1cf9c4358f3fda59033b7caa01c241',
|
||||||
'yt.solver.core.js': '9742868113d7b0c29e24a95c8eb2c2bec7cdf95513dc7f55f523ba053c0ecf2af7dcb0138b1d933578304f0dda633a6b3bfff64e912b4c547b99dad083428c4b',
|
'yt.solver.core.js': 'c163a6f376db6ce3da47d516a28a8f2a0554ae95c58dc766f0a6e2b3894f2cef1ee07fa84beb442fa471aac4f300985added1657c7c94c4d1cfefe68920ab599',
|
||||||
'yt.solver.core.min.js': 'aee8c3354cfd535809c871c2a517d03231f89cd184e903af82ee274bcc2e90991ef19cb3f65f2ccc858c4963856ea87f8692fe16d71209f4fc7f41c44b828e36',
|
'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',
|
||||||
|
|||||||
@@ -39,284 +39,8 @@ var jsc = (function (meriyah, astring) {
|
|||||||
function isOneOf(value, ...of) {
|
function isOneOf(value, ...of) {
|
||||||
return of.includes(value);
|
return of.includes(value);
|
||||||
}
|
}
|
||||||
function _optionalChain$2(ops) {
|
function generateArrowFunction(data) {
|
||||||
let lastAccessLHS = undefined;
|
return meriyah.parse(data).body[0].expression;
|
||||||
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 _optionalChain$1(ops) {
|
function _optionalChain$1(ops) {
|
||||||
let lastAccessLHS = undefined;
|
let lastAccessLHS = undefined;
|
||||||
@@ -341,156 +65,117 @@ var jsc = (function (meriyah, astring) {
|
|||||||
}
|
}
|
||||||
const identifier = {
|
const identifier = {
|
||||||
or: [
|
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',
|
type: 'VariableDeclaration',
|
||||||
kind: 'var',
|
|
||||||
declarations: {
|
declarations: {
|
||||||
anykey: [
|
anykey: [
|
||||||
{
|
{
|
||||||
type: 'VariableDeclarator',
|
type: 'VariableDeclarator',
|
||||||
id: { type: 'Identifier' },
|
init: { type: 'FunctionExpression', async: false },
|
||||||
init: {
|
|
||||||
type: 'ArrayExpression',
|
|
||||||
elements: [{ type: 'Identifier' }],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
type: 'ExpressionStatement',
|
|
||||||
expression: {
|
|
||||||
type: 'AssignmentExpression',
|
|
||||||
left: { type: 'Identifier' },
|
|
||||||
operator: '=',
|
|
||||||
right: {
|
|
||||||
type: 'ArrayExpression',
|
|
||||||
elements: [{ type: 'Identifier' }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
const catchBlockBody = [
|
const asdasd = {
|
||||||
{
|
type: 'ExpressionStatement',
|
||||||
type: 'ReturnStatement',
|
expression: {
|
||||||
argument: {
|
type: 'CallExpression',
|
||||||
type: 'BinaryExpression',
|
callee: {
|
||||||
left: {
|
type: 'MemberExpression',
|
||||||
type: 'MemberExpression',
|
object: { type: 'Identifier' },
|
||||||
object: { type: 'Identifier' },
|
property: {},
|
||||||
computed: true,
|
optional: false,
|
||||||
property: { type: 'Literal' },
|
|
||||||
optional: false,
|
|
||||||
},
|
|
||||||
right: { type: 'Identifier' },
|
|
||||||
operator: '+',
|
|
||||||
},
|
},
|
||||||
|
arguments: [
|
||||||
|
{ type: 'Literal', value: 'alr' },
|
||||||
|
{ type: 'Literal', value: 'yes' },
|
||||||
|
],
|
||||||
|
optional: false,
|
||||||
},
|
},
|
||||||
];
|
};
|
||||||
function extract(node) {
|
function extract(node) {
|
||||||
if (!matchesStructure(node, identifier)) {
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
if (node.type === 'VariableDeclaration') {
|
const options = [];
|
||||||
for (const declaration of node.declarations) {
|
if (node.type === 'FunctionDeclaration') {
|
||||||
if (
|
if (
|
||||||
declaration.type !== 'VariableDeclarator' ||
|
node.id &&
|
||||||
!declaration.init ||
|
_optionalChain$1([
|
||||||
declaration.init.type !== 'ArrayExpression' ||
|
node,
|
||||||
declaration.init.elements.length !== 1
|
'access',
|
||||||
) {
|
(_) => _.body,
|
||||||
continue;
|
'optionalAccess',
|
||||||
}
|
(_2) => _2.body,
|
||||||
const [firstElement] = declaration.init.elements;
|
])
|
||||||
if (firstElement && firstElement.type === 'Identifier') {
|
) {
|
||||||
return makeSolverFuncFromName(firstElement.name);
|
options.push({
|
||||||
}
|
name: node.id,
|
||||||
|
statements: _optionalChain$1([
|
||||||
|
node,
|
||||||
|
'access',
|
||||||
|
(_3) => _3.body,
|
||||||
|
'optionalAccess',
|
||||||
|
(_4) => _4.body,
|
||||||
|
]),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else if (node.type === 'ExpressionStatement') {
|
} else if (node.type === 'ExpressionStatement') {
|
||||||
const expr = node.expression;
|
if (node.expression.type !== 'AssignmentExpression') {
|
||||||
if (
|
return null;
|
||||||
expr.type === 'AssignmentExpression' &&
|
}
|
||||||
expr.left.type === 'Identifier' &&
|
const name = node.expression.left;
|
||||||
expr.operator === '=' &&
|
const body = _optionalChain$1([
|
||||||
expr.right.type === 'ArrayExpression' &&
|
node.expression.right,
|
||||||
expr.right.elements.length === 1
|
'optionalAccess',
|
||||||
) {
|
(_5) => _5.body,
|
||||||
const [firstElement] = expr.right.elements;
|
'optionalAccess',
|
||||||
if (firstElement && firstElement.type === 'Identifier') {
|
(_6) => _6.body,
|
||||||
return makeSolverFuncFromName(firstElement.name);
|
]);
|
||||||
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
function makeSolverFuncFromName(name) {
|
function createSolver(expression) {
|
||||||
return {
|
return generateArrowFunction(
|
||||||
type: 'ArrowFunctionExpression',
|
`\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`,
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
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;
|
||||||
@@ -585,235 +270,59 @@ var jsc = (function (meriyah, astring) {
|
|||||||
function getSolutions(statements) {
|
function getSolutions(statements) {
|
||||||
const found = { n: [], sig: [] };
|
const found = { n: [], sig: [] };
|
||||||
for (const statement of statements) {
|
for (const statement of statements) {
|
||||||
const n = extract(statement);
|
const result = extract(statement);
|
||||||
if (n) {
|
if (result) {
|
||||||
found.n.push(n);
|
found.n.push(makeSolver(result, { type: 'Identifier', name: 'n' }));
|
||||||
}
|
found.sig.push(makeSolver(result, { type: 'Identifier', name: 'sig' }));
|
||||||
const sig = extract$1(statement);
|
|
||||||
if (sig) {
|
|
||||||
found.sig.push(sig);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return found;
|
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) {
|
function getFromPrepared(code) {
|
||||||
const resultObj = { n: null, sig: null };
|
const resultObj = { n: null, sig: null };
|
||||||
Function('_result', code)(resultObj);
|
Function('_result', code)(resultObj);
|
||||||
return resultObj;
|
return resultObj;
|
||||||
}
|
}
|
||||||
function multiTry(generators) {
|
function multiTry(generators) {
|
||||||
return {
|
return generateArrowFunction(
|
||||||
type: 'ArrowFunctionExpression',
|
`\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`,
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
function main(input) {
|
function main(input) {
|
||||||
const preprocessedPlayer =
|
const preprocessedPlayer =
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Autogenerated by devscripts/update-version.py
|
# Autogenerated by devscripts/update-version.py
|
||||||
|
|
||||||
__version__ = '2026.03.03'
|
__version__ = '2026.03.17'
|
||||||
|
|
||||||
RELEASE_GIT_HEAD = '2ecc4c3bc300701d85e2cbaeb2b28a921a68f0f0'
|
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.03'
|
_pkg_version = '2026.03.17'
|
||||||
|
|||||||
Reference in New Issue
Block a user