Compare commits

...

32 Commits

Author SHA1 Message Date
doe1080
cb309b3293
[utils] HTTPHeaderDict: Fix __ior__ (#16930)
Authored by: doe1080
2026-06-11 16:43:24 +02:00
bashonly
e47691215f
Fix allow-unsafe-ext compat option (#16920)
Fix bug in e578e265f7c6ca94a74b30e0d8d6196a4d19fb6a

Closes #16919
Authored by: bashonly
2026-06-10 23:00:05 +00:00
bashonly
a541df1ea5
[ie/bandcamp:weekly] Fix extractor (#16925)
Closes #16924
Authored by: bashonly
2026-06-10 22:34:16 +00:00
github-actions[bot]
7f7bdc974d Release 2026.06.09
Created by: bashonly

:ci skip all
2026-06-09 23:08:31 +00:00
Simon Sawicki
821bef0f00
[cleanup] Misc (#16697)
Authored by: Grub4K, bashonly

Co-authored-by: bashonly <bashonly@protonmail.com>
2026-06-09 23:01:32 +00:00
bashonly
25056f0d2d
[fd/external] aria2c: Remove support for m3u8/dash protocols
See https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-vx4q-3cr2-7cg2

Authored by: bashonly
2026-06-10 00:40:03 +02:00
Simon Sawicki
2726572520
[fd/external] curl: Fix cookie leak on redirect
See https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-f7j3-774f-rfhj

Authored by: Grub4K
2026-06-10 00:40:01 +02:00
Simon Sawicki
e578e265f7
Remove url, desktop and webloc from safe extensions
See https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-c6mh-fpjc-4pr3

Authored by: Grub4K
2026-06-10 00:39:57 +02:00
doe1080
3ba1534fa3
[cleanup] Remove dead extractors (#16137)
Closes #2623
Closes #2679
Closes #2821
Closes #3416
Closes #4828
Closes #4939
Closes #5421
Closes #7064
Closes #7264
Closes #7654
Closes #8075
Closes #8798
Closes #9313
Closes #9617
Closes #10162
Closes #10252
Closes #10264
Closes #15640

Authored by: doe1080, bashonly

Co-authored-by: bashonly <88596187+bashonly@users.noreply.github.com>
2026-06-09 22:35:57 +00:00
bashonly
e85da3b985
[pp/FFmpegMetadata] Avoid erroneous ISO 639 conversions (#16046)
Closes #16045
Authored by: bashonly
2026-06-09 22:09:45 +00:00
bashonly
a6791415e0
[fd/ffmpeg] Use info dict http_headers for direct merge downloads (#15456)
Addresses https://github.com/yt-dlp/yt-dlp/pull/13210#discussion_r2655976236

Authored by: bashonly
2026-06-09 22:05:24 +00:00
sepro
98c0beab97
[docs] Update badges (#14893)
Authored by: seproDev
2026-06-09 21:52:07 +00:00
sepro
3d1f8a4a0d
[ie/wikimedia] Rework extractor (#15413)
Closes #16411
Authored by: seproDev
2026-06-09 21:43:18 +00:00
Kacper Michajłow
aaa1c78956
[ie/twitch] Remove dead rechat subtitles (#16660)
Authored by: kasper93
2026-06-09 21:25:35 +00:00
Léane GRASSER
0d8460b407
[ie/soundcloud] Support --extractor-retries for original formats (#16690)
Partially addresses #15093

Authored by: HarmfulBreeze
2026-06-09 21:17:28 +00:00
garret1317
519a662aa2
[ie/AbemaTV] Extract subtitles (#16502)
Closes #16501
Authored by: garret1317
2026-06-09 21:03:41 +00:00
vpertys
1a1481e89b
[ie/iwara] Fix extractors (#16014)
Closes #13672, Closes #16009, Closes #16146
Authored by: vpertys
2026-06-09 20:59:42 +00:00
AnAwesomGuy
a75d66ae2c
[ie/monstercat] Support older URLs (#16780)
Authored by: AnAwesomGuy
2026-06-09 16:07:06 +00:00
Suntooth
174afac7e3
[ie/S4C] Extract more metadata (#16813)
Closes #15194
Authored by: Suntooth
2026-06-09 15:59:32 +00:00
MemoKing34
7edb5ee870
[ie/twitter] Fix view_count extraction (#16814)
Authored by: MemoKing34
2026-06-09 15:39:53 +00:00
Antony
37a8c6f42b
[rh:curl_cffi] Add actual reason to response (#16818)
Authored by: antorlovsky
2026-06-09 15:20:30 +00:00
0xvd
83564f85db
[ie/pornhub] Support browser impersonation (#16794)
Closes #16729
Authored by: 0xvd
2026-06-09 14:44:18 +00:00
chrisellsworth
618b5e446c
[ie] Extract supplemental codecs from DASH manifests (#16827)
Authored by: chrisellsworth
2026-06-09 06:42:48 +00:00
doe1080
bc90c36b13
[ie/onsen] Fix extraction (#16830)
Closes #16674
Authored by: doe1080
2026-06-09 06:31:34 +00:00
bashonly
70050a6262
[ie/thisoldhouse] Fix extractor (#16909)
Closes #16359
Authored by: bashonly, dirkf

Co-authored-by: dirkf <1222880+dirkf@users.noreply.github.com>
2026-06-09 06:22:59 +00:00
Julien Desgats
72ac620515
[ie/reddit] Fix unauthenticated extraction (#16839)
Closes #16877
Authored by: jdesgats, bashonly, 0xvd

Co-authored-by: bashonly <88596187+bashonly@users.noreply.github.com>
Co-authored-by: 0xvd <199783523+0xvd@users.noreply.github.com>
2026-06-09 06:18:39 +00:00
dlp-bot
59bba1be7b
[ci] Update 3 actions in 9 workflows (#16782)
* Bump actions/checkout v6.0.2 => v6.0.3
* Bump docker/setup-qemu-action v4.0.0 => v4.1.0
* Bump github/codeql-action v4.35.5 => v4.36.2

Authored by: dlp-bot
2026-06-09 00:52:34 +00:00
dlp-bot
10b54835cf
[build] Update 12 dependencies (#16903)
* Bump certifi 2026.4.22 => 2026.5.20
* Bump deno 2.7.14 => 2.8.1
* Bump idna 3.15 => 3.17
* Bump pip 26.1.1 => 26.1.2
* Bump platformdirs 4.9.6 => 4.10.0
* Bump pytest-rerunfailures 16.2 => 16.3
* Bump python-discovery 1.3.1 => 1.4.0
* Bump ruff 0.15.13 => 0.15.15
* Bump trove-classifiers 2026.5.7.17 => 2026.5.22.10
* Bump uv 0.11.14 => 0.11.17
* Bump virtualenv 21.3.3 => 21.4.2
* Bump zipp 3.23.1 => 4.1.0

Authored by: dlp-bot
2026-06-09 00:18:46 +00:00
dlp-bot
1e4668e9df
[utils] random_user_agent: Bump version range 142-148 => 143-149 (#16906)
Authored by: dlp-bot
2026-06-08 23:36:42 +00:00
bashonly
5faffa999f
[pp/exec] Restrict --exec template usage to safe conversions (#16883)
Authored by: bashonly
2026-06-06 21:24:53 +00:00
bashonly
7aac95eae6
[ci] Test with Python 3.15 (#16896)
Authored by: bashonly
2026-06-06 21:08:51 +00:00
Simon Sawicki
7fdc46d016
[ie/youtube] Fix PO token sanitization for Python 3.15 (#16884)
Closes #16876
Authored by: Grub4K
2026-06-06 20:49:26 +00:00
155 changed files with 1132 additions and 11387 deletions

View File

@ -195,7 +195,7 @@ jobs:
UPDATE_TO: yt-dlp/yt-dlp@2025.09.05 UPDATE_TO: yt-dlp/yt-dlp@2025.09.05
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: with:
fetch-depth: 0 # Needed for changelog fetch-depth: 0 # Needed for changelog
persist-credentials: false persist-credentials: false
@ -259,13 +259,13 @@ jobs:
SKIP_ONEFILE_BUILD: ${{ (!matrix.onefile && '1') || '' }} SKIP_ONEFILE_BUILD: ${{ (!matrix.onefile && '1') || '' }}
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: with:
persist-credentials: false persist-credentials: false
- name: Set up QEMU - name: Set up QEMU
if: matrix.qemu_platform if: matrix.qemu_platform
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.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
@ -312,7 +312,7 @@ jobs:
UPDATE_TO: yt-dlp/yt-dlp@2025.09.05 UPDATE_TO: yt-dlp/yt-dlp@2025.09.05
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: with:
persist-credentials: false persist-credentials: false
@ -438,7 +438,7 @@ jobs:
UPDATE_TO: yt-dlp/yt-dlp@2025.09.05 UPDATE_TO: yt-dlp/yt-dlp@2025.09.05
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: with:
persist-credentials: false persist-credentials: false

View File

@ -36,17 +36,18 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-latest, windows-latest] os: [ubuntu-latest, windows-latest]
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14', pypy-3.11] python-version: ['3.10', '3.11', '3.12', '3.13', '3.14', '3.15', pypy-3.11]
env: env:
QJS_VERSION: '2025-04-26' # Earliest version with rope strings QJS_VERSION: '2025-04-26' # Earliest version with rope strings
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: with:
persist-credentials: false persist-credentials: false
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
allow-prereleases: true
- name: Install Deno - name: Install Deno
uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4 uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4
with: with:

View File

@ -31,17 +31,17 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: with:
persist-credentials: false persist-credentials: false
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
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@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with: with:
category: "/language:${{matrix.language}}" category: "/language:${{matrix.language}}"

View File

@ -45,7 +45,7 @@ jobs:
matrix: matrix:
os: [ubuntu-latest] os: [ubuntu-latest]
# CPython 3.10 is in quick-test # CPython 3.10 is in quick-test
python-version: ['3.11', '3.12', '3.13', '3.14', pypy-3.11] python-version: ['3.11', '3.12', '3.13', '3.14', '3.15', pypy-3.11]
include: include:
# atleast one of each CPython/PyPy tests must be in windows # atleast one of each CPython/PyPy tests must be in windows
- os: windows-latest - os: windows-latest
@ -58,11 +58,13 @@ jobs:
python-version: '3.13' python-version: '3.13'
- os: windows-latest - os: windows-latest
python-version: '3.14' python-version: '3.14'
- os: windows-latest
python-version: '3.15'
- os: windows-latest - os: windows-latest
python-version: pypy-3.11 python-version: pypy-3.11
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: with:
fetch-depth: 0 fetch-depth: 0
persist-credentials: false persist-credentials: false
@ -71,6 +73,7 @@ jobs:
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
allow-prereleases: true
- name: Install test requirements (cpython) - name: Install test requirements (cpython)
if: ${{ !startsWith(matrix.python-version, 'pypy') }} if: ${{ !startsWith(matrix.python-version, 'pypy') }}

View File

@ -21,7 +21,7 @@ jobs:
contents: read contents: read
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: with:
persist-credentials: false persist-credentials: false
- name: Set up Python 3.10 - name: Set up Python 3.10
@ -48,7 +48,7 @@ jobs:
contents: read contents: read
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: with:
persist-credentials: false persist-credentials: false
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0

View File

@ -16,7 +16,7 @@ jobs:
outputs: outputs:
commit: ${{ steps.check_for_new_commits.outputs.commit }} commit: ${{ steps.check_for_new_commits.outputs.commit }}
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: with:
fetch-depth: 0 fetch-depth: 0
persist-credentials: false persist-credentials: false

View File

@ -84,7 +84,7 @@ jobs:
head_sha: ${{ steps.get_target.outputs.head_sha }} head_sha: ${{ steps.get_target.outputs.head_sha }}
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: with:
fetch-depth: 0 fetch-depth: 0
persist-credentials: true # Needed to git-push the release commit persist-credentials: true # Needed to git-push the release commit
@ -174,7 +174,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: with:
fetch-depth: 0 # Needed for changelog fetch-depth: 0 # Needed for changelog
persist-credentials: false persist-credentials: false
@ -240,7 +240,7 @@ jobs:
VERSION: ${{ needs.prepare.outputs.version }} VERSION: ${{ needs.prepare.outputs.version }}
HEAD_SHA: ${{ needs.prepare.outputs.head_sha }} HEAD_SHA: ${{ needs.prepare.outputs.head_sha }}
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: with:
fetch-depth: 0 fetch-depth: 0
persist-credentials: true # Needed to git-push the release tag persist-credentials: true # Needed to git-push the release tag

View File

@ -26,7 +26,7 @@ jobs:
contents: read contents: read
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: with:
persist-credentials: false persist-credentials: false
@ -78,7 +78,7 @@ jobs:
actions: read # Needed by zizmorcore/zizmor-action if repository is private actions: read # Needed by zizmorcore/zizmor-action if repository is private
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: with:
persist-credentials: false persist-credentials: false

View File

@ -18,7 +18,7 @@ jobs:
contents: write # Needed to git-push to the wiki contents: write # Needed to git-push to the wiki
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: with:
repository: yt-dlp/yt-dlp-wiki repository: yt-dlp/yt-dlp-wiki
ref: master ref: master

View File

@ -877,3 +877,12 @@ syphyr
FriederHannenheim FriederHannenheim
Peter-Devine Peter-Devine
SparseOrnament15 SparseOrnament15
AnAwesomGuy
antorlovsky
dlp-bot
HarmfulBreeze
jdesgats
MemoKing34
Suntooth
Ventriduct
vpertys

View File

@ -4,6 +4,107 @@
# 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.06.09
#### Important changes
- **The minimum supported versions of Deno, Node, and Bun have been raised.**
The minimum required version of [Deno](https://github.com/yt-dlp/yt-dlp/issues/16767) is now `v2.3.0`; supported [Node](https://github.com/yt-dlp/yt-dlp/issues/16765) versions are `v22` and up; [Bun support has been deprecated](https://github.com/yt-dlp/yt-dlp/issues/16766) and limited to versions `1.2.11` through `1.3.14`.
- Security
- Usage of vulnerable conversions (e.g. `%()s`) with the `--exec` option is an all-too-common pitfall. To remedy this, `--exec` now only allows safe conversions in its command templates.
- Most users can simply replace `%(...)s` with `%(...)q` in their `--exec` argument(s). Numeric conversions are unaffected by this change. Using unsafe conversions with `--exec` poses a significant security risk. [Read more](<https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-69qj-pvh9-c5wg>)
- [[CVE-2026-50019](https://nvd.nist.gov/vuln/detail/CVE-2026-50019)] [File Downloader cookie leak with curl](https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-f7j3-774f-rfhj)
- Impact is limited to users of `--downloader curl`; cookies are now properly passed to curl so that it respects their scope
- [[CVE-2026-50023](https://nvd.nist.gov/vuln/detail/CVE-2026-50023)] [Dangerous file type creation via insufficient filename sanitization](https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-c6mh-fpjc-4pr3)
- Writing files with the extensions `.desktop`, `.url`, or `.webloc` is now only allowed in the context of `--write-link` functionality
- [[CVE-2026-50574](https://nvd.nist.gov/vuln/detail/CVE-2026-50574)] [Arbitrary code execution via manifest downloads with aria2c](https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-vx4q-3cr2-7cg2)
- Impact is limited to users of `--downloader aria2c`
- Support for downloading HLS and DASH formats with aria2c has been removed. Users affected by this change should migrate to use `-N` for concurrent fragment downloads via the native downloader
#### Core changes
- [Add lockfile and pinned extras](https://github.com/yt-dlp/yt-dlp/commit/5f6a214616f6fc3831a2535bcd1f837e90549d10) ([#16421](https://github.com/yt-dlp/yt-dlp/issues/16421)) by [bashonly](https://github.com/bashonly), [Grub4K](https://github.com/Grub4K) (With fixes in [88c8a68](https://github.com/yt-dlp/yt-dlp/commit/88c8a68eb52268111e224293e9a6519944971096) by [bashonly](https://github.com/bashonly))
- [Fix `default` extra for `ios` platforms](https://github.com/yt-dlp/yt-dlp/commit/a5aae189452d11ca731a4fb409d0136c668bd7c6) ([#16376](https://github.com/yt-dlp/yt-dlp/issues/16376)) by [bashonly](https://github.com/bashonly)
- [Remove `url`, `desktop` and `webloc` from safe extensions](https://github.com/yt-dlp/yt-dlp/commit/e578e265f7c6ca94a74b30e0d8d6196a4d19fb6a) by [Grub4K](https://github.com/Grub4K)
- **update**: [Bump GitHub REST API version to `2026-03-10`](https://github.com/yt-dlp/yt-dlp/commit/fe5e67c0545a4aac9d404b220c21ba53d1048353) ([#16435](https://github.com/yt-dlp/yt-dlp/issues/16435)) by [bashonly](https://github.com/bashonly)
- **utils**
- `random_user_agent`
- [Bump version range 137-143 => 142-148](https://github.com/yt-dlp/yt-dlp/commit/acf8ab7a6e3024325f62426e35a17f365c4d5d54) ([#16588](https://github.com/yt-dlp/yt-dlp/issues/16588)) by [dlp-bot](https://github.com/dlp-bot)
- [Bump version range 142-148 => 143-149](https://github.com/yt-dlp/yt-dlp/commit/1e4668e9df78ff2409e139b51610442cbefe762e) ([#16906](https://github.com/yt-dlp/yt-dlp/issues/16906)) by [dlp-bot](https://github.com/dlp-bot)
#### Extractor changes
- [Extract supplemental codecs from DASH manifests](https://github.com/yt-dlp/yt-dlp/commit/618b5e446c4379c9d95fe7b30fd6a0fc6af19a70) ([#16827](https://github.com/yt-dlp/yt-dlp/issues/16827)) by [chrisellsworth](https://github.com/chrisellsworth)
- `_resolve_nuxt_array`: [Handle Pinia `skipHydrate`](https://github.com/yt-dlp/yt-dlp/commit/8001ff4349fa4eaafd0f88fd8abdf8756090596d) ([#16447](https://github.com/yt-dlp/yt-dlp/issues/16447)) by [doe1080](https://github.com/doe1080)
- **abematv**: [Extract subtitles](https://github.com/yt-dlp/yt-dlp/commit/519a662aa267ddcd48ea859729ba330361cff157) ([#16502](https://github.com/yt-dlp/yt-dlp/issues/16502)) by [garret1317](https://github.com/garret1317)
- **ard**: [Support new `ardsounds` domain](https://github.com/yt-dlp/yt-dlp/commit/165ee77a2be1b3360f1b82e03a933348ecd13e41) ([#16381](https://github.com/yt-dlp/yt-dlp/issues/16381)) by [evilpie](https://github.com/evilpie)
- **bandcamp**: weekly: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/2d7b278666bfbf12cf287072498dd275c946b968) ([#16373](https://github.com/yt-dlp/yt-dlp/issues/16373)) by [bashonly](https://github.com/bashonly)
- **iwara**: [Fix extractors](https://github.com/yt-dlp/yt-dlp/commit/1a1481e89b28ff1bf3f00eda00a66071f8caba9f) ([#16014](https://github.com/yt-dlp/yt-dlp/issues/16014)) by [vpertys](https://github.com/vpertys)
- **monstercat**: [Support older URLs](https://github.com/yt-dlp/yt-dlp/commit/a75d66ae2c3e86e38eb05ae06e4a416077df001b) ([#16780](https://github.com/yt-dlp/yt-dlp/issues/16780)) by [AnAwesomGuy](https://github.com/AnAwesomGuy)
- **onsen**: [Fix extraction](https://github.com/yt-dlp/yt-dlp/commit/bc90c36b139c87cdc59399561dc1650198047a72) ([#16830](https://github.com/yt-dlp/yt-dlp/issues/16830)) by [doe1080](https://github.com/doe1080)
- **pornhub**: [Support browser impersonation](https://github.com/yt-dlp/yt-dlp/commit/83564f85db7507486fbe3b0d0e72498f31ab0600) ([#16794](https://github.com/yt-dlp/yt-dlp/issues/16794)) by [0xvd](https://github.com/0xvd)
- **reddit**: [Fix unauthenticated extraction](https://github.com/yt-dlp/yt-dlp/commit/72ac62051593184552733e353da4a8cb8ec214ed) ([#16839](https://github.com/yt-dlp/yt-dlp/issues/16839)) by [0xvd](https://github.com/0xvd), [bashonly](https://github.com/bashonly), [jdesgats](https://github.com/jdesgats)
- **rtp**: [Support multi-part episodes and `--no-playlist`](https://github.com/yt-dlp/yt-dlp/commit/f01e1a1ced581c13f28c7da45eb6396cb9fff6e4) ([#16299](https://github.com/yt-dlp/yt-dlp/issues/16299)) by [bashonly](https://github.com/bashonly)
- **s4c**: [Extract more metadata](https://github.com/yt-dlp/yt-dlp/commit/174afac7e304fd1c055d33d52a1fccdb3dca3b37) ([#16813](https://github.com/yt-dlp/yt-dlp/issues/16813)) by [Suntooth](https://github.com/Suntooth)
- **soop**: [Adapt extractors to new domain](https://github.com/yt-dlp/yt-dlp/commit/068d5efd3047a39a7d869d49067aa1594e359276) ([#16436](https://github.com/yt-dlp/yt-dlp/issues/16436)) by [thematuu](https://github.com/thematuu)
- **soundcloud**
- [Improve error handling](https://github.com/yt-dlp/yt-dlp/commit/ebf0c0f61e3e578db26b45eb24d643f1a64bf17f) ([#16602](https://github.com/yt-dlp/yt-dlp/issues/16602)) by [bashonly](https://github.com/bashonly)
- [Support `--extractor-retries` for original formats](https://github.com/yt-dlp/yt-dlp/commit/0d8460b407fe8a9ac7ded8cbf21813554f9d2442) ([#16690](https://github.com/yt-dlp/yt-dlp/issues/16690)) by [HarmfulBreeze](https://github.com/HarmfulBreeze)
- **thisoldhouse**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/70050a626265922072cc56f69519214c47dbcad6) ([#16909](https://github.com/yt-dlp/yt-dlp/issues/16909)) by [bashonly](https://github.com/bashonly), [dirkf](https://github.com/dirkf)
- **twitch**
- [Remove dead `rechat` subtitles](https://github.com/yt-dlp/yt-dlp/commit/aaa1c78956ed4ff63df067671396de864afdc43e) ([#16660](https://github.com/yt-dlp/yt-dlp/issues/16660)) by [kasper93](https://github.com/kasper93)
- clips: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/c229d4b62053dc287337f300df13bd433d4084da) ([#16466](https://github.com/yt-dlp/yt-dlp/issues/16466)) by [Ventriduct](https://github.com/Ventriduct)
- **twitter**: [Fix `view_count` extraction](https://github.com/yt-dlp/yt-dlp/commit/7edb5ee8705cecc20537d519e1992ac202a30699) ([#16814](https://github.com/yt-dlp/yt-dlp/issues/16814)) by [MemoKing34](https://github.com/MemoKing34)
- **wikimedia**: [Rework extractor](https://github.com/yt-dlp/yt-dlp/commit/3d1f8a4a0d4da01fac484bd1593056a1dc9f30a9) ([#15413](https://github.com/yt-dlp/yt-dlp/issues/15413)) by [seproDev](https://github.com/seproDev)
- **youtube**
- [Drop support for `bun<1.2.11` and `bun>1.3.14`](https://github.com/yt-dlp/yt-dlp/commit/98e42eb04486e00bf86479b24dbfe19321f652ee) ([#16786](https://github.com/yt-dlp/yt-dlp/issues/16786)) by [bashonly](https://github.com/bashonly)
- [Drop support for `deno<2.3.0`](https://github.com/yt-dlp/yt-dlp/commit/e534a32619d1d944631a0483f28522bdd11f0745) ([#16788](https://github.com/yt-dlp/yt-dlp/issues/16788)) by [bashonly](https://github.com/bashonly)
- [Drop support for `node<22`](https://github.com/yt-dlp/yt-dlp/commit/b536d72c869e47eb048fc54746e4d2f384706d2f) ([#16787](https://github.com/yt-dlp/yt-dlp/issues/16787)) by [bashonly](https://github.com/bashonly)
- [Fix PO token sanitization for Python 3.15](https://github.com/yt-dlp/yt-dlp/commit/7fdc46d01619afbb2371b0465d6830602013148f) ([#16884](https://github.com/yt-dlp/yt-dlp/issues/16884)) by [Grub4K](https://github.com/Grub4K)
- [Fix PO token sanitization for Python>=3.14.4](https://github.com/yt-dlp/yt-dlp/commit/9c1f3cf3373620c593c4e315f276ac134f6beb20) ([#16453](https://github.com/yt-dlp/yt-dlp/issues/16453)) by [syphyr](https://github.com/syphyr)
- [Fix outdated quickjs-ng warning](https://github.com/yt-dlp/yt-dlp/commit/04b2261cbf1aafb964320062dbb33e74ec613291) ([#16437](https://github.com/yt-dlp/yt-dlp/issues/16437)) by [bashonly](https://github.com/bashonly)
#### Downloader changes
- **external**
- `aria2c`: [Remove support for m3u8/dash protocols](https://github.com/yt-dlp/yt-dlp/commit/25056f0d2d47adbd235a8d422fa62d68d0be2bc2) by [bashonly](https://github.com/bashonly)
- `curl`: [Fix cookie leak on redirect](https://github.com/yt-dlp/yt-dlp/commit/2726572520238356bcf64aba2040228648b44c82) by [Grub4K](https://github.com/Grub4K)
- **ffmpeg**: [Use info dict `http_headers` for direct merge downloads](https://github.com/yt-dlp/yt-dlp/commit/a6791415e04aaf4bb4c105991ceb3ca6b24afc18) ([#15456](https://github.com/yt-dlp/yt-dlp/issues/15456)) by [bashonly](https://github.com/bashonly)
#### Postprocessor changes
- **exec**: [Restrict `--exec` template usage to safe conversions](https://github.com/yt-dlp/yt-dlp/commit/5faffa999fd33b373d47773e8ee639d072accec2) ([#16883](https://github.com/yt-dlp/yt-dlp/issues/16883)) by [bashonly](https://github.com/bashonly)
- **ffmpegmetadata**: [Avoid erroneous ISO 639 conversions](https://github.com/yt-dlp/yt-dlp/commit/e85da3b98532761573d8b48ccd4d8d28dee94b15) ([#16046](https://github.com/yt-dlp/yt-dlp/issues/16046)) by [bashonly](https://github.com/bashonly)
#### Networking changes
- **Request Handler**
- curl_cffi
- [Add actual `reason` to response](https://github.com/yt-dlp/yt-dlp/commit/37a8c6f42b1e0866bbeec47ca57a68a6ae63784e) ([#16818](https://github.com/yt-dlp/yt-dlp/issues/16818)) by [antorlovsky](https://github.com/antorlovsky)
- [Fix supported impersonate targets](https://github.com/yt-dlp/yt-dlp/commit/565dcfec4e5c035b5544de4a369f654b8a60e9e6) ([#16440](https://github.com/yt-dlp/yt-dlp/issues/16440)) by [bashonly](https://github.com/bashonly)
- [Support `curl_cffi` 0.15.x](https://github.com/yt-dlp/yt-dlp/commit/0f45ecc920f31c3c5704c62bad8da2e2844ff9bc) ([#16429](https://github.com/yt-dlp/yt-dlp/issues/16429)) by [bashonly](https://github.com/bashonly)
#### Misc. changes
- **build**
- [Harden build/release workflows](https://github.com/yt-dlp/yt-dlp/commit/87eaf886f5a1fed00639baf3677ac76281cd98f9) ([#16358](https://github.com/yt-dlp/yt-dlp/issues/16358)) by [bashonly](https://github.com/bashonly), [Grub4K](https://github.com/Grub4K) (With fixes in [cdc465a](https://github.com/yt-dlp/yt-dlp/commit/cdc465a34674d15edf52b656457f6006b9e03edf) by [bashonly](https://github.com/bashonly))
- [Harden release workflow](https://github.com/yt-dlp/yt-dlp/commit/a4acc4223289eeb4d32af7b798bfe6e9c38f4b8d) ([#16444](https://github.com/yt-dlp/yt-dlp/issues/16444)) by [bashonly](https://github.com/bashonly)
- [Rename requirements files to clean up dependency graph](https://github.com/yt-dlp/yt-dlp/commit/32f1671a906bf375e5b5d39433dd13f917a8dfa7) ([#16740](https://github.com/yt-dlp/yt-dlp/issues/16740)) by [bashonly](https://github.com/bashonly)
- [Update 12 dependencies](https://github.com/yt-dlp/yt-dlp/commit/10b54835cf3f0d63cdd964a2f411db8f7cac3ff1) ([#16903](https://github.com/yt-dlp/yt-dlp/issues/16903)) by [dlp-bot](https://github.com/dlp-bot)
- [Update 14 dependencies](https://github.com/yt-dlp/yt-dlp/commit/dd17897eaeb0de7da31690d6d1807a1e9a041384) ([#16589](https://github.com/yt-dlp/yt-dlp/issues/16589)) by [dlp-bot](https://github.com/dlp-bot)
- [Update 28 dependencies](https://github.com/yt-dlp/yt-dlp/commit/c8695f52a91f0d2aabbba7b7200c1099bfa9a3e5) ([#16467](https://github.com/yt-dlp/yt-dlp/issues/16467)) by [bashonly](https://github.com/bashonly)
- [Upgrade all Linux binaries to Python 3.14](https://github.com/yt-dlp/yt-dlp/commit/410e0af5379c5fc5f2acb6abaa8965637f80ad76) ([#16738](https://github.com/yt-dlp/yt-dlp/issues/16738)) by [bashonly](https://github.com/bashonly)
- **ci**
- [Bump pytest to 9.x](https://github.com/yt-dlp/yt-dlp/commit/27973bae5ea3467ac412bea3b79cbeeb7de71e81) ([#16470](https://github.com/yt-dlp/yt-dlp/issues/16470)) by [bashonly](https://github.com/bashonly), [Grub4K](https://github.com/Grub4K)
- [Test with Python 3.15](https://github.com/yt-dlp/yt-dlp/commit/7aac95eae663be82cffeaf2a8c1193a5e349e401) ([#16896](https://github.com/yt-dlp/yt-dlp/issues/16896)) by [bashonly](https://github.com/bashonly)
- [Update 2 actions in 2 workflows](https://github.com/yt-dlp/yt-dlp/commit/125bb40468a8618e592d607c1c496095fda764f0) ([#16743](https://github.com/yt-dlp/yt-dlp/issues/16743)) by [dlp-bot](https://github.com/dlp-bot)
- [Update 3 actions in 9 workflows](https://github.com/yt-dlp/yt-dlp/commit/59bba1be7bb476d4445dc4eae94f602300cb865a) ([#16782](https://github.com/yt-dlp/yt-dlp/issues/16782)) by [dlp-bot](https://github.com/dlp-bot)
- [Update 8 actions in 7 workflows](https://github.com/yt-dlp/yt-dlp/commit/a85b38621286903b9124fdb05d177983d8273ec7) ([#16384](https://github.com/yt-dlp/yt-dlp/issues/16384)) by [bashonly](https://github.com/bashonly)
- [Update wiki via this repository](https://github.com/yt-dlp/yt-dlp/commit/40ffb79d499e6b37682fddbe6affec20186a3d86) ([#16446](https://github.com/yt-dlp/yt-dlp/issues/16446)) by [bashonly](https://github.com/bashonly) (With fixes in [9f0fc9a](https://github.com/yt-dlp/yt-dlp/commit/9f0fc9a6333b912c83b177542cd3a3cc1c6ff326))
- **cleanup**
- [Remove dead extractors](https://github.com/yt-dlp/yt-dlp/commit/3ba1534fa3f55ddb599a891b544ec96e5a15f3cc) ([#16137](https://github.com/yt-dlp/yt-dlp/issues/16137)) by [bashonly](https://github.com/bashonly), [doe1080](https://github.com/doe1080)
- Miscellaneous
- [35684c1](https://github.com/yt-dlp/yt-dlp/commit/35684c1171dd8b99da825cf43a0b2c06b43824b7), [3a12be7](https://github.com/yt-dlp/yt-dlp/commit/3a12be701c28aff4dd4824adb911cc7987dd86ba) by [bashonly](https://github.com/bashonly)
- [821bef0](https://github.com/yt-dlp/yt-dlp/commit/821bef0f00178916d60dbc86bc0bcb8cc3bae8d5) by [bashonly](https://github.com/bashonly), [Grub4K](https://github.com/Grub4K)
- **devscripts**
- [Handle `ejs` updates for requirements files](https://github.com/yt-dlp/yt-dlp/commit/fcccbc68496d8af1b7c24cd5e45e83af4ca76f18) ([#16374](https://github.com/yt-dlp/yt-dlp/issues/16374)) by [bashonly](https://github.com/bashonly), [Grub4K](https://github.com/Grub4K)
- `update_requirements`: [Add reporting functionality](https://github.com/yt-dlp/yt-dlp/commit/2c28ee5d76d2c0d350407fd81dbdd71394b67993) ([#16454](https://github.com/yt-dlp/yt-dlp/issues/16454)) by [bashonly](https://github.com/bashonly), [Grub4K](https://github.com/Grub4K)
- **docs**: [Update badges](https://github.com/yt-dlp/yt-dlp/commit/98c0beab97933e4115ed034f06a8b626eb282d3f) ([#14893](https://github.com/yt-dlp/yt-dlp/issues/14893)) by [seproDev](https://github.com/seproDev)
- **test**: [Add default and curl-cffi extras to hatch-test env](https://github.com/yt-dlp/yt-dlp/commit/f14d2f2d548a45fef221aa3821e5a1bf450d5c0b) ([#16397](https://github.com/yt-dlp/yt-dlp/issues/16397)) by [JSubelj](https://github.com/JSubelj)
### 2026.03.17 ### 2026.03.17
#### Extractor changes #### Extractor changes

View File

@ -3,15 +3,12 @@
[![YT-DLP](https://raw.githubusercontent.com/yt-dlp/yt-dlp/master/.github/banner.svg)](#readme) [![YT-DLP](https://raw.githubusercontent.com/yt-dlp/yt-dlp/master/.github/banner.svg)](#readme)
[![Release version](https://img.shields.io/github/v/release/yt-dlp/yt-dlp?color=brightgreen&label=Download&style=for-the-badge)](#installation "Installation") [![Release version](https://img.shields.io/github/v/release/yt-dlp/yt-dlp?color=brightgreen&label=Latest&style=for-the-badge)](#installation "Installation")
[![Python Version](https://img.shields.io/python/required-version-toml?tomlFilePath=https%3A%2F%2Fraw.githubusercontent.com%2Fyt-dlp%2Fyt-dlp%2Frefs%2Fheads%2Fmaster%2Fpyproject.toml&style=for-the-badge)](https://github.com/yt-dlp/yt-dlp/blob/master/pyproject.toml "Python Version")
[![PyPI](https://img.shields.io/badge/-PyPI-blue.svg?logo=pypi&labelColor=555555&style=for-the-badge)](https://pypi.org/project/yt-dlp "PyPI") [![PyPI](https://img.shields.io/badge/-PyPI-blue.svg?logo=pypi&labelColor=555555&style=for-the-badge)](https://pypi.org/project/yt-dlp "PyPI")
[![Donate](https://img.shields.io/badge/_-Donate-red.svg?logo=githubsponsors&labelColor=555555&style=for-the-badge)](Maintainers.md#maintainers "Donate") [![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?&logo=discord&logoColor=white&style=for-the-badge)]([#](https://discord.gg/H5MNcFW63r "Discord")
[![Discord](https://img.shields.io/discord/807245652072857610?color=blue&labelColor=555555&label=&logo=discord&style=for-the-badge)](https://discord.gg/H5MNcFW63r "Discord") [![License: Unlicense](https://img.shields.io/badge/-Unlicense-red.svg?style=for-the-badge)](LICENSE "License")
[![Supported Sites](https://img.shields.io/badge/-Supported_Sites-brightgreen.svg?style=for-the-badge)](supportedsites.md "Supported Sites")
[![License: Unlicense](https://img.shields.io/badge/-Unlicense-blue.svg?style=for-the-badge)](LICENSE "License")
[![CI Status](https://img.shields.io/github/actions/workflow/status/yt-dlp/yt-dlp/core.yml?branch=master&label=Tests&style=for-the-badge)](https://github.com/yt-dlp/yt-dlp/actions "CI Status")
[![Commits](https://img.shields.io/github/commit-activity/m/yt-dlp/yt-dlp?label=commits&style=for-the-badge)](https://github.com/yt-dlp/yt-dlp/commits "Commit History") [![Commits](https://img.shields.io/github/commit-activity/m/yt-dlp/yt-dlp?label=commits&style=for-the-badge)](https://github.com/yt-dlp/yt-dlp/commits "Commit History")
[![Last Commit](https://img.shields.io/github/last-commit/yt-dlp/yt-dlp/master?label=&style=for-the-badge&display_timestamp=committer)](https://github.com/yt-dlp/yt-dlp/pulse/monthly "Last activity")
</div> </div>
<!-- MANPAGE: END EXCLUDED SECTION --> <!-- MANPAGE: END EXCLUDED SECTION -->
@ -834,8 +831,7 @@ Tip: Use `CTRL`+`F` (or `Command`+`F`) to search by keywords
renegotiation renegotiation
--no-check-certificates Suppress HTTPS certificate validation --no-check-certificates Suppress HTTPS certificate validation
--prefer-insecure Use an unencrypted connection to retrieve --prefer-insecure Use an unencrypted connection to retrieve
information about the video (Currently information about the video
supported only for YouTube)
--add-headers FIELD:VALUE Specify a custom HTTP header and its value, --add-headers FIELD:VALUE Specify a custom HTTP header and its value,
separated by a colon ":". You can use this separated by a colon ":". You can use this
option multiple times option multiple times
@ -1053,10 +1049,14 @@ Tip: Use `CTRL`+`F` (or `Command`+`F`) to search by keywords
that of --use-postprocessor (default: that of --use-postprocessor (default:
after_move). The same syntax as the output after_move). The same syntax as the output
template can be used to pass any field as template can be used to pass any field as
arguments to the command. If no fields are arguments to the command; however, for
passed, %(filepath,_filename|)q is appended security reasons the only allowed
to the end of the command. This option can conversions are: "i"/"d" (signed integer
be used multiple times decimal), "f" (floating-point decimal) and
"q" (shell-quoted). If no fields are passed,
%(filepath,_filename|)q is appended to the
end of the command. This option can be used
multiple times
--no-exec Remove any previously defined --exec --no-exec Remove any previously defined --exec
--convert-subs FORMAT Convert the subtitles to another format --convert-subs FORMAT Convert the subtitles to another format
(currently supported: ass, lrc, srt, vtt). (currently supported: ass, lrc, srt, vtt).
@ -2275,8 +2275,6 @@ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
* **Multi-threaded fragment downloads**: Download multiple fragments of m3u8/mpd videos in parallel. Use `--concurrent-fragments` (`-N`) option to set the number of threads used * **Multi-threaded fragment downloads**: Download multiple fragments of m3u8/mpd videos in parallel. Use `--concurrent-fragments` (`-N`) option to set the number of threads used
* **Aria2c with HLS/DASH**: You can use `aria2c` as the external downloader for DASH(mpd) and HLS(m3u8) formats
* **New and fixed extractors**: Many new extractors have been added and a lot of existing ones have been fixed. See the [changelog](Changelog.md) or the [list of supported sites](supportedsites.md) * **New and fixed extractors**: Many new extractors have been added and a lot of existing ones have been fixed. See the [changelog](Changelog.md) or the [list of supported sites](supportedsites.md)
* **New MSOs**: Philo, Spectrum, SlingTV, Cablevision, RCN etc. * **New MSOs**: Philo, Spectrum, SlingTV, Cablevision, RCN etc.
@ -2331,9 +2329,9 @@ Some of yt-dlp's default options are different from that of youtube-dl and youtu
* When `--embed-subs` and `--write-subs` are used together, the subtitles are written to disk and also embedded in the media file. You can use just `--embed-subs` to embed the subs and automatically delete the separate file. See [#630 (comment)](https://github.com/yt-dlp/yt-dlp/issues/630#issuecomment-893659460) for more info. `--compat-options no-keep-subs` can be used to revert this * When `--embed-subs` and `--write-subs` are used together, the subtitles are written to disk and also embedded in the media file. You can use just `--embed-subs` to embed the subs and automatically delete the separate file. See [#630 (comment)](https://github.com/yt-dlp/yt-dlp/issues/630#issuecomment-893659460) for more info. `--compat-options no-keep-subs` can be used to revert this
* `certifi` will be used for SSL root certificates, if installed. If you want to use system certificates (e.g. self-signed), use `--compat-options no-certifi` * `certifi` will be used for SSL root certificates, if installed. If you want to use system certificates (e.g. self-signed), use `--compat-options no-certifi`
* yt-dlp's sanitization of invalid characters in filenames is different/smarter than in youtube-dl. You can use `--compat-options filename-sanitization` to revert to youtube-dl's behavior * yt-dlp's sanitization of invalid characters in filenames is different/smarter than in youtube-dl. You can use `--compat-options filename-sanitization` to revert to youtube-dl's behavior
* ~~yt-dlp tries to parse the external downloader outputs into the standard progress output if possible (Currently implemented: [aria2c](https://github.com/yt-dlp/yt-dlp/issues/5931)). You can use `--compat-options no-external-downloader-progress` to get the downloader output as-is~~ * (Not currently implemented) ~~yt-dlp tries to parse the external downloader outputs into the standard progress output if possible. You can use `--compat-options no-external-downloader-progress` to get the downloader output as-is~~
* yt-dlp versions between 2021.09.01 and 2023.01.02 applies `--match-filters` to nested playlists. This was an unintentional side-effect of [8f18ac](https://github.com/yt-dlp/yt-dlp/commit/8f18aca8717bb0dd49054555af8d386e5eda3a88) and is fixed in [d7b460](https://github.com/yt-dlp/yt-dlp/commit/d7b460d0e5fc710950582baed2e3fc616ed98a80). Use `--compat-options playlist-match-filter` to revert this * yt-dlp versions from 2021.09.01 to 2022.11.11 (inclusive) applied `--match-filters` to nested playlists. This was an unintentional side-effect of [8f18ac](https://github.com/yt-dlp/yt-dlp/commit/8f18aca8717bb0dd49054555af8d386e5eda3a88) and is fixed in [d7b460](https://github.com/yt-dlp/yt-dlp/commit/d7b460d0e5fc710950582baed2e3fc616ed98a80). Use `--compat-options playlist-match-filter` to revert this
* yt-dlp versions between 2021.11.10 and 2023.06.21 estimated `filesize_approx` values for fragmented/manifest formats. This was added for convenience in [f2fe69](https://github.com/yt-dlp/yt-dlp/commit/f2fe69c7b0d208bdb1f6292b4ae92bc1e1a7444a), but was reverted in [0dff8e](https://github.com/yt-dlp/yt-dlp/commit/0dff8e4d1e6e9fb938f4256ea9af7d81f42fd54f) due to the potentially extreme inaccuracy of the estimated values. Use `--compat-options manifest-filesize-approx` to keep extracting the estimated values * yt-dlp versions from 2021.11.10 to 2023.06.21 (inclusive) estimated `filesize_approx` values for fragmented/manifest formats. This was added for convenience in [f2fe69](https://github.com/yt-dlp/yt-dlp/commit/f2fe69c7b0d208bdb1f6292b4ae92bc1e1a7444a), but was reverted in [0dff8e](https://github.com/yt-dlp/yt-dlp/commit/0dff8e4d1e6e9fb938f4256ea9af7d81f42fd54f) due to the potentially extreme inaccuracy of the estimated values. Use `--compat-options manifest-filesize-approx` to keep extracting the estimated values
* yt-dlp uses modern http client backends such as `requests`. Use `--compat-options prefer-legacy-http-handler` to prefer the legacy http handler (`urllib`) to be used for standard http requests. * yt-dlp uses modern http client backends such as `requests`. Use `--compat-options prefer-legacy-http-handler` to prefer the legacy http handler (`urllib`) to be used for standard http requests.
* The sub-modules `swfinterp`, `casefold` are removed. * The sub-modules `swfinterp`, `casefold` are removed.
* Passing `--simulate` (or calling `extract_info` with `download=False`) no longer alters the default format selection. See [#9843](https://github.com/yt-dlp/yt-dlp/issues/9843) for details. * Passing `--simulate` (or calling `extract_info` with `download=False`) no longer alters the default format selection. See [#9843](https://github.com/yt-dlp/yt-dlp/issues/9843) for details.
@ -2342,8 +2340,8 @@ Some of yt-dlp's default options are different from that of youtube-dl and youtu
For convenience, there are some compat option aliases available to use: For convenience, there are some compat option aliases available to use:
* `--compat-options all`: Use all compat options (**Do NOT use this!**) * `--compat-options all`: Use all compat options (**Do NOT use this!**)
* `--compat-options youtube-dl`: Same as `--compat-options all,-multistreams,-playlist-match-filter,-manifest-filesize-approx,-allow-unsafe-ext,-prefer-vp9-sort` * `--compat-options youtube-dl`: Same as `--compat-options all,-multistreams,-playlist-match-filter,-manifest-filesize-approx,-allow-unsafe-ext,-prefer-vp9-sort,-allow-unsafe-exec-expansion`
* `--compat-options youtube-dlc`: Same as `--compat-options all,-no-live-chat,-no-youtube-channel-redirect,-playlist-match-filter,-manifest-filesize-approx,-allow-unsafe-ext,-prefer-vp9-sort` * `--compat-options youtube-dlc`: Same as `--compat-options all,-no-live-chat,-no-youtube-channel-redirect,-playlist-match-filter,-manifest-filesize-approx,-allow-unsafe-ext,-prefer-vp9-sort,-allow-unsafe-exec-expansion`
* `--compat-options 2021`: Same as `--compat-options 2022,no-certifi,filename-sanitization` * `--compat-options 2021`: Same as `--compat-options 2022,no-certifi,filename-sanitization`
* `--compat-options 2022`: Same as `--compat-options 2023,playlist-match-filter,no-external-downloader-progress,prefer-legacy-http-handler,manifest-filesize-approx` * `--compat-options 2022`: Same as `--compat-options 2023,playlist-match-filter,no-external-downloader-progress,prefer-legacy-http-handler,manifest-filesize-approx`
* `--compat-options 2023`: Same as `--compat-options 2024,prefer-vp9-sort` * `--compat-options 2023`: Same as `--compat-options 2024,prefer-vp9-sort`
@ -2358,7 +2356,12 @@ The following compat options restore vulnerable behavior from before security pa
> :warning: Only use if a valid file download is rejected because its extension is detected as uncommon > :warning: Only use if a valid file download is rejected because its extension is detected as uncommon
> >
> **This option can enable remote code execution! Consider [opening an issue](<https://github.com/yt-dlp/yt-dlp/issues/new/choose>) instead!** > **This option can enable remote code execution!** Consider [opening an issue](<https://github.com/yt-dlp/yt-dlp/issues/new/choose>) instead!
* `--compat-options allow-unsafe-exec-expansion`: The `--exec` option allows output template syntax to be used in its commands; however, for security reasons the conversions that can be used are restricted to `i`/`d` (signed integer decimal), `f` (floating-point decimal) and `q` (shell-quoted). yt-dlp versions from 2021.04.11 to 2026.03.17 (inclusive) did not apply this restriction. This option reverts this restriction
> :warning: **This option can enable remote code execution!** Consider using `%()q` conversions in your exec command templates for any string values.
### Deprecated options ### Deprecated options

View File

@ -81,11 +81,11 @@ tomli==2.4.1 ; python_full_version < '3.11' \
# via # via
# build # build
# hatchling # hatchling
trove-classifiers==2026.5.7.17 \ trove-classifiers==2026.5.22.10 \
--hash=sha256:5ec0800de5e2ddbd7c663cb4c0c15328f132dc168813897c18866c5c7b93db33 \ --hash=sha256:01fe864225726e03efb843827ecabfe319fc4dee8dd66d65b8996cb09be46e2c \
--hash=sha256:a04a48f8f0a787cb996514d3969ac7608aa3c60cb15d073c1e02801e60533e80 --hash=sha256:5477e9974e91904fb2cfa4a7581ab6e2f30c2c38d847fd00ed866080748101d5
# via hatchling # via hatchling
zipp==3.23.1 ; python_full_version < '3.10.2' \ zipp==4.1.0 ; python_full_version < '3.10.2' \
--hash=sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc \ --hash=sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f \
--hash=sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110 --hash=sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602
# via importlib-metadata # via importlib-metadata

View File

@ -68,9 +68,9 @@ brotlicffi==1.2.0.1 ; implementation_name != 'cpython' \
--hash=sha256:e015af99584c6db1490a69a210c765953e473e63adc2d891ac3062a737c9e851 \ --hash=sha256:e015af99584c6db1490a69a210c765953e473e63adc2d891ac3062a737c9e851 \
--hash=sha256:f2a5575653b0672638ba039b82fda56854934d7a6a24d4b8b5033f73ab43cbc1 --hash=sha256:f2a5575653b0672638ba039b82fda56854934d7a6a24d4b8b5033f73ab43cbc1
# via yt-dlp # via yt-dlp
certifi==2026.4.22 \ certifi==2026.5.20 \
--hash=sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a \ --hash=sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897 \
--hash=sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580 --hash=sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d
# via # via
# curl-cffi # curl-cffi
# requests # requests
@ -274,9 +274,9 @@ curl-cffi==0.15.0 ; implementation_name == 'cpython' \
--hash=sha256:bda66404010e9ed743b1b83c20c86f24fe21a9a6873e17479d6e67e29d8ded28 \ --hash=sha256:bda66404010e9ed743b1b83c20c86f24fe21a9a6873e17479d6e67e29d8ded28 \
--hash=sha256:ea0c67652bf6893d34ee0f82c944f37e488f6147e9421bef1771cc6545b02ded --hash=sha256:ea0c67652bf6893d34ee0f82c944f37e488f6147e9421bef1771cc6545b02ded
# via yt-dlp # via yt-dlp
idna==3.15 \ idna==3.17 \
--hash=sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8 \ --hash=sha256:466e48829084efe2548012b855df21540b96f2e20e51bd124c851536556a592c \
--hash=sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc --hash=sha256:5eb0cb53bc467c12eadcf6de83163ad8527cec9416f44b9b61b19caedad2b87f
# via requests # via requests
markdown-it-py==4.2.0 ; implementation_name == 'cpython' \ markdown-it-py==4.2.0 ; implementation_name == 'cpython' \
--hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \ --hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \

View File

@ -68,9 +68,9 @@ brotlicffi==1.2.0.1 ; implementation_name != 'cpython' \
--hash=sha256:e015af99584c6db1490a69a210c765953e473e63adc2d891ac3062a737c9e851 \ --hash=sha256:e015af99584c6db1490a69a210c765953e473e63adc2d891ac3062a737c9e851 \
--hash=sha256:f2a5575653b0672638ba039b82fda56854934d7a6a24d4b8b5033f73ab43cbc1 --hash=sha256:f2a5575653b0672638ba039b82fda56854934d7a6a24d4b8b5033f73ab43cbc1
# via yt-dlp # via yt-dlp
certifi==2026.4.22 \ certifi==2026.5.20 \
--hash=sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a \ --hash=sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897 \
--hash=sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580 --hash=sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d
# via # via
# requests # requests
# yt-dlp # yt-dlp
@ -174,9 +174,9 @@ charset-normalizer==3.4.7 \
--hash=sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79 \ --hash=sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79 \
--hash=sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464 --hash=sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464
# via requests # via requests
idna==3.15 \ idna==3.17 \
--hash=sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8 \ --hash=sha256:466e48829084efe2548012b855df21540b96f2e20e51bd124c851536556a592c \
--hash=sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc --hash=sha256:5eb0cb53bc467c12eadcf6de83163ad8527cec9416f44b9b61b19caedad2b87f
# via requests # via requests
mutagen==1.47.0 \ mutagen==1.47.0 \
--hash=sha256:719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99 \ --hash=sha256:719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99 \

View File

@ -41,9 +41,9 @@ packaging==26.2 \
# via # via
# pytest # pytest
# pytest-rerunfailures # pytest-rerunfailures
platformdirs==4.9.6 \ platformdirs==4.10.0 \
--hash=sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a \ --hash=sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7 \
--hash=sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917 --hash=sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a
# via # via
# python-discovery # python-discovery
# virtualenv # virtualenv
@ -66,12 +66,12 @@ pytest==9.0.3 \
--hash=sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9 \ --hash=sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9 \
--hash=sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c --hash=sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c
# via pytest-rerunfailures # via pytest-rerunfailures
pytest-rerunfailures==16.2 \ pytest-rerunfailures==16.3 \
--hash=sha256:5f5a32f15674a3d54f7598388fcd3cc1bc5c37284731a4704a44485dcdda5e23 \ --hash=sha256:37c9b1231c8083e9f4e724f50f7a21241822f9516c15c700ebbf218d6452355c \
--hash=sha256:c22a53d2827becc76f057d4ded123c0e726523f2f0e5f0bb4efb31fd59e1f14e --hash=sha256:6bdfb8ffb46c46072e6c16bdedee38b6c13eac620d9415ed5b63152cbf283170
python-discovery==1.3.1 \ python-discovery==1.4.0 \
--hash=sha256:62f6db28064c9613e7ca76cb3f00c38c839a07c31c00dfe7ed0986493d2150a6 \ --hash=sha256:26ed78d703e234879a66244c7d4114563fb13ec5cd30a2d1357e5fb4850782da \
--hash=sha256:ed188687ebb3b82c01a17cd5ac62fc94d9f6487a7f1a0f9dfe89753fec91039c --hash=sha256:eb8bc7daad3c226c147e45bb4e970a1feb1bf4048ee178e6db59e197b8010ce3
# via virtualenv # via virtualenv
pyyaml==6.0.3 \ pyyaml==6.0.3 \
--hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \
@ -132,25 +132,25 @@ pyyaml==6.0.3 \
--hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \ --hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \
--hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0
# via pre-commit # via pre-commit
ruff==0.15.13 \ ruff==0.15.15 \
--hash=sha256:1c26d2f66163deeb6e08d8b39fbbe983ce3c71cea06a6d7591cfd1421793c629 \ --hash=sha256:2728b93d7b23a603ea2c0ac6eb73d760bd38ec9de35f35fb41e18f7a3fee7622 \
--hash=sha256:2471da9bd1068c8c064b5fd9c0c4b6dddffd6369cb1cd68b29993b1709ff1b21 \ --hash=sha256:29428ea79694afbe756d45fd59b36f22b6b020dc0443cf7de0173046236964b9 \
--hash=sha256:2e2e39bff6c341f4b577a21b801326fab0b11847f48fcaa83f00a113c9b3cb55 \ --hash=sha256:3c8ceca6792f38196b8f589bc92eccd03eef286602da92e5dc05cc42ef6441b7 \
--hash=sha256:4044f94208b3b05ba0fc4a4abd0558cf4d6459bd18325eead7fd8cc66f909b41 \ --hash=sha256:3cdb1679e06a1f6b47bc384714ae96f6e2fb65ca441eb78c43d2ca554176ce1f \
--hash=sha256:444b580fc72fd6887e650acd3e575e18cdc79dbcf42fb4030b491057921f61f8 \ --hash=sha256:48decfa11d740de4889de623be1463308346312f2409a56e24aa280c86162dc4 \
--hash=sha256:6590d009e7cb7ebf36f83dbdd44a3fa48a0994ff6f1cdc1b08006abe58f98dc7 \ --hash=sha256:587a6278ed42059191c1a466e490bd7930fb50bd2e255398bc29616c895a61cb \
--hash=sha256:7064884d442b7d477b4e7473d12da7f08851d2b1982763c5d3f388a19468a1a4 \ --hash=sha256:7614ee79c69788cf6cedd568069ade9cecc22a1ad20494efe8d0c9ebb4b622d4 \
--hash=sha256:768494eb08b9cee54e2fd27969966f74db5a57f6eaa7a90fcb3306af34dfc4bd \ --hash=sha256:77d955a431430c66f72dd94e379ad38a16daea3d25094872ac4edf9e797be530 \
--hash=sha256:7ef823f817fcd191dc934e984be9cf4094f808effa16f2542ad8e821ba02bbf2 \ --hash=sha256:7aa77465b8ecaf1a27bea098d696f7fed5e1eccbd10b321b682d6de586ae5627 \
--hash=sha256:8cb74dd33bb2f6613faf7fc03b660053b5ac4f80e706d5788c6335e2a8048d51 \ --hash=sha256:8df0323902e15e24bc4bf246da830573d3cf3352bd0b9a164eab335d111ff4a4 \
--hash=sha256:9dbd6f94b434f896308e4d57fb7bfde0d02b99f7a64b3bdab0fdfa6a864203a5 \ --hash=sha256:a5015088452ca0081387063649ec67f06d3d1d6b8b936a1f836b5e9657ecd48c \
--hash=sha256:ae9c17e5eb4430c154e76abc25d79a318190f5a997f38fb6b114416c5319ffc9 \ --hash=sha256:ac5a646d1f6a7dadd5d50842dae2c1f9862ac887ef5d1b1375e02def791fde6e \
--hash=sha256:bf3259f3be4d181bda591da5db2571aed6853c6a048157756448020bc6c5cd22 \ --hash=sha256:b8dff018130b46d8e5bf0f926ef6b60cf871d6d5ae45fc9334e09632daa741d6 \
--hash=sha256:cc411dfebe5eebe55ce041c6ae080eb7668955e866daa2fbb16692a784f1c4ca \ --hash=sha256:be582fcc0db438902c7792b08d6ddf6c9b9e21addaa10092c2c741cfb09e5a45 \
--hash=sha256:e8d9a8e08013542e94d3220bc5b62cc3e5ef87c5f74bff367d3fac14fab013e6 \ --hash=sha256:cf93e5388f412e1b108b1f8b34a6e036b70fe8aff89393befad96fe48670311b \
--hash=sha256:f345a13937bd7f09f6f5d19fa0721b0c103e00e7f62bc67089a8e5e037719e0b \ --hash=sha256:db5bd4d802415cca656dc1616070b725952d6ae95eb5d4831e49fbd94a38f75f \
--hash=sha256:f9d89f17f7ba7fb2ed42921f0df75da797a9a5d71bc39049e2c687cf2baf44b7 \ --hash=sha256:df0c1c084f5f4be9812f61518a45c440d3c30d69ce4bf6c5270e66d38338f02a \
--hash=sha256:fb75f9a3a7e42ffe117d734494e6c5e5cb3565d66e12612cb63d0e572a41a5b6 --hash=sha256:f5294aab6356c81600fcdea3a62bb1b924dfd5e91767c12318d3f68f86af57cd
tomli==2.4.1 ; python_full_version < '3.11' \ tomli==2.4.1 ; python_full_version < '3.11' \
--hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \ --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \
--hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \ --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \
@ -208,7 +208,7 @@ typing-extensions==4.15.0 ; python_full_version < '3.11' \
# via # via
# exceptiongroup # exceptiongroup
# virtualenv # virtualenv
virtualenv==21.3.3 \ virtualenv==21.4.2 \
--hash=sha256:7d5987d8369e098e41406efb780a3d4ca79280097293899e351a6407ee153ab3 \ --hash=sha256:38e6ee0a555615c0ea9da2ac7e9998fe8dc3b911dd33ad8eaad2020957653b0c \
--hash=sha256:f5bda277e553b1c2b3c1a8debfc30496e1288cc93ce6b7b71b3280047e317328 --hash=sha256:854210ca524a1a4d0d744734f4acbc721c3ffe163b85bbf5d56d14d5ae2f0fae
# via pre-commit # via pre-commit

View File

@ -68,9 +68,9 @@ brotlicffi==1.2.0.1 ; implementation_name != 'cpython' \
--hash=sha256:e015af99584c6db1490a69a210c765953e473e63adc2d891ac3062a737c9e851 \ --hash=sha256:e015af99584c6db1490a69a210c765953e473e63adc2d891ac3062a737c9e851 \
--hash=sha256:f2a5575653b0672638ba039b82fda56854934d7a6a24d4b8b5033f73ab43cbc1 --hash=sha256:f2a5575653b0672638ba039b82fda56854934d7a6a24d4b8b5033f73ab43cbc1
# via yt-dlp # via yt-dlp
certifi==2026.4.22 \ certifi==2026.5.20 \
--hash=sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a \ --hash=sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897 \
--hash=sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580 --hash=sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d
# via # via
# curl-cffi # curl-cffi
# requests # requests
@ -326,9 +326,9 @@ curl-cffi==0.15.0 ; implementation_name == 'cpython' \
--hash=sha256:bda66404010e9ed743b1b83c20c86f24fe21a9a6873e17479d6e67e29d8ded28 \ --hash=sha256:bda66404010e9ed743b1b83c20c86f24fe21a9a6873e17479d6e67e29d8ded28 \
--hash=sha256:ea0c67652bf6893d34ee0f82c944f37e488f6147e9421bef1771cc6545b02ded --hash=sha256:ea0c67652bf6893d34ee0f82c944f37e488f6147e9421bef1771cc6545b02ded
# via yt-dlp # via yt-dlp
idna==3.15 \ idna==3.17 \
--hash=sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8 \ --hash=sha256:466e48829084efe2548012b855df21540b96f2e20e51bd124c851536556a592c \
--hash=sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc --hash=sha256:5eb0cb53bc467c12eadcf6de83163ad8527cec9416f44b9b61b19caedad2b87f
# via requests # via requests
jeepney==0.9.0 \ jeepney==0.9.0 \
--hash=sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683 \ --hash=sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683 \

View File

@ -74,9 +74,9 @@ brotlicffi==1.2.0.1 ; implementation_name != 'cpython' \
--hash=sha256:e015af99584c6db1490a69a210c765953e473e63adc2d891ac3062a737c9e851 \ --hash=sha256:e015af99584c6db1490a69a210c765953e473e63adc2d891ac3062a737c9e851 \
--hash=sha256:f2a5575653b0672638ba039b82fda56854934d7a6a24d4b8b5033f73ab43cbc1 --hash=sha256:f2a5575653b0672638ba039b82fda56854934d7a6a24d4b8b5033f73ab43cbc1
# via yt-dlp # via yt-dlp
certifi==2026.4.22 \ certifi==2026.5.20 \
--hash=sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a \ --hash=sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897 \
--hash=sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580 --hash=sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d
# via # via
# curl-cffi # curl-cffi
# requests # requests
@ -184,9 +184,9 @@ charset-normalizer==3.4.7 \
delocate==0.13.0 ; sys_platform == 'darwin' \ delocate==0.13.0 ; sys_platform == 'darwin' \
--hash=sha256:11f7596f88984c33f76b27fe2eea7637d1ce369a9e0b6737bbc706b6426e862c \ --hash=sha256:11f7596f88984c33f76b27fe2eea7637d1ce369a9e0b6737bbc706b6426e862c \
--hash=sha256:a93e67a9f56ee01a3f7096a042231d4ac37fecac873cd5ea34ea2b4f43a8fa13 --hash=sha256:a93e67a9f56ee01a3f7096a042231d4ac37fecac873cd5ea34ea2b4f43a8fa13
idna==3.15 \ idna==3.17 \
--hash=sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8 \ --hash=sha256:466e48829084efe2548012b855df21540b96f2e20e51bd124c851536556a592c \
--hash=sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc --hash=sha256:5eb0cb53bc467c12eadcf6de83163ad8527cec9416f44b9b61b19caedad2b87f
# via requests # via requests
macholib==1.16.4 ; sys_platform == 'darwin' \ macholib==1.16.4 ; sys_platform == 'darwin' \
--hash=sha256:da1a3fa8266e30f0ce7e97c6a54eefaae8edd1e5f86f3eb8b95457cae90265ea \ --hash=sha256:da1a3fa8266e30f0ce7e97c6a54eefaae8edd1e5f86f3eb8b95457cae90265ea \

View File

@ -1,3 +1,3 @@
pip==26.1.1 \ pip==26.1.2 \
--hash=sha256:99cb1c2899893b075ff56e4ed0af55669a955b49ad7fb8d8603ecdaf4ed653fb \ --hash=sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab \
--hash=sha256:d36762751d156a4ee895de8af39aa0abeeeb577f93a2eca6ab62467bbf0f8a78 --hash=sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605

View File

@ -5,25 +5,25 @@ pycodestyle==2.14.0 \
--hash=sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783 \ --hash=sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783 \
--hash=sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d --hash=sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d
# via autopep8 # via autopep8
ruff==0.15.13 \ ruff==0.15.15 \
--hash=sha256:1c26d2f66163deeb6e08d8b39fbbe983ce3c71cea06a6d7591cfd1421793c629 \ --hash=sha256:2728b93d7b23a603ea2c0ac6eb73d760bd38ec9de35f35fb41e18f7a3fee7622 \
--hash=sha256:2471da9bd1068c8c064b5fd9c0c4b6dddffd6369cb1cd68b29993b1709ff1b21 \ --hash=sha256:29428ea79694afbe756d45fd59b36f22b6b020dc0443cf7de0173046236964b9 \
--hash=sha256:2e2e39bff6c341f4b577a21b801326fab0b11847f48fcaa83f00a113c9b3cb55 \ --hash=sha256:3c8ceca6792f38196b8f589bc92eccd03eef286602da92e5dc05cc42ef6441b7 \
--hash=sha256:4044f94208b3b05ba0fc4a4abd0558cf4d6459bd18325eead7fd8cc66f909b41 \ --hash=sha256:3cdb1679e06a1f6b47bc384714ae96f6e2fb65ca441eb78c43d2ca554176ce1f \
--hash=sha256:444b580fc72fd6887e650acd3e575e18cdc79dbcf42fb4030b491057921f61f8 \ --hash=sha256:48decfa11d740de4889de623be1463308346312f2409a56e24aa280c86162dc4 \
--hash=sha256:6590d009e7cb7ebf36f83dbdd44a3fa48a0994ff6f1cdc1b08006abe58f98dc7 \ --hash=sha256:587a6278ed42059191c1a466e490bd7930fb50bd2e255398bc29616c895a61cb \
--hash=sha256:7064884d442b7d477b4e7473d12da7f08851d2b1982763c5d3f388a19468a1a4 \ --hash=sha256:7614ee79c69788cf6cedd568069ade9cecc22a1ad20494efe8d0c9ebb4b622d4 \
--hash=sha256:768494eb08b9cee54e2fd27969966f74db5a57f6eaa7a90fcb3306af34dfc4bd \ --hash=sha256:77d955a431430c66f72dd94e379ad38a16daea3d25094872ac4edf9e797be530 \
--hash=sha256:7ef823f817fcd191dc934e984be9cf4094f808effa16f2542ad8e821ba02bbf2 \ --hash=sha256:7aa77465b8ecaf1a27bea098d696f7fed5e1eccbd10b321b682d6de586ae5627 \
--hash=sha256:8cb74dd33bb2f6613faf7fc03b660053b5ac4f80e706d5788c6335e2a8048d51 \ --hash=sha256:8df0323902e15e24bc4bf246da830573d3cf3352bd0b9a164eab335d111ff4a4 \
--hash=sha256:9dbd6f94b434f896308e4d57fb7bfde0d02b99f7a64b3bdab0fdfa6a864203a5 \ --hash=sha256:a5015088452ca0081387063649ec67f06d3d1d6b8b936a1f836b5e9657ecd48c \
--hash=sha256:ae9c17e5eb4430c154e76abc25d79a318190f5a997f38fb6b114416c5319ffc9 \ --hash=sha256:ac5a646d1f6a7dadd5d50842dae2c1f9862ac887ef5d1b1375e02def791fde6e \
--hash=sha256:bf3259f3be4d181bda591da5db2571aed6853c6a048157756448020bc6c5cd22 \ --hash=sha256:b8dff018130b46d8e5bf0f926ef6b60cf871d6d5ae45fc9334e09632daa741d6 \
--hash=sha256:cc411dfebe5eebe55ce041c6ae080eb7668955e866daa2fbb16692a784f1c4ca \ --hash=sha256:be582fcc0db438902c7792b08d6ddf6c9b9e21addaa10092c2c741cfb09e5a45 \
--hash=sha256:e8d9a8e08013542e94d3220bc5b62cc3e5ef87c5f74bff367d3fac14fab013e6 \ --hash=sha256:cf93e5388f412e1b108b1f8b34a6e036b70fe8aff89393befad96fe48670311b \
--hash=sha256:f345a13937bd7f09f6f5d19fa0721b0c103e00e7f62bc67089a8e5e037719e0b \ --hash=sha256:db5bd4d802415cca656dc1616070b725952d6ae95eb5d4831e49fbd94a38f75f \
--hash=sha256:f9d89f17f7ba7fb2ed42921f0df75da797a9a5d71bc39049e2c687cf2baf44b7 \ --hash=sha256:df0c1c084f5f4be9812f61518a45c440d3c30d69ce4bf6c5270e66d38338f02a \
--hash=sha256:fb75f9a3a7e42ffe117d734494e6c5e5cb3565d66e12612cb63d0e572a41a5b6 --hash=sha256:f5294aab6356c81600fcdea3a62bb1b924dfd5e91767c12318d3f68f86af57cd
tomli==2.4.1 ; python_full_version < '3.11' \ tomli==2.4.1 ; python_full_version < '3.11' \
--hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \ --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \
--hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \ --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \

View File

@ -28,9 +28,9 @@ pytest==9.0.3 \
--hash=sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9 \ --hash=sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9 \
--hash=sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c --hash=sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c
# via pytest-rerunfailures # via pytest-rerunfailures
pytest-rerunfailures==16.2 \ pytest-rerunfailures==16.3 \
--hash=sha256:5f5a32f15674a3d54f7598388fcd3cc1bc5c37284731a4704a44485dcdda5e23 \ --hash=sha256:37c9b1231c8083e9f4e724f50f7a21241822f9516c15c700ebbf218d6452355c \
--hash=sha256:c22a53d2827becc76f057d4ded123c0e726523f2f0e5f0bb4efb31fd59e1f14e --hash=sha256:6bdfb8ffb46c46072e6c16bdedee38b6c13eac620d9415ed5b63152cbf283170
tomli==2.4.1 ; python_full_version < '3.11' \ tomli==2.4.1 ; python_full_version < '3.11' \
--hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \ --hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \
--hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \ --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \

View File

@ -1,20 +1,20 @@
uv==0.11.14 \ uv==0.11.17 \
--hash=sha256:078f2e63da89c8fcf6d578f02156045c5990c57d76464aab3f3f798d3fff95cd \ --hash=sha256:036d6e2940afe8b79637530b01b9241d8cfd174b07f1179a1ebbd42409c38ca3 \
--hash=sha256:0ea006a117b586b2681b6dfd9703a540d2ad2a136ec0f48d272767e599cc3dfb \ --hash=sha256:12b701fa32c5be3691759a73956e4462f30fa7b0dfa52ec66cb305bbb6ea4129 \
--hash=sha256:29c12a562441fc2d604e6920c558cacce74a55f889468708683a79b35a6e18a1 \ --hash=sha256:1a817eeb3026f27a53d3f4b7855a5105f6787dd192140e201eda4d2b9a11b72e \
--hash=sha256:379e64b236cf55f762a8308d7efe4365d5296ba29f3a4868761bc45b4e915a71 \ --hash=sha256:1d1be74deec997db1dda05a7e67541c904d65cbfd72e455d3c0a2a1e4bf2cddf \
--hash=sha256:3b0759ca504e48dcd4fafb1a61ef69aeb24c5a60fbf5f504a7873c8db1b24718 \ --hash=sha256:283186700c3e65a4644a73a917232da7d3e4a94d25ea0377a44f5b263fa49577 \
--hash=sha256:6a13e7e064563050c6606b3fd77091d427cdbdc5938b6f134baf8d8ec79bfdb7 \ --hash=sha256:2ccd5487a4a192bc832ea04c867a26883757db8fdfe88bed85d8129c82f9e505 \
--hash=sha256:78411a883f230a710af19f2ac6e6f0ba8eae90f0e5af4605f923fd367539fff4 \ --hash=sha256:37c915bfcf86f99c1c5be7c9ed21e0d80624067ba47bc8916a3cb0530bc94d27 \
--hash=sha256:78b51b117549ee4db7197ea5ece0848cecd443e464fb9dff9f254cdc1e4ed96f \ --hash=sha256:44ec1fe3af839f87370dcf0400c0cab917cc1ce697d563e860fc7d9ed72655e7 \
--hash=sha256:9923da7c63d70de9fe71829503d7e7ebfd6304e804d7232aad5f716e190db25b \ --hash=sha256:58c07ffc272c847d29cd98ca5082fa4304a645f87c718ec900e3cca9026bd096 \
--hash=sha256:a1ddbe8a2ab160affc179e9c3a40913b23a08cdf55254e1f3829cc22a51a0d8d \ --hash=sha256:659227cac719b618cc91e02be9e274ad5bd72d74fa278123e6373537e9f28216 \
--hash=sha256:b15bf7c146e38d7c938d3a207115d5fdd8ef764fe1f866c225b1bed27e88da1e \ --hash=sha256:6d1a033cc68cabb4141d6c1e3b66ffc6e970b98ba42e210f33270251e0bd8697 \
--hash=sha256:b384d873d0d18552c7524226125efd3965d921b7134c2f476c333771beb733e1 \ --hash=sha256:8426bfe315564d414cbc5ba5467595dc6348965e19acec742914f47da3ff269f \
--hash=sha256:d5c8f9ea36274ef2f9d24f0522085e280844172e901d9213f66a21b212266706 \ --hash=sha256:9da839e5a491c9a701d7d327a199cafc76ac27a03ac84fd2a8d4bf32c3af2448 \
--hash=sha256:dcdad43d52c130e3159e84ab1844e04d819d2c4a2495a687d27f80d560a3650e \ --hash=sha256:bf8f5ad959583dcd2c4ae445c754a97c05700246ff89259f3fd285c9c20f4c00 \
--hash=sha256:ddda5c5e41097814adac535c74851bae55e8097b9afc79aeae7fcffd8d86c06d \ --hash=sha256:ce16892a45134d20165c1ceababe06f3e9ce6a58902db1eff812c8c93626823f \
--hash=sha256:e54326703f1eca83a6fd73275e0f398b16b7d3f81531bf58899c2869bc403f6c \ --hash=sha256:e301d844eed9401f0f0351de12c55f1306ca05372acb0f28d35717c8ba663a22 \
--hash=sha256:e84069681c0334e07cbc7f114eb09d7fe1335e1db0297a66dbca80a1b393fe6d \ --hash=sha256:ec004b3c9bf9cb7756067ad1bd0bf64eb843e6fa2edbfbb3135ee152c14cea91 \
--hash=sha256:f0a8b58b38e984241bca5d7a5a47bf9ffe1ca2ab392a640887db8a04c4a9ec95 \ --hash=sha256:f0bf483c0d9fa14283992d56061b498b9d3d4adebd285af8744dc33f64dadfba \
--hash=sha256:f3005a2db1e8d72e125630d4f22ac4ceddb2c033e1f9b94b7f3ea38ebac46dd6 --hash=sha256:f2e44dfbfc7778d0d90edc6738f237c91e5e37e4e3cfe94c8a312cec56a41485

View File

@ -342,5 +342,30 @@
"action": "add", "action": "add",
"when": "1fbbe29b99dc61375bf6d786f824d9fcf6ea9c1a", "when": "1fbbe29b99dc61375bf6d786f824d9fcf6ea9c1a",
"short": "[priority] Security: [[CVE-2026-26331](https://nvd.nist.gov/vuln/detail/CVE-2026-26331)] [Arbitrary command injection with the `--netrc-cmd` option](https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-g3gw-q23r-pgqm)\n - The argument passed to the command in `--netrc-cmd` is now limited to a safe subset of characters" "short": "[priority] Security: [[CVE-2026-26331](https://nvd.nist.gov/vuln/detail/CVE-2026-26331)] [Arbitrary command injection with the `--netrc-cmd` option](https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-g3gw-q23r-pgqm)\n - The argument passed to the command in `--netrc-cmd` is now limited to a safe subset of characters"
},
{
"action": "add",
"when": "98e42eb04486e00bf86479b24dbfe19321f652ee",
"short": "[priority] **The minimum supported versions of Deno, Node, and Bun have been raised.**\n The minimum required version of [Deno](https://github.com/yt-dlp/yt-dlp/issues/16767) is now `v2.3.0`; supported [Node](https://github.com/yt-dlp/yt-dlp/issues/16765) versions are `v22` and up; [Bun support has been deprecated](https://github.com/yt-dlp/yt-dlp/issues/16766) and limited to versions `1.2.11` through `1.3.14`."
},
{
"action": "add",
"when": "5faffa999fd33b373d47773e8ee639d072accec2",
"short": "[priority] Security: Usage of vulnerable conversions (e.g. `%()s`) with the `--exec` option is an all-too-common pitfall. To remedy this, `--exec` now only allows safe conversions in its command templates.\n - Most users can simply replace `%(...)s` with `%(...)q` in their `--exec` argument(s). Numeric conversions are unaffected by this change. Using unsafe conversions with `--exec` poses a significant security risk. [Read more](<https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-69qj-pvh9-c5wg>)"
},
{
"action": "add",
"when": "e578e265f7c6ca94a74b30e0d8d6196a4d19fb6a",
"short": "[priority] Security: [[CVE-2026-50023](https://nvd.nist.gov/vuln/detail/CVE-2026-50023)] [Dangerous file type creation via insufficient filename sanitization](https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-c6mh-fpjc-4pr3)\n - Writing files with the extensions `.desktop`, `.url`, or `.webloc` is now only allowed in the context of `--write-link` functionality"
},
{
"action": "add",
"when": "2726572520238356bcf64aba2040228648b44c82",
"short": "[priority] Security: [[CVE-2026-50019](https://nvd.nist.gov/vuln/detail/CVE-2026-50019)] [File Downloader cookie leak with curl](https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-f7j3-774f-rfhj)\n - Impact is limited to users of `--downloader curl`; cookies are now properly passed to curl so that it respects their scope"
},
{
"action": "add",
"when": "25056f0d2d47adbd235a8d422fa62d68d0be2bc2",
"short": "[priority] Security: [[CVE-2026-50574](https://nvd.nist.gov/vuln/detail/CVE-2026-50574)] [Arbitrary code execution via manifest downloads with aria2c](https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-vx4q-3cr2-7cg2)\n - Impact is limited to users of `--downloader aria2c`\n - Support for downloading HLS and DASH formats with aria2c has been removed. Users affected by this change should migrate to use `-N` for concurrent fragment downloads via the native downloader"
} }
] ]

View File

@ -650,8 +650,8 @@ def update_requirements(
modify_and_write_pyproject(pyproject_text, table_name=EXTRAS_TABLE, table=extras) modify_and_write_pyproject(pyproject_text, table_name=EXTRAS_TABLE, table=extras)
# Generate/upgrade final lockfile that includes pinned extras # Generate/upgrade final lockfile that includes pinned extras
print(f'Running: uv lock {upgrade_arg}', file=sys.stderr) print('Running: uv lock', file=sys.stderr)
run_process('uv', 'lock', upgrade_arg, env=env) run_process('uv', 'lock', env=env)
# Export bundle requirements; any updates to these are already recorded w/ uv.lock package diff # Export bundle requirements; any updates to these are already recorded w/ uv.lock package diff
for target_suffix, target in BUNDLE_TARGETS.items(): for target_suffix, target in BUNDLE_TARGETS.items():

View File

@ -69,9 +69,9 @@ deno = [
pin = [ pin = [
"brotli==1.2.0 ; implementation_name == 'cpython' and sys_platform != 'ios'", "brotli==1.2.0 ; implementation_name == 'cpython' and sys_platform != 'ios'",
"brotlicffi==1.2.0.1 ; implementation_name != 'cpython'", "brotlicffi==1.2.0.1 ; implementation_name != 'cpython'",
"certifi==2026.4.22", "certifi==2026.5.20",
"charset-normalizer==3.4.7", "charset-normalizer==3.4.7",
"idna==3.15", "idna==3.17",
"mutagen==1.47.0", "mutagen==1.47.0",
"pycryptodomex==3.23.0", "pycryptodomex==3.23.0",
"requests==2.34.2", "requests==2.34.2",
@ -80,7 +80,7 @@ pin = [
"yt-dlp-ejs==0.8.0", "yt-dlp-ejs==0.8.0",
] ]
pin-curl-cffi = [ pin-curl-cffi = [
"certifi==2026.4.22 ; implementation_name == 'cpython'", "certifi==2026.5.20 ; implementation_name == 'cpython'",
"cffi==2.0.0 ; implementation_name == 'cpython'", "cffi==2.0.0 ; implementation_name == 'cpython'",
"curl-cffi==0.15.0 ; implementation_name == 'cpython'", "curl-cffi==0.15.0 ; implementation_name == 'cpython'",
"markdown-it-py==4.2.0 ; implementation_name == 'cpython'", "markdown-it-py==4.2.0 ; implementation_name == 'cpython'",
@ -98,7 +98,7 @@ pin-secretstorage = [
"typing-extensions==4.15.0 ; python_full_version < '3.11'", "typing-extensions==4.15.0 ; python_full_version < '3.11'",
] ]
pin-deno = [ pin-deno = [
"deno==2.7.14", "deno==2.8.1",
] ]
[dependency-groups] [dependency-groups]

View File

@ -22,7 +22,6 @@ The only reliable way to check if a site is supported is to try it.
- **4tube** - **4tube**
- **56.com** - **56.com**
- **7plus** - **7plus**
- **8tracks**
- **9c9media** - **9c9media**
- **9gag**: 9GAG - **9gag**: 9GAG
- **9News** - **9News**
@ -51,7 +50,6 @@ The only reliable way to check if a site is supported is to try it.
- **aenetworks:show** - **aenetworks:show**
- **AeonCo** - **AeonCo**
- **agalega:videos** - **agalega:videos**
- **AirTV**
- **AitubeKZVideo** - **AitubeKZVideo**
- **Alibaba** - **Alibaba**
- **AliExpressLive** - **AliExpressLive**
@ -60,8 +58,6 @@ The only reliable way to check if a site is supported is to try it.
- **Allstar** - **Allstar**
- **AllstarProfile** - **AllstarProfile**
- **AlphaPorno** - **AlphaPorno**
- **Alsace20TV**
- **Alsace20TVEmbed**
- **altcensored** - **altcensored**
- **altcensored:channel** - **altcensored:channel**
- **Alura**: [*alura*](## "netrc machine") - **Alura**: [*alura*](## "netrc machine")
@ -77,7 +73,6 @@ The only reliable way to check if a site is supported is to try it.
- **AmericasTestKitchen** - **AmericasTestKitchen**
- **AmericasTestKitchenSeason** - **AmericasTestKitchenSeason**
- **AmHistoryChannel** - **AmHistoryChannel**
- **AnchorFMEpisode**
- **anderetijden**: npo.nl, ntr.nl, omroepwnl.nl, zapp.nl and npo3.nl - **anderetijden**: npo.nl, ntr.nl, omroepwnl.nl, zapp.nl and npo3.nl
- **Angel** - **Angel**
- **AnimalPlanet** - **AnimalPlanet**
@ -90,8 +85,6 @@ The only reliable way to check if a site is supported is to try it.
- **Aparat** - **Aparat**
- **apple:music:connect**: Apple Music Connect - **apple:music:connect**: Apple Music Connect
- **ApplePodcasts** - **ApplePodcasts**
- **appletrailers**
- **appletrailers:section**
- **archive.org**: archive.org video and audio - **archive.org**: archive.org video and audio
- **ArcPublishing** - **ArcPublishing**
- **ARD** - **ARD**
@ -111,7 +104,6 @@ The only reliable way to check if a site is supported is to try it.
- **AsobiStage**: ASOBISTAGE (アソビステージ) - **AsobiStage**: ASOBISTAGE (アソビステージ)
- **AtresPlayer**: [*atresplayer*](## "netrc machine") - **AtresPlayer**: [*atresplayer*](## "netrc machine")
- **AtScaleConfEvent** - **AtScaleConfEvent**
- **ATVAt**
- **AudiMedia** - **AudiMedia**
- **AudioBoom** - **AudioBoom**
- **Audiodraft:custom** - **Audiodraft:custom**
@ -122,11 +114,6 @@ The only reliable way to check if a site is supported is to try it.
- **audius:artist**: Audius.co profile/artist pages - **audius:artist**: Audius.co profile/artist pages
- **audius:playlist**: Audius.co playlists - **audius:playlist**: Audius.co playlists
- **audius:track**: Audius track ID or API link. Prepend with "audius:" - **audius:track**: Audius track ID or API link. Prepend with "audius:"
- **AWAAN**
- **awaan:live**
- **awaan:season**
- **awaan:video**
- **axs.tv**
- **AZMedien**: AZ Medien videos - **AZMedien**: AZ Medien videos
- **BaiduVideo**: 百度视频 - **BaiduVideo**: 百度视频
- **BanBye** - **BanBye**
@ -148,8 +135,6 @@ The only reliable way to check if a site is supported is to try it.
- **BBVTVLive**: [*bbvtv*](## "netrc machine") - **BBVTVLive**: [*bbvtv*](## "netrc machine")
- **BBVTVRecordings**: [*bbvtv*](## "netrc machine") - **BBVTVRecordings**: [*bbvtv*](## "netrc machine")
- **BeaconTv** - **BeaconTv**
- **BeatBumpPlaylist**
- **BeatBumpVideo**
- **Beatport** - **Beatport**
- **Beeg** - **Beeg**
- **BehindKink**: (**Currently broken**) - **BehindKink**: (**Currently broken**)
@ -162,7 +147,6 @@ The only reliable way to check if a site is supported is to try it.
- **bibeltv:live**: BibelTV live program - **bibeltv:live**: BibelTV live program
- **bibeltv:series**: BibelTV series playlist - **bibeltv:series**: BibelTV series playlist
- **bibeltv:video**: BibelTV single video - **bibeltv:video**: BibelTV single video
- **Bigflix**
- **Bigo** - **Bigo**
- **Bild**: Bild.de - **Bild**: Bild.de
- **BiliBili** - **BiliBili**
@ -200,7 +184,6 @@ The only reliable way to check if a site is supported is to try it.
- **blogger.com** - **blogger.com**
- **Bloomberg** - **Bloomberg**
- **Bluesky** - **Bluesky**
- **BokeCC**: CC视频
- **BongaCams** - **BongaCams**
- **Boosty** - **Boosty**
- **BostonGlobe** - **BostonGlobe**
@ -229,12 +212,8 @@ The only reliable way to check if a site is supported is to try it.
- **BusinessInsider** - **BusinessInsider**
- **BuzzFeed** - **BuzzFeed**
- **BYUtv**: (**Currently broken**) - **BYUtv**: (**Currently broken**)
- **CaffeineTV**
- **Callin**
- **Caltrans** - **Caltrans**
- **CAM4** - **CAM4**
- **Camdemy**
- **CamdemyFolder**
- **CamFMEpisode** - **CamFMEpisode**
- **CamFMShow** - **CamFMShow**
- **CamModels** - **CamModels**
@ -282,7 +261,6 @@ The only reliable way to check if a site is supported is to try it.
- **ciscowebex**: Cisco Webex - **ciscowebex**: Cisco Webex
- **CJSW** - **CJSW**
- **Clipchamp** - **Clipchamp**
- **Clippit**
- **ClipRs**: (**Currently broken**) - **ClipRs**: (**Currently broken**)
- **CloserToTruth**: (**Currently broken**) - **CloserToTruth**: (**Currently broken**)
- **CloudflareStream** - **CloudflareStream**
@ -295,7 +273,6 @@ The only reliable way to check if a site is supported is to try it.
- **ComedyCentral** - **ComedyCentral**
- **ConanClassic**: (**Currently broken**) - **ConanClassic**: (**Currently broken**)
- **CondeNast**: Condé Nast media group: Allure, Architectural Digest, Ars Technica, Bon Appétit, Brides, Condé Nast, Condé Nast Traveler, Details, Epicurious, GQ, Glamour, Golf Digest, SELF, Teen Vogue, The New Yorker, Vanity Fair, Vogue, W Magazine, WIRED - **CondeNast**: Condé Nast media group: Allure, Architectural Digest, Ars Technica, Bon Appétit, Brides, Condé Nast, Condé Nast Traveler, Details, Epicurious, GQ, Glamour, Golf Digest, SELF, Teen Vogue, The New Yorker, Vanity Fair, Vogue, W Magazine, WIRED
- **CONtv**
- **CookingChannel** - **CookingChannel**
- **Corus** - **Corus**
- **Coub** - **Coub**
@ -370,7 +347,6 @@ The only reliable way to check if a site is supported is to try it.
- **DouyuTV**: 斗鱼直播 - **DouyuTV**: 斗鱼直播
- **DPlay** - **DPlay**
- **DRBonanza** - **DRBonanza**
- **Drooble**
- **Dropbox** - **Dropbox**
- **Dropout**: [*dropout*](## "netrc machine") - **Dropout**: [*dropout*](## "netrc machine")
- **DropoutSeason** - **DropoutSeason**
@ -381,8 +357,6 @@ The only reliable way to check if a site is supported is to try it.
- **drtv:season** - **drtv:season**
- **drtv:series** - **drtv:series**
- **DTube**: (**Currently broken**) - **DTube**: (**Currently broken**)
- **duboku**: www.duboku.io
- **duboku:list**: www.duboku.io entire series
- **Dumpert** - **Dumpert**
- **Duoplay** - **Duoplay**
- **dvtv**: http://video.aktualne.cz/ - **dvtv**: http://video.aktualne.cz/
@ -399,7 +373,6 @@ The only reliable way to check if a site is supported is to try it.
- **EinsUndEinsTV**: [*1und1tv*](## "netrc machine") - **EinsUndEinsTV**: [*1und1tv*](## "netrc machine")
- **EinsUndEinsTVLive**: [*1und1tv*](## "netrc machine") - **EinsUndEinsTVLive**: [*1und1tv*](## "netrc machine")
- **EinsUndEinsTVRecordings**: [*1und1tv*](## "netrc machine") - **EinsUndEinsTVRecordings**: [*1und1tv*](## "netrc machine")
- **eitb.tv**
- **ElementorEmbed** - **ElementorEmbed**
- **Elonet** - **Elonet**
- **ElPais**: El País - **ElPais**: El País
@ -433,7 +406,6 @@ The only reliable way to check if a site is supported is to try it.
- **EWETVLive**: [*ewetv*](## "netrc machine") - **EWETVLive**: [*ewetv*](## "netrc machine")
- **EWETVRecordings**: [*ewetv*](## "netrc machine") - **EWETVRecordings**: [*ewetv*](## "netrc machine")
- **Expressen** - **Expressen**
- **EyedoTV**
- **facebook** - **facebook**
- **facebook:ads** - **facebook:ads**
- **facebook:reel** - **facebook:reel**
@ -473,7 +445,6 @@ The only reliable way to check if a site is supported is to try it.
- **FrancaisFacile** - **FrancaisFacile**
- **FranceCulture** - **FranceCulture**
- **franceinfo**: franceinfo.fr (formerly francetvinfo.fr) - **franceinfo**: franceinfo.fr (formerly francetvinfo.fr)
- **FranceInter**
- **francetv** - **francetv**
- **francetv:site** - **francetv:site**
- **Freesound** - **Freesound**
@ -483,13 +454,11 @@ The only reliable way to check if a site is supported is to try it.
- **FrontendMasters**: [*frontendmasters*](## "netrc machine") - **FrontendMasters**: [*frontendmasters*](## "netrc machine")
- **FrontendMastersCourse**: [*frontendmasters*](## "netrc machine") - **FrontendMastersCourse**: [*frontendmasters*](## "netrc machine")
- **FrontendMastersLesson**: [*frontendmasters*](## "netrc machine") - **FrontendMastersLesson**: [*frontendmasters*](## "netrc machine")
- **FujiTVFODPlus7**
- **Funk** - **Funk**
- **Funker530** - **Funker530**
- **Fux** - **Fux**
- **FuyinTV** - **FuyinTV**
- **Gab** - **Gab**
- **GabTV**
- **Gaia**: [*gaia*](## "netrc machine") - **Gaia**: [*gaia*](## "netrc machine")
- **GameDevTVDashboard**: [*gamedevtv*](## "netrc machine") - **GameDevTVDashboard**: [*gamedevtv*](## "netrc machine")
- **GameJolt** - **GameJolt**
@ -538,14 +507,10 @@ The only reliable way to check if a site is supported is to try it.
- **Gofile** - **Gofile**
- **Golem** - **Golem**
- **goodgame:stream** - **goodgame:stream**
- **google:podcasts**
- **google:podcasts:feed**
- **GoogleDrive** - **GoogleDrive**
- **GoogleDrive:Folder** - **GoogleDrive:Folder**
- **GoPro** - **GoPro**
- **Goshgay**
- **GoToStage** - **GoToStage**
- **GPUTechConf**
- **Graspop** - **Graspop**
- **Gronkh** - **Gronkh**
- **gronkh:feed** - **gronkh:feed**
@ -565,7 +530,6 @@ The only reliable way to check if a site is supported is to try it.
- **history:player** - **history:player**
- **history:topic**: History.com Topic - **history:topic**: History.com Topic
- **HitRecord** - **HitRecord**
- **hketv**: 香港教育局教育電視 (HKETV) Educational Television, Hong Kong Educational Bureau
- **HollywoodReporter** - **HollywoodReporter**
- **HollywoodReporterPlaylist** - **HollywoodReporterPlaylist**
- **Holodex** - **Holodex**
@ -593,7 +557,6 @@ The only reliable way to check if a site is supported is to try it.
- **IdagioPlaylist** - **IdagioPlaylist**
- **IdagioRecording** - **IdagioRecording**
- **IdagioTrack** - **IdagioTrack**
- **IdolPlus**
- **iflix:episode** - **iflix:episode**
- **IflixSeries** - **IflixSeries**
- **ign.com** - **ign.com**
@ -618,7 +581,6 @@ The only reliable way to check if a site is supported is to try it.
- **instagram:user**: Instagram user profile (**Currently broken**) - **instagram:user**: Instagram user profile (**Currently broken**)
- **InstagramIOS**: IOS instagram:// URL - **InstagramIOS**: IOS instagram:// URL
- **Internazionale** - **Internazionale**
- **InternetVideoArchive**
- **InvestigationDiscovery** - **InvestigationDiscovery**
- **IPrima**: [*iprima*](## "netrc machine") - **IPrima**: [*iprima*](## "netrc machine")
- **IPrimaCNN** - **IPrimaCNN**
@ -636,12 +598,10 @@ The only reliable way to check if a site is supported is to try it.
- **ivi:compilation**: ivi.ru compilations - **ivi:compilation**: ivi.ru compilations
- **ivideon**: Ivideon TV - **ivideon**: Ivideon TV
- **Ivoox** - **Ivoox**
- **IVXPlayer**
- **iwara**: [*iwara*](## "netrc machine") - **iwara**: [*iwara*](## "netrc machine")
- **iwara:playlist**: [*iwara*](## "netrc machine") - **iwara:playlist**: [*iwara*](## "netrc machine")
- **iwara:user**: [*iwara*](## "netrc machine") - **iwara:user**: [*iwara*](## "netrc machine")
- **Ixigua** - **Ixigua**
- **Izlesene**
- **Jamendo** - **Jamendo**
- **JamendoAlbum** - **JamendoAlbum**
- **JeuxVideo**: (**Currently broken**) - **JeuxVideo**: (**Currently broken**)
@ -674,11 +634,9 @@ The only reliable way to check if a site is supported is to try it.
- **KickStarter** - **KickStarter**
- **Kika**: KiKA.de - **Kika**: KiKA.de
- **KikaPlaylist** - **KikaPlaylist**
- **kinja:embed**
- **KinoPoisk** - **KinoPoisk**
- **Kommunetv** - **Kommunetv**
- **KompasVideo** - **KompasVideo**
- **Koo**: (**Currently broken**)
- **KrasView**: Красвью (**Currently broken**) - **KrasView**: Красвью (**Currently broken**)
- **KTH** - **KTH**
- **Ku6** - **Ku6**
@ -716,7 +674,6 @@ The only reliable way to check if a site is supported is to try it.
- **Lemonde** - **Lemonde**
- **Lenta**: (**Currently broken**) - **Lenta**: (**Currently broken**)
- **LePlaylist** - **LePlaylist**
- **LetvCloud**: 乐视云
- **Libsyn** - **Libsyn**
- **life**: Life.ru - **life**: Life.ru
- **life:embed** - **life:embed**
@ -730,8 +687,6 @@ The only reliable way to check if a site is supported is to try it.
- **ListenNotes** - **ListenNotes**
- **LiTV** - **LiTV**
- **LiveJournal**: (**Currently broken**) - **LiveJournal**: (**Currently broken**)
- **livestream**
- **livestream:original**
- **Livestreamfails** - **Livestreamfails**
- **Lnk** - **Lnk**
- **loc**: Library of Congress - **loc**: Library of Congress
@ -748,8 +703,6 @@ The only reliable way to check if a site is supported is to try it.
- **LSMLTVEmbed** - **LSMLTVEmbed**
- **LSMReplay** - **LSMReplay**
- **Lumni** - **Lumni**
- **lynda**: [*lynda*](## "netrc machine") lynda.com videos
- **lynda:course**: [*lynda*](## "netrc machine") lynda.com online courses
- **maariv.co.il** - **maariv.co.il**
- **MagellanTV** - **MagellanTV**
- **MagentaMusik** - **MagentaMusik**
@ -799,11 +752,9 @@ The only reliable way to check if a site is supported is to try it.
- **MicrosoftLearnPlaylist** - **MicrosoftLearnPlaylist**
- **MicrosoftLearnSession** - **MicrosoftLearnSession**
- **MicrosoftMedius** - **MicrosoftMedius**
- **microsoftstream**: Microsoft Stream
- **minds** - **minds**
- **minds:channel** - **minds:channel**
- **minds:group** - **minds:group**
- **Minoto**
- **mir24.tv** - **mir24.tv**
- **mirrativ** - **mirrativ**
- **mirrativ:user** - **mirrativ:user**
@ -826,18 +777,11 @@ The only reliable way to check if a site is supported is to try it.
- **MNetTVRecordings**: [*mnettv*](## "netrc machine") - **MNetTVRecordings**: [*mnettv*](## "netrc machine")
- **MochaVideo** - **MochaVideo**
- **Mojevideo**: mojevideo.sk - **Mojevideo**: mojevideo.sk
- **Mojvideo**
- **Monstercat** - **Monstercat**
- **monstersiren**: 塞壬唱片 - **monstersiren**: 塞壬唱片
- **Motherless**
- **MotherlessGallery**
- **MotherlessGroup**
- **MotherlessUploader**
- **Motorsport**: motorsport.com (**Currently broken**) - **Motorsport**: motorsport.com (**Currently broken**)
- **MovieFap** - **MovieFap**
- **moviepilot**: Moviepilot trailer - **moviepilot**: Moviepilot trailer
- **MoviewPlay**
- **Moviezine**
- **MovingImage** - **MovingImage**
- **MSN** - **MSN**
- **mtg**: MTG services - **mtg**: MTG services
@ -849,10 +793,6 @@ The only reliable way to check if a site is supported is to try it.
- **MurrtubeUser**: Murrtube user profile (**Currently broken**) - **MurrtubeUser**: Murrtube user profile (**Currently broken**)
- **MuseAI** - **MuseAI**
- **MuseScore** - **MuseScore**
- **MusicdexAlbum**
- **MusicdexArtist**
- **MusicdexPlaylist**
- **MusicdexSong**
- **Mux** - **Mux**
- **Mx3** - **Mx3**
- **Mx3Neo** - **Mx3Neo**
@ -871,11 +811,9 @@ The only reliable way to check if a site is supported is to try it.
- **NascarClassics** - **NascarClassics**
- **Nate** - **Nate**
- **NateProgram** - **NateProgram**
- **natgeo:video**
- **NationalGeographicTV** - **NationalGeographicTV**
- **Naver** - **Naver**
- **Naver:live** - **Naver:live**
- **navernow**
- **nba**: (**Currently broken**) - **nba**: (**Currently broken**)
- **nba:channel**: (**Currently broken**) - **nba:channel**: (**Currently broken**)
- **nba:embed**: (**Currently broken**) - **nba:embed**: (**Currently broken**)
@ -900,7 +838,6 @@ The only reliable way to check if a site is supported is to try it.
- **nebula:subscriptions**: [*watchnebula*](## "netrc machine") - **nebula:subscriptions**: [*watchnebula*](## "netrc machine")
- **nebula:video**: [*watchnebula*](## "netrc machine") - **nebula:video**: [*watchnebula*](## "netrc machine")
- **NekoHacker** - **NekoHacker**
- **NerdCubedFeed**
- **Nest** - **Nest**
- **NestClip** - **NestClip**
- **NetAppCollection** - **NetAppCollection**
@ -915,9 +852,6 @@ The only reliable way to check if a site is supported is to try it.
- **NetPlusTV**: [*netplus*](## "netrc machine") - **NetPlusTV**: [*netplus*](## "netrc machine")
- **NetPlusTVLive**: [*netplus*](## "netrc machine") - **NetPlusTVLive**: [*netplus*](## "netrc machine")
- **NetPlusTVRecordings**: [*netplus*](## "netrc machine") - **NetPlusTVRecordings**: [*netplus*](## "netrc machine")
- **Netverse**
- **NetversePlaylist**
- **NetverseSearch**: "netsearch:" prefix
- **Netzkino** - **Netzkino**
- **Newgrounds**: [*newgrounds*](## "netrc machine") - **Newgrounds**: [*newgrounds*](## "netrc machine")
- **Newgrounds:playlist** - **Newgrounds:playlist**
@ -993,9 +927,6 @@ The only reliable way to check if a site is supported is to try it.
- **nts.live** - **nts.live**
- **ntv.ru** - **ntv.ru**
- **NubilesPorn**: [*nubiles-porn*](## "netrc machine") - **NubilesPorn**: [*nubiles-porn*](## "netrc machine")
- **nuum:live**
- **nuum:media**
- **nuum:tab**
- **Nuvid** - **Nuvid**
- **NYTimes** - **NYTimes**
- **NYTimesArticle** - **NYTimesArticle**
@ -1020,14 +951,12 @@ The only reliable way to check if a site is supported is to try it.
- **onet.tv** - **onet.tv**
- **onet.tv:channel** - **onet.tv:channel**
- **OnetMVP** - **OnetMVP**
- **OnionStudios**
- **onsen**: [*onsen*](## "netrc machine") インターネットラジオステーション<音泉> - **onsen**: [*onsen*](## "netrc machine") インターネットラジオステーション<音泉>
- **Opencast** - **Opencast**
- **OpencastPlaylist** - **OpencastPlaylist**
- **openrec** - **openrec**
- **openrec:capture** - **openrec:capture**
- **openrec:movie** - **openrec:movie**
- **OraTV**
- **orf:fm4:story**: fm4.orf.at stories - **orf:fm4:story**: fm4.orf.at stories
- **orf:iptv**: iptv.ORF.at - **orf:iptv**: iptv.ORF.at
- **orf:on** - **orf:on**
@ -1079,24 +1008,17 @@ The only reliable way to check if a site is supported is to try it.
- **Pinkbike** - **Pinkbike**
- **Pinterest** - **Pinterest**
- **PinterestCollection** - **PinterestCollection**
- **PiramideTV**
- **PiramideTVChannel**
- **PlanetMarathi**
- **Platzi**: [*platzi*](## "netrc machine") - **Platzi**: [*platzi*](## "netrc machine")
- **PlatziCourse**: [*platzi*](## "netrc machine") - **PlatziCourse**: [*platzi*](## "netrc machine")
- **play.tv**: [*goplay*](## "netrc machine") PLAY (formerly goplay.be) - **play.tv**: [*goplay*](## "netrc machine") PLAY (formerly goplay.be)
- **player.sky.it** - **player.sky.it**
- **PlayerFm** - **PlayerFm**
- **playeur**
- **PlayPlusTV**: [*playplustv*](## "netrc machine")
- **PlaySuisse**: [*playsuisse*](## "netrc machine") - **PlaySuisse**: [*playsuisse*](## "netrc machine")
- **Playtvak**: Playtvak.cz, iDNES.cz and Lidovky.cz - **Playtvak**: Playtvak.cz, iDNES.cz and Lidovky.cz
- **PlayVids** - **PlayVids**
- **Playwire**
- **pluralsight**: [*pluralsight*](## "netrc machine") - **pluralsight**: [*pluralsight*](## "netrc machine")
- **pluralsight:course** - **pluralsight:course**
- **PlutoTV**: (**Currently broken**) - **PlutoTV**: (**Currently broken**)
- **PlVideo**: Платформа
- **PlyrEmbed** - **PlyrEmbed**
- **PodbayFM** - **PodbayFM**
- **PodbayFMChannel** - **PodbayFMChannel**
@ -1133,7 +1055,6 @@ The only reliable way to check if a site is supported is to try it.
- **PremiershipRugby** - **PremiershipRugby**
- **PressTV** - **PressTV**
- **ProjectVeritas**: (**Currently broken**) - **ProjectVeritas**: (**Currently broken**)
- **prosiebensat1**: ProSiebenSat.1 Digital
- **PRXAccount** - **PRXAccount**
- **PRXSeries** - **PRXSeries**
- **prxseries:search**: PRX Series Search; "prxseries:" prefix - **prxseries:search**: PRX Series Search; "prxseries:" prefix
@ -1141,7 +1062,6 @@ The only reliable way to check if a site is supported is to try it.
- **PRXStory** - **PRXStory**
- **puhutv** - **puhutv**
- **puhutv:serie** - **puhutv:serie**
- **Puls4**
- **Pyvideo** - **Pyvideo**
- **QDance**: [*qdance*](## "netrc machine") - **QDance**: [*qdance*](## "netrc machine")
- **QingTing** - **QingTing**
@ -1162,8 +1082,6 @@ The only reliable way to check if a site is supported is to try it.
- **Radio1Be** - **Radio1Be**
- **radiocanada** - **radiocanada**
- **radiocanada:audiovideo** - **radiocanada:audiovideo**
- **RadioComercial**
- **RadioComercialPlaylist**
- **radiofrance** - **radiofrance**
- **RadioFranceLive** - **RadioFranceLive**
- **RadioFrancePodcast** - **RadioFrancePodcast**
@ -1203,7 +1121,6 @@ The only reliable way to check if a site is supported is to try it.
- **RedBullEmbed** - **RedBullEmbed**
- **RedBullTV** - **RedBullTV**
- **RedBullTVRrnContent** - **RedBullTVRrnContent**
- **redcdnlivx**
- **Reddit**: [*reddit*](## "netrc machine") - **Reddit**: [*reddit*](## "netrc machine")
- **RedGifs** - **RedGifs**
- **RedGifsSearch**: Redgifs search - **RedGifsSearch**: Redgifs search
@ -1214,11 +1131,9 @@ The only reliable way to check if a site is supported is to try it.
- **Restudy**: (**Currently broken**) - **Restudy**: (**Currently broken**)
- **Reuters**: (**Currently broken**) - **Reuters**: (**Currently broken**)
- **ReverbNation** - **ReverbNation**
- **RheinMainTV**
- **RideHome** - **RideHome**
- **RinseFM** - **RinseFM**
- **RinseFMArtistPlaylist** - **RinseFMArtistPlaylist**
- **RMCDecouverte**
- **RockstarGames**: (**Currently broken**) - **RockstarGames**: (**Currently broken**)
- **Rokfin**: [*rokfin*](## "netrc machine") - **Rokfin**: [*rokfin*](## "netrc machine")
- **rokfin:channel**: Rokfin Channels - **rokfin:channel**: Rokfin Channels
@ -1302,12 +1217,11 @@ The only reliable way to check if a site is supported is to try it.
- **ScrippsNetworks** - **ScrippsNetworks**
- **scrippsnetworks:watch** - **scrippsnetworks:watch**
- **Scrolller** - **Scrolller**
- **sejm** - **sejm**: (**Currently broken**)
- **Sen** - **Sen**
- **SenalColombiaLive**: (**Currently broken**) - **SenalColombiaLive**: (**Currently broken**)
- **senate.gov** - **senate.gov**
- **senate.gov:isvp** - **senate.gov:isvp**
- **SendtoNews**: (**Currently broken**)
- **Servus** - **Servus**
- **Sexu**: (**Currently broken**) - **Sexu**: (**Currently broken**)
- **SeznamZpravy** - **SeznamZpravy**
@ -1315,7 +1229,6 @@ The only reliable way to check if a site is supported is to try it.
- **Shahid**: [*shahid*](## "netrc machine") - **Shahid**: [*shahid*](## "netrc machine")
- **ShahidShow** - **ShahidShow**
- **SharePoint** - **SharePoint**
- **ShareVideosEmbed**
- **ShemarooMe** - **ShemarooMe**
- **Shiey** - **Shiey**
- **ShowRoomLive** - **ShowRoomLive**
@ -1345,15 +1258,14 @@ The only reliable way to check if a site is supported is to try it.
- **smotrim:live** - **smotrim:live**
- **smotrim:playlist** - **smotrim:playlist**
- **SnapchatSpotlight** - **SnapchatSpotlight**
- **Snotr**
- **SoftWhiteUnderbelly**: [*softwhiteunderbelly*](## "netrc machine") - **SoftWhiteUnderbelly**: [*softwhiteunderbelly*](## "netrc machine")
- **Sohu** - **Sohu**
- **SohuV** - **SohuV**
- **SonyLIV**: [*sonyliv*](## "netrc machine") - **SonyLIV**: [*sonyliv*](## "netrc machine")
- **SonyLIVSeries** - **SonyLIVSeries**
- **soop**: [*afreecatv*](## "netrc machine") sooplive.co.kr - **soop**: [*afreecatv*](## "netrc machine") sooplive.com
- **soop:catchstory**: [*afreecatv*](## "netrc machine") sooplive.co.kr catch story - **soop:catchstory**: [*afreecatv*](## "netrc machine") sooplive.com catch story
- **soop:live**: [*afreecatv*](## "netrc machine") sooplive.co.kr livestreams - **soop:live**: [*afreecatv*](## "netrc machine") sooplive.com livestreams
- **soop:user**: [*afreecatv*](## "netrc machine") - **soop:user**: [*afreecatv*](## "netrc machine")
- **soundcloud**: [*soundcloud*](## "netrc machine") - **soundcloud**: [*soundcloud*](## "netrc machine")
- **soundcloud:playlist**: [*soundcloud*](## "netrc machine") - **soundcloud:playlist**: [*soundcloud*](## "netrc machine")
@ -1383,7 +1295,6 @@ The only reliable way to check if a site is supported is to try it.
- **sporteurope** - **sporteurope**
- **Spreaker** - **Spreaker**
- **SpreakerShow** - **SpreakerShow**
- **SpringboardPlatform**
- **SproutVideo** - **SproutVideo**
- **sr:mediathek**: Saarländischer Rundfunk - **sr:mediathek**: Saarländischer Rundfunk
- **SRGSSR** - **SRGSSR**
@ -1391,14 +1302,11 @@ The only reliable way to check if a site is supported is to try it.
- **StacommuLive**: [*stacommu*](## "netrc machine") - **StacommuLive**: [*stacommu*](## "netrc machine")
- **StacommuVOD**: [*stacommu*](## "netrc machine") - **StacommuVOD**: [*stacommu*](## "netrc machine")
- **StagePlusVODConcert**: [*stageplus*](## "netrc machine") - **StagePlusVODConcert**: [*stageplus*](## "netrc machine")
- **stanfordoc**: Stanford Open ClassRoom
- **startrek**: STAR TREK - **startrek**: STAR TREK
- **startv** - **startv**
- **Steam** - **Steam**
- **SteamCommunity** - **SteamCommunity**
- **SteamCommunityBroadcast** - **SteamCommunityBroadcast**
- **Stitcher**
- **StitcherShow**
- **StoryFire** - **StoryFire**
- **StoryFireSeries** - **StoryFireSeries**
- **StoryFireUser** - **StoryFireUser**
@ -1406,7 +1314,6 @@ The only reliable way to check if a site is supported is to try it.
- **Streamable** - **Streamable**
- **StreamCZ** - **StreamCZ**
- **StreetVoice** - **StreetVoice**
- **StretchInternet**
- **Stripchat** - **Stripchat**
- **stv:player** - **stv:player**
- **stvr**: Slovak Television and Radio (formerly RTVS) - **stvr**: Slovak Television and Radio (formerly RTVS)
@ -1419,9 +1326,7 @@ The only reliable way to check if a site is supported is to try it.
- **svt:page** - **svt:page**
- **svt:play**: SVT Play and Öppet arkiv - **svt:play**: SVT Play and Öppet arkiv
- **svt:play:series** - **svt:play:series**
- **SwearnetEpisode**
- **Syfy** - **Syfy**
- **SYVDK**
- **SztvHu** - **SztvHu**
- **t-online.de**: (**Currently broken**) - **t-online.de**: (**Currently broken**)
- **Tagesschau**: (**Currently broken**) - **Tagesschau**: (**Currently broken**)
@ -1465,7 +1370,6 @@ The only reliable way to check if a site is supported is to try it.
- **TeleQuebecVideo** - **TeleQuebecVideo**
- **TeleTask**: (**Currently broken**) - **TeleTask**: (**Currently broken**)
- **Telewebion**: (**Currently broken**) - **Telewebion**: (**Currently broken**)
- **Tempo**
- **TennisTV**: [*tennistv*](## "netrc machine") - **TennisTV**: [*tennistv*](## "netrc machine")
- **TF1** - **TF1**
- **TFO**: (**Currently broken**) - **TFO**: (**Currently broken**)
@ -1476,7 +1380,6 @@ The only reliable way to check if a site is supported is to try it.
- **TheGuardianPodcast** - **TheGuardianPodcast**
- **TheGuardianPodcastPlaylist** - **TheGuardianPodcastPlaylist**
- **TheHighWire** - **TheHighWire**
- **TheHoleTv**
- **TheIntercept** - **TheIntercept**
- **ThePlatform** - **ThePlatform**
- **ThePlatformFeed** - **ThePlatformFeed**
@ -1510,11 +1413,7 @@ The only reliable way to check if a site is supported is to try it.
- **toutiao**: 今日头条 - **toutiao**: 今日头条
- **Toypics**: Toypics video (**Currently broken**) - **Toypics**: Toypics video (**Currently broken**)
- **ToypicsUser**: Toypics user profile (**Currently broken**) - **ToypicsUser**: Toypics user profile (**Currently broken**)
- **TrailerAddict**: (**Currently broken**)
- **TravelChannel** - **TravelChannel**
- **Triller**: [*triller*](## "netrc machine")
- **TrillerShort**
- **TrillerUser**: [*triller*](## "netrc machine")
- **Trovo** - **Trovo**
- **TrovoChannelClip**: All Clips of a trovo.live channel; "trovoclip:" prefix - **TrovoChannelClip**: All Clips of a trovo.live channel; "trovoclip:" prefix
- **TrovoChannelVod**: All VODs of a trovo.live channel; "trovovod:" prefix - **TrovoChannelVod**: All VODs of a trovo.live channel; "trovovod:" prefix
@ -1568,7 +1467,6 @@ The only reliable way to check if a site is supported is to try it.
- **tvp:stream** - **tvp:stream**
- **tvp:vod** - **tvp:vod**
- **tvp:vod:series** - **tvp:vod:series**
- **TVPlayer**
- **TVPlayHome** - **TVPlayHome**
- **tvw** - **tvw**
- **tvw:news** - **tvw:news**
@ -1594,10 +1492,8 @@ The only reliable way to check if a site is supported is to try it.
- **udemy**: [*udemy*](## "netrc machine") - **udemy**: [*udemy*](## "netrc machine")
- **udemy:course**: [*udemy*](## "netrc machine") - **udemy:course**: [*udemy*](## "netrc machine")
- **UDNEmbed**: 聯合影音 - **UDNEmbed**: 聯合影音
- **UFCArabia**: [*ufcarabia*](## "netrc machine")
- **UFCTV**: [*ufctv*](## "netrc machine") - **UFCTV**: [*ufctv*](## "netrc machine")
- **ukcolumn**: (**Currently broken**) - **ukcolumn**: (**Currently broken**)
- **UKTVPlay**
- **UlizaPlayer** - **UlizaPlayer**
- **UlizaPortal**: ulizaportal.jp - **UlizaPortal**: ulizaportal.jp
- **umg:de**: Universal Music Deutschland - **umg:de**: Universal Music Deutschland
@ -1620,7 +1516,7 @@ The only reliable way to check if a site is supported is to try it.
- **Veo** - **Veo**
- **Vevo** - **Vevo**
- **VevoPlaylist** - **VevoPlaylist**
- **VGTV**: VGTV, BTTV, FTV, Aftenposten and Aftonbladet - **VGTV**: VGTV, BTTV, FTV, Aftenposten and Aftonbladet (**Currently broken**)
- **vh1.com** - **vh1.com**
- **vhx:embed**: [*vimeo*](## "netrc machine") - **vhx:embed**: [*vimeo*](## "netrc machine")
- **vice**: (**Currently broken**) - **vice**: (**Currently broken**)
@ -1632,16 +1528,7 @@ The only reliable way to check if a site is supported is to try it.
- **video.google:search**: Google Video search; "gvsearch:" prefix - **video.google:search**: Google Video search; "gvsearch:" prefix
- **video.sky.it** - **video.sky.it**
- **video.sky.it:live** - **video.sky.it:live**
- **VideoDetective**
- **videofy.me**: (**Currently broken**)
- **VideoKen**
- **VideoKenCategory**
- **VideoKenPlayer** - **VideoKenPlayer**
- **VideoKenPlaylist**
- **VideoKenTopic**
- **videomore**
- **videomore:season**
- **videomore:video**
- **VideoPress** - **VideoPress**
- **Vidflex** - **Vidflex**
- **Vidio**: [*vidio*](## "netrc machine") - **Vidio**: [*vidio*](## "netrc machine")
@ -1665,8 +1552,6 @@ The only reliable way to check if a site is supported is to try it.
- **vimeo:review**: [*vimeo*](## "netrc machine") Review pages on vimeo - **vimeo:review**: [*vimeo*](## "netrc machine") Review pages on vimeo
- **vimeo:user**: [*vimeo*](## "netrc machine") - **vimeo:user**: [*vimeo*](## "netrc machine")
- **vimeo:watchlater**: [*vimeo*](## "netrc machine") Vimeo watch later list, ":vimeowatchlater" keyword (requires authentication) - **vimeo:watchlater**: [*vimeo*](## "netrc machine") Vimeo watch later list, ":vimeowatchlater" keyword (requires authentication)
- **Vimm:recording**
- **Vimm:stream**
- **ViMP** - **ViMP**
- **ViMP:Playlist** - **ViMP:Playlist**
- **Viously** - **Viously**
@ -1683,7 +1568,6 @@ The only reliable way to check if a site is supported is to try it.
- **VKPlayLive** - **VKPlayLive**
- **vm.tiktok** - **vm.tiktok**
- **Vocaroo** - **Vocaroo**
- **VODPl**
- **VODPlatform** - **VODPlatform**
- **voicy**: (**Currently broken**) - **voicy**: (**Currently broken**)
- **voicy:channel**: (**Currently broken**) - **voicy:channel**: (**Currently broken**)
@ -1707,9 +1591,6 @@ The only reliable way to check if a site is supported is to try it.
- **VTXTV**: [*vtxtv*](## "netrc machine") - **VTXTV**: [*vtxtv*](## "netrc machine")
- **VTXTVLive**: [*vtxtv*](## "netrc machine") - **VTXTVLive**: [*vtxtv*](## "netrc machine")
- **VTXTVRecordings**: [*vtxtv*](## "netrc machine") - **VTXTVRecordings**: [*vtxtv*](## "netrc machine")
- **VuClip**
- **VVVVID**
- **VVVVIDShow**
- **Walla** - **Walla**
- **WalyTV**: [*walytv*](## "netrc machine") - **WalyTV**: [*walytv*](## "netrc machine")
- **WalyTVLive**: [*walytv*](## "netrc machine") - **WalyTVLive**: [*walytv*](## "netrc machine")
@ -1719,7 +1600,6 @@ The only reliable way to check if a site is supported is to try it.
- **wat.tv** - **wat.tv**
- **WatchESPN** - **WatchESPN**
- **WDR** - **WDR**
- **wdr:mobile**: (**Currently broken**)
- **WDRElefant** - **WDRElefant**
- **WDRPage** - **WDRPage**
- **web.archive:youtube**: web.archive.org saved youtube videos, "ytarchive:" prefix - **web.archive:youtube**: web.archive.org saved youtube videos, "ytarchive:" prefix
@ -1741,7 +1621,6 @@ The only reliable way to check if a site is supported is to try it.
- **WeverseMediaTab**: [*weverse*](## "netrc machine") - **WeverseMediaTab**: [*weverse*](## "netrc machine")
- **WeverseMoment**: [*weverse*](## "netrc machine") - **WeverseMoment**: [*weverse*](## "netrc machine")
- **WeVidi** - **WeVidi**
- **Weyyak**
- **whowatch** - **whowatch**
- **Whyp** - **Whyp**
- **wikimedia.org** - **wikimedia.org**
@ -1778,7 +1657,6 @@ The only reliable way to check if a site is supported is to try it.
- **Xinpianchang**: 新片场 - **Xinpianchang**: 新片场
- **XMinus**: (**Currently broken**) - **XMinus**: (**Currently broken**)
- **XNXX** - **XNXX**
- **Xstream**
- **XVideos** - **XVideos**
- **xvideos:quickies** - **xvideos:quickies**
- **XXXYMovies** - **XXXYMovies**
@ -1837,8 +1715,6 @@ The only reliable way to check if a site is supported is to try it.
- **ZattooRecordings**: [*zattoo*](## "netrc machine") - **ZattooRecordings**: [*zattoo*](## "netrc machine")
- **zdf** - **zdf**
- **zdf:channel** - **zdf:channel**
- **Zee5**: [*zee5*](## "netrc machine")
- **zee5:series**
- **ZeeNews**: (**Currently broken**) - **ZeeNews**: (**Currently broken**)
- **ZenPorn** - **ZenPorn**
- **ZetlandDKArticle** - **ZetlandDKArticle**

View File

@ -1415,6 +1415,51 @@ jwplayer("mediaplayer").setup({"abouttext":"Visit Indie DB","aboutlink":"http:\/
}, },
], ],
}, },
), (
# SCTE 214 supplemental codecs (e.g. Dolby AV1)
# Based on unfragmented.mpd with scte214:supplementalCodecs on VIDEO-1 only
'supplemental_codecs',
'https://v.redd.it/hw1x7rcg7zl21/DASHPlaylist.mpd', # mpd_url
'https://v.redd.it/hw1x7rcg7zl21', # mpd_base_url
[{
'url': 'https://v.redd.it/hw1x7rcg7zl21/audio',
'manifest_url': 'https://v.redd.it/hw1x7rcg7zl21/DASHPlaylist.mpd',
'ext': 'm4a',
'format_id': 'AUDIO-1',
'format_note': 'DASH audio',
'container': 'm4a_dash',
'acodec': 'mp4a.40.2',
'vcodec': 'none',
'tbr': 129.87,
'asr': 48000,
}, {
'url': 'https://v.redd.it/hw1x7rcg7zl21/DASH_240',
'manifest_url': 'https://v.redd.it/hw1x7rcg7zl21/DASHPlaylist.mpd',
'ext': 'mp4',
'format_id': 'VIDEO-2',
'format_note': 'DASH video',
'container': 'mp4_dash',
'acodec': 'none',
'vcodec': 'avc1.4d401e',
'tbr': 608.0,
'width': 240,
'height': 240,
'fps': 30,
}, {
'url': 'https://v.redd.it/hw1x7rcg7zl21/DASH_360',
'manifest_url': 'https://v.redd.it/hw1x7rcg7zl21/DASHPlaylist.mpd',
'ext': 'mp4',
'format_id': 'VIDEO-1',
'format_note': 'DASH video, dav1.10.02',
'container': 'mp4_dash',
'acodec': 'none',
'vcodec': 'avc1.4d401e',
'tbr': 804.261,
'width': 360,
'height': 360,
'fps': 30,
}],
{},
), ),
] ]

View File

@ -6,11 +6,11 @@ import sys
import unittest import unittest
from unittest.mock import patch from unittest.mock import patch
from yt_dlp.globals import all_plugins_loaded
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from yt_dlp.globals import all_plugins_loaded
import contextlib import contextlib
import copy import copy
import json import json

View File

@ -8,8 +8,15 @@ import unittest
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import http.cookiejar import http.cookiejar
import http.server
import ipaddress
import pytest
import json
import tempfile
import threading
from test.helper import FakeYDL from test.helper import FakeYDL
from yt_dlp.networking.common import HTTPHeaderDict
from yt_dlp.downloader.external import ( from yt_dlp.downloader.external import (
Aria2cFD, Aria2cFD,
AxelFD, AxelFD,
@ -75,34 +82,114 @@ class TestWgetFD(unittest.TestCase):
def test_make_cmd(self): def test_make_cmd(self):
with FakeYDL() as ydl: with FakeYDL() as ydl:
downloader = WgetFD(ydl, {}) downloader = WgetFD(ydl, {})
self.assertNotIn('--load-cookies', downloader._make_cmd('test', TEST_INFO))
# Test cookiejar tempfile arg is added
ydl.cookiejar.set_cookie(http.cookiejar.Cookie(**TEST_COOKIE)) ydl.cookiejar.set_cookie(http.cookiejar.Cookie(**TEST_COOKIE))
self.assertIn('--load-cookies', downloader._make_cmd('test', TEST_INFO)) assert '--load-cookies' in downloader._make_cmd('test', TEST_INFO)
class TestCurlFD(unittest.TestCase): class HTTPTestHandler(http.server.BaseHTTPRequestHandler):
def test_make_cmd(self): def do_GET(self, /):
if self.path.startswith('/redirect'):
target = self.headers.get('X-Redirect-Location')
if not target:
self.send_error(500)
return
self.send_response(301)
self.send_header('Location', target)
self.end_headers()
elif self.path == '/headers':
self.send_response(200)
self.end_headers()
self.wfile.write(json.dumps(list(self.headers.items())).encode())
class HTTPTestServer(http.server.HTTPServer):
@property
def address(self, /):
return ipaddress.ip_address(self.server_address[0])
@property
def uri(self, /):
addr, port, *_ = self.server_address
if ':' in addr:
addr = f'[{addr}]'
return f'http://{addr}:{port}'
def __enter__(self, /):
result = super().__enter__()
thread = threading.Thread(target=self.serve_forever)
thread.start()
return result
def __exit__(self, /, *exc):
self.shutdown()
return super().__exit__(*exc)
class TestDownloaderCookieBehavior:
@pytest.mark.parametrize('downloader_cls', [
pytest.param(CurlFD, marks=pytest.mark.skipif(not CurlFD.available() or CurlFD._curl_version < CurlFD._MIN_VERSION_FOR_STDIN_COOKIES, reason='curl unavailable or too old')),
pytest.param(WgetFD, marks=pytest.mark.skipif(not WgetFD.available(), reason='wget unavailable')),
pytest.param(Aria2cFD, marks=pytest.mark.skipif(not Aria2cFD.available(), reason='aria2c unavailable')),
])
def test_cookie_behavior(self, /, downloader_cls):
with FakeYDL() as ydl: with FakeYDL() as ydl:
downloader = CurlFD(ydl, {}) downloader = downloader_cls(ydl, {})
self.assertNotIn('--cookie', downloader._make_cmd('test', TEST_INFO))
# Test cookie header is added with HTTPTestServer(('localhost', 0), HTTPTestHandler) as server_a:
ydl.cookiejar.set_cookie(http.cookiejar.Cookie(**TEST_COOKIE)) second_addr = server_a.address + 1
self.assertIn('--cookie', downloader._make_cmd('test', TEST_INFO)) if not second_addr.is_loopback:
self.assertIn('test=ytdlp', downloader._make_cmd('test', TEST_INFO)) second_addr = server_a.address - 1
assert second_addr.is_loopback, f'failed to find derived loopback address for {server_a.address}'
ydl.cookiejar.set_cookie(http.cookiejar.Cookie(
1,
'c',
'test',
server_a.server_address[1],
True,
str(server_a.address),
True,
False,
'/',
False,
False,
0,
True,
None,
None,
{},
))
with tempfile.NamedTemporaryFile(delete=False) as file:
file.close()
assert downloader.real_download(file.name, {'url': f'{server_a.uri}/headers'}), 'Expected download (/headers) to succeed'
with open(file.name, 'rb') as f:
data = HTTPHeaderDict(json.load(f))
assert 'c=test' in data.get('Cookie', '').split(';'), 'Expected cookie to be set in initial request'
with HTTPTestServer((str(second_addr), 0), HTTPTestHandler) as server_b:
assert downloader.real_download(file.name, {
'url': f'{server_a.uri}/redirect',
'http_headers': {
'X-Redirect-Location': f'{server_b.uri}/headers',
},
}), 'Expected download (/redirect) to succeed'
with open(file.name, 'rb') as f:
data = HTTPHeaderDict(json.load(f))
assert data.get('Cookie') is None, 'Expected cookie to be unset in redirected request'
class TestAria2cFD(unittest.TestCase): class TestAria2cFD(unittest.TestCase):
def test_make_cmd(self): def test_make_cmd(self):
with FakeYDL() as ydl: with FakeYDL() as ydl:
downloader = Aria2cFD(ydl, {}) downloader = Aria2cFD(ydl, {})
downloader._make_cmd('test', TEST_INFO)
self.assertFalse(hasattr(downloader, '_cookies_tempfile'))
# Test cookiejar tempfile arg is added
ydl.cookiejar.set_cookie(http.cookiejar.Cookie(**TEST_COOKIE)) ydl.cookiejar.set_cookie(http.cookiejar.Cookie(**TEST_COOKIE))
cmd = downloader._make_cmd('test', TEST_INFO) cmd = downloader._make_cmd('test', TEST_INFO)
self.assertIn(f'--load-cookies={downloader._cookies_tempfile}', cmd) assert f'--load-cookies={downloader._cookies_tempfile}' in cmd
@unittest.skipUnless(FFmpegFD.available(), 'ffmpeg not found') @unittest.skipUnless(FFmpegFD.available(), 'ffmpeg not found')

View File

@ -20,7 +20,12 @@ LAZY_EXTRACTORS = 'yt_dlp/extractor/lazy_extractors.py'
class TestExecution(unittest.TestCase): class TestExecution(unittest.TestCase):
def run_yt_dlp(self, exe=(sys.executable, 'yt_dlp/__main__.py'), opts=('--version', )): def run_yt_dlp(self, exe=(sys.executable, 'yt_dlp/__main__.py'), opts=('--version', )):
stdout, stderr, returncode = Popen.run( stdout, stderr, returncode = Popen.run(
[*exe, '--ignore-config', *opts], cwd=rootDir, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) [*exe, '--no-update', '--ignore-config', *opts],
cwd=rootDir,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
print(stderr, file=sys.stderr) print(stderr, file=sys.stderr)
self.assertEqual(returncode, 0) self.assertEqual(returncode, 0)
return stdout.strip(), stderr.strip() return stdout.strip(), stderr.strip()

View File

@ -11,7 +11,10 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import subprocess import subprocess
from yt_dlp import YoutubeDL from yt_dlp import YoutubeDL
from yt_dlp.utils import shell_quote from yt_dlp.utils import (
UnsafeExecExpansionError,
shell_quote,
)
from yt_dlp.postprocessor import ( from yt_dlp.postprocessor import (
ExecPP, ExecPP,
FFmpegThumbnailsConvertorPP, FFmpegThumbnailsConvertorPP,
@ -91,6 +94,51 @@ class TestExec(unittest.TestCase):
self.assertEqual(pp.parse_cmd('echo {}', info), cmd) self.assertEqual(pp.parse_cmd('echo {}', info), cmd)
self.assertEqual(pp.parse_cmd('echo %(filepath)q', info), cmd) self.assertEqual(pp.parse_cmd('echo %(filepath)q', info), cmd)
def test_unsafe_exec_expansion(self):
# Test unsafe placeholder
ydl = YoutubeDL({'outtmpl_na_placeholder': ';'})
self.assertRaisesRegex(UnsafeExecExpansionError, r'Unsafe placeholder', ExecPP, ydl, 'echo %(title)q')
ydl = YoutubeDL()
# Test unsafe commands
self.assertRaisesRegex(UnsafeExecExpansionError, r'Unsafe conversion', ExecPP, ydl, 'echo "%(title)s"')
self.assertRaisesRegex(UnsafeExecExpansionError, r'Unsafe conversion', ExecPP, ydl, 'echo "%(title).100B"')
self.assertRaisesRegex(UnsafeExecExpansionError, r'Unsafe conversion', ExecPP, ydl, 'echo "%(title)S"')
self.assertRaisesRegex(UnsafeExecExpansionError, r'Unsafe conversion', ExecPP, ydl, 'echo "%(title)#j"')
self.assertRaisesRegex(UnsafeExecExpansionError, r'Unsafe default', ExecPP, ydl, 'echo %(title|;)q')
# Test safe commands
self.assertIsInstance(
ExecPP(ydl, [
'echo',
'echo {}',
'echo %(title)q',
'echo %(title|NA)q',
'echo %(title)#q',
'echo %(view_count)i',
'echo %(view_count)02d',
'echo %(aspect_ratio)f',
'echo %(aspect_ratio).2f',
]),
ExecPP)
# Test compat opt
ydl = YoutubeDL({
'outtmpl_na_placeholder': ';',
'compat_opts': {'allow-unsafe-exec-expansion'},
})
self.assertIsInstance(
ExecPP(ydl, [
'echo "%(title)s"',
'echo %(title)q',
'echo "%(title).100B"',
'echo "%(title)S"',
'echo "%(title)#S"',
'echo "%(title)j"',
'echo "%(title)#j"',
'echo %(title|;)q',
]),
ExecPP)
class TestModifyChaptersPP(unittest.TestCase): class TestModifyChaptersPP(unittest.TestCase):
def setUp(self): def setUp(self):

View File

@ -16,7 +16,6 @@ from yt_dlp.extractor import (
CeskaTelevizeIE, CeskaTelevizeIE,
DailymotionIE, DailymotionIE,
DemocracynowIE, DemocracynowIE,
LyndaIE,
RaiPlayIE, RaiPlayIE,
RTVEALaCartaIE, RTVEALaCartaIE,
TedTalkIE, TedTalkIE,
@ -250,20 +249,6 @@ class TestCeskaTelevizeSubtitles(BaseTestSubtitles):
self.assertFalse(subtitles) self.assertFalse(subtitles)
@is_download_test
@unittest.skip('IE broken')
class TestLyndaSubtitles(BaseTestSubtitles):
url = 'http://www.lynda.com/Bootstrap-tutorials/Using-exercise-files/110885/114408-4.html'
IE = LyndaIE
def test_allsubtitles(self):
self.DL.params['writesubtitles'] = True
self.DL.params['allsubtitles'] = True
subtitles = self.getSubtitles()
self.assertEqual(set(subtitles.keys()), {'en'})
self.assertEqual(md5(subtitles['en']), '09bbe67222259bed60deaa26997d73a7')
@is_download_test @is_download_test
@unittest.skip('IE broken') @unittest.skip('IE broken')
class TestNPOSubtitles(BaseTestSubtitles): class TestNPOSubtitles(BaseTestSubtitles):

View File

@ -327,6 +327,12 @@ class TestUtil(unittest.TestCase):
with self.assertRaises(_UnsafeExtensionError): with self.assertRaises(_UnsafeExtensionError):
prepend_extension('abc.unexpected_ext', ext, 'ext') prepend_extension('abc.unexpected_ext', ext, 'ext')
# Test allow-unsafe-ext compat option
_UnsafeExtensionError._enabled = False
self.assertEqual(prepend_extension('abc.ext', 'un/safe'), 'abc.un/safe.ext')
# Re-enable sanitization for other tests
_UnsafeExtensionError._enabled = True
def test_replace_extension(self): def test_replace_extension(self):
self.assertEqual(replace_extension('abc.ext', 'temp'), 'abc.temp') self.assertEqual(replace_extension('abc.ext', 'temp'), 'abc.temp')
self.assertEqual(replace_extension('abc.ext', 'temp', 'ext'), 'abc.temp') self.assertEqual(replace_extension('abc.ext', 'temp', 'ext'), 'abc.temp')
@ -345,6 +351,12 @@ class TestUtil(unittest.TestCase):
with self.assertRaises(_UnsafeExtensionError): with self.assertRaises(_UnsafeExtensionError):
replace_extension('abc.unexpected_ext', ext, 'ext') replace_extension('abc.unexpected_ext', ext, 'ext')
# Test allow-unsafe-ext compat option
_UnsafeExtensionError._enabled = False
self.assertEqual(replace_extension('abc.ext', 'bin'), 'abc.bin')
# Re-enable sanitization for other tests
_UnsafeExtensionError._enabled = True
def test_subtitles_filename(self): def test_subtitles_filename(self):
self.assertEqual(subtitles_filename('abc.ext', 'en', 'vtt'), 'abc.en.vtt') self.assertEqual(subtitles_filename('abc.ext', 'en', 'vtt'), 'abc.en.vtt')
self.assertEqual(subtitles_filename('abc.ext', 'en', 'vtt', 'ext'), 'abc.en.vtt') self.assertEqual(subtitles_filename('abc.ext', 'en', 'vtt', 'ext'), 'abc.en.vtt')
@ -2160,6 +2172,10 @@ Line 1
headers6 = HTTPHeaderDict(a=1, b=2) headers6 = HTTPHeaderDict(a=1, b=2)
self.assertEqual(pickle.loads(pickle.dumps(headers6)), headers6) self.assertEqual(pickle.loads(pickle.dumps(headers6)), headers6)
headers7 = HTTPHeaderDict()
headers7 |= {'X-dlp': 'data'}
self.assertEqual(headers7.sensitive(), {'X-dlp': 'data'})
def test_extract_basic_auth(self): def test_extract_basic_auth(self):
assert extract_basic_auth('http://:foo.bar') == ('http://:foo.bar', None) assert extract_basic_auth('http://:foo.bar') == ('http://:foo.bar', None)
assert extract_basic_auth('http://foo.bar') == ('http://foo.bar', None) assert extract_basic_auth('http://foo.bar') == ('http://foo.bar', None)

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<MPD mediaPresentationDuration="PT54.915S" minBufferTime="PT1.500S" profiles="urn:mpeg:dash:profile:isoff-on-demand:2011" type="static" xmlns="urn:mpeg:dash:schema:mpd:2011" xmlns:scte214="urn:scte:dash:scte214-extensions">
<Period duration="PT54.915S">
<AdaptationSet segmentAlignment="true" subsegmentAlignment="true" subsegmentStartsWithSAP="1">
<Representation bandwidth="804261" codecs="avc1.4d401e" frameRate="30" height="360" id="VIDEO-1" mimeType="video/mp4" scte214:supplementalCodecs="dav1.10.02" startWithSAP="1" width="360">
<BaseURL>DASH_360</BaseURL>
<SegmentBase indexRange="915-1114" indexRangeExact="true">
<Initialization range="0-914"/>
</SegmentBase>
</Representation>
<Representation bandwidth="608000" codecs="avc1.4d401e" frameRate="30" height="240" id="VIDEO-2" mimeType="video/mp4" startWithSAP="1" width="240">
<BaseURL>DASH_240</BaseURL>
<SegmentBase indexRange="913-1112" indexRangeExact="true">
<Initialization range="0-912"/>
</SegmentBase>
</Representation>
</AdaptationSet>
<AdaptationSet>
<Representation audioSamplingRate="48000" bandwidth="129870" codecs="mp4a.40.2" id="AUDIO-1" mimeType="audio/mp4" startWithSAP="1">
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/>
<BaseURL>audio</BaseURL>
<SegmentBase indexRange="832-1007" indexRangeExact="true">
<Initialization range="0-831"/>
</SegmentBase>
</Representation>
</AdaptationSet>
</Period>
</MPD>

154
uv.lock generated
View File

@ -137,11 +137,11 @@ wheels = [
[[package]] [[package]]
name = "certifi" name = "certifi"
version = "2026.4.22" version = "2026.5.20"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" },
] ]
[[package]] [[package]]
@ -458,15 +458,15 @@ wheels = [
[[package]] [[package]]
name = "deno" name = "deno"
version = "2.7.14" version = "2.8.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/30/e4/adbeb37143551a4e575049e8b291bedc5f57191753ade31f7cf6665b1d52/deno-2.7.14.tar.gz", hash = "sha256:63a79caa105368813e434593f2c059d4cc3e2562ec57f606348e9321a525d582", size = 8168, upload-time = "2026-04-28T17:34:10.315Z" } sdist = { url = "https://files.pythonhosted.org/packages/43/eb/b743a520cdd668e070a4535296123f1c62d054b00699f58e49c32ab5925c/deno-2.8.1.tar.gz", hash = "sha256:fb65e568bef30b1a7e63f033713f1a6792a8456e339febdb7d638c6bb2c4c008", size = 8167, upload-time = "2026-05-27T13:01:06.508Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/a1/82951e37de914ca9445e3dd99a5f06e36e857000f9d34beb3f9fc5ff0971/deno-2.7.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3860efe76d9c8491ee847256f31ff5f1b1d1871137daab91eb4f28c0543eeb4d", size = 47841490, upload-time = "2026-04-28T17:33:53.968Z" }, { url = "https://files.pythonhosted.org/packages/d1/71/f9dc8ad874dcc26cf1a154c8f89d77e1155ced1f6a64be9d21127bd555ce/deno-2.8.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:733e24e4883c9516534cae7a10277048ffea7cc034f9f726e8c145d48ba75d19", size = 42749443, upload-time = "2026-05-27T13:00:49.439Z" },
{ url = "https://files.pythonhosted.org/packages/fe/1f/809cac7474d86ad41f2938b4713cf846b261bba7a1fdb5ab27b3fabc435e/deno-2.7.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:194560abc29021d3b5b4be9bfd628f2b21152a3bae031bdcd30e41584ba4b9d8", size = 44600497, upload-time = "2026-04-28T17:33:57.426Z" }, { url = "https://files.pythonhosted.org/packages/f5/db/1721aca1a9bd132a3f721bd547022534fcdd6221701561c5e33705cdfb6d/deno-2.8.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7b4638504f3730f9b25229db08d1bce87bc64f52498f9f8f5aeba702c7ff2115", size = 39460813, upload-time = "2026-05-27T13:00:52.578Z" },
{ url = "https://files.pythonhosted.org/packages/52/51/c60816c902e5fa9daf4bab5869a5895e58748f05739eab399c1ae6d0ee37/deno-2.7.14-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:f47511d0c8080c2bdb0ee58f2b96d657b1e5f2ede088b1bb9a98bfb80362af22", size = 48267415, upload-time = "2026-04-28T17:34:00.553Z" }, { url = "https://files.pythonhosted.org/packages/1a/20/b50646f865562b8f21532cad6f9804b126efd4030cfd0c5e1d11217b15ca/deno-2.8.1-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:91f29a2df4cb6135872d68f38005c08fe0c389c84cb7349d7399eedbfbe19829", size = 43893026, upload-time = "2026-05-27T13:00:55.939Z" },
{ url = "https://files.pythonhosted.org/packages/79/a8/ea52407ebe29caadc67b6899437898880aad8d207ed889dd4f9ba7d9ddf7/deno-2.7.14-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0a92040900311849a812d47e29e1494b1a4f4c7431536721888356c7a3662d22", size = 50283833, upload-time = "2026-04-28T17:34:04.094Z" }, { url = "https://files.pythonhosted.org/packages/0d/1e/75b84e7096b53f077ac01f9b5c979b7b42b0a6497f56d7c4ae072381e059/deno-2.8.1-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:f0958910ffa88f6b6e142129ab34b7a3aec361fc63904ff803ce1594beca230a", size = 46038250, upload-time = "2026-05-27T13:00:59.662Z" },
{ url = "https://files.pythonhosted.org/packages/98/0e/2581a39773004081f40ef4811059fe867953d86166b3f7fb77abb5d68757/deno-2.7.14-py3-none-win_amd64.whl", hash = "sha256:1646dbcd447465bc6eb9a9e25eef0283f74dd68ad5bd9c50bcaec641737eb730", size = 49278963, upload-time = "2026-04-28T17:34:07.671Z" }, { url = "https://files.pythonhosted.org/packages/14/d6/14f6cf25025644bb8b7d9b4606780b5ec6ee429a55c0d1f05681718c7fc0/deno-2.8.1-py3-none-win_amd64.whl", hash = "sha256:71ec55c0a0944beee376aa824722734cf3e617661bca5e143caa83991921e4f5", size = 41962337, upload-time = "2026-05-27T13:01:03.453Z" },
] ]
[[package]] [[package]]
@ -526,11 +526,11 @@ wheels = [
[[package]] [[package]]
name = "idna" name = "idna"
version = "3.15" version = "3.17"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } sdist = { url = "https://files.pythonhosted.org/packages/b9/28/99c51f664567218d824af024c0251650fb27e4ca066df188dab0769c5b91/idna-3.17.tar.gz", hash = "sha256:5eb0cb53bc467c12eadcf6de83163ad8527cec9416f44b9b61b19caedad2b87f", size = 196048, upload-time = "2026-05-28T14:32:38.55Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, { url = "https://files.pythonhosted.org/packages/de/a7/f76514cc40ad6234098ecdebda08732d75964776c51a42845b7da10649e2/idna-3.17-py3-none-any.whl", hash = "sha256:466e48829084efe2548012b855df21540b96f2e20e51bd124c851536556a592c", size = 65316, upload-time = "2026-05-28T14:32:37.035Z" },
] ]
[[package]] [[package]]
@ -643,20 +643,20 @@ wheels = [
[[package]] [[package]]
name = "pip" name = "pip"
version = "26.1.1" version = "26.1.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b6/48/cb9b7a682f6fe01a4221e1728941dd4ac3cd9090a17db3779d6ff490b602/pip-26.1.1.tar.gz", hash = "sha256:d36762751d156a4ee895de8af39aa0abeeeb577f93a2eca6ab62467bbf0f8a78", size = 1840400, upload-time = "2026-05-04T19:02:21.248Z" } sdist = { url = "https://files.pythonhosted.org/packages/01/91/47e7d486260f618783899587af63ccf7980fb60245c3e63dd4571c6b57ad/pip-26.1.2.tar.gz", hash = "sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605", size = 1840799, upload-time = "2026-05-31T17:33:58.56Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl", hash = "sha256:99cb1c2899893b075ff56e4ed0af55669a955b49ad7fb8d8603ecdaf4ed653fb", size = 1812777, upload-time = "2026-05-04T19:02:18.9Z" }, { url = "https://files.pythonhosted.org/packages/5d/95/6b5cb3461ea5673ba0995989746db58eb18b91b54dbf331e72f569540946/pip-26.1.2-py3-none-any.whl", hash = "sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab", size = 1813144, upload-time = "2026-05-31T17:33:56.772Z" },
] ]
[[package]] [[package]]
name = "platformdirs" name = "platformdirs"
version = "4.9.6" version = "4.10.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" },
] ]
[[package]] [[package]]
@ -825,28 +825,28 @@ wheels = [
[[package]] [[package]]
name = "pytest-rerunfailures" name = "pytest-rerunfailures"
version = "16.2" version = "16.3"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "packaging" }, { name = "packaging" },
{ name = "pytest" }, { name = "pytest" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/6b/27/fd0209642f3a1069da3e0be3c7e339f942d052d81ccb1fb4eb9b187d3633/pytest_rerunfailures-16.2.tar.gz", hash = "sha256:5f5a32f15674a3d54f7598388fcd3cc1bc5c37284731a4704a44485dcdda5e23", size = 32121, upload-time = "2026-05-13T08:13:26.998Z" } sdist = { url = "https://files.pythonhosted.org/packages/4d/f0/74f8e685be7ecd1572c1256132f18fce3a665d7e07649a3f23b7eb2d3bec/pytest_rerunfailures-16.3.tar.gz", hash = "sha256:37c9b1231c8083e9f4e724f50f7a21241822f9516c15c700ebbf218d6452355c", size = 34148, upload-time = "2026-05-22T06:51:22.292Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/3f/a5/d8c1ad74529b483044b787ead2d24ecc624bca4084a509002102e4bab8cc/pytest_rerunfailures-16.2-py3-none-any.whl", hash = "sha256:c22a53d2827becc76f057d4ded123c0e726523f2f0e5f0bb4efb31fd59e1f14e", size = 14505, upload-time = "2026-05-13T08:13:25.485Z" }, { url = "https://files.pythonhosted.org/packages/f8/98/58a71d68d3126d7f6a6ed1944c37ec207a4ff3dc66cad3bed7b59d38df61/pytest_rerunfailures-16.3-py3-none-any.whl", hash = "sha256:6bdfb8ffb46c46072e6c16bdedee38b6c13eac620d9415ed5b63152cbf283170", size = 15396, upload-time = "2026-05-22T06:51:20.547Z" },
] ]
[[package]] [[package]]
name = "python-discovery" name = "python-discovery"
version = "1.3.1" version = "1.4.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "filelock" }, { name = "filelock" },
{ name = "platformdirs" }, { name = "platformdirs" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/48/60/e88788207d81e46362cfbef0d4aaf4c0f49efc3c12d4c3fa3f542c34ebec/python_discovery-1.3.1.tar.gz", hash = "sha256:62f6db28064c9613e7ca76cb3f00c38c839a07c31c00dfe7ed0986493d2150a6", size = 68011, upload-time = "2026-05-12T20:53:36.336Z" } sdist = { url = "https://files.pythonhosted.org/packages/a6/12/38c1a0b1e64806780c9563e3fc9f6e472251839662587cfbe9bfaf2ae10a/python_discovery-1.4.0.tar.gz", hash = "sha256:eb8bc7daad3c226c147e45bb4e970a1feb1bf4048ee178e6db59e197b8010ce3", size = 68455, upload-time = "2026-05-28T01:15:37.639Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/6f/a05a317a66fee0aad270011461f1a63a453ed12471249f172f7d2e2bc7b4/python_discovery-1.3.1-py3-none-any.whl", hash = "sha256:ed188687ebb3b82c01a17cd5ac62fc94d9f6487a7f1a0f9dfe89753fec91039c", size = 33185, upload-time = "2026-05-12T20:53:34.969Z" }, { url = "https://files.pythonhosted.org/packages/c8/8d/3d316429f65029532bb1e28ff77b797d86b5ac3915bb44ca4e19aa283d43/python_discovery-1.4.0-py3-none-any.whl", hash = "sha256:26ed78d703e234879a66244c7d4114563fb13ec5cd30a2d1357e5fb4850782da", size = 33217, upload-time = "2026-05-28T01:15:36.573Z" },
] ]
[[package]] [[package]]
@ -952,27 +952,27 @@ wheels = [
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.15.13" version = "0.15.15"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/24/21/a7d5c126d5b557715ef81098f3db2fe20f622a039ff2e626af28d674ab80/ruff-0.15.13.tar.gz", hash = "sha256:f9d89f17f7ba7fb2ed42921f0df75da797a9a5d71bc39049e2c687cf2baf44b7", size = 4678180, upload-time = "2026-05-14T13:44:37.869Z" } sdist = { url = "https://files.pythonhosted.org/packages/84/6f/a76f7d96e5c962f5b69cee865e49c15c1116897c01990faa8a57edb62e7f/ruff-0.15.15.tar.gz", hash = "sha256:b8dff018130b46d8e5bf0f926ef6b60cf871d6d5ae45fc9334e09632daa741d6", size = 4706985, upload-time = "2026-05-28T14:16:57.784Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/c6/61/11d458dc6ac22504fd8e237b29dfd40504c7fbbcc8930402cfe51a8e63ed/ruff-0.15.13-py3-none-linux_armv6l.whl", hash = "sha256:444b580fc72fd6887e650acd3e575e18cdc79dbcf42fb4030b491057921f61f8", size = 10738279, upload-time = "2026-05-14T13:44:18.7Z" }, { url = "https://files.pythonhosted.org/packages/fa/9d/3a45c05b8ab04b4705989de70a79008e27c8003296a0feaee9edc18dd7e9/ruff-0.15.15-py3-none-linux_armv6l.whl", hash = "sha256:cf93e5388f412e1b108b1f8b34a6e036b70fe8aff89393befad96fe48670311b", size = 10710652, upload-time = "2026-05-28T14:16:06.701Z" },
{ url = "https://files.pythonhosted.org/packages/86/ca/caa871ee7be718c45256fada4e16a218ee3e33f0c4a46b729a60a24912e6/ruff-0.15.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6590d009e7cb7ebf36f83dbdd44a3fa48a0994ff6f1cdc1b08006abe58f98dc7", size = 11124798, upload-time = "2026-05-14T13:44:06.427Z" }, { url = "https://files.pythonhosted.org/packages/05/66/da974431624bf3b49f6ee1f9543c02d929ff1cba78b0d5a79c38cf21f744/ruff-0.15.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac5a646d1f6a7dadd5d50842dae2c1f9862ac887ef5d1b1375e02def791fde6e", size = 11096615, upload-time = "2026-05-28T14:16:23.313Z" },
{ url = "https://files.pythonhosted.org/packages/d3/19/43f5f2e568dddde567fc41f8471f9432c09563e19d3e617a48cfa52f8f0a/ruff-0.15.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1c26d2f66163deeb6e08d8b39fbbe983ce3c71cea06a6d7591cfd1421793c629", size = 10460761, upload-time = "2026-05-14T13:44:04.375Z" }, { url = "https://files.pythonhosted.org/packages/8c/09/7443452e5d290230a712103f2fdceeef7184f3ec99a2bd01c8be78aaceb5/ruff-0.15.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:77d955a431430c66f72dd94e379ad38a16daea3d25094872ac4edf9e797be530", size = 10436683, upload-time = "2026-05-28T14:16:40.974Z" },
{ url = "https://files.pythonhosted.org/packages/99/df/cf938cd6de3003178f03ad7c1ea2a6c099468c03a35037985070b37e76be/ruff-0.15.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbd6f94b434f896308e4d57fb7bfde0d02b99f7a64b3bdab0fdfa6a864203a5", size = 10804451, upload-time = "2026-05-14T13:44:25.221Z" }, { url = "https://files.pythonhosted.org/packages/53/01/d330c26a57fa4f3943a14424904027428315b700fe4d14a84bb123a649e5/ruff-0.15.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7614ee79c69788cf6cedd568069ade9cecc22a1ad20494efe8d0c9ebb4b622d4", size = 10769064, upload-time = "2026-05-28T14:16:28.905Z" },
{ url = "https://files.pythonhosted.org/packages/c7/7d/5d0973129b154ded2225729169d7068f26b467760b146493fde138415f23/ruff-0.15.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3259f3be4d181bda591da5db2571aed6853c6a048157756448020bc6c5cd22", size = 10534285, upload-time = "2026-05-14T13:44:08.888Z" }, { url = "https://files.pythonhosted.org/packages/1d/85/cc8770f8bdff541b1da8392d1634141fe4a0e3f4ee596605959b7906c27f/ruff-0.15.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3cdb1679e06a1f6b47bc384714ae96f6e2fb65ca441eb78c43d2ca554176ce1f", size = 10511987, upload-time = "2026-05-28T14:16:43.732Z" },
{ url = "https://files.pythonhosted.org/packages/1f/e3/6b999bbc66cd51e5f073842bc2a3995e99c5e0e72e16b15e7261f7abf57a/ruff-0.15.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae9c17e5eb4430c154e76abc25d79a318190f5a997f38fb6b114416c5319ffc9", size = 11312063, upload-time = "2026-05-14T13:44:11.274Z" }, { url = "https://files.pythonhosted.org/packages/7c/29/8c190c1472b63013583ba391f3342036e02010544c1270455ed8e519bdf3/ruff-0.15.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2728b93d7b23a603ea2c0ac6eb73d760bd38ec9de35f35fb41e18f7a3fee7622", size = 11275100, upload-time = "2026-05-28T14:16:55.244Z" },
{ url = "https://files.pythonhosted.org/packages/af/5a/642639e9f5db04f1e97fbd6e091c6fd20725bdf072fb114d00eefb9e6eb8/ruff-0.15.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e2e39bff6c341f4b577a21b801326fab0b11847f48fcaa83f00a113c9b3cb55", size = 12183079, upload-time = "2026-05-14T13:44:01.634Z" }, { url = "https://files.pythonhosted.org/packages/9f/6b/7e145ce2cc8e63d6834eca03d83a0e18d121def5c69f91b4cf4011ed4879/ruff-0.15.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be582fcc0db438902c7792b08d6ddf6c9b9e21addaa10092c2c741cfb09e5a45", size = 12176903, upload-time = "2026-05-28T14:16:14.368Z" },
{ url = "https://files.pythonhosted.org/packages/19/4c/7585735f6b53b0f12de13618b2f7d250a844f018822efc899df2e7b8295f/ruff-0.15.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e8d9a8e08013542e94d3220bc5b62cc3e5ef87c5f74bff367d3fac14fab013e6", size = 11440833, upload-time = "2026-05-14T13:43:59.043Z" }, { url = "https://files.pythonhosted.org/packages/80/a3/d5974637f68e451f7fadf015cf3101d1cd7d8ba5027cffe0b9e3826ebe6b/ruff-0.15.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7aa77465b8ecaf1a27bea098d696f7fed5e1eccbd10b321b682d6de586ae5627", size = 11404550, upload-time = "2026-05-28T14:16:20.138Z" },
{ url = "https://files.pythonhosted.org/packages/e8/31/bf1a0803d077e679cfeee5f2f67290a0fa79c7385b5d9a8c17b9db2c48f0/ruff-0.15.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc411dfebe5eebe55ce041c6ae080eb7668955e866daa2fbb16692a784f1c4ca", size = 11434486, upload-time = "2026-05-14T13:44:27.761Z" }, { url = "https://files.pythonhosted.org/packages/fe/1c/e6e5e568f22be4fb05d6244234aba384c06b451252453b821e1a529263cf/ruff-0.15.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48decfa11d740de4889de623be1463308346312f2409a56e24aa280c86162dc4", size = 11382027, upload-time = "2026-05-28T14:16:46.615Z" },
{ url = "https://files.pythonhosted.org/packages/e1/4e/62c9b999875d4f14db80f277c030578f5e249c9852d65b7ac7ad0b43c041/ruff-0.15.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:768494eb08b9cee54e2fd27969966f74db5a57f6eaa7a90fcb3306af34dfc4bd", size = 11385189, upload-time = "2026-05-14T13:44:13.704Z" }, { url = "https://files.pythonhosted.org/packages/1d/01/170921b49fcd2e8858825593f91cf7146c3e40a5c3e6df763e4bb0484dde/ruff-0.15.15-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a5015088452ca0081387063649ec67f06d3d1d6b8b936a1f836b5e9657ecd48c", size = 11366041, upload-time = "2026-05-28T14:16:26.247Z" },
{ url = "https://files.pythonhosted.org/packages/fc/89/7e959047a104df3eb12863447c110140191fc5b6c4f379ea2e803fcdb0e4/ruff-0.15.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fb75f9a3a7e42ffe117d734494e6c5e5cb3565d66e12612cb63d0e572a41a5b6", size = 10781380, upload-time = "2026-05-14T13:43:56.734Z" }, { url = "https://files.pythonhosted.org/packages/87/54/a7bad711d7de93254e15e06a4c375b89a03d18de45d3e5dcc86a4472fb1a/ruff-0.15.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5294aab6356c81600fcdea3a62bb1b924dfd5e91767c12318d3f68f86af57cd", size = 10741795, upload-time = "2026-05-28T14:16:17.11Z" },
{ url = "https://files.pythonhosted.org/packages/ff/52/5fd18f3b88cab63e88aa11516b3b4e1e5f720e5c330f8dbe5c26210f41f8/ruff-0.15.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8cb74dd33bb2f6613faf7fc03b660053b5ac4f80e706d5788c6335e2a8048d51", size = 10540605, upload-time = "2026-05-14T13:44:20.748Z" }, { url = "https://files.pythonhosted.org/packages/c9/31/38c075963668f8b41c6914ee0f6f318727fbe30ab9145cb29e6df464c5fa/ruff-0.15.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:db5bd4d802415cca656dc1616070b725952d6ae95eb5d4831e49fbd94a38f75f", size = 10511117, upload-time = "2026-05-28T14:16:31.767Z" },
{ url = "https://files.pythonhosted.org/packages/e8/e0/9e35f338990d3e41a82875ff7053ffe97541dae81c9d02143177f381d572/ruff-0.15.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7ef823f817fcd191dc934e984be9cf4094f808effa16f2542ad8e821ba02bbf2", size = 11036554, upload-time = "2026-05-14T13:44:16.256Z" }, { url = "https://files.pythonhosted.org/packages/9d/96/6ff689e1f7e375d1d97075eca022f74c2bab59554a432fe4d2e6f091986a/ruff-0.15.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:587a6278ed42059191c1a466e490bd7930fb50bd2e255398bc29616c895a61cb", size = 10994867, upload-time = "2026-05-28T14:16:35.149Z" },
{ url = "https://files.pythonhosted.org/packages/c2/13/070fb048c24080fba188f66371e2a92785be257ad02242066dc7255ac6e9/ruff-0.15.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f345a13937bd7f09f6f5d19fa0721b0c103e00e7f62bc67089a8e5e037719e0b", size = 11528133, upload-time = "2026-05-14T13:44:22.808Z" }, { url = "https://files.pythonhosted.org/packages/c3/c2/5dce0ab9f92a8d534fa62b9bf9caca3eddb8c1a81b616f5e195ada4f0d6e/ruff-0.15.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:df0c1c084f5f4be9812f61518a45c440d3c30d69ce4bf6c5270e66d38338f02a", size = 11482101, upload-time = "2026-05-28T14:16:49.598Z" },
{ url = "https://files.pythonhosted.org/packages/6b/8c/b1e1666aef7fc6555094d73ae6cd981701781ae85b97ceefc0eebd0b4668/ruff-0.15.13-py3-none-win32.whl", hash = "sha256:4044f94208b3b05ba0fc4a4abd0558cf4d6459bd18325eead7fd8cc66f909b41", size = 10721455, upload-time = "2026-05-14T13:44:35.697Z" }, { url = "https://files.pythonhosted.org/packages/b1/c0/1003b60edd697c649faf61f1a34094b1abb38fb3d1181e3f895781250a08/ruff-0.15.15-py3-none-win32.whl", hash = "sha256:29428ea79694afbe756d45fd59b36f22b6b020dc0443cf7de0173046236964b9", size = 10716774, upload-time = "2026-05-28T14:16:52.337Z" },
{ url = "https://files.pythonhosted.org/packages/ab/a6/870a3e8a50590bb92be184ad928c2922f088b00d9dc5c5ec7b924ee08c22/ruff-0.15.13-py3-none-win_amd64.whl", hash = "sha256:7064884d442b7d477b4e7473d12da7f08851d2b1982763c5d3f388a19468a1a4", size = 11900409, upload-time = "2026-05-14T13:44:30.389Z" }, { url = "https://files.pythonhosted.org/packages/02/a8/1269eddd6945a06c23f055ef7848886e37cf9d6a8bebb386a3115f01470c/ruff-0.15.15-py3-none-win_amd64.whl", hash = "sha256:8df0323902e15e24bc4bf246da830573d3cf3352bd0b9a164eab335d111ff4a4", size = 11868463, upload-time = "2026-05-28T14:16:11.333Z" },
{ url = "https://files.pythonhosted.org/packages/9b/36/9c015cd052fca743dae8cb2aeb16b551444787467db42ceab0fc968865af/ruff-0.15.13-py3-none-win_arm64.whl", hash = "sha256:2471da9bd1068c8c064b5fd9c0c4b6dddffd6369cb1cd68b29993b1709ff1b21", size = 11179336, upload-time = "2026-05-14T13:44:33.026Z" }, { url = "https://files.pythonhosted.org/packages/4e/b2/920464c907b191e37469d477a1aa8bc048b8f36c4c1610dfa4ab87b39e18/ruff-0.15.15-py3-none-win_arm64.whl", hash = "sha256:3c8ceca6792f38196b8f589bc92eccd03eef286602da92e5dc05cc42ef6441b7", size = 11138498, upload-time = "2026-05-28T14:16:38.425Z" },
] ]
[[package]] [[package]]
@ -1053,11 +1053,11 @@ wheels = [
[[package]] [[package]]
name = "trove-classifiers" name = "trove-classifiers"
version = "2026.5.7.17" version = "2026.5.22.10"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/35/68/175e7c07c5be13200387d5c0995b0da1e198e360047c08eb17d1002fcd92/trove_classifiers-2026.5.7.17.tar.gz", hash = "sha256:a04a48f8f0a787cb996514d3969ac7608aa3c60cb15d073c1e02801e60533e80", size = 17041, upload-time = "2026-05-07T17:48:01.931Z" } sdist = { url = "https://files.pythonhosted.org/packages/86/b6/1c41aa221b157b624ea1a72e975404ef228724d249011ee411ac211a615e/trove_classifiers-2026.5.22.10.tar.gz", hash = "sha256:5477e9974e91904fb2cfa4a7581ab6e2f30c2c38d847fd00ed866080748101d5", size = 17061, upload-time = "2026-05-22T10:17:28.99Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/7b/e3/d81b065a2d866a33a541ac63a2a4cc5737e03ce2379ac3191c98bb8867e3/trove_classifiers-2026.5.7.17-py3-none-any.whl", hash = "sha256:5ec0800de5e2ddbd7c663cb4c0c15328f132dc168813897c18866c5c7b93db33", size = 14201, upload-time = "2026-05-07T17:48:00.488Z" }, { url = "https://files.pythonhosted.org/packages/c9/02/9a14d3048ffa4f45b7c60956a9b22688dd925d6de50f6baf7e55f3664942/trove_classifiers-2026.5.22.10-py3-none-any.whl", hash = "sha256:01fe864225726e03efb843827ecabfe319fc4dee8dd66d65b8996cb09be46e2c", size = 14225, upload-time = "2026-05-22T10:17:27.569Z" },
] ]
[[package]] [[package]]
@ -1080,33 +1080,33 @@ wheels = [
[[package]] [[package]]
name = "uv" name = "uv"
version = "0.11.14" version = "0.11.17"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/30/a3/be4a946c7c2fc4094c020c8f7d8bd0a739bad55ebe4e2817d6e2b1bc6bff/uv-0.11.14.tar.gz", hash = "sha256:0ea006a117b586b2681b6dfd9703a540d2ad2a136ec0f48d272767e599cc3dfb", size = 4130699, upload-time = "2026-05-12T18:00:37.321Z" } sdist = { url = "https://files.pythonhosted.org/packages/2c/8e/ec34c19d0f254fcbcc5c1ce8c7f06e47e0f69a7e1a0269c1d59cb0b0f279/uv-0.11.17.tar.gz", hash = "sha256:1d1be74deec997db1dda05a7e67541c904d65cbfd72e455d3c0a2a1e4bf2cddf", size = 4203607, upload-time = "2026-05-28T20:39:47.707Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/f7/15/9b2138b16eb1fa8c2cd84b1037ad10c38b3acc36ce96c6d27000bfb7e716/uv-0.11.14-py3-none-linux_armv6l.whl", hash = "sha256:78411a883f230a710af19f2ac6e6f0ba8eae90f0e5af4605f923fd367539fff4", size = 23545199, upload-time = "2026-05-12T18:01:34.526Z" }, { url = "https://files.pythonhosted.org/packages/15/2e/e6d42f9d39009eee976f1e5dfd31d3d1943e6e593ad7b191cf11e9744a36/uv-0.11.17-py3-none-linux_armv6l.whl", hash = "sha256:8426bfe315564d414cbc5ba5467595dc6348965e19acec742914f47da3ff269f", size = 23551216, upload-time = "2026-05-28T20:39:05.395Z" },
{ url = "https://files.pythonhosted.org/packages/75/81/c678e8b9a8e624f9c338c66cd57dd9cfc6b5a0501ad3c87fd0cc0bf8850a/uv-0.11.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:078f2e63da89c8fcf6d578f02156045c5990c57d76464aab3f3f798d3fff95cd", size = 22957064, upload-time = "2026-05-12T18:00:54.225Z" }, { url = "https://files.pythonhosted.org/packages/d0/ee/d72bcc60f3585653a4b768425854d737d98d65c1765547d25c2999547ea9/uv-0.11.17-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6d1a033cc68cabb4141d6c1e3b66ffc6e970b98ba42e210f33270251e0bd8697", size = 22997377, upload-time = "2026-05-28T20:39:25.21Z" },
{ url = "https://files.pythonhosted.org/packages/f7/ad/95fbd15b23f26f36d0cfb0ddf159b9602a1b1c0feced60a7f98385e919f1/uv-0.11.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:dcdad43d52c130e3159e84ab1844e04d819d2c4a2495a687d27f80d560a3650e", size = 21678307, upload-time = "2026-05-12T18:00:57.132Z" }, { url = "https://files.pythonhosted.org/packages/58/34/1bc69798d9ae998fbc42c61b02883f2ba00d04bdd858e589604d01846287/uv-0.11.17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:58c07ffc272c847d29cd98ca5082fa4304a645f87c718ec900e3cca9026bd096", size = 21630197, upload-time = "2026-05-28T20:39:28.935Z" },
{ url = "https://files.pythonhosted.org/packages/8b/cb/b3da1c4d95d6dd507896bca16dbd643118013b2b151f5f35a08d3391728c/uv-0.11.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:9923da7c63d70de9fe71829503d7e7ebfd6304e804d7232aad5f716e190db25b", size = 23353409, upload-time = "2026-05-12T18:01:27.512Z" }, { url = "https://files.pythonhosted.org/packages/6b/93/1be48ec6a8933d9a77d0ce5240ed63f68869f68517ccf5d62268ed03f3e8/uv-0.11.17-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:036d6e2940afe8b79637530b01b9241d8cfd174b07f1179a1ebbd42409c38ca3", size = 23414940, upload-time = "2026-05-28T20:39:55.015Z" },
{ url = "https://files.pythonhosted.org/packages/51/ad/78c6b8d6bcc04c5043b50631e9b413422a03a0bd7c4a997748f8e9cbac25/uv-0.11.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:3b0759ca504e48dcd4fafb1a61ef69aeb24c5a60fbf5f504a7873c8db1b24718", size = 23103964, upload-time = "2026-05-12T18:01:31.094Z" }, { url = "https://files.pythonhosted.org/packages/00/31/b7488ff49d80090ea9d05d67a4d381a1b4479502e9853e654caa1c1c678e/uv-0.11.17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:283186700c3e65a4644a73a917232da7d3e4a94d25ea0377a44f5b263fa49577", size = 23096330, upload-time = "2026-05-28T20:39:01.284Z" },
{ url = "https://files.pythonhosted.org/packages/0f/7d/acb66e09bc54a74e4288e996d841af04d88588fd6bdbfbab2468ab7169a7/uv-0.11.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:78b51b117549ee4db7197ea5ece0848cecd443e464fb9dff9f254cdc1e4ed96f", size = 23104638, upload-time = "2026-05-12T18:01:10.093Z" }, { url = "https://files.pythonhosted.org/packages/fe/95/42b6137c5de06278d229c7eef2f314df2a738cd799795bbb44dace21bd6e/uv-0.11.17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f2e44dfbfc7778d0d90edc6738f237c91e5e37e4e3cfe94c8a312cec56a41485", size = 23101906, upload-time = "2026-05-28T20:39:17.149Z" },
{ url = "https://files.pythonhosted.org/packages/31/0a/8497be61accdb8e56d02e11edd3ac471466259420e0bd9c05c1966df134a/uv-0.11.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a1ddbe8a2ab160affc179e9c3a40913b23a08cdf55254e1f3829cc22a51a0d8d", size = 24625888, upload-time = "2026-05-12T18:01:17.192Z" }, { url = "https://files.pythonhosted.org/packages/17/7c/0ca03b2d19965db6d5dfe0c8cf96a3d0b424503c8cbc3cd2ffdc5869a15d/uv-0.11.17-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a817eeb3026f27a53d3f4b7855a5105f6787dd192140e201eda4d2b9a11b72e", size = 24444409, upload-time = "2026-05-28T20:39:59.218Z" },
{ url = "https://files.pythonhosted.org/packages/95/91/f730799fd20a45777b255e20cf9f648a4e4e0979bf65e87a8633197cf7d9/uv-0.11.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3005a2db1e8d72e125630d4f22ac4ceddb2c033e1f9b94b7f3ea38ebac46dd6", size = 25445231, upload-time = "2026-05-12T18:00:40.012Z" }, { url = "https://files.pythonhosted.org/packages/b5/fb/179f55a3b19d47c30ec1f41b9b964da74dfa7053ff310a70a9c4d8cb998d/uv-0.11.17-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bf8f5ad959583dcd2c4ae445c754a97c05700246ff89259f3fd285c9c20f4c00", size = 25540153, upload-time = "2026-05-28T20:39:09.535Z" },
{ url = "https://files.pythonhosted.org/packages/f5/4d/106463fc27e63e402aec2e791774dac2db5bd5e1c36cdcf38125aa97ab1c/uv-0.11.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5c8f9ea36274ef2f9d24f0522085e280844172e901d9213f66a21b212266706", size = 24571961, upload-time = "2026-05-12T18:00:43.713Z" }, { url = "https://files.pythonhosted.org/packages/f7/29/592f42012765c43ae45c112110e214bca7b0cfc08c4c1b52e1dfa47dedd5/uv-0.11.17-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce16892a45134d20165c1ceababe06f3e9ce6a58902db1eff812c8c93626823f", size = 24665906, upload-time = "2026-05-28T20:39:41.254Z" },
{ url = "https://files.pythonhosted.org/packages/12/4d/163fe746b97bd1129627e8b1f943e17583ddc143eaab532d56a799a9ba5a/uv-0.11.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:379e64b236cf55f762a8308d7efe4365d5296ba29f3a4868761bc45b4e915a71", size = 24718523, upload-time = "2026-05-12T18:01:06.587Z" }, { url = "https://files.pythonhosted.org/packages/0e/51/b75808766f895248553c6370968509cd4f726e6943e310a8f7a171036ad0/uv-0.11.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9da839e5a491c9a701d7d327a199cafc76ac27a03ac84fd2a8d4bf32c3af2448", size = 24863325, upload-time = "2026-05-28T20:39:51.006Z" },
{ url = "https://files.pythonhosted.org/packages/19/fb/7a3673494a0cf70267559166398f9c50c4925ff20122f99a28d6c5a80d83/uv-0.11.14-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:29c12a562441fc2d604e6920c558cacce74a55f889468708683a79b35a6e18a1", size = 23454821, upload-time = "2026-05-12T18:00:51.166Z" }, { url = "https://files.pythonhosted.org/packages/ee/6a/6f27ee69e97f480104bb8ec335f04c2a12add98edfcc4844a68e9538b6e2/uv-0.11.17-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:ec004b3c9bf9cb7756067ad1bd0bf64eb843e6fa2edbfbb3135ee152c14cea91", size = 23521674, upload-time = "2026-05-28T20:38:55.869Z" },
{ url = "https://files.pythonhosted.org/packages/bb/43/6358394a567d865f3a5ce27b1e0d939549911e36d9b59f0c545a167f92f7/uv-0.11.14-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:e84069681c0334e07cbc7f114eb09d7fe1335e1db0297a66dbca80a1b393fe6d", size = 24087843, upload-time = "2026-05-12T18:00:47.272Z" }, { url = "https://files.pythonhosted.org/packages/df/11/1344aca7c710f794750f74de0e552a54ab24193ecc01fa3b3ae22ff822a1/uv-0.11.17-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:659227cac719b618cc91e02be9e274ad5bd72d74fa278123e6373537e9f28216", size = 24224725, upload-time = "2026-05-28T20:39:32.945Z" },
{ url = "https://files.pythonhosted.org/packages/ef/f6/7d0ae1e1f52b85057ca24d8876d6a4cc87b541ea6aca627fe36594c06099/uv-0.11.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:b15bf7c146e38d7c938d3a207115d5fdd8ef764fe1f866c225b1bed27e88da1e", size = 24147611, upload-time = "2026-05-12T18:01:20.499Z" }, { url = "https://files.pythonhosted.org/packages/ad/44/7b11550c1453ea13b81e549c83523e6ab6ed3231d09b2fd6b9eb19acceaf/uv-0.11.17-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e301d844eed9401f0f0351de12c55f1306ca05372acb0f28d35717c8ba663a22", size = 24301643, upload-time = "2026-05-28T20:39:45.183Z" },
{ url = "https://files.pythonhosted.org/packages/5a/a2/511ad0c5da5697fd990b99569425b62b81cbc3458c35acc845211b55d6b5/uv-0.11.14-py3-none-musllinux_1_1_i686.whl", hash = "sha256:ddda5c5e41097814adac535c74851bae55e8097b9afc79aeae7fcffd8d86c06d", size = 23920348, upload-time = "2026-05-12T18:01:24.033Z" }, { url = "https://files.pythonhosted.org/packages/1a/36/8f683bc60547b8f93d0e752a8574d13fad776999cb978482b360c053ca22/uv-0.11.17-py3-none-musllinux_1_1_i686.whl", hash = "sha256:f0bf483c0d9fa14283992d56061b498b9d3d4adebd285af8744dc33f64dadfba", size = 23786049, upload-time = "2026-05-28T20:39:20.999Z" },
{ url = "https://files.pythonhosted.org/packages/6b/b6/7084e3401b1f1020f215a125136eec1ed2bd541e10a5fea1625515579599/uv-0.11.14-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:e54326703f1eca83a6fd73275e0f398b16b7d3f81531bf58899c2869bc403f6c", size = 24928981, upload-time = "2026-05-12T18:01:13.961Z" }, { url = "https://files.pythonhosted.org/packages/10/dc/7a495db39c2970de4fa375c337dbd617b16780911f88f0511f8fe7f6747c/uv-0.11.17-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:2ccd5487a4a192bc832ea04c867a26883757db8fdfe88bed85d8129c82f9e505", size = 25049786, upload-time = "2026-05-28T20:40:03.292Z" },
{ url = "https://files.pythonhosted.org/packages/4d/6a/7e81729fe729889c8cc63bbf64291734359bd7f6ba84852dc0504453511d/uv-0.11.14-py3-none-win32.whl", hash = "sha256:b384d873d0d18552c7524226125efd3965d921b7134c2f476c333771beb733e1", size = 22573503, upload-time = "2026-05-12T18:00:34.36Z" }, { url = "https://files.pythonhosted.org/packages/37/dd/74eff72d749eaf7e19f489878e21a368a7fef58d26ea0c63ec044ecd78b1/uv-0.11.17-py3-none-win32.whl", hash = "sha256:12b701fa32c5be3691759a73956e4462f30fa7b0dfa52ec66cb305bbb6ea4129", size = 22479213, upload-time = "2026-05-28T20:39:13.316Z" },
{ url = "https://files.pythonhosted.org/packages/94/5d/f8905f9af5cd46af2a688b2246dbb5a4d95b8557eeffd7f241e037659d9e/uv-0.11.14-py3-none-win_amd64.whl", hash = "sha256:f0a8b58b38e984241bca5d7a5a47bf9ffe1ca2ab392a640887db8a04c4a9ec95", size = 25175590, upload-time = "2026-05-12T18:01:00.38Z" }, { url = "https://files.pythonhosted.org/packages/79/99/8af4a92b99a8a4823297c26df727fe957267e03e1196e3caa803c3f6ccb2/uv-0.11.17-py3-none-win_amd64.whl", hash = "sha256:44ec1fe3af839f87370dcf0400c0cab917cc1ce697d563e860fc7d9ed72655e7", size = 25083161, upload-time = "2026-05-28T20:40:07.931Z" },
{ url = "https://files.pythonhosted.org/packages/04/cb/7333d08d944f3018eb89242cd5e646e7b37faa1b567faeaf9254a8b59d53/uv-0.11.14-py3-none-win_arm64.whl", hash = "sha256:6a13e7e064563050c6606b3fd77091d427cdbdc5938b6f134baf8d8ec79bfdb7", size = 23594775, upload-time = "2026-05-12T18:01:03.55Z" }, { url = "https://files.pythonhosted.org/packages/00/76/a689077832d585d29d87f9cd0d65eca1af58abd29a4eab004d0a8a858b9c/uv-0.11.17-py3-none-win_arm64.whl", hash = "sha256:37c915bfcf86f99c1c5be7c9ed21e0d80624067ba47bc8916a3cb0530bc94d27", size = 23544936, upload-time = "2026-05-28T20:39:37.137Z" },
] ]
[[package]] [[package]]
name = "virtualenv" name = "virtualenv"
version = "21.3.3" version = "21.4.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "distlib" }, { name = "distlib" },
@ -1115,9 +1115,9 @@ dependencies = [
{ name = "python-discovery" }, { name = "python-discovery" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/15/ba/1f6e8c957e4932be060dcdc482d339c12e0216351478add3645cdaa53c05/virtualenv-21.3.3.tar.gz", hash = "sha256:f5bda277e553b1c2b3c1a8debfc30496e1288cc93ce6b7b71b3280047e317328", size = 7613784, upload-time = "2026-05-13T18:01:30.19Z" } sdist = { url = "https://files.pythonhosted.org/packages/e1/0d/4e93c8e6d1001a75763f87d8f5ecda8ebc7f4aa2153dddfaf4ae8892821a/virtualenv-21.4.2.tar.gz", hash = "sha256:38e6ee0a555615c0ea9da2ac7e9998fe8dc3b911dd33ad8eaad2020957653b0c", size = 7613326, upload-time = "2026-05-31T17:01:22.827Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/34/a9dbe051de88a63eb7408ea66630bac38e72f7f6077d4be58737106860d9/virtualenv-21.3.3-py3-none-any.whl", hash = "sha256:7d5987d8369e098e41406efb780a3d4ca79280097293899e351a6407ee153ab3", size = 7594554, upload-time = "2026-05-13T18:01:27.815Z" }, { url = "https://files.pythonhosted.org/packages/bf/c4/557dc082be035381b85fdb2b74e21d3d21b57750b74f2b47a32f3a639ff9/virtualenv-21.4.2-py3-none-any.whl", hash = "sha256:854210ca524a1a4d0d744734f4acbc721c3ffe163b85bbf5d56d14d5ae2f0fae", size = 7594079, upload-time = "2026-05-31T17:01:20.735Z" },
] ]
[[package]] [[package]]
@ -1290,9 +1290,9 @@ requires-dist = [
{ name = "brotli", marker = "implementation_name == 'cpython' and sys_platform != 'ios' and extra == 'pin'", specifier = "==1.2.0" }, { name = "brotli", marker = "implementation_name == 'cpython' and sys_platform != 'ios' and extra == 'pin'", specifier = "==1.2.0" },
{ name = "brotlicffi", marker = "implementation_name != 'cpython' and extra == 'default'" }, { name = "brotlicffi", marker = "implementation_name != 'cpython' and extra == 'default'" },
{ name = "brotlicffi", marker = "implementation_name != 'cpython' and extra == 'pin'", specifier = "==1.2.0.1" }, { name = "brotlicffi", marker = "implementation_name != 'cpython' and extra == 'pin'", specifier = "==1.2.0.1" },
{ name = "certifi", marker = "implementation_name == 'cpython' and extra == 'pin-curl-cffi'", specifier = "==2026.4.22" }, { name = "certifi", marker = "implementation_name == 'cpython' and extra == 'pin-curl-cffi'", specifier = "==2026.5.20" },
{ name = "certifi", marker = "extra == 'default'" }, { name = "certifi", marker = "extra == 'default'" },
{ name = "certifi", marker = "extra == 'pin'", specifier = "==2026.4.22" }, { name = "certifi", marker = "extra == 'pin'", specifier = "==2026.5.20" },
{ name = "cffi", marker = "platform_python_implementation != 'PyPy' and extra == 'pin-secretstorage'", specifier = "==2.0.0" }, { name = "cffi", marker = "platform_python_implementation != 'PyPy' and extra == 'pin-secretstorage'", specifier = "==2.0.0" },
{ name = "cffi", marker = "implementation_name == 'cpython' and extra == 'pin-curl-cffi'", specifier = "==2.0.0" }, { name = "cffi", marker = "implementation_name == 'cpython' and extra == 'pin-curl-cffi'", specifier = "==2.0.0" },
{ name = "charset-normalizer", marker = "extra == 'pin'", specifier = "==3.4.7" }, { name = "charset-normalizer", marker = "extra == 'pin'", specifier = "==3.4.7" },
@ -1300,8 +1300,8 @@ requires-dist = [
{ name = "curl-cffi", marker = "implementation_name == 'cpython' and extra == 'curl-cffi'", specifier = ">=0.5.10,!=0.6.*,!=0.7.*,!=0.8.*,!=0.9.*,<0.16" }, { name = "curl-cffi", marker = "implementation_name == 'cpython' and extra == 'curl-cffi'", specifier = ">=0.5.10,!=0.6.*,!=0.7.*,!=0.8.*,!=0.9.*,<0.16" },
{ name = "curl-cffi", marker = "implementation_name == 'cpython' and extra == 'pin-curl-cffi'", specifier = "==0.15.0" }, { name = "curl-cffi", marker = "implementation_name == 'cpython' and extra == 'pin-curl-cffi'", specifier = "==0.15.0" },
{ name = "deno", marker = "extra == 'deno'", specifier = ">=2.6.6" }, { name = "deno", marker = "extra == 'deno'", specifier = ">=2.6.6" },
{ name = "deno", marker = "extra == 'pin-deno'", specifier = "==2.7.14" }, { name = "deno", marker = "extra == 'pin-deno'", specifier = "==2.8.1" },
{ name = "idna", marker = "extra == 'pin'", specifier = "==3.15" }, { name = "idna", marker = "extra == 'pin'", specifier = "==3.17" },
{ name = "jeepney", marker = "extra == 'pin-secretstorage'", specifier = "==0.9.0" }, { name = "jeepney", marker = "extra == 'pin-secretstorage'", specifier = "==0.9.0" },
{ name = "markdown-it-py", marker = "implementation_name == 'cpython' and extra == 'pin-curl-cffi'", specifier = "==4.2.0" }, { name = "markdown-it-py", marker = "implementation_name == 'cpython' and extra == 'pin-curl-cffi'", specifier = "==4.2.0" },
{ name = "mdurl", marker = "implementation_name == 'cpython' and extra == 'pin-curl-cffi'", specifier = "==0.1.2" }, { name = "mdurl", marker = "implementation_name == 'cpython' and extra == 'pin-curl-cffi'", specifier = "==0.1.2" },
@ -1364,9 +1364,9 @@ wheels = [
[[package]] [[package]]
name = "zipp" name = "zipp"
version = "3.23.1" version = "4.1.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/30/21/093488dfc7cc8964ded15ab726fad40f25fd3d788fd741cc1c5a17d78ee8/zipp-3.23.1.tar.gz", hash = "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110", size = 25965, upload-time = "2026-04-13T23:21:46.6Z" } sdist = { url = "https://files.pythonhosted.org/packages/b9/d8/eab98a517c14134c0b2eb4e2387bc5f457334293ec5d2dd3857ec2966802/zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602", size = 26214, upload-time = "2026-05-18T20:08:57.967Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/08/8a/0861bec20485572fbddf3dfba2910e38fe249796cb73ecdeb74e07eeb8d3/zipp-3.23.1-py3-none-any.whl", hash = "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc", size = 10378, upload-time = "2026-04-13T23:21:45.386Z" }, { url = "https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f", size = 10238, upload-time = "2026-05-18T20:08:57.045Z" },
] ]

View File

@ -112,6 +112,7 @@ from .utils import (
RejectedVideoReached, RejectedVideoReached,
SameFileError, SameFileError,
UnavailableVideoError, UnavailableVideoError,
UnsafeExecExpansionError,
UserNotLive, UserNotLive,
YoutubeDLError, YoutubeDLError,
age_restricted, age_restricted,
@ -826,9 +827,14 @@ class YoutubeDL:
for pp_def_raw in self.params.get('postprocessors', []): for pp_def_raw in self.params.get('postprocessors', []):
pp_def = dict(pp_def_raw) pp_def = dict(pp_def_raw)
when = pp_def.pop('when', 'post_process') when = pp_def.pop('when', 'post_process')
# Handle errors for ExecPP command validation
try:
self.add_post_processor( self.add_post_processor(
get_postprocessor(pp_def.pop('key'))(self, **pp_def), get_postprocessor(pp_def.pop('key'))(self, **pp_def),
when=when) when=when)
except UnsafeExecExpansionError as e:
self.report_error(e)
raise
def preload_download_archive(fn): def preload_download_archive(fn):
"""Preload the archive, if any is specified""" """Preload the archive, if any is specified"""
@ -1254,7 +1260,7 @@ class YoutubeDL:
info_dict.pop('__pending_error', None) info_dict.pop('__pending_error', None)
return info_dict return info_dict
def prepare_outtmpl(self, outtmpl, info_dict, sanitize=False): def prepare_outtmpl(self, outtmpl, info_dict, sanitize=False, *, _exec=False):
""" Make the outtmpl and info_dict suitable for substitution: ydl.escape_outtmpl(outtmpl) % info_dict """ Make the outtmpl and info_dict suitable for substitution: ydl.escape_outtmpl(outtmpl) % info_dict
@param sanitize Whether to sanitize the output as a filename @param sanitize Whether to sanitize the output as a filename
""" """
@ -1305,6 +1311,9 @@ class YoutubeDL:
(?:&(?P<replacement>.*?))? (?:&(?P<replacement>.*?))?
(?:\|(?P<default>.*?))? (?:\|(?P<default>.*?))?
)$''') )$''')
SAFE_EXEC_CONVERSIONS = 'difq'
UNSAFE_DEFAULT_CHARS = '"\' \n\t;&|^$%*<>{}()[]`#\\'
EXEC_ADVISORY_MSG = 'See https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-69qj-pvh9-c5wg for details'
def _from_user_input(field): def _from_user_input(field):
if field == ':': if field == ':':
@ -1429,6 +1438,25 @@ class YoutubeDL:
if fmt == 's' and last_field in field_size_compat_map and isinstance(value, int): if fmt == 's' and last_field in field_size_compat_map and isinstance(value, int):
fmt = f'0{field_size_compat_map[last_field]:d}d' fmt = f'0{field_size_compat_map[last_field]:d}d'
# Validate safety of exec commands
if _exec:
if fmt[-1] not in SAFE_EXEC_CONVERSIONS:
raise UnsafeExecExpansionError(
f'Unsafe conversion(s) in exec command: {outtmpl!r}\n'
f'Conversions such as %()s are too dangerous to be used in '
f'--exec command templates; use %()q instead. {EXEC_ADVISORY_MSG}')
elif any(unsafe_char in default for unsafe_char in UNSAFE_DEFAULT_CHARS):
if default == na:
raise UnsafeExecExpansionError(
f'Unsafe placeholder for exec command: {na!r}\n'
f'The --output-na-placeholder argument also applies to '
f'--exec command templates. {EXEC_ADVISORY_MSG}')
else:
raise UnsafeExecExpansionError(
f'Unsafe default(s) in exec command: {outtmpl!r}\n'
f'Conversions are not applied to --exec command template defaults, '
f'e.g. %(...|DEFAULT;)q. {EXEC_ADVISORY_MSG}')
flags = outer_mobj.group('conversion') or '' flags = outer_mobj.group('conversion') or ''
str_fmt = f'{fmt[:-1]}s' str_fmt = f'{fmt[:-1]}s'
if value is None: if value is None:
@ -3377,7 +3405,9 @@ class YoutubeDL:
self.report_warning( self.report_warning(
f'Cannot write internet shortcut file because the actual URL of "{info_dict["webpage_url"]}" is unknown') f'Cannot write internet shortcut file because the actual URL of "{info_dict["webpage_url"]}" is unknown')
return True return True
linkfn = replace_extension(self.prepare_filename(info_dict, 'link'), link_type, info_dict.get('ext')) linkfn = replace_extension(
self.prepare_filename(info_dict, 'link'), link_type,
info_dict.get('ext'), _allowed_exts=tuple(LINK_TEMPLATES))
if not self._ensure_dir_exists(linkfn): if not self._ensure_dir_exists(linkfn):
return False return False
if self.params.get('overwrites', True) and os.path.exists(linkfn): if self.params.get('overwrites', True) and os.path.exists(linkfn):

View File

@ -44,6 +44,7 @@ from .utils import (
GeoUtils, GeoUtils,
PlaylistEntries, PlaylistEntries,
SameFileError, SameFileError,
UnsafeExecExpansionError,
download_range_func, download_range_func,
expand_path, expand_path,
float_or_none, float_or_none,
@ -618,7 +619,7 @@ def validate_options(opts):
warnings.append( warnings.append(
'Using allow-unsafe-ext opens you up to potential attacks. ' 'Using allow-unsafe-ext opens you up to potential attacks. '
'Use with great care!') 'Use with great care!')
_UnsafeExtensionError.sanitize_extension = lambda x, prepend=False: x _UnsafeExtensionError._enabled = False
return warnings, deprecation_warnings return warnings, deprecation_warnings
@ -1077,7 +1078,7 @@ def main(argv=None):
IN_CLI.value = True IN_CLI.value = True
try: try:
_exit(*variadic(_real_main(argv))) _exit(*variadic(_real_main(argv)))
except (CookieLoadError, DownloadError): except (CookieLoadError, DownloadError, UnsafeExecExpansionError):
_exit(1) _exit(1)
except SameFileError as e: except SameFileError as e:
_exit(f'ERROR: {e}') _exit(f'ERROR: {e}')

View File

@ -1,21 +1,21 @@
import enum import enum
import functools import functools
import json import io
import os import os
import re import re
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
import time import time
import uuid
from .fragment import FragmentFD from .fragment import FragmentFD
from ..networking import Request
from ..postprocessor.ffmpeg import EXT_TO_OUT_FORMATS, FFmpegPostProcessor from ..postprocessor.ffmpeg import EXT_TO_OUT_FORMATS, FFmpegPostProcessor
from ..utils import ( from ..utils import (
DownloadError,
Popen, Popen,
RetryManager, RetryManager,
_configuration_args, _configuration_args,
_get_exe_version_output,
check_executable, check_executable,
classproperty, classproperty,
cli_bool_option, cli_bool_option,
@ -23,9 +23,9 @@ from ..utils import (
cli_valueless_option, cli_valueless_option,
determine_ext, determine_ext,
encodeArgument, encodeArgument,
find_available_port,
remove_end, remove_end,
traverse_obj, traverse_obj,
version_tuple,
) )
@ -136,7 +136,7 @@ class ExternalFD(FragmentFD):
self._cookies_tempfile = tmp_cookies.name self._cookies_tempfile = tmp_cookies.name
self.to_screen(f'[download] Writing temporary cookies file to "{self._cookies_tempfile}"') self.to_screen(f'[download] Writing temporary cookies file to "{self._cookies_tempfile}"')
# real_download resets _cookies_tempfile; if it's None then save() will write to cookiejar.filename # real_download resets _cookies_tempfile; if it's None then save() will write to cookiejar.filename
self.ydl.cookiejar.save(self._cookies_tempfile) self.ydl.cookiejar.save(self._cookies_tempfile, True, True)
return self.ydl.cookiejar.filename or self._cookies_tempfile return self.ydl.cookiejar.filename or self._cookies_tempfile
def _call_downloader(self, tmpfilename, info_dict): def _call_downloader(self, tmpfilename, info_dict):
@ -195,12 +195,38 @@ class ExternalFD(FragmentFD):
class CurlFD(ExternalFD): class CurlFD(ExternalFD):
AVAILABLE_OPT = '-V' AVAILABLE_OPT = '-V'
_CAPTURE_STDERR = False # curl writes the progress to stderr _CAPTURE_STDERR = False # curl writes the progress to stderr
_MIN_VERSION_FOR_STDIN_COOKIES = (7, 59)
@classmethod
def available(cls, path=None):
if path is None:
path = 'curl'
output = _get_exe_version_output(path, ['-V'])
if not output:
return False
parts = output.split(' ', maxsplit=2)
if len(parts) < 3:
return False
cls.exe = path
cls._curl_version = version_tuple(parts[1])
return path
def _make_cmd(self, tmpfilename, info_dict): def _make_cmd(self, tmpfilename, info_dict):
cmd = [self.exe, '--location', '-o', tmpfilename, '--compressed'] cmd = [self.exe, '--location', '-o', tmpfilename, '--compressed']
cookie_header = self.ydl.cookiejar.get_cookie_header(info_dict['url'])
if cookie_header: if self._curl_version >= self._MIN_VERSION_FOR_STDIN_COOKIES:
cmd += ['--cookie', cookie_header] # Supports `--cookies -`
cmd += ['--cookie', '-']
elif os.path.islink('/dev/fd/0'):
cmd += ['--cookie', '/dev/fd/0']
else:
cookies_file = self._write_cookies()
if '=' in cookies_file:
raise DownloadError('curl version too old or temp directory contains `=`; please use another downloader or update curl')
assert cookies_file != '-'
cmd += ['--cookie', cookies_file]
if info_dict.get('http_headers') is not None: if info_dict.get('http_headers') is not None:
for key, val in info_dict['http_headers'].items(): for key, val in info_dict['http_headers'].items():
cmd += ['--header', f'{key}: {val}'] cmd += ['--header', f'{key}: {val}']
@ -222,6 +248,16 @@ class CurlFD(ExternalFD):
cmd += ['--', info_dict['url']] cmd += ['--', info_dict['url']]
return cmd return cmd
def _call_process(self, cmd, info_dict):
if self._curl_version > self._MIN_VERSION_FOR_STDIN_COOKIES or os.path.islink('/dev/fd/0'):
# Supports `--cookies -` or reading from device file as `--cookies /dev/fd/0`
buffer = io.StringIO()
self.ydl.cookiejar._really_save(buffer, True, True)
return Popen.run(cmd, text=True, input=buffer.getvalue())
# Cookies already passed via cookiesfile
return Popen.run(cmd, text=True)
class AxelFD(ExternalFD): class AxelFD(ExternalFD):
AVAILABLE_OPT = '-V' AVAILABLE_OPT = '-V'
@ -244,7 +280,6 @@ class WgetFD(ExternalFD):
def _make_cmd(self, tmpfilename, info_dict): def _make_cmd(self, tmpfilename, info_dict):
cmd = [self.exe, '-O', tmpfilename, '-nv', '--compression=auto'] cmd = [self.exe, '-O', tmpfilename, '-nv', '--compression=auto']
if self.ydl.cookiejar.get_cookie_header(info_dict['url']):
cmd += ['--load-cookies', self._write_cookies()] cmd += ['--load-cookies', self._write_cookies()]
if info_dict.get('http_headers') is not None: if info_dict.get('http_headers') is not None:
for key, val in info_dict['http_headers'].items(): for key, val in info_dict['http_headers'].items():
@ -268,40 +303,18 @@ class WgetFD(ExternalFD):
class Aria2cFD(ExternalFD): class Aria2cFD(ExternalFD):
AVAILABLE_OPT = '-v' AVAILABLE_OPT = '-v'
SUPPORTED_PROTOCOLS = ('http', 'https', 'ftp', 'ftps', 'dash_frag_urls', 'm3u8_frag_urls') SUPPORTED_PROTOCOLS = ('http', 'https', 'ftp', 'ftps')
@staticmethod
def supports_manifest(manifest):
UNSUPPORTED_FEATURES = [
r'#EXT-X-BYTERANGE', # playlists composed of byte ranges of media files [1]
# 1. https://tools.ietf.org/html/draft-pantos-http-live-streaming-17#section-4.3.2.2
]
check_results = (not re.search(feature, manifest) for feature in UNSUPPORTED_FEATURES)
return all(check_results)
@staticmethod @staticmethod
def _aria2c_filename(fn): def _aria2c_filename(fn):
return fn if os.path.isabs(fn) else f'.{os.path.sep}{fn}' return fn if os.path.isabs(fn) else f'.{os.path.sep}{fn}'
def _call_downloader(self, tmpfilename, info_dict):
# FIXME: Disabled due to https://github.com/yt-dlp/yt-dlp/issues/5931
if False and 'no-external-downloader-progress' not in self.params.get('compat_opts', []):
info_dict['__rpc'] = {
'port': find_available_port() or 19190,
'secret': str(uuid.uuid4()),
}
return super()._call_downloader(tmpfilename, info_dict)
def _make_cmd(self, tmpfilename, info_dict): def _make_cmd(self, tmpfilename, info_dict):
cmd = [self.exe, '-c', '--no-conf', cmd = [self.exe, '-c', '--no-conf',
'--console-log-level=warn', '--summary-interval=0', '--download-result=hide', '--console-log-level=warn', '--summary-interval=0', '--download-result=hide',
'--http-accept-gzip=true', '--file-allocation=none', '-x16', '-j16', '-s16'] '--http-accept-gzip=true', '--file-allocation=none', '-x16', '-j16', '-s16',
if 'fragments' in info_dict: '--min-split-size', '1M']
cmd += ['--allow-overwrite=true', '--allow-piece-length-change=true']
else:
cmd += ['--min-split-size', '1M']
if self.ydl.cookiejar.get_cookie_header(info_dict['url']):
cmd += [f'--load-cookies={self._write_cookies()}'] cmd += [f'--load-cookies={self._write_cookies()}']
if info_dict.get('http_headers') is not None: if info_dict.get('http_headers') is not None:
for key, val in info_dict['http_headers'].items(): for key, val in info_dict['http_headers'].items():
@ -314,12 +327,6 @@ class Aria2cFD(ExternalFD):
cmd += self._bool_option('--show-console-readout', 'noprogress', 'false', 'true', '=') cmd += self._bool_option('--show-console-readout', 'noprogress', 'false', 'true', '=')
cmd += self._configuration_args() cmd += self._configuration_args()
if '__rpc' in info_dict:
cmd += [
'--enable-rpc',
f'--rpc-listen-port={info_dict["__rpc"]["port"]}',
f'--rpc-secret={info_dict["__rpc"]["secret"]}']
# aria2c strips out spaces from the beginning/end of filenames and paths. # aria2c strips out spaces from the beginning/end of filenames and paths.
# We work around this issue by adding a "./" to the beginning of the # We work around this issue by adding a "./" to the beginning of the
# filename and relative path, and adding a "/" at the end of the path. # filename and relative path, and adding a "/" at the end of the path.
@ -329,106 +336,17 @@ class Aria2cFD(ExternalFD):
dn = os.path.dirname(tmpfilename) dn = os.path.dirname(tmpfilename)
if dn: if dn:
cmd += ['--dir', self._aria2c_filename(dn) + os.path.sep] cmd += ['--dir', self._aria2c_filename(dn) + os.path.sep]
if 'fragments' not in info_dict:
cmd += ['--out', self._aria2c_filename(os.path.basename(tmpfilename))]
cmd += ['--auto-file-renaming=false']
if 'fragments' in info_dict: cmd += [
cmd += ['--uri-selector=inorder'] '--out',
url_list_file = f'{tmpfilename}.frag.urls' self._aria2c_filename(os.path.basename(tmpfilename)),
url_list = [] '--auto-file-renaming=false',
for frag_index, fragment in enumerate(info_dict['fragments']): '--',
fragment_filename = f'{os.path.basename(tmpfilename)}-Frag{frag_index}' info_dict['url'],
url_list.append('{}\n\tout={}'.format(fragment['url'], self._aria2c_filename(fragment_filename))) ]
stream, _ = self.sanitize_open(url_list_file, 'wb')
stream.write('\n'.join(url_list).encode())
stream.close()
cmd += ['-i', self._aria2c_filename(url_list_file)]
else:
cmd += ['--', info_dict['url']]
return cmd return cmd
def aria2c_rpc(self, rpc_port, rpc_secret, method, params=()):
# Does not actually need to be UUID, just unique
sanitycheck = str(uuid.uuid4())
d = json.dumps({
'jsonrpc': '2.0',
'id': sanitycheck,
'method': method,
'params': [f'token:{rpc_secret}', *params],
}).encode()
request = Request(
f'http://localhost:{rpc_port}/jsonrpc',
data=d, headers={
'Content-Type': 'application/json',
'Content-Length': f'{len(d)}',
}, proxies={'all': None})
with self.ydl.urlopen(request) as r:
resp = json.load(r)
assert resp.get('id') == sanitycheck, 'Something went wrong with RPC server'
return resp['result']
def _call_process(self, cmd, info_dict):
if '__rpc' not in info_dict:
return super()._call_process(cmd, info_dict)
send_rpc = functools.partial(self.aria2c_rpc, info_dict['__rpc']['port'], info_dict['__rpc']['secret'])
started = time.time()
fragmented = 'fragments' in info_dict
frag_count = len(info_dict['fragments']) if fragmented else 1
status = {
'filename': info_dict.get('_filename'),
'status': 'downloading',
'elapsed': 0,
'downloaded_bytes': 0,
'fragment_count': frag_count if fragmented else None,
'fragment_index': 0 if fragmented else None,
}
self._hook_progress(status, info_dict)
def get_stat(key, *obj, average=False):
val = tuple(filter(None, map(float, traverse_obj(obj, (..., ..., key))))) or [0]
return sum(val) / (len(val) if average else 1)
with Popen(cmd, text=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE) as p:
# Add a small sleep so that RPC client can receive response,
# or the connection stalls infinitely
time.sleep(0.2)
retval = p.poll()
while retval is None:
# We don't use tellStatus as we won't know the GID without reading stdout
# Ref: https://aria2.github.io/manual/en/html/aria2c.html#aria2.tellActive
active = send_rpc('aria2.tellActive')
completed = send_rpc('aria2.tellStopped', [0, frag_count])
downloaded = get_stat('totalLength', completed) + get_stat('completedLength', active)
speed = get_stat('downloadSpeed', active)
total = frag_count * get_stat('totalLength', active, completed, average=True)
if total < downloaded:
total = None
status.update({
'downloaded_bytes': int(downloaded),
'speed': speed,
'total_bytes': None if fragmented else total,
'total_bytes_estimate': total,
'eta': (total - downloaded) / (speed or 1),
'fragment_index': min(frag_count, len(completed) + 1) if fragmented else None,
'elapsed': time.time() - started,
})
self._hook_progress(status, info_dict)
if not active and len(completed) >= frag_count:
send_rpc('aria2.shutdown')
retval = p.wait()
break
time.sleep(0.1)
retval = p.poll()
return '', p.stderr.read(), retval
class HttpieFD(ExternalFD): class HttpieFD(ExternalFD):
AVAILABLE_OPT = '--version' AVAILABLE_OPT = '--version'
@ -521,10 +439,11 @@ class FFmpegFD(ExternalFD):
args.extend(['-cookies', ''.join( args.extend(['-cookies', ''.join(
f'{cookie.name}={cookie.value}; path={cookie.path}; domain={cookie.domain};\r\n' f'{cookie.name}={cookie.value}; path={cookie.path}; domain={cookie.domain};\r\n'
for cookie in cookies)]) for cookie in cookies)])
if fmt.get('http_headers') and is_http: http_headers = fmt.get('http_headers') or info_dict.get('http_headers')
if http_headers and is_http:
# Trailing \r\n after each HTTP header is important to prevent warning from ffmpeg: # Trailing \r\n after each HTTP header is important to prevent warning from ffmpeg:
# [http @ 00000000003d2fa0] No trailing CRLF found in HTTP header. # [http @ 00000000003d2fa0] No trailing CRLF found in HTTP header.
args.extend(['-headers', ''.join(f'{key}: {val}\r\n' for key, val in fmt['http_headers'].items())]) args.extend(['-headers', ''.join(f'{key}: {val}\r\n' for key, val in http_headers.items())])
if start_time: if start_time:
args += ['-ss', str(start_time)] args += ['-ss', str(start_time)]

View File

@ -54,7 +54,6 @@ from .agora import (
WyborczaPodcastIE, WyborczaPodcastIE,
WyborczaVideoIE, WyborczaVideoIE,
) )
from .airtv import AirTVIE
from .aitube import AitubeKZVideoIE from .aitube import AitubeKZVideoIE
from .alibaba import AlibabaIE from .alibaba import AlibabaIE
from .aliexpress import AliExpressLiveIE from .aliexpress import AliExpressLiveIE
@ -65,10 +64,6 @@ from .allstar import (
AllstarProfileIE, AllstarProfileIE,
) )
from .alphaporno import AlphaPornoIE from .alphaporno import AlphaPornoIE
from .alsace20tv import (
Alsace20TVEmbedIE,
Alsace20TVIE,
)
from .altcensored import ( from .altcensored import (
AltCensoredChannelIE, AltCensoredChannelIE,
AltCensoredIE, AltCensoredIE,
@ -93,7 +88,6 @@ from .americastestkitchen import (
AmericasTestKitchenIE, AmericasTestKitchenIE,
AmericasTestKitchenSeasonIE, AmericasTestKitchenSeasonIE,
) )
from .anchorfm import AnchorFMEpisodeIE
from .angel import AngelIE from .angel import AngelIE
from .antenna import ( from .antenna import (
Ant1NewsGrArticleIE, Ant1NewsGrArticleIE,
@ -106,10 +100,6 @@ from .apa import APAIE
from .aparat import AparatIE from .aparat import AparatIE
from .appleconnect import AppleConnectIE from .appleconnect import AppleConnectIE
from .applepodcasts import ApplePodcastsIE from .applepodcasts import ApplePodcastsIE
from .appletrailers import (
AppleTrailersIE,
AppleTrailersSectionIE,
)
from .archiveorg import ( from .archiveorg import (
ArchiveOrgIE, ArchiveOrgIE,
YoutubeWebArchiveIE, YoutubeWebArchiveIE,
@ -140,7 +130,6 @@ from .asobichannel import (
from .asobistage import AsobiStageIE from .asobistage import AsobiStageIE
from .atresplayer import AtresPlayerIE from .atresplayer import AtresPlayerIE
from .atscaleconf import AtScaleConfEventIE from .atscaleconf import AtScaleConfEventIE
from .atvat import ATVAtIE
from .audimedia import AudiMediaIE from .audimedia import AudiMediaIE
from .audioboom import AudioBoomIE from .audioboom import AudioBoomIE
from .audiodraft import ( from .audiodraft import (
@ -157,13 +146,6 @@ from .audius import (
AudiusProfileIE, AudiusProfileIE,
AudiusTrackIE, AudiusTrackIE,
) )
from .awaan import (
AWAANIE,
AWAANLiveIE,
AWAANSeasonIE,
AWAANVideoIE,
)
from .axs import AxsIE
from .azmedien import AZMedienIE from .azmedien import AZMedienIE
from .baidu import BaiduVideoIE from .baidu import BaiduVideoIE
from .banbye import ( from .banbye import (
@ -190,10 +172,6 @@ from .bbc import (
BBCCoUkPlaylistIE, BBCCoUkPlaylistIE,
) )
from .beacon import BeaconTvIE from .beacon import BeaconTvIE
from .beatbump import (
BeatBumpPlaylistIE,
BeatBumpVideoIE,
)
from .beatport import BeatportIE from .beatport import BeatportIE
from .beeg import BeegIE from .beeg import BeegIE
from .behindkink import BehindKinkIE from .behindkink import BehindKinkIE
@ -210,7 +188,6 @@ from .bibeltv import (
BibelTVSeriesIE, BibelTVSeriesIE,
BibelTVVideoIE, BibelTVVideoIE,
) )
from .bigflix import BigflixIE
from .bigo import BigoIE from .bigo import BigoIE
from .bild import BildIE from .bild import BildIE
from .bilibili import ( from .bilibili import (
@ -255,7 +232,6 @@ from .blerp import BlerpIE
from .blogger import BloggerIE from .blogger import BloggerIE
from .bloomberg import BloombergIE from .bloomberg import BloombergIE
from .bluesky import BlueskyIE from .bluesky import BlueskyIE
from .bokecc import BokeCCIE
from .bongacams import BongaCamsIE from .bongacams import BongaCamsIE
from .boosty import BoostyIE from .boosty import BoostyIE
from .bostonglobe import BostonGlobeIE from .bostonglobe import BostonGlobeIE
@ -288,14 +264,8 @@ from .businessinsider import BusinessInsiderIE
from .buzzfeed import BuzzFeedIE from .buzzfeed import BuzzFeedIE
from .byutv import BYUtvIE from .byutv import BYUtvIE
from .c56 import C56IE from .c56 import C56IE
from .caffeinetv import CaffeineTVIE
from .callin import CallinIE
from .caltrans import CaltransIE from .caltrans import CaltransIE
from .cam4 import CAM4IE from .cam4 import CAM4IE
from .camdemy import (
CamdemyFolderIE,
CamdemyIE,
)
from .camfm import ( from .camfm import (
CamFMEpisodeIE, CamFMEpisodeIE,
CamFMShowIE, CamFMShowIE,
@ -371,7 +341,6 @@ from .ciscolive import (
from .ciscowebex import CiscoWebexIE from .ciscowebex import CiscoWebexIE
from .cjsw import CJSWIE from .cjsw import CJSWIE
from .clipchamp import ClipchampIE from .clipchamp import ClipchampIE
from .clippit import ClippitIE
from .cliprs import ClipRsIE from .cliprs import ClipRsIE
from .closertotruth import CloserToTruthIE from .closertotruth import CloserToTruthIE
from .cloudflarestream import CloudflareStreamIE from .cloudflarestream import CloudflareStreamIE
@ -395,7 +364,6 @@ from .commonprotocols import (
ViewSourceIE, ViewSourceIE,
) )
from .condenast import CondeNastIE from .condenast import CondeNastIE
from .contv import CONtvIE
from .corus import CorusIE from .corus import CorusIE
from .coub import CoubIE from .coub import CoubIE
from .cozytv import CozyTVIE from .cozytv import CozyTVIE
@ -510,7 +478,6 @@ from .dplay import (
) )
from .drbonanza import DRBonanzaIE from .drbonanza import DRBonanzaIE
from .dreisat import DreiSatIE from .dreisat import DreiSatIE
from .drooble import DroobleIE
from .dropbox import DropboxIE from .dropbox import DropboxIE
from .dropout import ( from .dropout import (
DropoutIE, DropoutIE,
@ -525,10 +492,6 @@ from .drtv import (
DRTVSeriesIE, DRTVSeriesIE,
) )
from .dtube import DTubeIE from .dtube import DTubeIE
from .duboku import (
DubokuIE,
DubokuPlaylistIE,
)
from .dumpert import DumpertIE from .dumpert import DumpertIE
from .duoplay import DuoplayIE from .duoplay import DuoplayIE
from .dvtv import DVTVIE from .dvtv import DVTVIE
@ -546,8 +509,6 @@ from .eggs import (
EggsArtistIE, EggsArtistIE,
EggsIE, EggsIE,
) )
from .eighttracks import EightTracksIE
from .eitb import EitbIE
from .elementorembed import ElementorEmbedIE from .elementorembed import ElementorEmbedIE
from .elonet import ElonetIE from .elonet import ElonetIE
from .elpais import ElPaisIE from .elpais import ElPaisIE
@ -591,7 +552,6 @@ from .europeantour import EuropeanTourIE
from .eurosport import EurosportIE from .eurosport import EurosportIE
from .euscreen import EUScreenIE from .euscreen import EUScreenIE
from .expressen import ExpressenIE from .expressen import ExpressenIE
from .eyedotv import EyedoTVIE
from .facebook import ( from .facebook import (
FacebookAdsIE, FacebookAdsIE,
FacebookIE, FacebookIE,
@ -655,7 +615,6 @@ from .foxnews import (
from .foxsports import FoxSportsIE from .foxsports import FoxSportsIE
from .fptplay import FptplayIE from .fptplay import FptplayIE
from .francaisfacile import FrancaisFacileIE from .francaisfacile import FrancaisFacileIE
from .franceinter import FranceInterIE
from .francetv import ( from .francetv import (
FranceTVIE, FranceTVIE,
FranceTVInfoIE, FranceTVInfoIE,
@ -672,14 +631,10 @@ from .frontendmasters import (
FrontendMastersIE, FrontendMastersIE,
FrontendMastersLessonIE, FrontendMastersLessonIE,
) )
from .fujitv import FujiTVFODPlus7IE
from .funk import FunkIE from .funk import FunkIE
from .funker530 import Funker530IE from .funker530 import Funker530IE
from .fuyintv import FuyinTVIE from .fuyintv import FuyinTVIE
from .gab import ( from .gab import GabIE
GabIE,
GabTVIE,
)
from .gaia import GaiaIE from .gaia import GaiaIE
from .gamedevtv import GameDevTVDashboardIE from .gamedevtv import GameDevTVDashboardIE
from .gamejolt import ( from .gamejolt import (
@ -743,16 +698,10 @@ from .googledrive import (
GoogleDriveFolderIE, GoogleDriveFolderIE,
GoogleDriveIE, GoogleDriveIE,
) )
from .googlepodcasts import (
GooglePodcastsFeedIE,
GooglePodcastsIE,
)
from .googlesearch import GoogleSearchIE from .googlesearch import GoogleSearchIE
from .goplay import GoPlayIE from .goplay import GoPlayIE
from .gopro import GoProIE from .gopro import GoProIE
from .goshgay import GoshgayIE
from .gotostage import GoToStageIE from .gotostage import GoToStageIE
from .gputechconf import GPUTechConfIE
from .graspop import GraspopIE from .graspop import GraspopIE
from .gronkh import ( from .gronkh import (
GronkhFeedIE, GronkhFeedIE,
@ -769,7 +718,6 @@ from .hgtv import HGTVComShowIE
from .hidive import HiDiveIE from .hidive import HiDiveIE
from .historicfilms import HistoricFilmsIE from .historicfilms import HistoricFilmsIE
from .hitrecord import HitRecordIE from .hitrecord import HitRecordIE
from .hketv import HKETVIE
from .hollywoodreporter import ( from .hollywoodreporter import (
HollywoodReporterIE, HollywoodReporterIE,
HollywoodReporterPlaylistIE, HollywoodReporterPlaylistIE,
@ -818,7 +766,6 @@ from .idagio import (
IdagioRecordingIE, IdagioRecordingIE,
IdagioTrackIE, IdagioTrackIE,
) )
from .idolplus import IdolPlusIE
from .ign import ( from .ign import (
IGNIE, IGNIE,
IGNArticleIE, IGNArticleIE,
@ -851,7 +798,6 @@ from .instagram import (
InstagramUserIE, InstagramUserIE,
) )
from .internazionale import InternazionaleIE from .internazionale import InternazionaleIE
from .internetvideoarchive import InternetVideoArchiveIE
from .iprima import ( from .iprima import (
IPrimaCNNIE, IPrimaCNNIE,
IPrimaIE, IPrimaIE,
@ -886,7 +832,6 @@ from .iwara import (
IwaraUserIE, IwaraUserIE,
) )
from .ixigua import IxiguaIE from .ixigua import IxiguaIE
from .izlesene import IzleseneIE
from .jamendo import ( from .jamendo import (
JamendoAlbumIE, JamendoAlbumIE,
JamendoIE, JamendoIE,
@ -939,11 +884,9 @@ from .kika import (
KikaIE, KikaIE,
KikaPlaylistIE, KikaPlaylistIE,
) )
from .kinja import KinjaEmbedIE
from .kinopoisk import KinoPoiskIE from .kinopoisk import KinoPoiskIE
from .kommunetv import KommunetvIE from .kommunetv import KommunetvIE
from .kompas import KompasVideoIE from .kompas import KompasVideoIE
from .koo import KooIE
from .krasview import KrasViewIE from .krasview import KrasViewIE
from .kth import KTHIE from .kth import KTHIE
from .ku6 import Ku6IE from .ku6 import Ku6IE
@ -991,7 +934,6 @@ from .lecturio import (
from .leeco import ( from .leeco import (
LeIE, LeIE,
LePlaylistIE, LePlaylistIE,
LetvCloudIE,
) )
from .lefigaro import ( from .lefigaro import (
LeFigaroVideoEmbedIE, LeFigaroVideoEmbedIE,
@ -1020,11 +962,6 @@ from .liputan6 import Liputan6IE
from .listennotes import ListenNotesIE from .listennotes import ListenNotesIE
from .litv import LiTVIE from .litv import LiTVIE
from .livejournal import LiveJournalIE from .livejournal import LiveJournalIE
from .livestream import (
LivestreamIE,
LivestreamOriginalIE,
LivestreamShortenerIE,
)
from .livestreamfails import LivestreamfailsIE from .livestreamfails import LivestreamfailsIE
from .lnk import LnkIE from .lnk import LnkIE
from .locipo import ( from .locipo import (
@ -1048,10 +985,6 @@ from .lsm import (
LSMReplayIE, LSMReplayIE,
) )
from .lumni import LumniIE from .lumni import LumniIE
from .lynda import (
LyndaCourseIE,
LyndaIE,
)
from .maariv import MaarivIE from .maariv import MaarivIE
from .magellantv import MagellanTVIE from .magellantv import MagellanTVIE
from .magentamusik import MagentaMusikIE from .magentamusik import MagentaMusikIE
@ -1117,13 +1050,11 @@ from .microsoftembed import (
MicrosoftLearnSessionIE, MicrosoftLearnSessionIE,
MicrosoftMediusIE, MicrosoftMediusIE,
) )
from .microsoftstream import MicrosoftStreamIE
from .minds import ( from .minds import (
MindsChannelIE, MindsChannelIE,
MindsGroupIE, MindsGroupIE,
MindsIE, MindsIE,
) )
from .minoto import MinotoIE
from .mir24tv import Mir24TvIE from .mir24tv import Mir24TvIE
from .mirrativ import ( from .mirrativ import (
MirrativIE, MirrativIE,
@ -1157,18 +1088,9 @@ from .mlb import (
from .mlssoccer import MLSSoccerIE from .mlssoccer import MLSSoccerIE
from .mocha import MochaVideoIE from .mocha import MochaVideoIE
from .mojevideo import MojevideoIE from .mojevideo import MojevideoIE
from .mojvideo import MojvideoIE
from .monstercat import MonstercatIE from .monstercat import MonstercatIE
from .motherless import (
MotherlessGalleryIE,
MotherlessGroupIE,
MotherlessIE,
MotherlessUploaderIE,
)
from .motorsport import MotorsportIE from .motorsport import MotorsportIE
from .moviepilot import MoviepilotIE from .moviepilot import MoviepilotIE
from .moview import MoviewPlayIE
from .moviezine import MoviezineIE
from .movingimage import MovingImageIE from .movingimage import MovingImageIE
from .msn import MSNIE from .msn import MSNIE
from .mtv import MTVIE from .mtv import MTVIE
@ -1179,12 +1101,6 @@ from .murrtube import (
) )
from .museai import MuseAIIE from .museai import MuseAIIE
from .musescore import MuseScoreIE from .musescore import MuseScoreIE
from .musicdex import (
MusicdexAlbumIE,
MusicdexArtistIE,
MusicdexPlaylistIE,
MusicdexSongIE,
)
from .mux import MuxIE from .mux import MuxIE
from .mx3 import ( from .mx3 import (
Mx3IE, Mx3IE,
@ -1212,14 +1128,10 @@ from .nate import (
NateIE, NateIE,
NateProgramIE, NateProgramIE,
) )
from .nationalgeographic import ( from .nationalgeographic import NationalGeographicTVIE
NationalGeographicTVIE,
NationalGeographicVideoIE,
)
from .naver import ( from .naver import (
NaverIE, NaverIE,
NaverLiveIE, NaverLiveIE,
NaverNowIE,
) )
from .nba import ( from .nba import (
NBAIE, NBAIE,
@ -1257,7 +1169,6 @@ from .nebula import (
NebulaSubscriptionsIE, NebulaSubscriptionsIE,
) )
from .nekohacker import NekoHackerIE from .nekohacker import NekoHackerIE
from .nerdcubed import NerdCubedFeedIE
from .nest import ( from .nest import (
NestClipIE, NestClipIE,
NestIE, NestIE,
@ -1275,11 +1186,6 @@ from .neteasemusic import (
NetEaseMusicProgramIE, NetEaseMusicProgramIE,
NetEaseMusicSingerIE, NetEaseMusicSingerIE,
) )
from .netverse import (
NetverseIE,
NetversePlaylistIE,
NetverseSearchIE,
)
from .netzkino import NetzkinoIE from .netzkino import NetzkinoIE
from .newgrounds import ( from .newgrounds import (
NewgroundsIE, NewgroundsIE,
@ -1389,11 +1295,6 @@ from .ntvcojp import NTVCoJpCUIE
from .ntvde import NTVDeIE from .ntvde import NTVDeIE
from .ntvru import NTVRuIE from .ntvru import NTVRuIE
from .nubilesporn import NubilesPornIE from .nubilesporn import NubilesPornIE
from .nuum import (
NuumLiveIE,
NuumMediaIE,
NuumTabIE,
)
from .nuvid import NuvidIE from .nuvid import NuvidIE
from .nytimes import ( from .nytimes import (
NYTimesArticleIE, NYTimesArticleIE,
@ -1426,7 +1327,6 @@ from .onet import (
OnetMVPIE, OnetMVPIE,
OnetPlIE, OnetPlIE,
) )
from .onionstudios import OnionStudiosIE
from .onsen import OnsenIE from .onsen import OnsenIE
from .opencast import ( from .opencast import (
OpencastIE, OpencastIE,
@ -1437,7 +1337,6 @@ from .openrec import (
OpenRecIE, OpenRecIE,
OpenRecMovieIE, OpenRecMovieIE,
) )
from .ora import OraTVIE
from .orf import ( from .orf import (
ORFIPTVIE, ORFIPTVIE,
ORFONIE, ORFONIE,
@ -1511,26 +1410,18 @@ from .pinterest import (
PinterestCollectionIE, PinterestCollectionIE,
PinterestIE, PinterestIE,
) )
from .piramidetv import (
PiramideTVChannelIE,
PiramideTVIE,
)
from .planetmarathi import PlanetMarathiIE
from .platzi import ( from .platzi import (
PlatziCourseIE, PlatziCourseIE,
PlatziIE, PlatziIE,
) )
from .playerfm import PlayerFmIE from .playerfm import PlayerFmIE
from .playplustv import PlayPlusTVIE
from .playsuisse import PlaySuisseIE from .playsuisse import PlaySuisseIE
from .playtvak import PlaytvakIE from .playtvak import PlaytvakIE
from .playwire import PlaywireIE
from .pluralsight import ( from .pluralsight import (
PluralsightCourseIE, PluralsightCourseIE,
PluralsightIE, PluralsightIE,
) )
from .plutotv import PlutoTVIE from .plutotv import PlutoTVIE
from .plvideo import PlVideoIE
from .plyr import PlyrEmbedIE from .plyr import PlyrEmbedIE
from .podbayfm import ( from .podbayfm import (
PodbayFMChannelIE, PodbayFMChannelIE,
@ -1574,7 +1465,6 @@ from .prankcast import (
from .premiershiprugby import PremiershipRugbyIE from .premiershiprugby import PremiershipRugbyIE
from .presstv import PressTVIE from .presstv import PressTVIE
from .projectveritas import ProjectVeritasIE from .projectveritas import ProjectVeritasIE
from .prosiebensat1 import ProSiebenSat1IE
from .prx import ( from .prx import (
PRXAccountIE, PRXAccountIE,
PRXSeriesIE, PRXSeriesIE,
@ -1586,7 +1476,6 @@ from .puhutv import (
PuhuTVIE, PuhuTVIE,
PuhuTVSerieIE, PuhuTVSerieIE,
) )
from .puls4 import Puls4IE
from .pyvideo import PyvideoIE from .pyvideo import PyvideoIE
from .qdance import QDanceIE from .qdance import QDanceIE
from .qingting import QingTingIE from .qingting import QingTingIE
@ -1610,10 +1499,6 @@ from .radiocanada import (
RadioCanadaAudioVideoIE, RadioCanadaAudioVideoIE,
RadioCanadaIE, RadioCanadaIE,
) )
from .radiocomercial import (
RadioComercialIE,
RadioComercialPlaylistIE,
)
from .radiode import RadioDeIE from .radiode import RadioDeIE
from .radiofrance import ( from .radiofrance import (
FranceCultureIE, FranceCultureIE,
@ -1678,7 +1563,6 @@ from .redbulltv import (
RedBullTVRrnContentIE, RedBullTVRrnContentIE,
) )
from .reddit import RedditIE from .reddit import RedditIE
from .redge import RedCDNLivxIE
from .redgifs import ( from .redgifs import (
RedGifsIE, RedGifsIE,
RedGifsSearchIE, RedGifsSearchIE,
@ -1692,13 +1576,11 @@ from .rentv import (
from .restudy import RestudyIE from .restudy import RestudyIE
from .reuters import ReutersIE from .reuters import ReutersIE
from .reverbnation import ReverbNationIE from .reverbnation import ReverbNationIE
from .rheinmaintv import RheinMainTVIE
from .ridehome import RideHomeIE from .ridehome import RideHomeIE
from .rinsefm import ( from .rinsefm import (
RinseFMArtistPlaylistIE, RinseFMArtistPlaylistIE,
RinseFMIE, RinseFMIE,
) )
from .rmcdecouverte import RMCDecouverteIE
from .rockstargames import RockstarGamesIE from .rockstargames import RockstarGamesIE
from .rokfin import ( from .rokfin import (
RokfinChannelIE, RokfinChannelIE,
@ -1815,7 +1697,6 @@ from .senategov import (
SenateGovIE, SenateGovIE,
SenateISVPIE, SenateISVPIE,
) )
from .sendtonews import SendtoNewsIE
from .servus import ServusIE from .servus import ServusIE
from .sevenplus import SevenPlusIE from .sevenplus import SevenPlusIE
from .sexu import SexuIE from .sexu import SexuIE
@ -1828,7 +1709,6 @@ from .shahid import (
ShahidShowIE, ShahidShowIE,
) )
from .sharepoint import SharePointIE from .sharepoint import SharePointIE
from .sharevideos import ShareVideosEmbedIE
from .shemaroome import ShemarooMeIE from .shemaroome import ShemarooMeIE
from .shiey import ShieyIE from .shiey import ShieyIE
from .showroomlive import ShowRoomLiveIE from .showroomlive import ShowRoomLiveIE
@ -1873,7 +1753,6 @@ from .smotrim import (
SmotrimPlaylistIE, SmotrimPlaylistIE,
) )
from .snapchat import SnapchatSpotlightIE from .snapchat import SnapchatSpotlightIE
from .snotr import SnotrIE
from .softwhiteunderbelly import SoftWhiteUnderbellyIE from .softwhiteunderbelly import SoftWhiteUnderbellyIE
from .sohu import ( from .sohu import (
SohuIE, SohuIE,
@ -1923,7 +1802,6 @@ from .spreaker import (
SpreakerIE, SpreakerIE,
SpreakerShowIE, SpreakerShowIE,
) )
from .springboardplatform import SpringboardPlatformIE
from .sproutvideo import ( from .sproutvideo import (
SproutVideoIE, SproutVideoIE,
VidsIoIE, VidsIoIE,
@ -1940,7 +1818,6 @@ from .stacommu import (
TheaterComplexTownVODIE, TheaterComplexTownVODIE,
) )
from .stageplus import StagePlusVODConcertIE from .stageplus import StagePlusVODConcertIE
from .stanfordoc import StanfordOpenClassroomIE
from .startrek import StarTrekIE from .startrek import StarTrekIE
from .startv import StarTVIE from .startv import StarTVIE
from .steam import ( from .steam import (
@ -1948,10 +1825,6 @@ from .steam import (
SteamCommunityIE, SteamCommunityIE,
SteamIE, SteamIE,
) )
from .stitcher import (
StitcherIE,
StitcherShowIE,
)
from .storyfire import ( from .storyfire import (
StoryFireIE, StoryFireIE,
StoryFireSeriesIE, StoryFireSeriesIE,
@ -1961,7 +1834,6 @@ from .streaks import StreaksIE
from .streamable import StreamableIE from .streamable import StreamableIE
from .streamcz import StreamCZIE from .streamcz import StreamCZIE
from .streetvoice import StreetVoiceIE from .streetvoice import StreetVoiceIE
from .stretchinternet import StretchInternetIE
from .stripchat import StripchatIE from .stripchat import StripchatIE
from .stv import STVPlayerIE from .stv import STVPlayerIE
from .subsplash import ( from .subsplash import (
@ -1979,8 +1851,6 @@ from .svt import (
SVTPlayIE, SVTPlayIE,
SVTSeriesIE, SVTSeriesIE,
) )
from .swearnet import SwearnetEpisodeIE
from .syvdk import SYVDKIE
from .sztvhu import SztvHuIE from .sztvhu import SztvHuIE
from .tagesschau import TagesschauIE from .tagesschau import TagesschauIE
from .taptap import ( from .taptap import (
@ -2039,10 +1909,6 @@ from .telequebec import (
) )
from .teletask import TeleTaskIE from .teletask import TeleTaskIE
from .telewebion import TelewebionIE from .telewebion import TelewebionIE
from .tempo import (
IVXPlayerIE,
TempoIE,
)
from .tencent import ( from .tencent import (
IflixEpisodeIE, IflixEpisodeIE,
IflixSeriesIE, IflixSeriesIE,
@ -2068,7 +1934,6 @@ from .theguardian import (
TheGuardianPodcastPlaylistIE, TheGuardianPodcastPlaylistIE,
) )
from .thehighwire import TheHighWireIE from .thehighwire import TheHighWireIE
from .theholetv import TheHoleTvIE
from .theintercept import TheInterceptIE from .theintercept import TheInterceptIE
from .theplatform import ( from .theplatform import (
ThePlatformFeedIE, ThePlatformFeedIE,
@ -2120,12 +1985,6 @@ from .toypics import (
ToypicsIE, ToypicsIE,
ToypicsUserIE, ToypicsUserIE,
) )
from .traileraddict import TrailerAddictIE
from .triller import (
TrillerIE,
TrillerShortIE,
TrillerUserIE,
)
from .trovo import ( from .trovo import (
TrovoChannelClipIE, TrovoChannelClipIE,
TrovoChannelVodIE, TrovoChannelVodIE,
@ -2208,7 +2067,6 @@ from .tvplay import (
TVPlayHomeIE, TVPlayHomeIE,
TVPlayIE, TVPlayIE,
) )
from .tvplayer import TVPlayerIE
from .tvw import ( from .tvw import (
TvwIE, TvwIE,
TvwNewsIE, TvwNewsIE,
@ -2248,12 +2106,8 @@ from .udemy import (
UdemyIE, UdemyIE,
) )
from .udn import UDNEmbedIE from .udn import UDNEmbedIE
from .ufctv import ( from .ufctv import UFCTVIE
UFCTVIE,
UFCArabiaIE,
)
from .ukcolumn import UkColumnIE from .ukcolumn import UkColumnIE
from .uktvplay import UKTVPlayIE
from .uliza import ( from .uliza import (
UlizaPlayerIE, UlizaPlayerIE,
UlizaPortalIE, UlizaPortalIE,
@ -2283,7 +2137,6 @@ from .ustudio import (
UstudioEmbedIE, UstudioEmbedIE,
UstudioIE, UstudioIE,
) )
from .utreon import UtreonIE
from .varzesh3 import Varzesh3IE from .varzesh3 import Varzesh3IE
from .vbox7 import Vbox7IE from .vbox7 import Vbox7IE
from .veo import VeoIE from .veo import VeoIE
@ -2308,20 +2161,7 @@ from .videocampus_sachsen import (
VideocampusSachsenIE, VideocampusSachsenIE,
ViMPPlaylistIE, ViMPPlaylistIE,
) )
from .videodetective import VideoDetectiveIE from .videoken import VideoKenPlayerIE
from .videofyme import VideofyMeIE
from .videoken import (
VideoKenCategoryIE,
VideoKenIE,
VideoKenPlayerIE,
VideoKenPlaylistIE,
VideoKenTopicIE,
)
from .videomore import (
VideomoreIE,
VideomoreSeasonIE,
VideomoreVideoIE,
)
from .videopress import VideoPressIE from .videopress import VideoPressIE
from .vidflex import VidflexIE from .vidflex import VidflexIE
from .vidio import ( from .vidio import (
@ -2351,10 +2191,6 @@ from .vimeo import (
VimeoUserIE, VimeoUserIE,
VimeoWatchLaterIE, VimeoWatchLaterIE,
) )
from .vimm import (
VimmIE,
VimmRecordingIE,
)
from .viously import ViouslyIE from .viously import ViouslyIE
from .viqeo import ViqeoIE from .viqeo import ViqeoIE
from .visir import VisirIE from .visir import VisirIE
@ -2372,7 +2208,6 @@ from .vk import (
VKWallPostIE, VKWallPostIE,
) )
from .vocaroo import VocarooIE from .vocaroo import VocarooIE
from .vodpl import VODPlIE
from .vodplatform import VODPlatformIE from .vodplatform import VODPlatformIE
from .voicy import ( from .voicy import (
VoicyChannelIE, VoicyChannelIE,
@ -2404,11 +2239,6 @@ from .vtv import (
VTVIE, VTVIE,
VTVGoIE, VTVGoIE,
) )
from .vuclip import VuClipIE
from .vvvvid import (
VVVVIDIE,
VVVVIDShowIE,
)
from .walla import WallaIE from .walla import WallaIE
from .washingtonpost import ( from .washingtonpost import (
WashingtonPostArticleIE, WashingtonPostArticleIE,
@ -2418,7 +2248,6 @@ from .wat import WatIE
from .wdr import ( from .wdr import (
WDRIE, WDRIE,
WDRElefantIE, WDRElefantIE,
WDRMobileIE,
WDRPageIE, WDRPageIE,
) )
from .webcamerapl import WebcameraplIE from .webcamerapl import WebcameraplIE
@ -2445,7 +2274,6 @@ from .weverse import (
WeverseMomentIE, WeverseMomentIE,
) )
from .wevidi import WeVidiIE from .wevidi import WeVidiIE
from .weyyak import WeyyakIE
from .whowatch import WhoWatchIE from .whowatch import WhoWatchIE
from .whyp import WhypIE from .whyp import WhypIE
from .wikimedia import WikimediaIE from .wikimedia import WikimediaIE
@ -2494,7 +2322,6 @@ from .ximalaya import (
from .xinpianchang import XinpianchangIE from .xinpianchang import XinpianchangIE
from .xminus import XMinusIE from .xminus import XMinusIE
from .xnxx import XNXXIE from .xnxx import XNXXIE
from .xstream import XstreamIE
from .xvideos import ( from .xvideos import (
XVideosIE, XVideosIE,
XVideosQuickiesIE, XVideosQuickiesIE,
@ -2618,10 +2445,6 @@ from .zdf import (
ZDFIE, ZDFIE,
ZDFChannelIE, ZDFChannelIE,
) )
from .zee5 import (
Zee5IE,
Zee5SeriesIE,
)
from .zeenews import ZeeNewsIE from .zeenews import ZeeNewsIE
from .zenporn import ZenPornIE from .zenporn import ZenPornIE
from .zetland import ZetlandDKArticleIE from .zetland import ZetlandDKArticleIE

View File

@ -407,7 +407,7 @@ class AbemaTVIE(AbemaTVBaseIE):
if is_live: if is_live:
self.report_warning("This is a livestream; yt-dlp doesn't support downloading natively, but FFmpeg cannot handle m3u8 manifests from AbemaTV") self.report_warning("This is a livestream; yt-dlp doesn't support downloading natively, but FFmpeg cannot handle m3u8 manifests from AbemaTV")
self.report_warning('Please consider using Streamlink to download these streams (https://github.com/streamlink/streamlink)') self.report_warning('Please consider using Streamlink to download these streams (https://github.com/streamlink/streamlink)')
formats = self._extract_m3u8_formats( formats, subtitles = self._extract_m3u8_formats_and_subtitles(
m3u8_url, video_id, ext='mp4', live=is_live) m3u8_url, video_id, ext='mp4', live=is_live)
info.update({ info.update({
@ -415,6 +415,7 @@ class AbemaTVIE(AbemaTVBaseIE):
'title': title, 'title': title,
'description': description, 'description': description,
'formats': formats, 'formats': formats,
'subtitles': subtitles,
'is_live': is_live, 'is_live': is_live,
'availability': availability, 'availability': availability,
}) })

View File

@ -1,96 +0,0 @@
from .common import InfoExtractor
from .youtube import YoutubeIE
from ..utils import (
determine_ext,
int_or_none,
mimetype2ext,
parse_iso8601,
traverse_obj,
)
class AirTVIE(InfoExtractor):
_VALID_URL = r'https?://www\.air\.tv/watch\?v=(?P<id>\w+)'
_TESTS = [{
# without youtube_id
'url': 'https://www.air.tv/watch?v=W87jcWleSn2hXZN47zJZsQ',
'info_dict': {
'id': 'W87jcWleSn2hXZN47zJZsQ',
'ext': 'mp4',
'release_date': '20221003',
'release_timestamp': 1664792603,
'channel_id': 'vgfManQlRQKgoFQ8i8peFQ',
'title': 'md5:c12d49ed367c3dadaa67659aff43494c',
'upload_date': '20221003',
'duration': 151,
'view_count': int,
'thumbnail': 'https://cdn-sp-gcs.air.tv/videos/W/8/W87jcWleSn2hXZN47zJZsQ/b13fc56464f47d9d62a36d110b9b5a72-4096x2160_9.jpg',
'timestamp': 1664792603,
},
}, {
# with youtube_id
'url': 'https://www.air.tv/watch?v=sv57EC8tRXG6h8dNXFUU1Q',
'info_dict': {
'id': '2ZTqmpee-bQ',
'ext': 'mp4',
'comment_count': int,
'tags': 'count:11',
'channel_follower_count': int,
'like_count': int,
'uploader': 'Newsflare',
'thumbnail': 'https://i.ytimg.com/vi_webp/2ZTqmpee-bQ/maxresdefault.webp',
'availability': 'public',
'title': 'Geese Chase Alligator Across Golf Course',
'uploader_id': 'NewsflareBreaking',
'channel_url': 'https://www.youtube.com/channel/UCzSSoloGEz10HALUAbYhngQ',
'description': 'md5:99b21d9cea59330149efbd9706e208f5',
'age_limit': 0,
'channel_id': 'UCzSSoloGEz10HALUAbYhngQ',
'uploader_url': 'http://www.youtube.com/user/NewsflareBreaking',
'view_count': int,
'categories': ['News & Politics'],
'live_status': 'not_live',
'playable_in_embed': True,
'channel': 'Newsflare',
'duration': 37,
'upload_date': '20180511',
},
}]
def _get_formats_and_subtitle(self, json_data, video_id):
formats, subtitles = [], {}
for source in traverse_obj(json_data, 'sources', 'sources_desktop', ...):
ext = determine_ext(source.get('src'), mimetype2ext(source.get('type')))
if ext == 'm3u8':
fmts, subs = self._extract_m3u8_formats_and_subtitles(source.get('src'), video_id)
formats.extend(fmts)
self._merge_subtitles(subs, target=subtitles)
else:
formats.append({'url': source.get('src'), 'ext': ext})
return formats, subtitles
def _real_extract(self, url):
display_id = self._match_id(url)
webpage = self._download_webpage(url, display_id)
nextjs_json = self._search_nextjs_data(webpage, display_id)['props']['pageProps']['initialState']['videos'][display_id]
if nextjs_json.get('youtube_id'):
return self.url_result(
f'https://www.youtube.com/watch?v={nextjs_json.get("youtube_id")}', YoutubeIE)
formats, subtitles = self._get_formats_and_subtitle(nextjs_json, display_id)
return {
'id': display_id,
'title': nextjs_json.get('title') or self._html_search_meta('og:title', webpage),
'formats': formats,
'subtitles': subtitles,
'description': nextjs_json.get('description') or None,
'duration': int_or_none(nextjs_json.get('duration')),
'thumbnails': [
{'url': thumbnail}
for thumbnail in traverse_obj(nextjs_json, ('default_thumbnails', ...))],
'channel_id': traverse_obj(nextjs_json, 'channel', 'channel_slug'),
'timestamp': parse_iso8601(nextjs_json.get('created')),
'release_timestamp': parse_iso8601(nextjs_json.get('published')),
'view_count': int_or_none(nextjs_json.get('views')),
}

View File

@ -1,83 +0,0 @@
from .common import InfoExtractor
from ..utils import (
clean_html,
dict_get,
get_element_by_class,
int_or_none,
unified_strdate,
url_or_none,
)
class Alsace20TVBaseIE(InfoExtractor):
def _extract_video(self, video_id, url=None):
info = self._download_json(
f'https://www.alsace20.tv/visionneuse/visio_v9_js.php?key={video_id}&habillage=0&mode=html',
video_id) or {}
title = info.get('titre')
formats = []
for res, fmt_url in (info.get('files') or {}).items():
formats.extend(
self._extract_smil_formats(fmt_url, video_id, fatal=False)
if '/smil:_' in fmt_url
else self._extract_mpd_formats(fmt_url, video_id, mpd_id=res, fatal=False))
webpage = (url and self._download_webpage(url, video_id, fatal=False)) or ''
thumbnail = url_or_none(dict_get(info, ('image', 'preview')) or self._og_search_thumbnail(webpage))
upload_date = self._search_regex(r'/(\d{6})_', thumbnail, 'upload_date', default=None)
upload_date = unified_strdate(f'20{upload_date[:2]}-{upload_date[2:4]}-{upload_date[4:]}') if upload_date else None
return {
'id': video_id,
'title': title,
'formats': formats,
'description': clean_html(get_element_by_class('wysiwyg', webpage)),
'upload_date': upload_date,
'thumbnail': thumbnail,
'duration': int_or_none(self._og_search_property('video:duration', webpage) if webpage else None),
'view_count': int_or_none(info.get('nb_vues')),
}
class Alsace20TVIE(Alsace20TVBaseIE):
_VALID_URL = r'https?://(?:www\.)?alsace20\.tv/(?:[\w-]+/)+[\w-]+-(?P<id>[\w]+)'
_TESTS = [{
'url': 'https://www.alsace20.tv/VOD/Actu/JT/Votre-JT-jeudi-3-fevrier-lyNHCXpYJh.html',
'info_dict': {
'id': 'lyNHCXpYJh',
'ext': 'mp4',
'description': 'md5:fc0bc4a0692d3d2dba4524053de4c7b7',
'title': 'Votre JT du jeudi 3 février',
'upload_date': '20220203',
'thumbnail': r're:https?://.+\.jpg',
'duration': 1073,
'view_count': int,
},
}]
def _real_extract(self, url):
video_id = self._match_id(url)
return self._extract_video(video_id, url)
class Alsace20TVEmbedIE(Alsace20TVBaseIE):
_VALID_URL = r'https?://(?:www\.)?alsace20\.tv/emb/(?P<id>[\w]+)'
_TESTS = [{
'url': 'https://www.alsace20.tv/emb/lyNHCXpYJh',
# 'md5': 'd91851bf9af73c0ad9b2cdf76c127fbb',
'info_dict': {
'id': 'lyNHCXpYJh',
'ext': 'mp4',
'title': 'Votre JT du jeudi 3 février',
'upload_date': '20220203',
'thumbnail': r're:https?://.+\.jpg',
'view_count': int,
},
'params': {
'format': 'bestvideo',
},
}]
def _real_extract(self, url):
video_id = self._match_id(url)
return self._extract_video(video_id)

View File

@ -1,98 +0,0 @@
from .common import InfoExtractor
from ..utils import (
clean_html,
float_or_none,
int_or_none,
str_or_none,
traverse_obj,
unified_timestamp,
)
class AnchorFMEpisodeIE(InfoExtractor):
_VALID_URL = r'https?://anchor\.fm/(?P<channel_name>\w+)/(?:embed/)?episodes/[\w-]+-(?P<episode_id>\w+)'
_EMBED_REGEX = [rf'<iframe[^>]+\bsrc=[\'"](?P<url>{_VALID_URL})']
_TESTS = [{
'url': 'https://anchor.fm/lovelyti/episodes/Chrisean-Rock-takes-to-twitter-to-announce-shes-pregnant--Blueface-denies-he-is-the-father-e1tpt3d',
'info_dict': {
'id': 'e1tpt3d',
'ext': 'mp3',
'title': ' Chrisean Rock takes to twitter to announce she\'s pregnant, Blueface denies he is the father!',
'description': 'md5:207d167de3e28ceb4ddc1ebf5a30044c',
'thumbnail': 'https://s3-us-west-2.amazonaws.com/anchor-generated-image-bank/production/podcast_uploaded_nologo/1034827/1034827-1658438968460-5f3bfdf3601e8.jpg',
'duration': 624.718,
'uploader': 'Lovelyti ',
'uploader_id': '991541',
'channel': 'lovelyti',
'modified_date': '20230121',
'modified_timestamp': 1674285178,
'release_date': '20230121',
'release_timestamp': 1674285179,
'episode_id': 'e1tpt3d',
},
}, {
# embed url
'url': 'https://anchor.fm/apakatatempo/embed/episodes/S2E75-Perang-Bintang-di-Balik-Kasus-Ferdy-Sambo-dan-Ismail-Bolong-e1shjqd',
'info_dict': {
'id': 'e1shjqd',
'ext': 'mp3',
'title': 'S2E75 Perang Bintang di Balik Kasus Ferdy Sambo dan Ismail Bolong',
'description': 'md5:9e95ad9293bf00178bf8d33e9cb92c41',
'duration': 1042.008,
'thumbnail': 'https://s3-us-west-2.amazonaws.com/anchor-generated-image-bank/production/podcast_uploaded_episode400/2627805/2627805-1671590688729-4db3882ac9e4b.jpg',
'release_date': '20221221',
'release_timestamp': 1671595916,
'modified_date': '20221221',
'modified_timestamp': 1671590834,
'channel': 'apakatatempo',
'uploader': 'Podcast Tempo',
'uploader_id': '2585461',
'season': 'Season 2',
'season_number': 2,
'episode_id': 'e1shjqd',
},
}]
_WEBPAGE_TESTS = [{
'url': 'https://podcast.tempo.co/podcast/192/perang-bintang-di-balik-kasus-ferdy-sambo-dan-ismail-bolong',
'info_dict': {
'id': 'e1shjqd',
'ext': 'mp3',
'release_date': '20221221',
'duration': 1042.008,
'season': 'Season 2',
'modified_timestamp': 1671590834,
'uploader_id': '2585461',
'modified_date': '20221221',
'description': 'md5:9e95ad9293bf00178bf8d33e9cb92c41',
'season_number': 2,
'title': 'S2E75 Perang Bintang di Balik Kasus Ferdy Sambo dan Ismail Bolong',
'release_timestamp': 1671595916,
'episode_id': 'e1shjqd',
'thumbnail': 'https://s3-us-west-2.amazonaws.com/anchor-generated-image-bank/production/podcast_uploaded_episode400/2627805/2627805-1671590688729-4db3882ac9e4b.jpg',
'uploader': 'Podcast Tempo',
'channel': 'apakatatempo',
},
}]
def _real_extract(self, url):
channel_name, episode_id = self._match_valid_url(url).group('channel_name', 'episode_id')
api_data = self._download_json(f'https://anchor.fm/api/v3/episodes/{episode_id}', episode_id)
return {
'id': episode_id,
'title': traverse_obj(api_data, ('episode', 'title')),
'url': traverse_obj(api_data, ('episode', 'episodeEnclosureUrl'), ('episodeAudios', 0, 'url')),
'ext': 'mp3',
'vcodec': 'none',
'thumbnail': traverse_obj(api_data, ('episode', 'episodeImage')),
'description': clean_html(traverse_obj(api_data, ('episode', ('description', 'descriptionPreview')), get_all=False)),
'duration': float_or_none(traverse_obj(api_data, ('episode', 'duration')), 1000),
'modified_timestamp': unified_timestamp(traverse_obj(api_data, ('episode', 'modified'))),
'release_timestamp': int_or_none(traverse_obj(api_data, ('episode', 'publishOnUnixTimestamp'))),
'episode_id': episode_id,
'uploader': traverse_obj(api_data, ('creator', 'name')),
'uploader_id': str_or_none(traverse_obj(api_data, ('creator', 'userId'))),
'season_number': int_or_none(traverse_obj(api_data, ('episode', 'podcastSeasonNumber'))),
'channel': channel_name or traverse_obj(api_data, ('creator', 'vanitySlug')),
}

View File

@ -1,277 +0,0 @@
import json
import re
import urllib.parse
from .common import InfoExtractor
from ..utils import (
int_or_none,
parse_duration,
unified_strdate,
)
class AppleTrailersIE(InfoExtractor):
IE_NAME = 'appletrailers'
_VALID_URL = r'https?://(?:www\.|movie)?trailers\.apple\.com/(?:trailers|ca)/(?P<company>[^/]+)/(?P<movie>[^/]+)'
_TESTS = [{
'url': 'http://trailers.apple.com/trailers/wb/manofsteel/',
'info_dict': {
'id': '5111',
'title': 'Man of Steel',
},
'playlist': [
{
'md5': 'd97a8e575432dbcb81b7c3acb741f8a8',
'info_dict': {
'id': 'manofsteel-trailer4',
'ext': 'mov',
'duration': 111,
'title': 'Trailer 4',
'upload_date': '20130523',
'uploader_id': 'wb',
},
},
{
'md5': 'b8017b7131b721fb4e8d6f49e1df908c',
'info_dict': {
'id': 'manofsteel-trailer3',
'ext': 'mov',
'duration': 182,
'title': 'Trailer 3',
'upload_date': '20130417',
'uploader_id': 'wb',
},
},
{
'md5': 'd0f1e1150989b9924679b441f3404d48',
'info_dict': {
'id': 'manofsteel-trailer',
'ext': 'mov',
'duration': 148,
'title': 'Trailer',
'upload_date': '20121212',
'uploader_id': 'wb',
},
},
{
'md5': '5fe08795b943eb2e757fa95cb6def1cb',
'info_dict': {
'id': 'manofsteel-teaser',
'ext': 'mov',
'duration': 93,
'title': 'Teaser',
'upload_date': '20120721',
'uploader_id': 'wb',
},
},
],
}, {
'url': 'http://trailers.apple.com/trailers/magnolia/blackthorn/',
'info_dict': {
'id': '4489',
'title': 'Blackthorn',
},
'playlist_mincount': 2,
'expected_warnings': ['Unable to download JSON metadata'],
}, {
# json data only available from http://trailers.apple.com/trailers/feeds/data/15881.json
'url': 'http://trailers.apple.com/trailers/fox/kungfupanda3/',
'info_dict': {
'id': '15881',
'title': 'Kung Fu Panda 3',
},
'playlist_mincount': 4,
}, {
'url': 'http://trailers.apple.com/ca/metropole/autrui/',
'only_matching': True,
}, {
'url': 'http://movietrailers.apple.com/trailers/focus_features/kuboandthetwostrings/',
'only_matching': True,
}]
_JSON_RE = r'iTunes.playURL\((.*?)\);'
def _real_extract(self, url):
mobj = self._match_valid_url(url)
movie = mobj.group('movie')
uploader_id = mobj.group('company')
webpage = self._download_webpage(url, movie)
film_id = self._search_regex(r"FilmId\s*=\s*'(\d+)'", webpage, 'film id')
film_data = self._download_json(
f'http://trailers.apple.com/trailers/feeds/data/{film_id}.json',
film_id, fatal=False)
if film_data:
entries = []
for clip in film_data.get('clips', []):
clip_title = clip['title']
formats = []
for version, version_data in clip.get('versions', {}).items():
for size, size_data in version_data.get('sizes', {}).items():
src = size_data.get('src')
if not src:
continue
formats.append({
'format_id': f'{version}-{size}',
'url': re.sub(r'_(\d+p\.mov)', r'_h\1', src),
'width': int_or_none(size_data.get('width')),
'height': int_or_none(size_data.get('height')),
'language': version[:2],
})
entries.append({
'id': movie + '-' + re.sub(r'[^a-zA-Z0-9]', '', clip_title).lower(),
'formats': formats,
'title': clip_title,
'thumbnail': clip.get('screen') or clip.get('thumb'),
'duration': parse_duration(clip.get('runtime') or clip.get('faded')),
'upload_date': unified_strdate(clip.get('posted')),
'uploader_id': uploader_id,
})
page_data = film_data.get('page', {})
return self.playlist_result(entries, film_id, page_data.get('movie_title'))
playlist_url = urllib.parse.urljoin(url, 'includes/playlists/itunes.inc')
def fix_html(s):
s = re.sub(r'(?s)<script[^<]*?>.*?</script>', '', s)
s = re.sub(r'<img ([^<]*?)/?>', r'<img \1/>', s)
# The ' in the onClick attributes are not escaped, it couldn't be parsed
# like: http://trailers.apple.com/trailers/wb/gravity/
def _clean_json(m):
return 'iTunes.playURL({});'.format(m.group(1).replace('\'', '&#39;'))
s = re.sub(self._JSON_RE, _clean_json, s)
return f'<html>{s}</html>'
doc = self._download_xml(playlist_url, movie, transform_source=fix_html)
playlist = []
for li in doc.findall('./div/ul/li'):
on_click = li.find('.//a').attrib['onClick']
trailer_info_json = self._search_regex(self._JSON_RE,
on_click, 'trailer info')
trailer_info = json.loads(trailer_info_json)
first_url = trailer_info.get('url')
if not first_url:
continue
title = trailer_info['title']
video_id = movie + '-' + re.sub(r'[^a-zA-Z0-9]', '', title).lower()
thumbnail = li.find('.//img').attrib['src']
upload_date = trailer_info['posted'].replace('-', '')
runtime = trailer_info['runtime']
m = re.search(r'(?P<minutes>[0-9]+):(?P<seconds>[0-9]{1,2})', runtime)
duration = None
if m:
duration = 60 * int(m.group('minutes')) + int(m.group('seconds'))
trailer_id = first_url.split('/')[-1].rpartition('_')[0].lower()
settings_json_url = urllib.parse.urljoin(url, f'includes/settings/{trailer_id}.json')
settings = self._download_json(settings_json_url, trailer_id, 'Downloading settings json')
formats = []
for fmt in settings['metadata']['sizes']:
# The src is a file pointing to the real video file
format_url = re.sub(r'_(\d*p\.mov)', r'_h\1', fmt['src'])
formats.append({
'url': format_url,
'format': fmt['type'],
'width': int_or_none(fmt['width']),
'height': int_or_none(fmt['height']),
})
playlist.append({
'_type': 'video',
'id': video_id,
'formats': formats,
'title': title,
'duration': duration,
'thumbnail': thumbnail,
'upload_date': upload_date,
'uploader_id': uploader_id,
'http_headers': {
'User-Agent': 'QuickTime compatible (yt-dlp)',
},
})
return {
'_type': 'playlist',
'id': movie,
'entries': playlist,
}
class AppleTrailersSectionIE(InfoExtractor):
IE_NAME = 'appletrailers:section'
_SECTIONS = {
'justadded': {
'feed_path': 'just_added',
'title': 'Just Added',
},
'exclusive': {
'feed_path': 'exclusive',
'title': 'Exclusive',
},
'justhd': {
'feed_path': 'just_hd',
'title': 'Just HD',
},
'mostpopular': {
'feed_path': 'most_pop',
'title': 'Most Popular',
},
'moviestudios': {
'feed_path': 'studios',
'title': 'Movie Studios',
},
}
_VALID_URL = r'https?://(?:www\.)?trailers\.apple\.com/#section=(?P<id>{})'.format('|'.join(_SECTIONS))
_TESTS = [{
'url': 'http://trailers.apple.com/#section=justadded',
'info_dict': {
'title': 'Just Added',
'id': 'justadded',
},
'playlist_mincount': 80,
}, {
'url': 'http://trailers.apple.com/#section=exclusive',
'info_dict': {
'title': 'Exclusive',
'id': 'exclusive',
},
'playlist_mincount': 80,
}, {
'url': 'http://trailers.apple.com/#section=justhd',
'info_dict': {
'title': 'Just HD',
'id': 'justhd',
},
'playlist_mincount': 80,
}, {
'url': 'http://trailers.apple.com/#section=mostpopular',
'info_dict': {
'title': 'Most Popular',
'id': 'mostpopular',
},
'playlist_mincount': 30,
}, {
'url': 'http://trailers.apple.com/#section=moviestudios',
'info_dict': {
'title': 'Movie Studios',
'id': 'moviestudios',
},
'playlist_mincount': 80,
}]
def _real_extract(self, url):
section = self._match_id(url)
section_data = self._download_json(
'http://trailers.apple.com/trailers/home/feeds/{}.json'.format(self._SECTIONS[section]['feed_path']),
section)
entries = [
self.url_result('http://trailers.apple.com' + e['location'])
for e in section_data]
return self.playlist_result(entries, section, self._SECTIONS[section]['title'])

View File

@ -1,107 +0,0 @@
import datetime as dt
from .common import InfoExtractor
from ..utils import (
ExtractorError,
float_or_none,
jwt_encode,
try_get,
)
class ATVAtIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?atv\.at/tv/(?:[^/]+/){2,3}(?P<id>.*)'
_TESTS = [{
'url': 'https://www.atv.at/tv/bauer-sucht-frau/staffel-18/bauer-sucht-frau/bauer-sucht-frau-staffel-18-folge-3-die-hofwochen',
'md5': '3c3b4aaca9f63e32b35e04a9c2515903',
'info_dict': {
'id': 'v-ce9cgn1e70n5-1',
'ext': 'mp4',
'title': 'Bauer sucht Frau - Staffel 18 Folge 3 - Die Hofwochen',
},
}, {
'url': 'https://www.atv.at/tv/bauer-sucht-frau/staffel-18/episode-01/bauer-sucht-frau-staffel-18-vorstellungsfolge-1',
'only_matching': True,
}]
# extracted from bootstrap.js function (search for e.encryption_key and use your browser's debugger)
_ACCESS_ID = 'x_atv'
_ENCRYPTION_KEY = 'Hohnaekeishoogh2omaeghooquooshia'
def _extract_video_info(self, url, content, video):
clip_id = content.get('splitId', content['id'])
formats = []
clip_urls = video['urls']
for protocol, variant in clip_urls.items():
source_url = try_get(variant, lambda x: x['clear']['url'])
if not source_url:
continue
if protocol == 'dash':
formats.extend(self._extract_mpd_formats(
source_url, clip_id, mpd_id=protocol, fatal=False))
elif protocol == 'hls':
formats.extend(self._extract_m3u8_formats(
source_url, clip_id, 'mp4', 'm3u8_native',
m3u8_id=protocol, fatal=False))
else:
formats.append({
'url': source_url,
'format_id': protocol,
})
return {
'id': clip_id,
'title': content.get('title'),
'duration': float_or_none(content.get('duration')),
'series': content.get('tvShowTitle'),
'formats': formats,
}
def _real_extract(self, url):
video_id = self._match_id(url)
webpage = self._download_webpage(url, video_id)
json_data = self._parse_json(
self._search_regex(r'<script id="state" type="text/plain">(.*)</script>', webpage, 'json_data'),
video_id=video_id)
video_title = json_data['views']['default']['page']['title']
content_resource = json_data['views']['default']['page']['contentResource']
content_id = content_resource[0]['id']
content_ids = [{'id': id_, 'subclip_start': content['start'], 'subclip_end': content['end']}
for id_, content in enumerate(content_resource)]
time_of_request = dt.datetime.now()
not_before = time_of_request - dt.timedelta(minutes=5)
expire = time_of_request + dt.timedelta(minutes=5)
payload = {
'content_ids': {
content_id: content_ids,
},
'secure_delivery': True,
'iat': int(time_of_request.timestamp()),
'nbf': int(not_before.timestamp()),
'exp': int(expire.timestamp()),
}
videos = self._download_json(
'https://vas-v4.p7s1video.net/4.0/getsources',
content_id, 'Downloading videos JSON', query={
'token': jwt_encode(payload, self._ENCRYPTION_KEY, headers={'kid': self._ACCESS_ID}),
})
video_id, videos_data = next(iter(videos['data'].items()))
error_msg = try_get(videos_data, lambda x: x['error']['title'])
if error_msg == 'Geo check failed':
self.raise_geo_restricted(error_msg)
elif error_msg:
raise ExtractorError(error_msg)
entries = [
self._extract_video_info(url, content_resource[video['id']], video)
for video in videos_data]
return {
'_type': 'multi_video',
'id': video_id,
'title': video_title,
'entries': entries,
}

View File

@ -1,181 +0,0 @@
import base64
import urllib.parse
from .common import InfoExtractor
from ..utils import (
format_field,
int_or_none,
parse_iso8601,
smuggle_url,
unsmuggle_url,
urlencode_postdata,
)
class AWAANIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?(?:awaan|dcndigital)\.ae/(?:#/)?show/(?P<show_id>\d+)/[^/]+(?:/(?P<id>\d+)/(?P<season_id>\d+))?'
def _real_extract(self, url):
show_id, video_id, season_id = self._match_valid_url(url).groups()
if video_id and int(video_id) > 0:
return self.url_result(
f'http://awaan.ae/media/{video_id}', 'AWAANVideo')
elif season_id and int(season_id) > 0:
return self.url_result(smuggle_url(
f'http://awaan.ae/program/season/{season_id}',
{'show_id': show_id}), 'AWAANSeason')
else:
return self.url_result(
f'http://awaan.ae/program/{show_id}', 'AWAANSeason')
class AWAANBaseIE(InfoExtractor):
def _parse_video_data(self, video_data, video_id, is_live):
title = video_data.get('title_en') or video_data['title_ar']
img = video_data.get('img')
return {
'id': video_id,
'title': title,
'description': video_data.get('description_en') or video_data.get('description_ar'),
'thumbnail': format_field(img, None, 'http://admin.mangomolo.com/analytics/%s'),
'duration': int_or_none(video_data.get('duration')),
'timestamp': parse_iso8601(video_data.get('create_time'), ' '),
'is_live': is_live,
'uploader_id': video_data.get('user_id'),
}
class AWAANVideoIE(AWAANBaseIE):
IE_NAME = 'awaan:video'
_VALID_URL = r'https?://(?:www\.)?(?:awaan|dcndigital)\.ae/(?:#/)?(?:video(?:/[^/]+)?|media|catchup/[^/]+/[^/]+)/(?P<id>\d+)'
_TESTS = [{
'url': 'http://www.dcndigital.ae/#/video/%D8%B1%D8%AD%D9%84%D8%A9-%D8%A7%D9%84%D8%B9%D9%85%D8%B1-%D8%A7%D9%84%D8%AD%D9%84%D9%82%D8%A9-1/17375',
'md5': '5f61c33bfc7794315c671a62d43116aa',
'info_dict':
{
'id': '17375',
'ext': 'mp4',
'title': 'رحلة العمر : الحلقة 1',
'description': 'md5:0156e935d870acb8ef0a66d24070c6d6',
'duration': 2041,
'timestamp': 1227504126,
'upload_date': '20081124',
'uploader_id': '71',
},
}, {
'url': 'http://awaan.ae/video/26723981/%D8%AF%D8%A7%D8%B1-%D8%A7%D9%84%D8%B3%D9%84%D8%A7%D9%85:-%D8%AE%D9%8A%D8%B1-%D8%AF%D9%88%D8%B1-%D8%A7%D9%84%D8%A3%D9%86%D8%B5%D8%A7%D8%B1',
'only_matching': True,
}]
def _real_extract(self, url):
video_id = self._match_id(url)
video_data = self._download_json(
f'http://admin.mangomolo.com/analytics/index.php/plus/video?id={video_id}',
video_id, headers={'Origin': 'http://awaan.ae'})
info = self._parse_video_data(video_data, video_id, False)
embed_url = 'http://admin.mangomolo.com/analytics/index.php/customers/embed/video?' + urllib.parse.urlencode({
'id': video_data['id'],
'user_id': video_data['user_id'],
'signature': video_data['signature'],
'countries': 'Q0M=',
'filter': 'DENY',
})
info.update({
'_type': 'url_transparent',
'url': embed_url,
'ie_key': 'MangomoloVideo',
})
return info
class AWAANLiveIE(AWAANBaseIE):
IE_NAME = 'awaan:live'
_VALID_URL = r'https?://(?:www\.)?(?:awaan|dcndigital)\.ae/(?:#/)?live/(?P<id>\d+)'
_TEST = {
'url': 'http://awaan.ae/live/6/dubai-tv',
'info_dict': {
'id': '6',
'ext': 'mp4',
'title': 're:Dubai Al Oula [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
'upload_date': '20150107',
'timestamp': 1420588800,
'uploader_id': '71',
},
'params': {
# m3u8 download
'skip_download': True,
},
}
def _real_extract(self, url):
channel_id = self._match_id(url)
channel_data = self._download_json(
f'http://admin.mangomolo.com/analytics/index.php/plus/getchanneldetails?channel_id={channel_id}',
channel_id, headers={'Origin': 'http://awaan.ae'})
info = self._parse_video_data(channel_data, channel_id, True)
embed_url = 'http://admin.mangomolo.com/analytics/index.php/customers/embed/index?' + urllib.parse.urlencode({
'id': base64.b64encode(channel_data['user_id'].encode()).decode(),
'channelid': base64.b64encode(channel_data['id'].encode()).decode(),
'signature': channel_data['signature'],
'countries': 'Q0M=',
'filter': 'DENY',
})
info.update({
'_type': 'url_transparent',
'url': embed_url,
'ie_key': 'MangomoloLive',
})
return info
class AWAANSeasonIE(InfoExtractor):
IE_NAME = 'awaan:season'
_VALID_URL = r'https?://(?:www\.)?(?:awaan|dcndigital)\.ae/(?:#/)?program/(?:(?P<show_id>\d+)|season/(?P<season_id>\d+))'
_TEST = {
'url': 'http://dcndigital.ae/#/program/205024/%D9%85%D8%AD%D8%A7%D8%B6%D8%B1%D8%A7%D8%AA-%D8%A7%D9%84%D8%B4%D9%8A%D8%AE-%D8%A7%D9%84%D8%B4%D8%B9%D8%B1%D8%A7%D9%88%D9%8A',
'info_dict':
{
'id': '7910',
'title': 'محاضرات الشيخ الشعراوي',
},
'playlist_mincount': 27,
}
def _real_extract(self, url):
url, smuggled_data = unsmuggle_url(url, {})
show_id, season_id = self._match_valid_url(url).groups()
data = {}
if season_id:
data['season'] = season_id
show_id = smuggled_data.get('show_id')
if show_id is None:
season = self._download_json(
f'http://admin.mangomolo.com/analytics/index.php/plus/season_info?id={season_id}',
season_id, headers={'Origin': 'http://awaan.ae'})
show_id = season['id']
data['show_id'] = show_id
show = self._download_json(
'http://admin.mangomolo.com/analytics/index.php/plus/show',
show_id, data=urlencode_postdata(data), headers={
'Origin': 'http://awaan.ae',
'Content-Type': 'application/x-www-form-urlencoded',
})
if not season_id:
season_id = show['default_season']
for season in show['seasons']:
if season['id'] == season_id:
title = season.get('title_en') or season['title_ar']
entries = []
for video in show['videos']:
video_id = str(video['id'])
entries.append(self.url_result(
f'http://awaan.ae/media/{video_id}', 'AWAANVideo', video_id))
return self.playlist_result(entries, season_id, title)

View File

@ -1,89 +0,0 @@
from .common import InfoExtractor
from ..utils import (
float_or_none,
js_to_json,
parse_iso8601,
traverse_obj,
url_or_none,
)
class AxsIE(InfoExtractor):
IE_NAME = 'axs.tv'
_VALID_URL = r'https?://(?:www\.)?axs\.tv/(?:channel/(?:[^/?#]+/)+)?video/(?P<id>[^/?#]+)'
_TESTS = [{
'url': 'https://www.axs.tv/video/5f4dc776b70e4f1c194f22ef/',
'md5': '8d97736ae8e50c64df528e5e676778cf',
'info_dict': {
'id': '5f4dc776b70e4f1c194f22ef',
'title': 'Small Town',
'ext': 'mp4',
'description': 'md5:e314d28bfaa227a4d7ec965fae19997f',
'upload_date': '20230602',
'timestamp': 1685729564,
'duration': 1284.216,
'series': 'Rock & Roll Road Trip with Sammy Hagar',
'season': 'Season 2',
'season_number': 2,
'episode': '3',
'thumbnail': 'https://images.dotstudiopro.com/5f4e9d330a0c3b295a7e8394',
},
}, {
'url': 'https://www.axs.tv/channel/rock-star-interview/video/daryl-hall',
'md5': '300ae795cd8f9984652c0949734ffbdc',
'info_dict': {
'id': '5f488148b70e4f392572977c',
'display_id': 'daryl-hall',
'title': 'Daryl Hall',
'ext': 'mp4',
'description': 'md5:e54ecaa0f4b5683fc9259e9e4b196628',
'upload_date': '20230214',
'timestamp': 1676403615,
'duration': 2570.668,
'series': 'The Big Interview with Dan Rather',
'season': 'Season 3',
'season_number': 3,
'episode': '5',
'thumbnail': 'https://images.dotstudiopro.com/5f4d1901f340b50d937cec32',
},
}]
def _real_extract(self, url):
display_id = self._match_id(url)
webpage = self._download_webpage(url, display_id)
webpage_json_data = self._search_json(
r'mountObj\s*=', webpage, 'video ID data', display_id,
transform_source=js_to_json)
video_id = webpage_json_data['video_id']
company_id = webpage_json_data['company_id']
meta = self._download_json(
f'https://api.myspotlight.tv/dotplayer/video/{company_id}/{video_id}',
video_id, query={'device_type': 'desktop_web'})['video']
formats = self._extract_m3u8_formats(
meta['video_m3u8'], video_id, 'mp4', m3u8_id='hls')
subtitles = {}
for cc in traverse_obj(meta, ('closeCaption', lambda _, v: url_or_none(v['srtPath']))):
subtitles.setdefault(cc.get('srtShortLang') or 'en', []).append(
{'ext': cc.get('srtExt'), 'url': cc['srtPath']})
return {
'id': video_id,
'display_id': display_id,
'formats': formats,
**traverse_obj(meta, {
'title': ('title', {str}),
'description': ('description', {str}),
'series': ('seriestitle', {str}),
'season_number': ('season', {int}),
'episode': ('episode', {str}),
'duration': ('duration', {float_or_none}),
'timestamp': ('updated_at', {parse_iso8601}),
'thumbnail': ('thumb', {url_or_none}),
}),
'subtitles': subtitles,
}

View File

@ -420,10 +420,10 @@ class BandcampWeeklyIE(BandcampIE): # XXX: Do not subclass from concrete IE
'info_dict': { 'info_dict': {
'id': '224', 'id': '224',
'ext': 'mp3', 'ext': 'mp3',
'title': 'Bandcamp Weekly, 2017-04-04', 'title': 'Magic Moments, 2017-04-04',
'description': 'md5:5d48150916e8e02d030623a48512c874', 'description': 'md5:5d48150916e8e02d030623a48512c874',
'thumbnail': 'https://f4.bcbits.com/img/9982549_0.jpg', 'thumbnail': 'https://f4.bcbits.com/img/9982549_0.jpg',
'series': 'Bandcamp Weekly', 'series': 'Magic Moments',
'episode_id': '224', 'episode_id': '224',
'release_timestamp': 1491264000, 'release_timestamp': 1491264000,
'release_date': '20170404', 'release_date': '20170404',
@ -440,10 +440,10 @@ class BandcampWeeklyIE(BandcampIE): # XXX: Do not subclass from concrete IE
def _real_extract(self, url): def _real_extract(self, url):
show_id = self._match_id(url) show_id = self._match_id(url)
show_data = self._download_json( show_data = self._download_json(
'https://bandcamp.com/api/bcradio_api/1/get_show', 'https://bandcamp.com/api/player/2/player_data_web',
show_id, 'Downloading radio show JSON', show_id, 'Downloading radio show JSON',
data=json.dumps({'id': show_id}).encode(), data=json.dumps({'item_id': int(show_id), 'item_type': 'radio'}).encode(),
headers={'Content-Type': 'application/json'}) headers={'Content-Type': 'application/json'})['tracklist']
audio_data = show_data['compiledTrack'] audio_data = show_data['compiledTrack']
stream_url = audio_data['streamUrl'] stream_url = audio_data['streamUrl']

View File

@ -1,111 +0,0 @@
from .common import InfoExtractor
from .youtube import YoutubeIE, YoutubeTabIE
class BeatBumpVideoIE(InfoExtractor):
_VALID_URL = r'https?://beatbump\.(?:ml|io)/listen\?id=(?P<id>[\w-]+)'
_TESTS = [{
'url': 'https://beatbump.ml/listen?id=MgNrAu2pzNs',
'md5': '5ff3fff41d3935b9810a9731e485fe66',
'info_dict': {
'id': 'MgNrAu2pzNs',
'ext': 'mp4',
'artist': 'Stephen',
'thumbnail': 'https://i.ytimg.com/vi_webp/MgNrAu2pzNs/maxresdefault.webp',
'channel_url': 'https://www.youtube.com/channel/UC-pWHpBjdGG69N9mM2auIAA',
'upload_date': '20190312',
'categories': ['Music'],
'playable_in_embed': True,
'duration': 169,
'like_count': int,
'alt_title': 'Voyeur Girl',
'view_count': int,
'track': 'Voyeur Girl',
'uploader': 'Stephen',
'title': 'Voyeur Girl',
'channel_follower_count': int,
'age_limit': 0,
'availability': 'public',
'live_status': 'not_live',
'album': 'it\'s too much love to know my dear',
'channel': 'Stephen',
'comment_count': int,
'description': 'md5:7ae382a65843d6df2685993e90a8628f',
'tags': 'count:11',
'creator': 'Stephen',
'channel_id': 'UC-pWHpBjdGG69N9mM2auIAA',
'channel_is_verified': True,
'heatmap': 'count:100',
},
}, {
'url': 'https://beatbump.io/listen?id=LDGZAprNGWo',
'only_matching': True,
}]
def _real_extract(self, url):
id_ = self._match_id(url)
return self.url_result(f'https://music.youtube.com/watch?v={id_}', YoutubeIE, id_)
class BeatBumpPlaylistIE(InfoExtractor):
_VALID_URL = r'https?://beatbump\.(?:ml|io)/(?:release\?id=|artist/|playlist/)(?P<id>[\w-]+)'
_TESTS = [{
'url': 'https://beatbump.ml/release?id=MPREb_gTAcphH99wE',
'playlist_count': 50,
'info_dict': {
'id': 'OLAK5uy_l1m0thk3g31NmIIz_vMIbWtyv7eZixlH0',
'availability': 'unlisted',
'view_count': int,
'title': 'Album - Royalty Free Music Library V2 (50 Songs)',
'description': '',
'tags': [],
'modified_date': '20231110',
},
'expected_warnings': ['YouTube Music is not directly supported'],
}, {
'url': 'https://beatbump.ml/artist/UC_aEa8K-EOJ3D6gOs7HcyNg',
'playlist_mincount': 1,
'params': {'flatplaylist': True},
'info_dict': {
'id': 'UC_aEa8K-EOJ3D6gOs7HcyNg',
'uploader_url': 'https://www.youtube.com/@NoCopyrightSounds',
'channel_url': 'https://www.youtube.com/channel/UC_aEa8K-EOJ3D6gOs7HcyNg',
'uploader_id': '@NoCopyrightSounds',
'channel_follower_count': int,
'title': 'NoCopyrightSounds',
'uploader': 'NoCopyrightSounds',
'description': 'md5:cd4fd53d81d363d05eee6c1b478b491a',
'channel': 'NoCopyrightSounds',
'tags': 'count:65',
'channel_id': 'UC_aEa8K-EOJ3D6gOs7HcyNg',
'channel_is_verified': True,
},
'expected_warnings': ['YouTube Music is not directly supported'],
}, {
'url': 'https://beatbump.ml/playlist/VLPLRBp0Fe2GpgmgoscNFLxNyBVSFVdYmFkq',
'playlist_mincount': 1,
'params': {'flatplaylist': True},
'info_dict': {
'id': 'PLRBp0Fe2GpgmgoscNFLxNyBVSFVdYmFkq',
'uploader_url': 'https://www.youtube.com/@NoCopyrightSounds',
'description': 'Providing you with copyright free / safe music for gaming, live streaming, studying and more!',
'view_count': int,
'channel_url': 'https://www.youtube.com/channel/UC_aEa8K-EOJ3D6gOs7HcyNg',
'uploader_id': '@NoCopyrightSounds',
'title': 'NCS : All Releases 💿',
'uploader': 'NoCopyrightSounds',
'availability': 'public',
'channel': 'NoCopyrightSounds',
'tags': [],
'modified_date': '20231112',
'channel_id': 'UC_aEa8K-EOJ3D6gOs7HcyNg',
},
'expected_warnings': ['YouTube Music is not directly supported'],
}, {
'url': 'https://beatbump.io/playlist/VLPLFCHGavqRG-q_2ZhmgU2XB2--ZY6irT1c',
'only_matching': True,
}]
def _real_extract(self, url):
id_ = self._match_id(url)
return self.url_result(f'https://music.youtube.com/browse/{id_}', YoutubeTabIE, id_)

View File

@ -1,71 +0,0 @@
import base64
import re
import urllib.parse
from .common import InfoExtractor
class BigflixIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?bigflix\.com/.+/(?P<id>[0-9]+)'
_TESTS = [{
# 2 formats
'url': 'http://www.bigflix.com/Tamil-movies/Drama-movies/Madarasapatinam/16070',
'info_dict': {
'id': '16070',
'ext': 'mp4',
'title': 'Madarasapatinam',
'description': 'md5:9f0470b26a4ba8e824c823b5d95c2f6b',
'formats': 'mincount:2',
},
'params': {
'skip_download': True,
},
}, {
# multiple formats
'url': 'http://www.bigflix.com/Malayalam-movies/Drama-movies/Indian-Rupee/15967',
'only_matching': True,
}]
def _real_extract(self, url):
video_id = self._match_id(url)
webpage = self._download_webpage(url, video_id)
title = self._html_search_regex(
r'<div[^>]+class=["\']pagetitle["\'][^>]*>(.+?)</div>',
webpage, 'title')
def decode_url(quoted_b64_url):
return base64.b64decode(urllib.parse.unquote(
quoted_b64_url)).decode('utf-8')
formats = []
for height, encoded_url in re.findall(
r'ContentURL_(\d{3,4})[pP][^=]+=([^&]+)', webpage):
video_url = decode_url(encoded_url)
f = {
'url': video_url,
'format_id': f'{height}p',
'height': int(height),
}
if video_url.startswith('rtmp'):
f['ext'] = 'flv'
formats.append(f)
file_url = self._search_regex(
r'file=([^&]+)', webpage, 'video url', default=None)
if file_url:
video_url = decode_url(file_url)
if all(f['url'] != video_url for f in formats):
formats.append({
'url': decode_url(file_url),
})
description = self._html_search_meta('description', webpage)
return {
'id': video_id,
'title': title,
'description': description,
'formats': formats,
}

View File

@ -1,52 +0,0 @@
import urllib.parse
from .common import InfoExtractor
from ..utils import ExtractorError
class BokeCCBaseIE(InfoExtractor):
def _extract_bokecc_formats(self, webpage, video_id, format_id=None):
player_params_str = self._html_search_regex(
r'<(?:script|embed)[^>]+src=(?P<q>["\'])(?:https?:)?//p\.bokecc\.com/(?:player|flash/player\.swf)\?(?P<query>.+?)(?P=q)',
webpage, 'player params', group='query')
player_params = urllib.parse.parse_qs(player_params_str)
info_xml = self._download_xml(
'http://p.bokecc.com/servlet/playinfo?uid={}&vid={}&m=1'.format(
player_params['siteid'][0], player_params['vid'][0]), video_id)
return [{
'format_id': format_id,
'url': quality.find('./copy').attrib['playurl'],
'quality': int(quality.attrib['value']),
} for quality in info_xml.findall('./video/quality')]
class BokeCCIE(BokeCCBaseIE):
IE_DESC = 'CC视频'
_VALID_URL = r'https?://union\.bokecc\.com/playvideo\.bo\?(?P<query>.*)'
_TESTS = [{
'url': 'http://union.bokecc.com/playvideo.bo?vid=E0ABAE9D4F509B189C33DC5901307461&uid=FE644790DE9D154A',
'info_dict': {
'id': 'FE644790DE9D154A_E0ABAE9D4F509B189C33DC5901307461',
'ext': 'flv',
'title': 'BokeCC Video',
},
}]
def _real_extract(self, url):
qs = urllib.parse.parse_qs(self._match_valid_url(url).group('query'))
if not qs.get('vid') or not qs.get('uid'):
raise ExtractorError('Invalid URL', expected=True)
video_id = '{}_{}'.format(qs['uid'][0], qs['vid'][0])
webpage = self._download_webpage(url, video_id)
return {
'id': video_id,
'title': 'BokeCC Video', # no title provided in the webpage
'formats': self._extract_bokecc_formats(webpage, video_id),
}

View File

@ -1,74 +0,0 @@
from .common import InfoExtractor
from ..utils import (
determine_ext,
int_or_none,
parse_iso8601,
traverse_obj,
urljoin,
)
class CaffeineTVIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?caffeine\.tv/[^/?#]+/video/(?P<id>[\da-f-]+)'
_TESTS = [{
'url': 'https://www.caffeine.tv/TsuSurf/video/cffc0a00-e73f-11ec-8080-80017d29f26e',
'info_dict': {
'id': 'cffc0a00-e73f-11ec-8080-80017d29f26e',
'ext': 'mp4',
'title': 'GOOOOD MORNINNNNN #highlights',
'timestamp': 1654702180,
'upload_date': '20220608',
'uploader': 'RahJON Wicc',
'uploader_id': 'TsuSurf',
'duration': 3145,
'age_limit': 17,
'thumbnail': 'https://www.caffeine.tv/broadcasts/776b6f84-9cd5-42e3-af1d-4a776eeed697/replay/lobby.jpg',
'comment_count': int,
'view_count': int,
'like_count': int,
'tags': ['highlights', 'battlerap'],
},
'params': {
'skip_download': 'm3u8',
},
}]
def _real_extract(self, url):
video_id = self._match_id(url)
json_data = self._download_json(
f'https://api.caffeine.tv/social/public/activity/{video_id}', video_id)
broadcast_info = traverse_obj(json_data, ('broadcast_info', {dict})) or {}
video_url = broadcast_info['video_url']
ext = determine_ext(video_url)
if ext == 'm3u8':
formats = self._extract_m3u8_formats(video_url, video_id, 'mp4')
else:
formats = [{'url': video_url}]
return {
'id': video_id,
'formats': formats,
**traverse_obj(json_data, {
'like_count': ('like_count', {int_or_none}),
'view_count': ('view_count', {int_or_none}),
'comment_count': ('comment_count', {int_or_none}),
'tags': ('tags', ..., {str}, filter),
'uploader': ('user', 'name', {str}),
'uploader_id': (((None, 'user'), 'username'), {str}, any),
'is_live': ('is_live', {bool}),
}),
**traverse_obj(broadcast_info, {
'title': ('broadcast_title', {str}),
'duration': ('content_duration', {int_or_none}),
'timestamp': ('broadcast_start_time', {parse_iso8601}),
'thumbnail': ('preview_image_path', {urljoin(url)}),
}),
'age_limit': {
# assume Apple Store ratings: https://en.wikipedia.org/wiki/Mobile_software_content_rating_system
'FOUR_PLUS': 0,
'NINE_PLUS': 9,
'TWELVE_PLUS': 12,
'SEVENTEEN_PLUS': 17,
}.get(broadcast_info.get('content_rating'), 17),
}

View File

@ -1,155 +0,0 @@
from .common import InfoExtractor
from ..utils import float_or_none, int_or_none, make_archive_id, traverse_obj
class CallinIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?callin\.com/episode/(?P<id>[-a-zA-Z]+)'
_TESTS = [{
'url': 'https://www.callin.com/episode/the-title-ix-regime-and-the-long-march-through-EBfXYSrsjc',
'info_dict': {
'id': '218b979630a35ead12c6fd096f2996c56c37e4d0dc1f6dc0feada32dcf7b31cd',
'title': 'The Title IX Regime and the Long March Through and Beyond the Institutions',
'ext': 'ts',
'display_id': 'the-title-ix-regime-and-the-long-march-through-EBfXYSrsjc',
'thumbnail': 're:https://.+\\.png',
'description': 'First episode',
'uploader': 'Wesley Yang',
'timestamp': 1639404128.65,
'upload_date': '20211213',
'uploader_id': 'wesyang',
'uploader_url': 'http://wesleyyang.substack.com',
'channel': 'Conversations in Year Zero',
'channel_id': '436d1f82ddeb30cd2306ea9156044d8d2cfdc3f1f1552d245117a42173e78553',
'channel_url': 'https://callin.com/show/conversations-in-year-zero-oJNllRFSfx',
'duration': 9951.936,
'view_count': int,
'categories': ['News & Politics', 'History', 'Technology'],
'cast': ['Wesley Yang', 'KC Johnson', 'Gabi Abramovich'],
'series': 'Conversations in Year Zero',
'series_id': '436d1f82ddeb30cd2306ea9156044d8d2cfdc3f1f1552d245117a42173e78553',
'episode': 'The Title IX Regime and the Long March Through and Beyond the Institutions',
'episode_number': 1,
'episode_id': '218b979630a35ead12c6fd096f2996c56c37e4d0dc1f6dc0feada32dcf7b31cd',
},
}, {
'url': 'https://www.callin.com/episode/fcc-commissioner-brendan-carr-on-elons-PrumRdSQJW',
'md5': '14ede27ee2c957b7e4db93140fc0745c',
'info_dict': {
'id': 'c3dab47f237bf953d180d3f243477a84302798be0e0b29bc9ade6d60a69f04f5',
'ext': 'ts',
'title': 'FCC Commissioner Brendan Carr on Elons Starlink',
'description': 'Or, why the government doesnt like SpaceX',
'channel': 'The Pull Request',
'channel_url': 'https://callin.com/show/the-pull-request-ucnDJmEKAa',
'duration': 3182.472,
'series_id': '7e9c23156e4aecfdcaef46bfb2ed7ca268509622ec006c0f0f25d90e34496638',
'uploader_url': 'http://thepullrequest.com',
'upload_date': '20220902',
'episode': 'FCC Commissioner Brendan Carr on Elons Starlink',
'display_id': 'fcc-commissioner-brendan-carr-on-elons-PrumRdSQJW',
'series': 'The Pull Request',
'channel_id': '7e9c23156e4aecfdcaef46bfb2ed7ca268509622ec006c0f0f25d90e34496638',
'view_count': int,
'uploader': 'Antonio García Martínez',
'thumbnail': 'https://d1z76fhpoqkd01.cloudfront.net/shows/legacy/1ade9142625344045dc17cf523469ced1d93610762f4c886d06aa190a2f979e8.png',
'episode_id': 'c3dab47f237bf953d180d3f243477a84302798be0e0b29bc9ade6d60a69f04f5',
'timestamp': 1662100688.005,
},
}, {
'url': 'https://www.callin.com/episode/episode-81-elites-melt-down-over-student-debt-lzxMidUnjA',
'md5': '16f704ddbf82a27e3930533b12062f07',
'info_dict': {
'id': '8d06f869798f93a7814e380bceabea72d501417e620180416ff6bd510596e83c',
'ext': 'ts',
'title': 'Episode 81- Elites MELT DOWN over Student Debt Victory? Rumble in NYC?',
'description': 'Lets talk todays episode about the primary election shake up in NYC and the elites melting down over student debt cancelation.',
'channel': 'The DEBRIEF With Briahna Joy Gray',
'channel_url': 'https://callin.com/show/the-debrief-with-briahna-joy-gray-siiFDzGegm',
'duration': 10043.16,
'series_id': '61cea58444465fd26674069703bd8322993bc9e5b4f1a6d0872690554a046ff7',
'uploader_url': 'http://patreon.com/badfaithpodcast',
'upload_date': '20220826',
'episode': 'Episode 81- Elites MELT DOWN over Student Debt Victory? Rumble in NYC?',
'display_id': 'episode-',
'series': 'The DEBRIEF With Briahna Joy Gray',
'channel_id': '61cea58444465fd26674069703bd8322993bc9e5b4f1a6d0872690554a046ff7',
'view_count': int,
'uploader': 'Briahna Gray',
'thumbnail': 'https://d1z76fhpoqkd01.cloudfront.net/shows/legacy/461ea0d86172cb6aff7d6c80fd49259cf5e64bdf737a4650f8bc24cf392ca218.png',
'episode_id': '8d06f869798f93a7814e380bceabea72d501417e620180416ff6bd510596e83c',
'timestamp': 1661476708.282,
},
}]
def try_get_user_name(self, d):
names = [d.get(n) for n in ('first', 'last')]
if None in names:
return next((n for n in names if n), default=None)
return ' '.join(names)
def _real_extract(self, url):
display_id = self._match_id(url)
webpage = self._download_webpage(url, display_id)
next_data = self._search_nextjs_data(webpage, display_id)
episode = next_data['props']['pageProps']['episode']
video_id = episode['id']
title = episode.get('title') or self._generic_title('', webpage)
url = episode['m3u8']
formats = self._extract_m3u8_formats(url, display_id, ext='ts')
show = traverse_obj(episode, ('show', 'title'))
show_id = traverse_obj(episode, ('show', 'id'))
show_json = None
app_slug = (self._html_search_regex(
'<script\\s+src=["\']/_next/static/([-_a-zA-Z0-9]+)/_',
webpage, 'app slug', fatal=False) or next_data.get('buildId'))
show_slug = traverse_obj(episode, ('show', 'linkObj', 'resourceUrl'))
if app_slug and show_slug and '/' in show_slug:
show_slug = show_slug.rsplit('/', 1)[1]
show_json_url = f'https://www.callin.com/_next/data/{app_slug}/show/{show_slug}.json'
show_json = self._download_json(show_json_url, display_id, fatal=False)
host = (traverse_obj(show_json, ('pageProps', 'show', 'hosts', 0))
or traverse_obj(episode, ('speakers', 0)))
host_nick = traverse_obj(host, ('linkObj', 'resourceUrl'))
host_nick = host_nick.rsplit('/', 1)[1] if (host_nick and '/' in host_nick) else None
cast = list(filter(None, [
self.try_get_user_name(u) for u in
traverse_obj(episode, (('speakers', 'callerTags'), ...)) or []
]))
episode_list = traverse_obj(show_json, ('pageProps', 'show', 'episodes')) or []
episode_number = next(
(len(episode_list) - i for i, e in enumerate(episode_list) if e.get('id') == video_id),
None)
return {
'id': video_id,
'_old_archive_ids': [make_archive_id(self, display_id.rsplit('-', 1)[-1])],
'display_id': display_id,
'title': title,
'formats': formats,
'thumbnail': traverse_obj(episode, ('show', 'photo')),
'description': episode.get('description'),
'uploader': self.try_get_user_name(host) if host else None,
'timestamp': episode.get('publishedAt'),
'uploader_id': host_nick,
'uploader_url': traverse_obj(show_json, ('pageProps', 'show', 'url')),
'channel': show,
'channel_id': show_id,
'channel_url': traverse_obj(episode, ('show', 'linkObj', 'resourceUrl')),
'duration': float_or_none(episode.get('runtime')),
'view_count': int_or_none(episode.get('plays')),
'categories': traverse_obj(episode, ('show', 'categorizations', ..., 'name')),
'cast': cast if cast else None,
'series': show,
'series_id': show_id,
'episode': title,
'episode_number': episode_number,
'episode_id': video_id,
}

View File

@ -1,155 +0,0 @@
import re
import urllib.parse
from .common import InfoExtractor
from ..utils import (
clean_html,
parse_duration,
str_to_int,
unified_strdate,
)
class CamdemyIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?camdemy\.com/media/(?P<id>\d+)'
_TESTS = [{
# single file
'url': 'http://www.camdemy.com/media/5181/',
'md5': '5a5562b6a98b37873119102e052e311b',
'info_dict': {
'id': '5181',
'ext': 'mp4',
'title': 'Ch1-1 Introduction, Signals (02-23-2012)',
'thumbnail': r're:^https?://.*\.jpg$',
'creator': 'ss11spring',
'duration': 1591,
'upload_date': '20130114',
'view_count': int,
},
}, {
# With non-empty description
# webpage returns "No permission or not login"
'url': 'http://www.camdemy.com/media/13885',
'md5': '4576a3bb2581f86c61044822adbd1249',
'info_dict': {
'id': '13885',
'ext': 'mp4',
'title': 'EverCam + Camdemy QuickStart',
'thumbnail': r're:^https?://.*\.jpg$',
'description': 'md5:2a9f989c2b153a2342acee579c6e7db6',
'creator': 'evercam',
'duration': 318,
},
}, {
# External source (YouTube)
'url': 'http://www.camdemy.com/media/14842',
'info_dict': {
'id': '2vsYQzNIsJo',
'ext': 'mp4',
'title': 'Excel 2013 Tutorial - How to add Password Protection',
'description': 'Excel 2013 Tutorial for Beginners - How to add Password Protection',
'upload_date': '20130211',
'uploader': 'Hun Kim',
'uploader_id': 'hunkimtutorials',
},
'params': {
'skip_download': True,
},
}]
def _real_extract(self, url):
video_id = self._match_id(url)
webpage = self._download_webpage(url, video_id)
src_from = self._html_search_regex(
r"class=['\"]srcFrom['\"][^>]*>Sources?(?:\s+from)?\s*:\s*<a[^>]+(?:href|title)=(['\"])(?P<url>(?:(?!\1).)+)\1",
webpage, 'external source', default=None, group='url')
if src_from:
return self.url_result(src_from)
oembed_obj = self._download_json(
'http://www.camdemy.com/oembed/?format=json&url=' + url, video_id)
title = oembed_obj['title']
thumb_url = oembed_obj['thumbnail_url']
video_folder = urllib.parse.urljoin(thumb_url, 'video/')
file_list_doc = self._download_xml(
urllib.parse.urljoin(video_folder, 'fileList.xml'),
video_id, 'Downloading filelist XML')
file_name = file_list_doc.find('./video/item/fileName').text
video_url = urllib.parse.urljoin(video_folder, file_name)
# Some URLs return "No permission or not login" in a webpage despite being
# freely available via oembed JSON URL (e.g. http://www.camdemy.com/media/13885)
upload_date = unified_strdate(self._search_regex(
r'>published on ([^<]+)<', webpage,
'upload date', default=None))
view_count = str_to_int(self._search_regex(
r'role=["\']viewCnt["\'][^>]*>([\d,.]+) views',
webpage, 'view count', default=None))
description = self._html_search_meta(
'description', webpage, default=None) or clean_html(
oembed_obj.get('description'))
return {
'id': video_id,
'url': video_url,
'title': title,
'thumbnail': thumb_url,
'description': description,
'creator': oembed_obj.get('author_name'),
'duration': parse_duration(oembed_obj.get('duration')),
'upload_date': upload_date,
'view_count': view_count,
}
class CamdemyFolderIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?camdemy\.com/folder/(?P<id>\d+)'
_TESTS = [{
# links with trailing slash
'url': 'http://www.camdemy.com/folder/450',
'info_dict': {
'id': '450',
'title': '信號與系統 2012 & 2011 (Signals and Systems)',
},
'playlist_mincount': 145,
}, {
# links without trailing slash
# and multi-page
'url': 'http://www.camdemy.com/folder/853',
'info_dict': {
'id': '853',
'title': '科學計算 - 使用 Matlab',
},
'playlist_mincount': 20,
}, {
# with displayMode parameter. For testing the codes to add parameters
'url': 'http://www.camdemy.com/folder/853/?displayMode=defaultOrderByOrg',
'info_dict': {
'id': '853',
'title': '科學計算 - 使用 Matlab',
},
'playlist_mincount': 20,
}]
def _real_extract(self, url):
folder_id = self._match_id(url)
# Add displayMode=list so that all links are displayed in a single page
parsed_url = list(urllib.parse.urlparse(url))
query = dict(urllib.parse.parse_qsl(parsed_url[4]))
query.update({'displayMode': 'list'})
parsed_url[4] = urllib.parse.urlencode(query)
final_url = urllib.parse.urlunparse(parsed_url)
page = self._download_webpage(final_url, folder_id)
matches = re.findall(r"href='(/media/\d+/?)'", page)
entries = [self.url_result('http://www.camdemy.com' + media_path)
for media_path in matches]
folder_title = self._html_search_meta('keywords', page)
return self.playlist_result(entries, folder_id, folder_title)

View File

@ -1,70 +0,0 @@
import re
from .common import InfoExtractor
from ..utils import (
parse_iso8601,
qualities,
)
class ClippitIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?clippituser\.tv/c/(?P<id>[a-z]+)'
_TEST = {
'url': 'https://www.clippituser.tv/c/evmgm',
'md5': '963ae7a59a2ec4572ab8bf2f2d2c5f09',
'info_dict': {
'id': 'evmgm',
'ext': 'mp4',
'title': 'Bye bye Brutus. #BattleBots - Clippit',
'uploader': 'lizllove',
'uploader_url': 'https://www.clippituser.tv/p/lizllove',
'timestamp': 1472183818,
'upload_date': '20160826',
'description': 'BattleBots | ABC',
'thumbnail': r're:^https?://.*\.jpg$',
},
}
def _real_extract(self, url):
video_id = self._match_id(url)
webpage = self._download_webpage(url, video_id)
title = self._html_search_regex(r'<title.*>(.+?)</title>', webpage, 'title')
FORMATS = ('sd', 'hd')
quality = qualities(FORMATS)
formats = []
for format_id in FORMATS:
url = self._html_search_regex(rf'data-{format_id}-file="(.+?)"',
webpage, 'url', fatal=False)
if not url:
continue
match = re.search(r'/(?P<height>\d+)\.mp4', url)
formats.append({
'url': url,
'format_id': format_id,
'quality': quality(format_id),
'height': int(match.group('height')) if match else None,
})
uploader = self._html_search_regex(r'class="username".*>\s+(.+?)\n',
webpage, 'uploader', fatal=False)
uploader_url = ('https://www.clippituser.tv/p/' + uploader
if uploader else None)
timestamp = self._html_search_regex(r'datetime="(.+?)"',
webpage, 'date', fatal=False)
thumbnail = self._html_search_regex(r'data-image="(.+?)"',
webpage, 'thumbnail', fatal=False)
return {
'id': video_id,
'title': title,
'formats': formats,
'uploader': uploader,
'uploader_url': uploader_url,
'timestamp': parse_iso8601(timestamp),
'description': self._og_search_description(webpage),
'thumbnail': thumbnail,
}

View File

@ -2970,6 +2970,8 @@ class InfoExtractor:
content_type = representation_attrib.get('contentType', mime_type.split('/')[0]) content_type = representation_attrib.get('contentType', mime_type.split('/')[0])
codec_str = representation_attrib.get('codecs', '') codec_str = representation_attrib.get('codecs', '')
supplemental_codecs = representation_attrib.get(
'{urn:scte:dash:scte214-extensions}supplementalCodecs')
# Some kind of binary subtitle found in some youtube livestreams # Some kind of binary subtitle found in some youtube livestreams
if mime_type == 'application/x-rawcc': if mime_type == 'application/x-rawcc':
codecs = {'scodec': codec_str} codecs = {'scodec': codec_str}
@ -3025,7 +3027,7 @@ class InfoExtractor:
'asr': int_or_none(representation_attrib.get('audioSamplingRate')), 'asr': int_or_none(representation_attrib.get('audioSamplingRate')),
'fps': int_or_none(representation_attrib.get('frameRate')), 'fps': int_or_none(representation_attrib.get('frameRate')),
'language': lang if lang not in ('mul', 'und', 'zxx', 'mis') else None, 'language': lang if lang not in ('mul', 'und', 'zxx', 'mis') else None,
'format_note': f'DASH {content_type}', 'format_note': join_nonempty(f'DASH {content_type}', supplemental_codecs, delim=', '),
'filesize': filesize, 'filesize': filesize,
'container': mimetype2ext(mime_type) + '_dash', 'container': mimetype2ext(mime_type) + '_dash',
**codecs, **codecs,

View File

@ -1,113 +0,0 @@
from .common import InfoExtractor
from ..utils import (
float_or_none,
int_or_none,
)
class CONtvIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?contv\.com/details-movie/(?P<id>[^/]+)'
_TESTS = [{
'url': 'https://www.contv.com/details-movie/CEG10022949/days-of-thrills-&-laughter',
'info_dict': {
'id': 'CEG10022949',
'ext': 'mp4',
'title': 'Days Of Thrills & Laughter',
'description': 'md5:5d6b3d0b1829bb93eb72898c734802eb',
'upload_date': '20180703',
'timestamp': 1530634789.61,
},
'params': {
# m3u8 download
'skip_download': True,
},
}, {
'url': 'https://www.contv.com/details-movie/CLIP-show_fotld_bts/fight-of-the-living-dead:-behind-the-scenes-bites',
'info_dict': {
'id': 'CLIP-show_fotld_bts',
'title': 'Fight of the Living Dead: Behind the Scenes Bites',
},
'playlist_mincount': 7,
}]
def _real_extract(self, url):
video_id = self._match_id(url)
details = self._download_json(
'http://metax.contv.live.junctiontv.net/metax/2.5/details/' + video_id,
video_id, query={'device': 'web'})
if details.get('type') == 'episodic':
seasons = self._download_json(
'http://metax.contv.live.junctiontv.net/metax/2.5/seriesfeed/json/' + video_id,
video_id)
entries = []
for season in seasons:
for episode in season.get('episodes', []):
episode_id = episode.get('id')
if not episode_id:
continue
entries.append(self.url_result(
'https://www.contv.com/details-movie/' + episode_id,
CONtvIE.ie_key(), episode_id))
return self.playlist_result(entries, video_id, details.get('title'))
m_details = details['details']
title = details['title']
formats = []
media_hls_url = m_details.get('media_hls_url')
if media_hls_url:
formats.extend(self._extract_m3u8_formats(
media_hls_url, video_id, 'mp4',
m3u8_id='hls', fatal=False))
media_mp4_url = m_details.get('media_mp4_url')
if media_mp4_url:
formats.append({
'format_id': 'http',
'url': media_mp4_url,
})
subtitles = {}
captions = m_details.get('captions') or {}
for caption_url in captions.values():
subtitles.setdefault('en', []).append({
'url': caption_url,
})
thumbnails = []
for image in m_details.get('images', []):
image_url = image.get('url')
if not image_url:
continue
thumbnails.append({
'url': image_url,
'width': int_or_none(image.get('width')),
'height': int_or_none(image.get('height')),
})
description = None
for p in ('large_', 'medium_', 'small_', ''):
d = m_details.get(p + 'description')
if d:
description = d
break
return {
'id': video_id,
'title': title,
'formats': formats,
'thumbnails': thumbnails,
'description': description,
'timestamp': float_or_none(details.get('metax_added_on'), 1000),
'subtitles': subtitles,
'duration': float_or_none(m_details.get('duration'), 1000),
'view_count': int_or_none(details.get('num_watched')),
'like_count': int_or_none(details.get('num_fav')),
'categories': details.get('category'),
'tags': details.get('tags'),
'season_number': int_or_none(details.get('season')),
'episode_number': int_or_none(details.get('episode')),
'release_year': int_or_none(details.get('pub_year')),
}

View File

@ -1,113 +0,0 @@
import json
from .common import InfoExtractor
from ..utils import (
ExtractorError,
int_or_none,
try_get,
)
class DroobleIE(InfoExtractor):
_VALID_URL = r'''(?x)https?://drooble\.com/(?:
(?:(?P<user>[^/]+)/)?(?P<kind>song|videos|music/albums)/(?P<id>\d+)|
(?P<user_2>[^/]+)/(?P<kind_2>videos|music))
'''
_TESTS = [{
'url': 'https://drooble.com/song/2858030',
'md5': '5ffda90f61c7c318dc0c3df4179eb064',
'info_dict': {
'id': '2858030',
'ext': 'mp3',
'title': 'Skankocillin',
'upload_date': '20200801',
'timestamp': 1596241390,
'uploader_id': '95894',
'uploader': 'Bluebeat Shelter',
},
}, {
'url': 'https://drooble.com/karl340758/videos/2859183',
'info_dict': {
'id': 'J6QCQY_I5Tk',
'ext': 'mp4',
'title': 'Skankocillin',
'uploader_id': 'UCrSRoI5vVyeYihtWEYua7rg',
'description': 'md5:ffc0bd8ba383db5341a86a6cd7d9bcca',
'upload_date': '20200731',
'uploader': 'Bluebeat Shelter',
},
}, {
'url': 'https://drooble.com/karl340758/music/albums/2858031',
'info_dict': {
'id': '2858031',
},
'playlist_mincount': 8,
}, {
'url': 'https://drooble.com/karl340758/music',
'info_dict': {
'id': 'karl340758',
},
'playlist_mincount': 8,
}, {
'url': 'https://drooble.com/karl340758/videos',
'info_dict': {
'id': 'karl340758',
},
'playlist_mincount': 8,
}]
def _call_api(self, method, video_id, data=None):
response = self._download_json(
f'https://drooble.com/api/dt/{method}', video_id, data=json.dumps(data).encode())
if not response[0]:
raise ExtractorError('Unable to download JSON metadata')
return response[1]
def _real_extract(self, url):
mobj = self._match_valid_url(url)
user = mobj.group('user') or mobj.group('user_2')
kind = mobj.group('kind') or mobj.group('kind_2')
display_id = mobj.group('id') or user
if mobj.group('kind_2') == 'videos':
data = {'from_user': display_id, 'album': -1, 'limit': 18, 'offset': 0, 'order': 'new2old', 'type': 'video'}
elif kind in ('music/albums', 'music'):
data = {'user': user, 'public_only': True, 'individual_limit': {'singles': 1, 'albums': 1, 'playlists': 1}}
else:
data = {'url_slug': display_id, 'children': 10, 'order': 'old2new'}
method = 'getMusicOverview' if kind in ('music/albums', 'music') else 'getElements'
json_data = self._call_api(method, display_id, data=data)
if kind in ('music/albums', 'music'):
json_data = json_data['singles']['list']
entites = []
for media in json_data:
url = media.get('external_media_url') or media.get('link')
if url.startswith('https://www.youtube.com'):
entites.append({
'_type': 'url',
'url': url,
'ie_key': 'Youtube',
})
continue
is_audio = (media.get('type') or '').lower() == 'audio'
entites.append({
'url': url,
'id': media['id'],
'title': media['title'],
'duration': int_or_none(media.get('duration')),
'timestamp': int_or_none(media.get('timestamp')),
'album': try_get(media, lambda x: x['album']['title']),
'uploader': try_get(media, lambda x: x['creator']['display_name']),
'uploader_id': try_get(media, lambda x: x['creator']['id']),
'thumbnail': media.get('image_comment'),
'like_count': int_or_none(media.get('likes')),
'vcodec': 'none' if is_audio else None,
'ext': 'mp3' if is_audio else None,
})
if len(entites) > 1:
return self.playlist_result(entites, display_id)
return entites[0]

View File

@ -1,246 +0,0 @@
import base64
import re
import urllib.parse
from .common import InfoExtractor
from ..utils import (
ExtractorError,
clean_html,
extract_attributes,
get_elements_by_class,
int_or_none,
js_to_json,
smuggle_url,
unescapeHTML,
)
def _get_elements_by_tag_and_attrib(html, tag=None, attribute=None, value=None, escape_value=True):
"""Return the content of the tag with the specified attribute in the passed HTML document"""
if tag is None:
tag = '[a-zA-Z0-9:._-]+'
if attribute is None:
attribute = ''
else:
attribute = rf'\s+(?P<attribute>{re.escape(attribute)})'
if value is None:
value = ''
else:
value = re.escape(value) if escape_value else value
value = f'=[\'"]?(?P<value>{value})[\'"]?'
retlist = []
for m in re.finditer(rf'''(?xs)
<(?P<tag>{tag})
(?:\s+[a-zA-Z0-9:._-]+(?:=[a-zA-Z0-9:._-]*|="[^"]*"|='[^']*'|))*?
{attribute}{value}
(?:\s+[a-zA-Z0-9:._-]+(?:=[a-zA-Z0-9:._-]*|="[^"]*"|='[^']*'|))*?
\s*>
(?P<content>.*?)
</\1>
''', html):
retlist.append(m)
return retlist
def _get_element_by_tag_and_attrib(html, tag=None, attribute=None, value=None, escape_value=True):
retval = _get_elements_by_tag_and_attrib(html, tag, attribute, value, escape_value)
return retval[0] if retval else None
class DubokuIE(InfoExtractor):
IE_NAME = 'duboku'
IE_DESC = 'www.duboku.io'
_VALID_URL = r'(?:https?://[^/]+\.duboku\.io/vodplay/)(?P<id>[0-9]+-[0-9-]+)\.html.*'
_TESTS = [{
'url': 'https://w.duboku.io/vodplay/1575-1-1.html',
'info_dict': {
'id': '1575-1-1',
'ext': 'mp4',
'series': '白色月光',
'title': 'contains:白色月光',
'season_number': 1,
'episode_number': 1,
'season': 'Season 1',
'episode_id': '1',
'season_id': '1',
'episode': 'Episode 1',
},
'params': {
'skip_download': 'm3u8 download',
},
}, {
'url': 'https://w.duboku.io/vodplay/1588-1-1.html',
'info_dict': {
'id': '1588-1-1',
'ext': 'mp4',
'series': '亲爱的自己',
'title': 'contains:第1集',
'season_number': 1,
'episode_number': 1,
'episode': 'Episode 1',
'season': 'Season 1',
'episode_id': '1',
'season_id': '1',
},
'params': {
'skip_download': 'm3u8 download',
},
}]
_PLAYER_DATA_PATTERN = r'player_data\s*=\s*(\{\s*(.*)})\s*;?\s*</script'
def _real_extract(self, url):
video_id = self._match_id(url)
temp = video_id.split('-')
series_id = temp[0]
season_id = temp[1]
episode_id = temp[2]
webpage_url = f'https://w.duboku.io/vodplay/{video_id}.html'
webpage_html = self._download_webpage(webpage_url, video_id)
# extract video url
player_data = self._search_regex(
self._PLAYER_DATA_PATTERN, webpage_html, 'player_data')
player_data = self._parse_json(player_data, video_id, js_to_json)
# extract title
temp = get_elements_by_class('title', webpage_html)
series_title = None
title = None
for html in temp:
mobj = re.search(r'<a\s+.*>(.*)</a>', html)
if mobj:
href = extract_attributes(mobj.group(0)).get('href')
if href:
mobj1 = re.search(r'/(\d+)\.html', href)
if mobj1 and mobj1.group(1) == series_id:
series_title = clean_html(mobj.group(0))
series_title = re.sub(r'[\s\r\n\t]+', ' ', series_title)
title = clean_html(html)
title = re.sub(r'[\s\r\n\t]+', ' ', title)
break
data_url = player_data.get('url')
if not data_url:
raise ExtractorError('Cannot find url in player_data')
player_encrypt = player_data.get('encrypt')
if player_encrypt == 1:
data_url = urllib.parse.unquote(data_url)
elif player_encrypt == 2:
data_url = urllib.parse.unquote(base64.b64decode(data_url).decode('ascii'))
# if it is an embedded iframe, maybe it's an external source
headers = {'Referer': webpage_url}
if player_data.get('from') == 'iframe':
# use _type url_transparent to retain the meaningful details
# of the video.
return {
'_type': 'url_transparent',
'url': smuggle_url(data_url, {'referer': webpage_url}),
'id': video_id,
'title': title,
'series': series_title,
'season_number': int_or_none(season_id),
'season_id': season_id,
'episode_number': int_or_none(episode_id),
'episode_id': episode_id,
}
formats = self._extract_m3u8_formats(data_url, video_id, 'mp4', headers=headers)
return {
'id': video_id,
'title': title,
'series': series_title,
'season_number': int_or_none(season_id),
'season_id': season_id,
'episode_number': int_or_none(episode_id),
'episode_id': episode_id,
'formats': formats,
'http_headers': headers,
}
class DubokuPlaylistIE(InfoExtractor):
IE_NAME = 'duboku:list'
IE_DESC = 'www.duboku.io entire series'
_VALID_URL = r'(?:https?://[^/]+\.duboku\.io/voddetail/)(?P<id>[0-9]+)\.html.*'
_TESTS = [{
'url': 'https://w.duboku.io/voddetail/1575.html',
'info_dict': {
'id': 'startswith:1575',
'title': '白色月光',
},
'playlist_count': 12,
}, {
'url': 'https://w.duboku.io/voddetail/1554.html',
'info_dict': {
'id': 'startswith:1554',
'title': '以家人之名',
},
'playlist_mincount': 30,
}]
def _real_extract(self, url):
mobj = self._match_valid_url(url)
if mobj is None:
raise ExtractorError(f'Invalid URL: {url}')
series_id = mobj.group('id')
fragment = urllib.parse.urlparse(url).fragment
webpage_url = f'https://w.duboku.io/voddetail/{series_id}.html'
webpage_html = self._download_webpage(webpage_url, series_id)
# extract title
title = _get_element_by_tag_and_attrib(webpage_html, 'h1', 'class', 'title')
title = unescapeHTML(title.group('content')) if title else None
if not title:
title = self._html_search_meta('keywords', webpage_html)
if not title:
title = _get_element_by_tag_and_attrib(webpage_html, 'title')
title = unescapeHTML(title.group('content')) if title else None
# extract playlists
playlists = {}
for div in _get_elements_by_tag_and_attrib(
webpage_html, attribute='id', value='playlist\\d+', escape_value=False):
playlist_id = div.group('value')
playlist = []
for a in _get_elements_by_tag_and_attrib(
div.group('content'), 'a', 'href', value='[^\'"]+?', escape_value=False):
playlist.append({
'href': unescapeHTML(a.group('value')),
'title': unescapeHTML(a.group('content')),
})
playlists[playlist_id] = playlist
# select the specified playlist if url fragment exists
playlist = None
playlist_id = None
if fragment:
playlist = playlists.get(fragment)
playlist_id = fragment
else:
first = next(iter(playlists.items()), None)
if first:
(playlist_id, playlist) = first
if not playlist:
raise ExtractorError(
f'Cannot find {fragment}' if fragment else 'Cannot extract playlist')
# return url results
return self.playlist_result([
self.url_result(
urllib.parse.urljoin('https://w.duboku.io', x['href']),
ie=DubokuIE.ie_key(), video_title=x.get('title'))
for x in playlist], series_id + '#' + playlist_id, title)

View File

@ -1,158 +0,0 @@
import json
import random
from .common import InfoExtractor
from ..utils import (
ExtractorError,
)
class EightTracksIE(InfoExtractor):
IE_NAME = '8tracks'
_VALID_URL = r'https?://8tracks\.com/(?P<user>[^/]+)/(?P<id>[^/#]+)(?:#.*)?$'
_TEST = {
'name': 'EightTracks',
'url': 'http://8tracks.com/ytdl/youtube-dl-test-tracks-a',
'info_dict': {
'id': '1336550',
'display_id': 'youtube-dl-test-tracks-a',
'description': "test chars: \"'/\\ä↭",
'title': "youtube-dl test tracks \"'/\\ä↭<>",
},
'playlist': [
{
'md5': '96ce57f24389fc8734ce47f4c1abcc55',
'info_dict': {
'id': '11885610',
'ext': 'm4a',
'title': "youtue-dl project<>\"' - youtube-dl test track 1 \"'/\\\u00e4\u21ad",
'uploader_id': 'ytdl',
},
},
{
'md5': '4ab26f05c1f7291ea460a3920be8021f',
'info_dict': {
'id': '11885608',
'ext': 'm4a',
'title': "youtube-dl project - youtube-dl test track 2 \"'/\\\u00e4\u21ad",
'uploader_id': 'ytdl',
},
},
{
'md5': 'd30b5b5f74217410f4689605c35d1fd7',
'info_dict': {
'id': '11885679',
'ext': 'm4a',
'title': "youtube-dl project as well - youtube-dl test track 3 \"'/\\\u00e4\u21ad",
'uploader_id': 'ytdl',
},
},
{
'md5': '4eb0a669317cd725f6bbd336a29f923a',
'info_dict': {
'id': '11885680',
'ext': 'm4a',
'title': "youtube-dl project as well - youtube-dl test track 4 \"'/\\\u00e4\u21ad",
'uploader_id': 'ytdl',
},
},
{
'md5': '1893e872e263a2705558d1d319ad19e8',
'info_dict': {
'id': '11885682',
'ext': 'm4a',
'title': "PH - youtube-dl test track 5 \"'/\\\u00e4\u21ad",
'uploader_id': 'ytdl',
},
},
{
'md5': 'b673c46f47a216ab1741ae8836af5899',
'info_dict': {
'id': '11885683',
'ext': 'm4a',
'title': "PH - youtube-dl test track 6 \"'/\\\u00e4\u21ad",
'uploader_id': 'ytdl',
},
},
{
'md5': '1d74534e95df54986da7f5abf7d842b7',
'info_dict': {
'id': '11885684',
'ext': 'm4a',
'title': "phihag - youtube-dl test track 7 \"'/\\\u00e4\u21ad",
'uploader_id': 'ytdl',
},
},
{
'md5': 'f081f47af8f6ae782ed131d38b9cd1c0',
'info_dict': {
'id': '11885685',
'ext': 'm4a',
'title': "phihag - youtube-dl test track 8 \"'/\\\u00e4\u21ad",
'uploader_id': 'ytdl',
},
},
],
}
def _real_extract(self, url):
playlist_id = self._match_id(url)
webpage = self._download_webpage(url, playlist_id)
data = self._parse_json(
self._search_regex(
r'(?s)PAGE\.mix\s*=\s*({.+?});\n', webpage, 'trax information'),
playlist_id)
session = str(random.randint(0, 1000000000))
mix_id = data['id']
track_count = data['tracks_count']
duration = data['duration']
avg_song_duration = float(duration) / track_count
# duration is sometimes negative, use predefined avg duration
if avg_song_duration <= 0:
avg_song_duration = 300
first_url = f'http://8tracks.com/sets/{session}/play?player=sm&mix_id={mix_id}&format=jsonh'
next_url = first_url
entries = []
for i in range(track_count):
api_json = None
download_tries = 0
while api_json is None:
try:
api_json = self._download_webpage(
next_url, playlist_id,
note='Downloading song information %d/%d' % (i + 1, track_count),
errnote='Failed to download song information')
except ExtractorError:
if download_tries > 3:
raise
else:
download_tries += 1
self._sleep(avg_song_duration, playlist_id)
api_data = json.loads(api_json)
track_data = api_data['set']['track']
info = {
'id': str(track_data['id']),
'url': track_data['track_file_stream_url'],
'title': track_data['performer'] + ' - ' + track_data['name'],
'raw_title': track_data['name'],
'uploader_id': data['user']['login'],
'ext': 'm4a',
}
entries.append(info)
next_url = 'http://8tracks.com/sets/{}/next?player=sm&mix_id={}&format=jsonh&track_id={}'.format(
session, mix_id, track_data['id'])
return {
'_type': 'playlist',
'entries': entries,
'id': str(mix_id),
'display_id': playlist_id,
'title': data.get('name'),
'description': data.get('description'),
}

View File

@ -1,81 +0,0 @@
from .common import InfoExtractor
from ..networking import Request
from ..utils import (
float_or_none,
int_or_none,
join_nonempty,
parse_iso8601,
)
class EitbIE(InfoExtractor):
IE_NAME = 'eitb.tv'
_VALID_URL = r'https?://(?:www\.)?eitb\.tv/(?:eu/bideoa|es/video)/[^/]+/\d+/(?P<id>\d+)'
_TEST = {
'url': 'http://www.eitb.tv/es/video/60-minutos-60-minutos-2013-2014/4104995148001/4090227752001/lasa-y-zabala-30-anos/',
'md5': 'edf4436247185adee3ea18ce64c47998',
'info_dict': {
'id': '4090227752001',
'ext': 'mp4',
'title': '60 minutos (Lasa y Zabala, 30 años)',
'description': 'Programa de reportajes de actualidad.',
'duration': 3996.76,
'timestamp': 1381789200,
'upload_date': '20131014',
'tags': list,
},
}
def _real_extract(self, url):
video_id = self._match_id(url)
video = self._download_json(
f'http://mam.eitb.eus/mam/REST/ServiceMultiweb/Video/MULTIWEBTV/{video_id}/',
video_id, 'Downloading video JSON')
media = video['web_media'][0]
formats = []
for rendition in media['RENDITIONS']:
video_url = rendition.get('PMD_URL')
if not video_url:
continue
tbr = float_or_none(rendition.get('ENCODING_RATE'), 1000)
formats.append({
'url': rendition['PMD_URL'],
'format_id': join_nonempty('http', int_or_none(tbr)),
'width': int_or_none(rendition.get('FRAME_WIDTH')),
'height': int_or_none(rendition.get('FRAME_HEIGHT')),
'tbr': tbr,
})
hls_url = media.get('HLS_SURL')
if hls_url:
request = Request(
'http://mam.eitb.eus/mam/REST/ServiceMultiweb/DomainRestrictedSecurity/TokenAuth/',
headers={'Referer': url})
token_data = self._download_json(
request, video_id, 'Downloading auth token', fatal=False)
if token_data:
token = token_data.get('token')
if token:
formats.extend(self._extract_m3u8_formats(
f'{hls_url}?hdnts={token}', video_id, m3u8_id='hls', fatal=False))
hds_url = media.get('HDS_SURL')
if hds_url:
formats.extend(self._extract_f4m_formats(
'{}?hdcore=3.7.0'.format(hds_url.replace('euskalsvod', 'euskalvod')),
video_id, f4m_id='hds', fatal=False))
return {
'id': video_id,
'title': media.get('NAME_ES') or media.get('name') or media['NAME_EU'],
'description': media.get('SHORT_DESC_ES') or video.get('desc_group') or media.get('SHORT_DESC_EU'),
'thumbnail': media.get('STILL_URL') or media.get('THUMBNAIL_URL'),
'duration': float_or_none(media.get('LENGTH'), 1000),
'timestamp': parse_iso8601(media.get('BROADCST_DATE'), ' '),
'tags': media.get('TAGS'),
'formats': formats,
}

View File

@ -1,61 +0,0 @@
from .common import InfoExtractor
from ..utils import (
ExtractorError,
parse_duration,
xpath_text,
)
class EyedoTVIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?eyedo\.tv/[^/]+/(?:#!/)?Live/Detail/(?P<id>[0-9]+)'
_TEST = {
'url': 'https://www.eyedo.tv/en-US/#!/Live/Detail/16301',
'md5': 'ba14f17995cdfc20c36ba40e21bf73f7',
'info_dict': {
'id': '16301',
'ext': 'mp4',
'title': 'Journée du conseil scientifique de l\'Afnic 2015',
'description': 'md5:4abe07293b2f73efc6e1c37028d58c98',
'uploader': 'Afnic Live',
'uploader_id': '8023',
},
}
_ROOT_URL = 'http://live.eyedo.net:1935/'
def _real_extract(self, url):
video_id = self._match_id(url)
video_data = self._download_xml(f'http://eyedo.tv/api/live/GetLive/{video_id}', video_id)
def _add_ns(path):
return self._xpath_ns(path, 'http://schemas.datacontract.org/2004/07/EyeDo.Core.Implementation.Web.ViewModels.Api')
title = xpath_text(video_data, _add_ns('Titre'), 'title', True)
state_live_code = xpath_text(video_data, _add_ns('StateLiveCode'), 'title', True)
if state_live_code == 'avenir':
raise ExtractorError(
f'{self.IE_NAME} said: We\'re sorry, but this video is not yet available.',
expected=True)
is_live = state_live_code == 'live'
m3u8_url = None
# http://eyedo.tv/Content/Html5/Scripts/html5view.js
if is_live:
if xpath_text(video_data, 'Cdn') == 'true':
m3u8_url = f'http://rrr.sz.xlcdn.com/?account=eyedo&file=A{video_id}&type=live&service=wowza&protocol=http&output=playlist.m3u8'
else:
m3u8_url = self._ROOT_URL + f'w/{video_id}/eyedo_720p/playlist.m3u8'
else:
m3u8_url = self._ROOT_URL + f'replay-w/{video_id}/mp4:{video_id}.mp4/playlist.m3u8'
return {
'id': video_id,
'title': title,
'formats': self._extract_m3u8_formats(
m3u8_url, video_id, 'mp4', 'm3u8_native'),
'description': xpath_text(video_data, _add_ns('Description')),
'duration': parse_duration(xpath_text(video_data, _add_ns('Duration'))),
'uploader': xpath_text(video_data, _add_ns('Createur')),
'uploader_id': xpath_text(video_data, _add_ns('CreateurId')),
'chapter': xpath_text(video_data, _add_ns('ChapitreTitre')),
'chapter_id': xpath_text(video_data, _add_ns('ChapitreId')),
}

View File

@ -12,14 +12,6 @@ class FootyRoomIE(InfoExtractor):
}, },
'playlist_count': 2, 'playlist_count': 2,
'add_ie': [StreamableIE.ie_key()], 'add_ie': [StreamableIE.ie_key()],
}, {
'url': 'http://footyroom.com/matches/75817984/georgia-vs-germany/review',
'info_dict': {
'id': '75817984',
'title': 'VIDEO Georgia 0 - 2 Germany',
},
'playlist_count': 1,
'add_ie': ['Playwire'],
}] }]
def _real_extract(self, url): def _real_extract(self, url):
@ -38,13 +30,6 @@ class FootyRoomIE(InfoExtractor):
payload = video.get('payload') payload = video.get('payload')
if not payload: if not payload:
continue continue
playwire_url = self._html_search_regex(
r'data-config="([^"]+)"', payload,
'playwire url', default=None)
if playwire_url:
entries.append(self.url_result(self._proto_relative_url(
playwire_url, 'http:'), 'Playwire'))
streamable_url = StreamableIE._extract_url(payload) streamable_url = StreamableIE._extract_url(payload)
if streamable_url: if streamable_url:
entries.append(self.url_result( entries.append(self.url_result(

View File

@ -1,56 +0,0 @@
from .common import InfoExtractor
from ..utils import month_by_name
class FranceInterIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?franceinter\.fr/emissions/(?P<id>[^?#]+)'
_TEST = {
'url': 'https://www.franceinter.fr/emissions/affaires-sensibles/affaires-sensibles-07-septembre-2016',
'md5': '9e54d7bdb6fdc02a841007f8a975c094',
'info_dict': {
'id': 'affaires-sensibles/affaires-sensibles-07-septembre-2016',
'ext': 'mp3',
'title': 'Affaire Cahuzac : le contentieux du compte en Suisse',
'description': 'md5:401969c5d318c061f86bda1fa359292b',
'thumbnail': r're:^https?://.*\.jpg',
'upload_date': '20160907',
},
}
def _real_extract(self, url):
video_id = self._match_id(url)
webpage = self._download_webpage(url, video_id)
video_url = self._search_regex(
r'(?s)<div[^>]+class=["\']page-diffusion["\'][^>]*>.*?<button[^>]+data-url=(["\'])(?P<url>(?:(?!\1).)+)\1',
webpage, 'video url', group='url')
title = self._og_search_title(webpage)
description = self._og_search_description(webpage)
thumbnail = self._html_search_meta(['og:image', 'twitter:image'], webpage)
upload_date_str = self._search_regex(
r'class=["\']\s*cover-emission-period\s*["\'][^>]*>[^<]+\s+(\d{1,2}\s+[^\s]+\s+\d{4})<',
webpage, 'upload date', fatal=False)
if upload_date_str:
upload_date_list = upload_date_str.split()
upload_date_list.reverse()
upload_date_list[1] = '%02d' % (month_by_name(upload_date_list[1], lang='fr') or 0)
upload_date_list[2] = '%02d' % int(upload_date_list[2])
upload_date = ''.join(upload_date_list)
else:
upload_date = None
return {
'id': video_id,
'title': title,
'description': description,
'thumbnail': thumbnail,
'upload_date': upload_date,
'formats': [{
'url': video_url,
'vcodec': 'none',
}],
}

View File

@ -1,73 +0,0 @@
from .common import InfoExtractor
from ..networking import HEADRequest
class FujiTVFODPlus7IE(InfoExtractor):
_VALID_URL = r'https?://fod\.fujitv\.co\.jp/title/(?P<sid>[0-9a-z]{4})/(?P<id>[0-9a-z]+)'
_BASE_URL = 'https://i.fod.fujitv.co.jp/'
_BITRATE_MAP = {
300: (320, 180),
800: (640, 360),
1200: (1280, 720),
2000: (1280, 720),
4000: (1920, 1080),
}
_TESTS = [{
'url': 'https://fod.fujitv.co.jp/title/5d40/5d40110076',
'info_dict': {
'id': '5d40110076',
'ext': 'ts',
'title': '#1318 『まる子、まぼろしの洋館を見る』の巻',
'series': 'ちびまる子ちゃん',
'series_id': '5d40',
'description': 'md5:b3f51dbfdda162ac4f789e0ff4d65750',
'thumbnail': 'https://i.fod.fujitv.co.jp/img/program/5d40/episode/5d40110076_a.jpg',
},
}, {
'url': 'https://fod.fujitv.co.jp/title/5d40/5d40810083',
'info_dict': {
'id': '5d40810083',
'ext': 'ts',
'title': '#1324 『まる子とオニの子』の巻『結成2月をムダにしない会』の巻',
'description': 'md5:3972d900b896adc8ab1849e310507efa',
'series': 'ちびまる子ちゃん',
'series_id': '5d40',
'thumbnail': 'https://i.fod.fujitv.co.jp/img/program/5d40/episode/5d40810083_a.jpg'},
'skip': 'Video available only in one week',
}]
def _real_extract(self, url):
series_id, video_id = self._match_valid_url(url).groups()
self._request_webpage(HEADRequest(url), video_id)
json_info = {}
token = self._get_cookies(url).get('CT')
if token:
json_info = self._download_json(
f'https://fod-sp.fujitv.co.jp/apps/api/episode/detail/?ep_id={video_id}&is_premium=false',
video_id, headers={'x-authorization': f'Bearer {token.value}'}, fatal=False)
else:
self.report_warning(f'The token cookie is needed to extract video metadata. {self._login_hint("cookies")}')
formats, subtitles = [], {}
src_json = self._download_json(f'{self._BASE_URL}abrjson_v2/tv_android/{video_id}', video_id)
for src in src_json['video_selector']:
if not src.get('url'):
continue
fmt, subs = self._extract_m3u8_formats_and_subtitles(src['url'], video_id, 'ts')
for f in fmt:
f.update(dict(zip(('height', 'width'),
self._BITRATE_MAP.get(f.get('tbr'), ()), strict=False)))
formats.extend(fmt)
subtitles = self._merge_subtitles(subtitles, subs)
return {
'id': video_id,
'title': json_info.get('ep_title'),
'series': json_info.get('lu_title'),
'series_id': series_id,
'description': json_info.get('ep_description'),
'formats': formats,
'subtitles': subtitles,
'thumbnail': f'{self._BASE_URL}img/program/{series_id}/episode/{video_id}_a.jpg',
'_format_sort_fields': ('tbr', ),
}

View File

@ -1,70 +1,13 @@
import re
from .common import InfoExtractor from .common import InfoExtractor
from ..utils import ( from ..utils import (
clean_html, clean_html,
int_or_none, int_or_none,
parse_codecs, parse_codecs,
parse_duration, parse_duration,
str_to_int,
unified_timestamp, unified_timestamp,
) )
class GabTVIE(InfoExtractor):
_VALID_URL = r'https?://tv\.gab\.com/channel/[^/]+/view/(?P<id>[a-z0-9-]+)'
_TESTS = [{
'url': 'https://tv.gab.com/channel/wurzelroot/view/why-was-america-in-afghanistan-61217eacea5665de450d0488',
'info_dict': {
'id': '61217eacea5665de450d0488',
'ext': 'mp4',
'title': 'WHY WAS AMERICA IN AFGHANISTAN - AMERICA FIRST AGAINST AMERICAN OLIGARCHY',
'uploader': 'Wurzelroot',
'uploader_id': '608fb0a85738fd1974984f7d',
'thumbnail': 'https://tv.gab.com/image/61217eacea5665de450d0488',
},
}]
def _real_extract(self, url):
video_id = self._match_id(url).split('-')[-1]
webpage = self._download_webpage(url, video_id)
channel_id = self._search_regex(r'data-channel-id=\"(?P<channel_id>[^\"]+)', webpage, 'channel_id')
channel_name = self._search_regex(r'data-channel-name=\"(?P<channel_id>[^\"]+)', webpage, 'channel_name')
title = self._search_regex(r'data-episode-title=\"(?P<channel_id>[^\"]+)', webpage, 'title')
view_key = self._search_regex(r'data-view-key=\"(?P<channel_id>[^\"]+)', webpage, 'view_key')
description = clean_html(
self._html_search_regex(self._meta_regex('description'), webpage, 'description', group='content')) or None
available_resolutions = re.findall(
rf'<a\ data-episode-id=\"{video_id}\"\ data-resolution=\"(?P<resolution>[^\"]+)', webpage)
formats = []
for resolution in available_resolutions:
frmt = {
'url': f'https://tv.gab.com/media/{video_id}?viewKey={view_key}&r={resolution}',
'format_id': resolution,
'vcodec': 'h264',
'acodec': 'aac',
'ext': 'mp4',
}
if 'audio-' in resolution:
frmt['abr'] = str_to_int(resolution.replace('audio-', ''))
frmt['height'] = 144
frmt['quality'] = -10
else:
frmt['height'] = str_to_int(resolution.replace('p', ''))
formats.append(frmt)
return {
'id': video_id,
'title': title,
'formats': formats,
'description': description,
'uploader': channel_name,
'uploader_id': channel_id,
'thumbnail': f'https://tv.gab.com/image/{video_id}',
}
class GabIE(InfoExtractor): class GabIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?gab\.com/[^/]+/posts/(?P<id>\d+)' _VALID_URL = r'https?://(?:www\.)?gab\.com/[^/]+/posts/(?P<id>\d+)'
_TESTS = [{ _TESTS = [{

View File

@ -1,84 +0,0 @@
import json
from .common import InfoExtractor
from ..utils import (
clean_podcast_url,
int_or_none,
try_get,
urlencode_postdata,
)
class GooglePodcastsBaseIE(InfoExtractor):
_VALID_URL_BASE = r'https?://podcasts\.google\.com/feed/'
def _batch_execute(self, func_id, video_id, params):
return json.loads(self._download_json(
'https://podcasts.google.com/_/PodcastsUi/data/batchexecute',
video_id, data=urlencode_postdata({
'f.req': json.dumps([[[func_id, json.dumps(params), None, '1']]]),
}), transform_source=lambda x: self._search_regex(r'(?s)(\[.+\])', x, 'data'))[0][2])
def _extract_episode(self, episode):
return {
'id': episode[4][3],
'title': episode[8],
'url': clean_podcast_url(episode[13]),
'thumbnail': episode[2],
'description': episode[9],
'creator': try_get(episode, lambda x: x[14]),
'timestamp': int_or_none(episode[11]),
'duration': int_or_none(episode[12]),
'series': episode[1],
}
class GooglePodcastsIE(GooglePodcastsBaseIE):
IE_NAME = 'google:podcasts'
_VALID_URL = GooglePodcastsBaseIE._VALID_URL_BASE + r'(?P<feed_url>[^/]+)/episode/(?P<id>[^/?&#]+)'
_TEST = {
'url': 'https://podcasts.google.com/feed/aHR0cHM6Ly9mZWVkcy5ucHIub3JnLzM0NDA5ODUzOS9wb2RjYXN0LnhtbA/episode/MzBlNWRlN2UtOWE4Yy00ODcwLTk2M2MtM2JlMmUyNmViOTRh',
'md5': 'fa56b2ee8bd0703e27e42d4b104c4766',
'info_dict': {
'id': '30e5de7e-9a8c-4870-963c-3be2e26eb94a',
'ext': 'mp3',
'title': 'WWDTM New Year 2021',
'description': 'We say goodbye to 2020 with Christine Baranksi, Doug Jones, Jonna Mendez, and Kellee Edwards.',
'upload_date': '20210102',
'timestamp': 1609606800,
'duration': 2901,
'series': "Wait Wait... Don't Tell Me!",
},
}
def _real_extract(self, url):
b64_feed_url, b64_guid = self._match_valid_url(url).groups()
episode = self._batch_execute(
'oNjqVe', b64_guid, [b64_feed_url, b64_guid])[1]
return self._extract_episode(episode)
class GooglePodcastsFeedIE(GooglePodcastsBaseIE):
IE_NAME = 'google:podcasts:feed'
_VALID_URL = GooglePodcastsBaseIE._VALID_URL_BASE + r'(?P<id>[^/?&#]+)/?(?:[?#&]|$)'
_TEST = {
'url': 'https://podcasts.google.com/feed/aHR0cHM6Ly9mZWVkcy5ucHIub3JnLzM0NDA5ODUzOS9wb2RjYXN0LnhtbA',
'info_dict': {
'title': "Wait Wait... Don't Tell Me!",
'description': "NPR's weekly current events quiz. Have a laugh and test your news knowledge while figuring out what's real and what we've made up.",
},
'playlist_mincount': 20,
}
def _real_extract(self, url):
b64_feed_url = self._match_id(url)
data = self._batch_execute('ncqJEe', b64_feed_url, [b64_feed_url])
entries = []
for episode in (try_get(data, lambda x: x[1][0]) or []):
entries.append(self._extract_episode(episode))
feed = try_get(data, lambda x: x[3]) or []
return self.playlist_result(
entries, playlist_title=try_get(feed, lambda x: x[0]),
playlist_description=try_get(feed, lambda x: x[2]))

View File

@ -1,47 +0,0 @@
import urllib.parse
from .common import InfoExtractor
from ..utils import (
parse_duration,
)
class GoshgayIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?goshgay\.com/video(?P<id>\d+?)($|/)'
_TEST = {
'url': 'http://www.goshgay.com/video299069/diesel_sfw_xxx_video',
'md5': '4b6db9a0a333142eb9f15913142b0ed1',
'info_dict': {
'id': '299069',
'ext': 'flv',
'title': 'DIESEL SFW XXX Video',
'thumbnail': r're:^http://.*\.jpg$',
'duration': 80,
'age_limit': 18,
},
}
def _real_extract(self, url):
video_id = self._match_id(url)
webpage = self._download_webpage(url, video_id)
title = self._html_search_regex(
r'<h2>(.*?)<', webpage, 'title')
duration = parse_duration(self._html_search_regex(
r'<span class="duration">\s*-?\s*(.*?)</span>',
webpage, 'duration', fatal=False))
flashvars = urllib.parse.parse_qs(self._html_search_regex(
r'<embed.+?id="flash-player-embed".+?flashvars="([^"]+)"',
webpage, 'flashvars'))
thumbnail = flashvars.get('url_bigthumb', [None])[0]
video_url = flashvars['flv_url'][0]
return {
'id': video_id,
'url': video_url,
'title': title,
'thumbnail': thumbnail,
'duration': duration,
'age_limit': 18,
}

View File

@ -1,32 +0,0 @@
from .common import InfoExtractor
class GPUTechConfIE(InfoExtractor):
_VALID_URL = r'https?://on-demand\.gputechconf\.com/gtc/2015/video/S(?P<id>\d+)\.html'
_TEST = {
'url': 'http://on-demand.gputechconf.com/gtc/2015/video/S5156.html',
'md5': 'a8862a00a0fd65b8b43acc5b8e33f798',
'info_dict': {
'id': '5156',
'ext': 'mp4',
'title': 'Coordinating More Than 3 Million CUDA Threads for Social Network Analysis',
'duration': 1219,
},
}
def _real_extract(self, url):
video_id = self._match_id(url)
webpage = self._download_webpage(url, video_id)
root_path = self._search_regex(
r'var\s+rootPath\s*=\s*"([^"]+)', webpage, 'root path',
default='http://evt.dispeak.com/nvidia/events/gtc15/')
xml_file_id = self._search_regex(
r'var\s+xmlFileId\s*=\s*"([^"]+)', webpage, 'xml file id')
return {
'_type': 'url_transparent',
'id': video_id,
'url': f'{root_path}xml/{xml_file_id}.xml',
'ie_key': 'DigitallySpeaking',
}

View File

@ -1,183 +0,0 @@
from .common import InfoExtractor
from ..utils import (
ExtractorError,
clean_html,
int_or_none,
merge_dicts,
parse_count,
str_or_none,
try_get,
unified_strdate,
urlencode_postdata,
urljoin,
)
class HKETVIE(InfoExtractor):
IE_NAME = 'hketv'
IE_DESC = '香港教育局教育電視 (HKETV) Educational Television, Hong Kong Educational Bureau'
_GEO_BYPASS = False
_GEO_COUNTRIES = ['HK']
_VALID_URL = r'https?://(?:www\.)?hkedcity\.net/etv/resource/(?P<id>[0-9]+)'
_TESTS = [{
'url': 'https://www.hkedcity.net/etv/resource/2932360618',
'md5': 'f193712f5f7abb208ddef3c5ea6ed0b7',
'info_dict': {
'id': '2932360618',
'ext': 'mp4',
'title': '喜閱一生(共享閱讀樂) (中、英文字幕可供選擇)',
'description': 'md5:d5286d05219ef50e0613311cbe96e560',
'upload_date': '20181024',
'duration': 900,
'subtitles': 'count:2',
},
'skip': 'Geo restricted to HK',
}, {
'url': 'https://www.hkedcity.net/etv/resource/972641418',
'md5': '1ed494c1c6cf7866a8290edad9b07dc9',
'info_dict': {
'id': '972641418',
'ext': 'mp4',
'title': '衣冠楚楚 (天使系列之一)',
'description': 'md5:10bb3d659421e74f58e5db5691627b0f',
'upload_date': '20070109',
'duration': 907,
'subtitles': {},
},
'skip': 'Geo restricted to HK',
}]
_CC_LANGS = {
'中文(繁體中文)': 'zh-Hant',
'中文(简体中文)': 'zh-Hans',
'English': 'en',
'Bahasa Indonesia': 'id',
'\u0939\u093f\u0928\u094d\u0926\u0940': 'hi',
'\u0928\u0947\u092a\u093e\u0932\u0940': 'ne',
'Tagalog': 'tl',
'\u0e44\u0e17\u0e22': 'th',
'\u0627\u0631\u062f\u0648': 'ur',
}
_FORMAT_HEIGHTS = {
'SD': 360,
'HD': 720,
}
_APPS_BASE_URL = 'https://apps.hkedcity.net'
def _real_extract(self, url):
video_id = self._match_id(url)
webpage = self._download_webpage(url, video_id)
title = (
self._html_search_meta(
('ed_title', 'search.ed_title'), webpage, default=None)
or self._search_regex(
r'data-favorite_title_(?:eng|chi)=(["\'])(?P<id>(?:(?!\1).)+)\1',
webpage, 'title', default=None, group='url')
or self._html_search_regex(
r'<h1>([^<]+)</h1>', webpage, 'title', default=None)
or self._og_search_title(webpage)
)
file_id = self._search_regex(
r'post_var\[["\']file_id["\']\s*\]\s*=\s*(.+?);',
webpage, 'file ID')
curr_url = self._search_regex(
r'post_var\[["\']curr_url["\']\s*\]\s*=\s*"(.+?)";',
webpage, 'curr URL')
data = {
'action': 'get_info',
'curr_url': curr_url,
'file_id': file_id,
'video_url': file_id,
}
response = self._download_json(
self._APPS_BASE_URL + '/media/play/handler.php', video_id,
data=urlencode_postdata(data),
headers=merge_dicts({
'Content-Type': 'application/x-www-form-urlencoded'},
self.geo_verification_headers()))
result = response['result']
if not response.get('success') or not response.get('access'):
error = clean_html(response.get('access_err_msg'))
if 'Video streaming is not available in your country' in error:
self.raise_geo_restricted(
msg=error, countries=self._GEO_COUNTRIES)
else:
raise ExtractorError(error, expected=True)
formats = []
width = int_or_none(result.get('width'))
height = int_or_none(result.get('height'))
playlist0 = result['playlist'][0]
for fmt in playlist0['sources']:
file_url = urljoin(self._APPS_BASE_URL, fmt.get('file'))
if not file_url:
continue
# If we ever wanted to provide the final resolved URL that
# does not require cookies, albeit with a shorter lifespan:
# urlh = self._downloader.urlopen(file_url)
# resolved_url = urlh.url
label = fmt.get('label')
h = self._FORMAT_HEIGHTS.get(label)
w = h * width // height if h and width and height else None
formats.append({
'format_id': label,
'ext': fmt.get('type'),
'url': file_url,
'width': w,
'height': h,
})
subtitles = {}
tracks = try_get(playlist0, lambda x: x['tracks'], list) or []
for track in tracks:
if not isinstance(track, dict):
continue
track_kind = str_or_none(track.get('kind'))
if not track_kind or not isinstance(track_kind, str):
continue
if track_kind.lower() not in ('captions', 'subtitles'):
continue
track_url = urljoin(self._APPS_BASE_URL, track.get('file'))
if not track_url:
continue
track_label = track.get('label')
subtitles.setdefault(self._CC_LANGS.get(
track_label, track_label), []).append({
'url': self._proto_relative_url(track_url),
'ext': 'srt',
})
# Likes
emotion = self._download_json(
'https://emocounter.hkedcity.net/handler.php', video_id,
data=urlencode_postdata({
'action': 'get_emotion',
'data[bucket_id]': 'etv',
'data[identifier]': video_id,
}),
headers={'Content-Type': 'application/x-www-form-urlencoded'},
fatal=False) or {}
like_count = int_or_none(try_get(
emotion, lambda x: x['data']['emotion_data'][0]['count']))
return {
'id': video_id,
'title': title,
'description': self._html_search_meta(
'description', webpage, fatal=False),
'upload_date': unified_strdate(self._html_search_meta(
'ed_date', webpage, fatal=False), day_first=False),
'duration': int_or_none(result.get('length')),
'formats': formats,
'subtitles': subtitles,
'thumbnail': urljoin(self._APPS_BASE_URL, result.get('image')),
'view_count': parse_count(result.get('view_count')),
'like_count': like_count,
}

View File

@ -1,115 +0,0 @@
from .common import InfoExtractor
from ..utils import traverse_obj, try_call, url_or_none
class IdolPlusIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?idolplus\.com/z[us]/(?:concert/|contents/?\?(?:[^#]+&)?albumId=)(?P<id>\w+)'
_TESTS = [{
'url': 'https://idolplus.com/zs/contents?albumId=M012077298PPV00',
'md5': '2ace3f4661c943a2f7e79f0b88cea1e7',
'info_dict': {
'id': 'M012077298PPV00',
'ext': 'mp4',
'title': '[MultiCam] Aegyo on Top of Aegyo (IZ*ONE EATING TRIP)',
'release_date': '20200707',
'formats': 'count:65',
},
'params': {'format': '532-KIM_MINJU'},
}, {
'url': 'https://idolplus.com/zs/contents?albumId=M01232H058PPV00&catId=E9TX5',
'info_dict': {
'id': 'M01232H058PPV00',
'ext': 'mp4',
'title': 'YENA (CIRCLE CHART MUSIC AWARDS 2022 RED CARPET)',
'release_date': '20230218',
'formats': 'count:5',
},
'params': {'skip_download': 'm3u8'},
}, {
# live stream
'url': 'https://idolplus.com/zu/contents?albumId=M012323174PPV00',
'info_dict': {
'id': 'M012323174PPV00',
'ext': 'mp4',
'title': 'Hanteo Music Awards 2022 DAY2',
'release_date': '20230211',
'formats': 'count:5',
},
'params': {'skip_download': 'm3u8'},
}, {
'url': 'https://idolplus.com/zs/concert/M012323039PPV00',
'info_dict': {
'id': 'M012323039PPV00',
'ext': 'mp4',
'title': 'CIRCLE CHART MUSIC AWARDS 2022',
'release_date': '20230218',
'formats': 'count:5',
},
'params': {'skip_download': 'm3u8'},
}]
def _real_extract(self, url):
video_id = self._match_id(url)
data_list = traverse_obj(self._download_json(
'https://idolplus.com/api/zs/viewdata/ruleset/build', video_id,
headers={'App_type': 'web', 'Country_Code': 'KR'}, query={
'rulesetId': 'contents',
'albumId': video_id,
'distribute': 'PRD',
'loggedIn': 'false',
'region': 'zs',
'countryGroup': '00010',
'lang': 'en',
'saId': '999999999998',
}), ('data', 'viewData', ...))
player_data = {}
while data_list:
player_data = data_list.pop()
if traverse_obj(player_data, 'type') == 'player':
break
elif traverse_obj(player_data, ('dataList', ...)):
data_list += player_data['dataList']
formats = self._extract_m3u8_formats(traverse_obj(player_data, (
'vodPlayerList', 'vodProfile', 0, 'vodServer', 0, 'video_url', {url_or_none})), video_id)
subtitles = {}
for caption in traverse_obj(player_data, ('vodPlayerList', 'caption')) or []:
subtitles.setdefault(caption.get('lang') or 'und', []).append({
'url': caption.get('smi_url'),
'ext': 'vtt',
})
# Add member multicams as alternative formats
if (traverse_obj(player_data, ('detail', 'has_cuesheet')) == 'Y'
and traverse_obj(player_data, ('detail', 'is_omni_member')) == 'Y'):
cuesheet = traverse_obj(self._download_json(
'https://idolplus.com/gapi/contents/v1.0/content/cuesheet', video_id,
'Downloading JSON metadata for member multicams',
headers={'App_type': 'web', 'Country_Code': 'KR'}, query={
'ALBUM_ID': video_id,
'COUNTRY_GRP': '00010',
'LANG': 'en',
'SA_ID': '999999999998',
'COUNTRY_CODE': 'KR',
}), ('data', 'cuesheet_item', 0))
for member in traverse_obj(cuesheet, ('members', ...)):
index = try_call(lambda: int(member['omni_view_index']) - 1)
member_video_url = traverse_obj(cuesheet, ('omni_view', index, 'cdn_url', 0, 'url', {url_or_none}))
if not member_video_url:
continue
member_formats = self._extract_m3u8_formats(
member_video_url, video_id, note=f'Downloading m3u8 for multicam {member["name"]}')
for mf in member_formats:
mf['format_id'] = f'{mf["format_id"]}-{member["name"].replace(" ", "_")}'
formats.extend(member_formats)
return {
'id': video_id,
'title': traverse_obj(player_data, ('detail', 'albumName')),
'formats': formats,
'subtitles': subtitles,
'release_date': traverse_obj(player_data, ('detail', 'broadcastDate')),
}

View File

@ -1,7 +1,7 @@
import base64 import base64
import urllib.parse import urllib.parse
from .bokecc import BokeCCBaseIE from .common import InfoExtractor
from ..utils import ( from ..utils import (
ExtractorError, ExtractorError,
determine_ext, determine_ext,
@ -10,7 +10,7 @@ from ..utils import (
) )
class InfoQIE(BokeCCBaseIE): class InfoQIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?infoq\.com/(?:[^/]+/)+(?P<id>[^/]+)' _VALID_URL = r'https?://(?:www\.)?infoq\.com/(?:[^/]+/)+(?P<id>[^/]+)'
_TESTS = [{ _TESTS = [{
@ -117,10 +117,6 @@ class InfoQIE(BokeCCBaseIE):
video_title = self._html_extract_title(webpage) video_title = self._html_extract_title(webpage)
video_description = self._html_search_meta('description', webpage, 'description') video_description = self._html_search_meta('description', webpage, 'description')
if '/cn/' in url:
# for China videos, HTTP video URL exists but always fails with 403
formats = self._extract_bokecc_formats(webpage, video_id)
else:
formats = ( formats = (
self._extract_rtmp_video(webpage) self._extract_rtmp_video(webpage)
+ self._extract_http_video(webpage) + self._extract_http_video(webpage)

View File

@ -1,58 +0,0 @@
import json
import re
from .common import InfoExtractor
from ..utils import parse_qs
class InternetVideoArchiveIE(InfoExtractor):
_VALID_URL = r'https?://video\.internetvideoarchive\.net/(?:player|flash/players)/.*?\?.*?publishedid.*?'
_TEST = {
'url': 'http://video.internetvideoarchive.net/player/6/configuration.ashx?customerid=69249&publishedid=194487&reporttag=vdbetatitle&playerid=641&autolist=0&domain=www.videodetective.com&maxrate=high&minrate=low&socialplayer=false',
'info_dict': {
'id': '194487',
'ext': 'mp4',
'title': 'Kick-Ass 2',
'description': 'md5:c189d5b7280400630a1d3dd17eaa8d8a',
},
'params': {
# m3u8 download
'skip_download': True,
},
}
@staticmethod
def _build_json_url(query):
return 'http://video.internetvideoarchive.net/player/6/configuration.ashx?' + query
def _real_extract(self, url):
query = parse_qs(url)
video_id = query['publishedid'][0]
data = self._download_json(
'https://video.internetvideoarchive.net/videojs7/videojs7.ivasettings.ashx',
video_id, data=json.dumps({
'customerid': query['customerid'][0],
'publishedid': video_id,
}).encode())
title = data['Title']
formats = self._extract_m3u8_formats(
data['VideoUrl'], video_id, 'mp4',
'm3u8_native', m3u8_id='hls', fatal=False)
file_url = formats[0]['url']
if '.ism/' in file_url:
replace_url = lambda x: re.sub(r'\.ism/[^?]+', '.ism/' + x, file_url)
formats.extend(self._extract_f4m_formats(
replace_url('.f4m'), video_id, f4m_id='hds', fatal=False))
formats.extend(self._extract_mpd_formats(
replace_url('.mpd'), video_id, mpd_id='dash', fatal=False))
formats.extend(self._extract_ism_formats(
replace_url('Manifest'), video_id, ism_id='mss', fatal=False))
return {
'id': video_id,
'title': title,
'formats': formats,
'thumbnail': data.get('PosterUrl'),
'description': data.get('Description'),
}

View File

@ -29,6 +29,12 @@ class IwaraBaseIE(InfoExtractor):
self.to_screen(f'{token_type} token has expired') self.to_screen(f'{token_type} token has expired')
return True return True
def _call_api(self, path, video_id, **kwargs):
impersonate = kwargs.pop('impersonate', True)
return self._download_json(
f'https://api.iwara.tv/{path}', video_id,
impersonate=impersonate, **kwargs)
def _get_user_token(self): def _get_user_token(self):
username, password = self._get_login_info() username, password = self._get_login_info()
if not username or not password: if not username or not password:
@ -36,8 +42,8 @@ class IwaraBaseIE(InfoExtractor):
user_token = IwaraBaseIE._USERTOKEN or self.cache.load(self._NETRC_MACHINE, username) user_token = IwaraBaseIE._USERTOKEN or self.cache.load(self._NETRC_MACHINE, username)
if not user_token or self._is_token_expired(user_token, 'User'): if not user_token or self._is_token_expired(user_token, 'User'):
response = self._download_json( response = self._call_api(
'https://api.iwara.tv/user/login', None, note='Logging in', 'user/login', None, note='Logging in',
headers={'Content-Type': 'application/json'}, data=json.dumps({ headers={'Content-Type': 'application/json'}, data=json.dumps({
'email': username, 'email': username,
'password': password, 'password': password,
@ -60,8 +66,8 @@ class IwaraBaseIE(InfoExtractor):
return # user has not passed credentials return # user has not passed credentials
if not IwaraBaseIE._MEDIATOKEN or self._is_token_expired(IwaraBaseIE._MEDIATOKEN, 'Media'): if not IwaraBaseIE._MEDIATOKEN or self._is_token_expired(IwaraBaseIE._MEDIATOKEN, 'Media'):
IwaraBaseIE._MEDIATOKEN = self._download_json( IwaraBaseIE._MEDIATOKEN = self._call_api(
'https://api.iwara.tv/user/token', None, note='Fetching media token', 'user/token', None, note='Fetching media token',
data=b'', headers={ data=b'', headers={
'Authorization': f'Bearer {IwaraBaseIE._USERTOKEN}', 'Authorization': f'Bearer {IwaraBaseIE._USERTOKEN}',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -147,11 +153,11 @@ class IwaraIE(IwaraBaseIE):
q = urllib.parse.parse_qs(up.query) q = urllib.parse.parse_qs(up.query)
paths = up.path.rstrip('/').split('/') paths = up.path.rstrip('/').split('/')
# https://github.com/yt-dlp/yt-dlp/issues/6549#issuecomment-1473771047 # https://github.com/yt-dlp/yt-dlp/issues/6549#issuecomment-1473771047
x_version = hashlib.sha1('_'.join((paths[-1], q['expires'][0], '5nFp9kmbNnHdAFhaqMvt')).encode()).hexdigest() x_version = hashlib.sha1('_'.join((paths[-1], q['expires'][0], 'mSvL05GfEmeEmsEYfGCnVpEjYgTJraJN')).encode()).hexdigest()
preference = qualities(['preview', '360', '540', 'Source']) preference = qualities(['preview', '360', '540', 'Source'])
files = self._download_json(fileurl, video_id, headers={'X-Version': x_version}) files = self._download_json(fileurl, video_id, headers={'X-Version': x_version}, impersonate=True)
for fmt in files: for fmt in files:
yield traverse_obj(fmt, { yield traverse_obj(fmt, {
'format_id': 'name', 'format_id': 'name',
@ -164,8 +170,8 @@ class IwaraIE(IwaraBaseIE):
def _real_extract(self, url): def _real_extract(self, url):
video_id = self._match_id(url) video_id = self._match_id(url)
username, _ = self._get_login_info() username, _ = self._get_login_info()
video_data = self._download_json( video_data = self._call_api(
f'https://api.iwara.tv/video/{video_id}', video_id, f'video/{video_id}', video_id,
expected_status=lambda x: True, headers=self._get_media_token()) expected_status=lambda x: True, headers=self._get_media_token())
errmsg = video_data.get('message') errmsg = video_data.get('message')
# at this point we can actually get uploaded user info, but do we need it? # at this point we can actually get uploaded user info, but do we need it?
@ -237,8 +243,8 @@ class IwaraUserIE(IwaraBaseIE):
}] }]
def _entries(self, playlist_id, user_id, page): def _entries(self, playlist_id, user_id, page):
videos = self._download_json( videos = self._call_api(
'https://api.iwara.tv/videos', playlist_id, 'videos', playlist_id,
note=f'Downloading page {page}', note=f'Downloading page {page}',
query={ query={
'page': page, 'page': page,
@ -251,8 +257,8 @@ class IwaraUserIE(IwaraBaseIE):
def _real_extract(self, url): def _real_extract(self, url):
playlist_id = self._match_id(url) playlist_id = self._match_id(url)
user_info = self._download_json( user_info = self._call_api(
f'https://api.iwara.tv/profile/{playlist_id}', playlist_id, f'profile/{playlist_id}', playlist_id,
note='Requesting user info') note='Requesting user info')
user_id = traverse_obj(user_info, ('user', 'id')) user_id = traverse_obj(user_info, ('user', 'id'))
@ -277,8 +283,8 @@ class IwaraPlaylistIE(IwaraBaseIE):
}] }]
def _entries(self, playlist_id, first_page, page): def _entries(self, playlist_id, first_page, page):
videos = self._download_json( videos = self._call_api(
'https://api.iwara.tv/videos', playlist_id, f'Downloading page {page}', 'videos', playlist_id, note=f'Downloading page {page}',
query={'page': page, 'limit': self._PER_PAGE}, query={'page': page, 'limit': self._PER_PAGE},
headers=self._get_media_token()) if page else first_page headers=self._get_media_token()) if page else first_page
for x in traverse_obj(videos, ('results', ..., 'id')): for x in traverse_obj(videos, ('results', ..., 'id')):
@ -286,9 +292,11 @@ class IwaraPlaylistIE(IwaraBaseIE):
def _real_extract(self, url): def _real_extract(self, url):
playlist_id = self._match_id(url) playlist_id = self._match_id(url)
page_0 = self._download_json( page_0 = self._call_api(
f'https://api.iwara.tv/playlist/{playlist_id}?page=0&limit={self._PER_PAGE}', playlist_id, f'playlist/{playlist_id}', playlist_id,
note='Requesting playlist info', headers=self._get_media_token()) note='Requesting playlist info',
query={'page': 0, 'limit': self._PER_PAGE},
headers=self._get_media_token())
return self.playlist_result( return self.playlist_result(
OnDemandPagedList( OnDemandPagedList(

View File

@ -1,111 +0,0 @@
import urllib.parse
from .common import InfoExtractor
from ..utils import (
determine_ext,
float_or_none,
get_element_by_id,
int_or_none,
parse_iso8601,
str_to_int,
)
class IzleseneIE(InfoExtractor):
_VALID_URL = r'''(?x)
https?://(?:(?:www|m)\.)?izlesene\.com/
(?:video|embedplayer)/(?:[^/]+/)?(?P<id>[0-9]+)
'''
_TESTS = [
{
'url': 'http://www.izlesene.com/video/sevincten-cildirtan-dogum-gunu-hediyesi/7599694',
'md5': '4384f9f0ea65086734b881085ee05ac2',
'info_dict': {
'id': '7599694',
'ext': 'mp4',
'title': 'Sevinçten Çıldırtan Doğum Günü Hediyesi',
'description': 'md5:253753e2655dde93f59f74b572454f6d',
'thumbnail': r're:^https?://.*\.jpg',
'uploader_id': 'pelikzzle',
'timestamp': int,
'upload_date': '20140702',
'duration': 95.395,
'age_limit': 0,
},
},
{
'url': 'http://www.izlesene.com/video/tarkan-dortmund-2006-konseri/17997',
'md5': '97f09b6872bffa284cb7fa4f6910cb72',
'info_dict': {
'id': '17997',
'ext': 'mp4',
'title': 'Tarkan Dortmund 2006 Konseri',
'thumbnail': r're:^https://.*\.jpg',
'uploader_id': 'parlayankiz',
'timestamp': int,
'upload_date': '20061112',
'duration': 253.666,
'age_limit': 0,
},
},
]
def _real_extract(self, url):
video_id = self._match_id(url)
webpage = self._download_webpage(f'http://www.izlesene.com/video/{video_id}', video_id)
video = self._parse_json(
self._search_regex(
r'videoObj\s*=\s*({.+?})\s*;\s*\n', webpage, 'streams'),
video_id)
title = video.get('videoTitle') or self._og_search_title(webpage)
formats = []
for stream in video['media']['level']:
source_url = stream.get('source')
if not source_url or not isinstance(source_url, str):
continue
ext = determine_ext(url, 'mp4')
quality = stream.get('value')
height = int_or_none(quality)
formats.append({
'format_id': f'{quality}p' if quality else 'sd',
'url': urllib.parse.unquote(source_url),
'ext': ext,
'height': height,
})
description = self._og_search_description(webpage, default=None)
thumbnail = video.get('posterURL') or self._proto_relative_url(
self._og_search_thumbnail(webpage), scheme='http:')
uploader = self._html_search_regex(
r"adduserUsername\s*=\s*'([^']+)';",
webpage, 'uploader', fatal=False)
timestamp = parse_iso8601(self._html_search_meta(
'uploadDate', webpage, 'upload date'))
duration = float_or_none(video.get('duration') or self._html_search_regex(
r'videoduration["\']?\s*=\s*(["\'])(?P<value>(?:(?!\1).)+)\1',
webpage, 'duration', fatal=False, group='value'), scale=1000)
view_count = str_to_int(get_element_by_id('videoViewCount', webpage))
comment_count = self._html_search_regex(
r'comment_count\s*=\s*\'([^\']+)\';',
webpage, 'comment_count', fatal=False)
return {
'id': video_id,
'title': title,
'description': description,
'thumbnail': thumbnail,
'uploader_id': uploader,
'timestamp': timestamp,
'duration': duration,
'view_count': int_or_none(view_count),
'comment_count': int_or_none(comment_count),
'age_limit': self._family_friendly_search(webpage),
'formats': formats,
}

View File

@ -1,206 +0,0 @@
import urllib.parse
from .common import InfoExtractor
from ..utils import (
int_or_none,
parse_iso8601,
strip_or_none,
try_get,
)
class KinjaEmbedIE(InfoExtractor):
IE_NAME = 'kinja:embed'
_DOMAIN_REGEX = r'''(?:[^.]+\.)?
(?:
avclub|
clickhole|
deadspin|
gizmodo|
jalopnik|
jezebel|
kinja|
kotaku|
lifehacker|
splinternews|
the(?:inventory|onion|root|takeout)
)\.com'''
_COMMON_REGEX = r'''/
(?:
ajax/inset|
embed/video
)/iframe\?.*?\bid='''
_VALID_URL = rf'''(?x)https?://{_DOMAIN_REGEX}{_COMMON_REGEX}
(?P<type>
fb|
imgur|
instagram|
jwp(?:layer)?-video|
kinjavideo|
mcp|
megaphone|
soundcloud(?:-playlist)?|
tumblr-post|
twitch-stream|
twitter|
ustream-channel|
vimeo|
vine|
youtube-(?:list|video)
)-(?P<id>[^&]+)'''
_EMBED_REGEX = [rf'(?x)<iframe[^>]+?src=(?P<q>["\'])(?P<url>(?:(?:https?:)?//{_DOMAIN_REGEX})?{_COMMON_REGEX}(?:(?!\1).)+)\1']
_TESTS = [{
'url': 'https://kinja.com/ajax/inset/iframe?id=fb-10103303356633621',
'only_matching': True,
}, {
'url': 'https://kinja.com/ajax/inset/iframe?id=kinjavideo-100313',
'only_matching': True,
}, {
'url': 'https://kinja.com/ajax/inset/iframe?id=megaphone-PPY1300931075',
'only_matching': True,
}, {
'url': 'https://kinja.com/ajax/inset/iframe?id=soundcloud-128574047',
'only_matching': True,
}, {
'url': 'https://kinja.com/ajax/inset/iframe?id=soundcloud-playlist-317413750',
'only_matching': True,
}, {
'url': 'https://kinja.com/ajax/inset/iframe?id=tumblr-post-160130699814-daydreams-at-midnight',
'only_matching': True,
}, {
'url': 'https://kinja.com/ajax/inset/iframe?id=twitch-stream-libratus_extra',
'only_matching': True,
}, {
'url': 'https://kinja.com/ajax/inset/iframe?id=twitter-1068875942473404422',
'only_matching': True,
}, {
'url': 'https://kinja.com/ajax/inset/iframe?id=ustream-channel-10414700',
'only_matching': True,
}, {
'url': 'https://kinja.com/ajax/inset/iframe?id=vimeo-120153502',
'only_matching': True,
}, {
'url': 'https://kinja.com/ajax/inset/iframe?id=vine-5BlvV5qqPrD',
'only_matching': True,
}, {
'url': 'https://kinja.com/ajax/inset/iframe?id=youtube-list-BCQ3KyrPjgA/PLE6509247C270A72E',
'only_matching': True,
}, {
'url': 'https://kinja.com/ajax/inset/iframe?id=youtube-video-00QyL0AgPAE',
'only_matching': True,
}]
_WEBPAGE_TESTS = [{
'url': 'http://www.clickhole.com/video/dont-understand-bitcoin-man-will-mumble-explanatio-2537',
'info_dict': {
'id': '106351',
'ext': 'mp4',
'title': 'Dont Understand Bitcoin? This Man Will Mumble An Explanation At You',
},
'skip': 'Invalid URL',
}]
_JWPLATFORM_PROVIDER = ('cdn.jwplayer.com/v2/media/', 'JWPlatform')
_PROVIDER_MAP = {
'fb': ('facebook.com/video.php?v=', 'Facebook'),
'imgur': ('imgur.com/', 'Imgur'),
'instagram': ('instagram.com/p/', 'Instagram'),
'jwplayer-video': _JWPLATFORM_PROVIDER,
'jwp-video': _JWPLATFORM_PROVIDER,
'megaphone': ('player.megaphone.fm/', 'Generic'),
'soundcloud': ('api.soundcloud.com/tracks/', 'Soundcloud'),
'soundcloud-playlist': ('api.soundcloud.com/playlists/', 'SoundcloudPlaylist'),
'tumblr-post': ('%s.tumblr.com/post/%s', 'Tumblr'),
'twitch-stream': ('twitch.tv/', 'TwitchStream'),
'twitter': ('twitter.com/i/cards/tfw/v1/', 'TwitterCard'),
'ustream-channel': ('ustream.tv/embed/', 'Ustream'),
'vimeo': ('vimeo.com/', 'Vimeo'),
'vine': ('vine.co/v/', 'Vine'),
'youtube-list': ('youtube.com/embed/%s?list=%s', 'YoutubePlaylist'),
'youtube-video': ('youtube.com/embed/', 'Youtube'),
}
def _real_extract(self, url):
video_type, video_id = self._match_valid_url(url).groups()
provider = self._PROVIDER_MAP.get(video_type)
if provider:
video_id = urllib.parse.unquote(video_id)
if video_type == 'tumblr-post':
video_id, blog = video_id.split('-', 1)
result_url = provider[0] % (blog, video_id)
elif video_type == 'youtube-list':
video_id, playlist_id = video_id.split('/')
result_url = provider[0] % (video_id, playlist_id)
else:
result_url = provider[0] + video_id
return self.url_result('http://' + result_url, provider[1])
if video_type == 'kinjavideo':
data = self._download_json(
'https://kinja.com/api/core/video/views/videoById',
video_id, query={'videoId': video_id})['data']
title = data['title']
formats = []
for k in ('signedPlaylist', 'streaming'):
m3u8_url = data.get(k + 'Url')
if m3u8_url:
formats.extend(self._extract_m3u8_formats(
m3u8_url, video_id, 'mp4', 'm3u8_native',
m3u8_id='hls', fatal=False))
thumbnail = None
poster = data.get('poster') or {}
poster_id = poster.get('id')
if poster_id:
thumbnail = 'https://i.kinja-img.com/gawker-media/image/upload/{}.{}'.format(poster_id, poster.get('format') or 'jpg')
return {
'id': video_id,
'title': title,
'description': strip_or_none(data.get('description')),
'formats': formats,
'tags': data.get('tags'),
'timestamp': int_or_none(try_get(
data, lambda x: x['postInfo']['publishTimeMillis']), 1000),
'thumbnail': thumbnail,
'uploader': data.get('network'),
}
else:
video_data = self._download_json(
'https://api.vmh.univision.com/metadata/v1/content/' + video_id,
video_id)['videoMetadata']
iptc = video_data['photoVideoMetadataIPTC']
title = iptc['title']['en']
fmg = video_data.get('photoVideoMetadata_fmg') or {}
tvss_domain = fmg.get('tvssDomain') or 'https://auth.univision.com'
data = self._download_json(
tvss_domain + '/api/v3/video-auth/url-signature-tokens',
video_id, query={'mcpids': video_id})['data'][0]
formats = []
rendition_url = data.get('renditionUrl')
if rendition_url:
formats = self._extract_m3u8_formats(
rendition_url, video_id, 'mp4',
'm3u8_native', m3u8_id='hls', fatal=False)
fallback_rendition_url = data.get('fallbackRenditionUrl')
if fallback_rendition_url:
formats.append({
'format_id': 'fallback',
'tbr': int_or_none(self._search_regex(
r'_(\d+)\.mp4', fallback_rendition_url,
'bitrate', default=None)),
'url': fallback_rendition_url,
})
return {
'id': video_id,
'title': title,
'thumbnail': try_get(iptc, lambda x: x['cloudinaryLink']['link'], str),
'uploader': fmg.get('network'),
'duration': int_or_none(iptc.get('fileDuration')),
'formats': formats,
'description': try_get(iptc, lambda x: x['description']['en'], str),
'timestamp': parse_iso8601(iptc.get('dateReleased')),
}

View File

@ -1,115 +0,0 @@
from .common import InfoExtractor
from ..utils import (
clean_html,
try_get,
)
class KooIE(InfoExtractor):
_WORKING = False
_VALID_URL = r'https?://(?:www\.)?kooapp\.com/koo/[^/]+/(?P<id>[^/&#$?]+)'
_TESTS = [{ # Test for video in the comments
'url': 'https://www.kooapp.com/koo/ytdlpTestAccount/946c4189-bc2d-4524-b95b-43f641e2adde',
'info_dict': {
'id': '946c4189-bc2d-4524-b95b-43f641e2adde',
'ext': 'mp4',
'title': 'test for video in comment',
'description': 'md5:daa77dc214add4da8b6ea7d2226776e7',
'timestamp': 1632215195,
'uploader_id': 'ytdlpTestAccount',
'uploader': 'yt-dlpTestAccount',
'duration': 7000,
'upload_date': '20210921',
},
'params': {'skip_download': True},
}, { # Test for koo with long title
'url': 'https://www.kooapp.com/koo/laxman_kumarDBFEC/33decbf7-5e1e-4bb8-bfd7-04744a064361',
'info_dict': {
'id': '33decbf7-5e1e-4bb8-bfd7-04744a064361',
'ext': 'mp4',
'title': 'md5:47a71c2337295330c5a19a8af1bbf450',
'description': 'md5:06a6a84e9321499486dab541693d8425',
'timestamp': 1632106884,
'uploader_id': 'laxman_kumarDBFEC',
'uploader': 'Laxman Kumar 🇮🇳',
'duration': 46000,
'upload_date': '20210920',
},
'params': {'skip_download': True},
}, { # Test for audio
'url': 'https://www.kooapp.com/koo/ytdlpTestAccount/a2a9c88e-ce4b-4d2d-952f-d06361c5b602',
'info_dict': {
'id': 'a2a9c88e-ce4b-4d2d-952f-d06361c5b602',
'ext': 'mp4',
'title': 'Test for audio',
'description': 'md5:ecb9a2b6a5d34b736cecb53788cb11e8',
'timestamp': 1632211634,
'uploader_id': 'ytdlpTestAccount',
'uploader': 'yt-dlpTestAccount',
'duration': 214000,
'upload_date': '20210921',
},
'params': {'skip_download': True},
}, { # Test for video
'url': 'https://www.kooapp.com/koo/ytdlpTestAccount/a3e56c53-c1ed-4ac9-ac02-ed1630e6b1d1',
'info_dict': {
'id': 'a3e56c53-c1ed-4ac9-ac02-ed1630e6b1d1',
'ext': 'mp4',
'title': 'Test for video',
'description': 'md5:7afc4eb839074ddeb2beea5dd6fe9500',
'timestamp': 1632211468,
'uploader_id': 'ytdlpTestAccount',
'uploader': 'yt-dlpTestAccount',
'duration': 14000,
'upload_date': '20210921',
},
'params': {'skip_download': True},
}, { # Test for link
'url': 'https://www.kooapp.com/koo/ytdlpTestAccount/01bf5b94-81a5-4d8e-a387-5f732022e15a',
'skip': 'No video/audio found at the provided url.',
'info_dict': {
'id': '01bf5b94-81a5-4d8e-a387-5f732022e15a',
'title': 'Test for link',
'ext': 'none',
},
}, { # Test for images
'url': 'https://www.kooapp.com/koo/ytdlpTestAccount/dc05d9cd-a61d-45fd-bb07-e8019d8ca8cb',
'skip': 'No video/audio found at the provided url.',
'info_dict': {
'id': 'dc05d9cd-a61d-45fd-bb07-e8019d8ca8cb',
'title': 'Test for images',
'ext': 'none',
},
}]
def _real_extract(self, url):
video_id = self._match_id(url)
data_json = self._download_json(
f'https://www.kooapp.com/apiV1/ku/{video_id}?limit=20&offset=0&showSimilarKoos=true', video_id)['parentContent']
item_json = next(content['items'][0] for content in data_json
if try_get(content, lambda x: x['items'][0]['id']) == video_id)
media_json = item_json['mediaMap']
formats = []
mp4_url = media_json.get('videoMp4')
video_m3u8_url = media_json.get('videoHls')
if mp4_url:
formats.append({
'url': mp4_url,
'ext': 'mp4',
})
if video_m3u8_url:
formats.extend(self._extract_m3u8_formats(video_m3u8_url, video_id, fatal=False, ext='mp4'))
if not formats:
self.raise_no_formats('No video/audio found at the provided url.', expected=True)
return {
'id': video_id,
'title': clean_html(item_json.get('title')),
'description': f'{clean_html(item_json.get("title"))}\n\n{clean_html(item_json.get("enTransliteration"))}',
'timestamp': item_json.get('createdAt'),
'uploader_id': item_json.get('handle'),
'uploader': item_json.get('name'),
'duration': media_json.get('duration'),
'formats': formats,
}

View File

@ -1,9 +1,6 @@
import base64
import datetime as dt import datetime as dt
import hashlib
import re import re
import time import time
import urllib.parse
from .common import InfoExtractor from .common import InfoExtractor
from ..compat import compat_ord from ..compat import compat_ord
@ -14,8 +11,6 @@ from ..utils import (
int_or_none, int_or_none,
orderedSet, orderedSet,
parse_iso8601, parse_iso8601,
str_or_none,
url_basename,
urshift, urshift,
) )
@ -248,114 +243,3 @@ class LePlaylistIE(InfoExtractor):
return self.playlist_result(entries, playlist_id, playlist_title=title, return self.playlist_result(entries, playlist_id, playlist_title=title,
playlist_description=description) playlist_description=description)
class LetvCloudIE(InfoExtractor):
# Most of *.letv.com is changed to *.le.com on 2016/01/02
# but yuntv.letv.com is kept, so also keep the extractor name
IE_DESC = '乐视云'
_VALID_URL = r'https?://yuntv\.letv\.com/bcloud.html\?.+'
_TESTS = [{
'url': 'http://yuntv.letv.com/bcloud.html?uu=p7jnfw5hw9&vu=467623dedf',
'md5': '26450599afd64c513bc77030ad15db44',
'info_dict': {
'id': 'p7jnfw5hw9_467623dedf',
'ext': 'mp4',
'title': 'Video p7jnfw5hw9_467623dedf',
},
}, {
'url': 'http://yuntv.letv.com/bcloud.html?uu=p7jnfw5hw9&vu=ec93197892&pu=2c7cd40209&auto_play=1&gpcflag=1&width=640&height=360',
'md5': 'e03d9cc8d9c13191e1caf277e42dbd31',
'info_dict': {
'id': 'p7jnfw5hw9_ec93197892',
'ext': 'mp4',
'title': 'Video p7jnfw5hw9_ec93197892',
},
}, {
'url': 'http://yuntv.letv.com/bcloud.html?uu=p7jnfw5hw9&vu=187060b6fd',
'md5': 'cb988699a776b22d4a41b9d43acfb3ac',
'info_dict': {
'id': 'p7jnfw5hw9_187060b6fd',
'ext': 'mp4',
'title': 'Video p7jnfw5hw9_187060b6fd',
},
}]
@staticmethod
def sign_data(obj):
if obj['cf'] == 'flash':
salt = '2f9d6924b33a165a6d8b5d3d42f4f987'
items = ['cf', 'format', 'ran', 'uu', 'ver', 'vu']
elif obj['cf'] == 'html5':
salt = 'fbeh5player12c43eccf2bec3300344'
items = ['cf', 'ran', 'uu', 'bver', 'vu']
input_data = ''.join([item + obj[item] for item in items]) + salt
obj['sign'] = hashlib.md5(input_data.encode()).hexdigest()
def _get_formats(self, cf, uu, vu, media_id):
def get_play_json(cf, timestamp):
data = {
'cf': cf,
'ver': '2.2',
'bver': 'firefox44.0',
'format': 'json',
'uu': uu,
'vu': vu,
'ran': str(timestamp),
}
self.sign_data(data)
return self._download_json(
'http://api.letvcloud.com/gpc.php?' + urllib.parse.urlencode(data),
media_id, f'Downloading playJson data for type {cf}')
play_json = get_play_json(cf, time.time())
# The server time may be different from local time
if play_json.get('code') == 10071:
play_json = get_play_json(cf, play_json['timestamp'])
if not play_json.get('data'):
if play_json.get('message'):
raise ExtractorError('Letv cloud said: {}'.format(play_json['message']), expected=True)
elif play_json.get('code'):
raise ExtractorError('Letv cloud returned error %d' % play_json['code'], expected=True)
else:
raise ExtractorError('Letv cloud returned an unknown error')
def b64decode(s):
return base64.b64decode(s).decode('utf-8')
formats = []
for media in play_json['data']['video_info']['media'].values():
play_url = media['play_url']
url = b64decode(play_url['main_url'])
decoded_url = b64decode(url_basename(url))
formats.append({
'url': url,
'ext': determine_ext(decoded_url),
'format_id': str_or_none(play_url.get('vtype')),
'format_note': str_or_none(play_url.get('definition')),
'width': int_or_none(play_url.get('vwidth')),
'height': int_or_none(play_url.get('vheight')),
})
return formats
def _real_extract(self, url):
uu_mobj = re.search(r'uu=([\w]+)', url)
vu_mobj = re.search(r'vu=([\w]+)', url)
if not uu_mobj or not vu_mobj:
raise ExtractorError(f'Invalid URL: {url}', expected=True)
uu = uu_mobj.group(1)
vu = vu_mobj.group(1)
media_id = uu + '_' + vu
formats = self._get_formats('flash', uu, vu, media_id) + self._get_formats('html5', uu, vu, media_id)
return {
'id': media_id,
'title': f'Video {media_id}',
'formats': formats,
}

View File

@ -1,386 +0,0 @@
import itertools
import re
import urllib.parse
from .common import InfoExtractor
from ..utils import (
determine_ext,
find_xpath_attr,
float_or_none,
int_or_none,
orderedSet,
parse_iso8601,
traverse_obj,
update_url_query,
xpath_attr,
xpath_text,
xpath_with_ns,
)
class LivestreamIE(InfoExtractor):
IE_NAME = 'livestream'
_VALID_URL = r'''(?x)
https?://(?:new\.)?livestream\.com/
(?:accounts/(?P<account_id>\d+)|(?P<account_name>[^/]+))
(?:/events/(?P<event_id>\d+)|/(?P<event_name>[^/]+))?
(?:/videos/(?P<id>\d+))?
'''
_EMBED_REGEX = [r'<iframe[^>]+src="(?P<url>https?://(?:new\.)?livestream\.com/[^"]+/player[^"]+)"']
_TESTS = [{
'url': 'http://new.livestream.com/CoheedandCambria/WebsterHall/videos/4719370',
'md5': '7876c5f5dc3e711b6b73acce4aac1527',
'info_dict': {
'id': '4719370',
'ext': 'mp4',
'title': 'Live from Webster Hall NYC',
'timestamp': 1350008072,
'upload_date': '20121012',
'duration': 5968.0,
'like_count': int,
'view_count': int,
'comment_count': int,
'thumbnail': r're:^http://.*\.jpg$',
},
}, {
'url': 'https://livestream.com/coheedandcambria/websterhall',
'info_dict': {
'id': '1585861',
'title': 'Live From Webster Hall',
},
'playlist_mincount': 1,
}, {
'url': 'https://livestream.com/dayananda/events/7954027',
'info_dict': {
'title': 'Live from Mevo',
'id': '7954027',
},
'playlist_mincount': 4,
}, {
'url': 'https://livestream.com/accounts/82',
'info_dict': {
'id': '253978',
'view_count': int,
'title': 'trsr',
'comment_count': int,
'like_count': int,
'upload_date': '20120306',
'timestamp': 1331042383,
'thumbnail': 'http://img.new.livestream.com/videos/0000000000000372/cacbeed6-fb68-4b5e-ad9c-e148124e68a9_640x427.jpg',
'duration': 15.332,
'ext': 'mp4',
},
}, {
'url': 'https://new.livestream.com/accounts/362/events/3557232/videos/67864563/player?autoPlay=false&height=360&mute=false&width=640',
'only_matching': True,
}, {
'url': 'http://livestream.com/bsww/concacafbeachsoccercampeonato2015',
'only_matching': True,
}]
_API_URL_TEMPLATE = 'http://livestream.com/api/accounts/%s/events/%s'
def _parse_smil_formats_and_subtitles(
self, smil, smil_url, video_id, namespace=None, f4m_params=None, transform_rtmp_url=None):
base_ele = find_xpath_attr(
smil, self._xpath_ns('.//meta', namespace), 'name', 'httpBase')
base = base_ele.get('content') if base_ele is not None else 'http://livestreamvod-f.akamaihd.net/'
formats = []
video_nodes = smil.findall(self._xpath_ns('.//video', namespace))
for vn in video_nodes:
tbr = int_or_none(vn.attrib.get('system-bitrate'), 1000)
furl = (
update_url_query(urllib.parse.urljoin(base, vn.attrib['src']), {
'v': '3.0.3',
'fp': 'WIN% 14,0,0,145',
}))
if 'clipBegin' in vn.attrib:
furl += '&ssek=' + vn.attrib['clipBegin']
formats.append({
'url': furl,
'format_id': 'smil_%d' % tbr,
'ext': 'flv',
'tbr': tbr,
'preference': -1000, # Strictly inferior than all other formats?
})
return formats, {}
def _extract_video_info(self, video_data):
video_id = str(video_data['id'])
FORMAT_KEYS = (
('sd', 'progressive_url'),
('hd', 'progressive_url_hd'),
)
formats = []
for format_id, key in FORMAT_KEYS:
video_url = video_data.get(key)
if video_url:
ext = determine_ext(video_url)
if ext == 'm3u8':
continue
bitrate = int_or_none(self._search_regex(
rf'(\d+)\.{ext}', video_url, 'bitrate', default=None))
formats.append({
'url': video_url,
'format_id': format_id,
'tbr': bitrate,
'ext': ext,
})
smil_url = video_data.get('smil_url')
if smil_url:
formats.extend(self._extract_smil_formats(smil_url, video_id, fatal=False))
m3u8_url = video_data.get('m3u8_url')
if m3u8_url:
formats.extend(self._extract_m3u8_formats(
m3u8_url, video_id, 'mp4', 'm3u8_native',
m3u8_id='hls', fatal=False))
f4m_url = video_data.get('f4m_url')
if f4m_url:
formats.extend(self._extract_f4m_formats(
f4m_url, video_id, f4m_id='hds', fatal=False))
comments = [{
'author_id': comment.get('author_id'),
'author': comment.get('author', {}).get('full_name'),
'id': comment.get('id'),
'text': comment['text'],
'timestamp': parse_iso8601(comment.get('created_at')),
} for comment in video_data.get('comments', {}).get('data', [])]
return {
'id': video_id,
'formats': formats,
'title': video_data['caption'],
'description': video_data.get('description'),
'thumbnail': video_data.get('thumbnail_url'),
'duration': float_or_none(video_data.get('duration'), 1000),
'timestamp': parse_iso8601(video_data.get('publish_at')),
'like_count': video_data.get('likes', {}).get('total'),
'comment_count': video_data.get('comments', {}).get('total'),
'view_count': video_data.get('views'),
'comments': comments,
}
def _extract_stream_info(self, stream_info):
broadcast_id = str(stream_info['broadcast_id'])
is_live = stream_info.get('is_live')
formats = []
smil_url = stream_info.get('play_url')
if smil_url:
formats.extend(self._extract_smil_formats(smil_url, broadcast_id))
m3u8_url = stream_info.get('m3u8_url')
if m3u8_url:
formats.extend(self._extract_m3u8_formats(
m3u8_url, broadcast_id, 'mp4', 'm3u8_native',
m3u8_id='hls', fatal=False))
rtsp_url = stream_info.get('rtsp_url')
if rtsp_url:
formats.append({
'url': rtsp_url,
'format_id': 'rtsp',
})
return {
'id': broadcast_id,
'formats': formats,
'title': stream_info['stream_title'],
'thumbnail': stream_info.get('thumbnail_url'),
'is_live': is_live,
}
def _generate_event_playlist(self, event_data):
event_id = str(event_data['id'])
account_id = str(event_data['owner_account_id'])
feed_root_url = self._API_URL_TEMPLATE % (account_id, event_id) + '/feed.json'
stream_info = event_data.get('stream_info')
if stream_info:
return self._extract_stream_info(stream_info)
last_video = None
for i in itertools.count(1):
if last_video is None:
info_url = feed_root_url
else:
info_url = f'{feed_root_url}?&id={last_video}&newer=-1&type=video'
videos_info = self._download_json(
info_url, event_id, f'Downloading page {i}')['data']
videos_info = [v['data'] for v in videos_info if v['type'] == 'video']
if not videos_info:
break
for v in videos_info:
v_id = str(v['id'])
yield self.url_result(
f'http://livestream.com/accounts/{account_id}/events/{event_id}/videos/{v_id}',
LivestreamIE, v_id, v.get('caption'))
last_video = videos_info[-1]['id']
def _real_extract(self, url):
mobj = self._match_valid_url(url)
video_id = mobj.group('id')
event = mobj.group('event_id') or mobj.group('event_name')
account = mobj.group('account_id') or mobj.group('account_name')
api_url = f'http://livestream.com/api/accounts/{account}'
if video_id:
video_data = self._download_json(
f'{api_url}/events/{event}/videos/{video_id}', video_id)
return self._extract_video_info(video_data)
elif event:
event_data = self._download_json(f'{api_url}/events/{event}', None)
return self.playlist_result(
self._generate_event_playlist(event_data), str(event_data['id']), event_data['full_name'])
account_data = self._download_json(api_url, None)
items = traverse_obj(account_data, (('upcoming_events', 'past_events'), 'data', ...))
return self.playlist_result(
itertools.chain.from_iterable(map(self._generate_event_playlist, items)),
account_data.get('id'), account_data.get('full_name'))
# The original version of Livestream uses a different system
class LivestreamOriginalIE(InfoExtractor):
IE_NAME = 'livestream:original'
_VALID_URL = r'''(?x)https?://original\.livestream\.com/
(?P<user>[^/\?#]+)(?:/(?P<type>video|folder)
(?:(?:\?.*?Id=|/)(?P<id>.*?)(&|$))?)?
'''
_TESTS = [{
'url': 'http://original.livestream.com/dealbook/video?clipId=pla_8aa4a3f1-ba15-46a4-893b-902210e138fb',
'info_dict': {
'id': 'pla_8aa4a3f1-ba15-46a4-893b-902210e138fb',
'ext': 'mp4',
'title': 'Spark 1 (BitCoin) with Cameron Winklevoss & Tyler Winklevoss of Winklevoss Capital',
'duration': 771.301,
'view_count': int,
},
}, {
'url': 'https://original.livestream.com/newplay/folder?dirId=a07bf706-d0e4-4e75-a747-b021d84f2fd3',
'info_dict': {
'id': 'a07bf706-d0e4-4e75-a747-b021d84f2fd3',
},
'playlist_mincount': 4,
}, {
# live stream
'url': 'http://original.livestream.com/znsbahamas',
'only_matching': True,
}]
def _extract_video_info(self, user, video_id):
api_url = f'http://x{user}x.api.channel.livestream.com/2.0/clipdetails?extendedInfo=true&id={video_id}'
info = self._download_xml(api_url, video_id)
item = info.find('channel').find('item')
title = xpath_text(item, 'title')
media_ns = {'media': 'http://search.yahoo.com/mrss'}
thumbnail_url = xpath_attr(
item, xpath_with_ns('media:thumbnail', media_ns), 'url')
duration = float_or_none(xpath_attr(
item, xpath_with_ns('media:content', media_ns), 'duration'))
ls_ns = {'ls': 'http://api.channel.livestream.com/2.0'}
view_count = int_or_none(xpath_text(
item, xpath_with_ns('ls:viewsCount', ls_ns)))
return {
'id': video_id,
'title': title,
'thumbnail': thumbnail_url,
'duration': duration,
'view_count': view_count,
}
def _extract_video_formats(self, video_data, video_id):
formats = []
progressive_url = video_data.get('progressiveUrl')
if progressive_url:
formats.append({
'url': progressive_url,
'format_id': 'http',
})
m3u8_url = video_data.get('httpUrl')
if m3u8_url:
formats.extend(self._extract_m3u8_formats(
m3u8_url, video_id, 'mp4', 'm3u8_native',
m3u8_id='hls', fatal=False))
rtsp_url = video_data.get('rtspUrl')
if rtsp_url:
formats.append({
'url': rtsp_url,
'format_id': 'rtsp',
})
return formats
def _extract_folder(self, url, folder_id):
webpage = self._download_webpage(url, folder_id)
paths = orderedSet(re.findall(
r'''(?x)(?:
<li\s+class="folder">\s*<a\s+href="|
<a\s+href="(?=https?://livestre\.am/)
)([^"]+)"''', webpage))
entries = [{
'_type': 'url',
'url': urllib.parse.urljoin(url, p),
} for p in paths]
return self.playlist_result(entries, folder_id)
def _real_extract(self, url):
mobj = self._match_valid_url(url)
user = mobj.group('user')
url_type = mobj.group('type')
content_id = mobj.group('id')
if url_type == 'folder':
return self._extract_folder(url, content_id)
else:
# this url is used on mobile devices
stream_url = f'http://x{user}x.api.channel.livestream.com/3.0/getstream.json'
info = {}
if content_id:
stream_url += f'?id={content_id}'
info = self._extract_video_info(user, content_id)
else:
content_id = user
webpage = self._download_webpage(url, content_id)
info = {
'title': self._og_search_title(webpage),
'description': self._og_search_description(webpage),
'thumbnail': self._search_regex(r'channelLogo\.src\s*=\s*"([^"]+)"', webpage, 'thumbnail', None),
}
video_data = self._download_json(stream_url, content_id)
is_live = video_data.get('isLive')
info.update({
'id': content_id,
'title': info['title'],
'formats': self._extract_video_formats(video_data, content_id),
'is_live': is_live,
})
return info
# The server doesn't support HEAD request, the generic extractor can't detect
# the redirection
class LivestreamShortenerIE(InfoExtractor):
IE_NAME = 'livestream:shortener'
IE_DESC = False # Do not list
_VALID_URL = r'https?://livestre\.am/(?P<id>.+)'
def _real_extract(self, url):
video_id = self._match_id(url)
webpage = self._download_webpage(url, video_id)
return self.url_result(self._og_search_url(webpage))

View File

@ -1,325 +0,0 @@
import itertools
import re
import urllib.parse
from .common import InfoExtractor
from ..utils import (
ExtractorError,
int_or_none,
urlencode_postdata,
)
class LyndaBaseIE(InfoExtractor):
_SIGNIN_URL = 'https://www.lynda.com/signin/lynda'
_PASSWORD_URL = 'https://www.lynda.com/signin/password'
_USER_URL = 'https://www.lynda.com/signin/user'
_ACCOUNT_CREDENTIALS_HINT = 'Use --username and --password options to provide lynda.com account credentials.'
_NETRC_MACHINE = 'lynda'
@staticmethod
def _check_error(json_string, key_or_keys):
keys = [key_or_keys] if isinstance(key_or_keys, str) else key_or_keys
for key in keys:
error = json_string.get(key)
if error:
raise ExtractorError(f'Unable to login: {error}', expected=True)
def _perform_login_step(self, form_html, fallback_action_url, extra_form_data, note, referrer_url):
action_url = self._search_regex(
r'<form[^>]+action=(["\'])(?P<url>.+?)\1', form_html,
'post url', default=fallback_action_url, group='url')
if not action_url.startswith('http'):
action_url = urllib.parse.urljoin(self._SIGNIN_URL, action_url)
form_data = self._hidden_inputs(form_html)
form_data.update(extra_form_data)
response = self._download_json(
action_url, None, note,
data=urlencode_postdata(form_data),
headers={
'Referer': referrer_url,
'X-Requested-With': 'XMLHttpRequest',
}, expected_status=(418, 500))
self._check_error(response, ('email', 'password', 'ErrorMessage'))
return response, action_url
def _perform_login(self, username, password):
# Step 1: download signin page
signin_page = self._download_webpage(
self._SIGNIN_URL, None, 'Downloading signin page')
# Already logged in
if any(re.search(p, signin_page) for p in (
r'isLoggedIn\s*:\s*true', r'logout\.aspx', r'>Log out<')):
return
# Step 2: submit email
signin_form = self._search_regex(
r'(?s)(<form[^>]+data-form-name=["\']signin["\'][^>]*>.+?</form>)',
signin_page, 'signin form')
signin_page, signin_url = self._login_step(
signin_form, self._PASSWORD_URL, {'email': username},
'Submitting email', self._SIGNIN_URL)
# Step 3: submit password
password_form = signin_page['body']
self._login_step(
password_form, self._USER_URL, {'email': username, 'password': password},
'Submitting password', signin_url)
class LyndaIE(LyndaBaseIE):
IE_NAME = 'lynda'
IE_DESC = 'lynda.com videos'
_VALID_URL = r'''(?x)
https?://
(?:www\.)?(?:lynda\.com|educourse\.ga)/
(?:
(?:[^/]+/){2,3}(?P<course_id>\d+)|
player/embed
)/
(?P<id>\d+)
'''
_TIMECODE_REGEX = r'\[(?P<timecode>\d+:\d+:\d+[\.,]\d+)\]'
_TESTS = [{
'url': 'https://www.lynda.com/Bootstrap-tutorials/Using-exercise-files/110885/114408-4.html',
# md5 is unstable
'info_dict': {
'id': '114408',
'ext': 'mp4',
'title': 'Using the exercise files',
'duration': 68,
},
}, {
'url': 'https://www.lynda.com/player/embed/133770?tr=foo=1;bar=g;fizz=rt&fs=0',
'only_matching': True,
}, {
'url': 'https://educourse.ga/Bootstrap-tutorials/Using-exercise-files/110885/114408-4.html',
'only_matching': True,
}, {
'url': 'https://www.lynda.com/de/Graphic-Design-tutorials/Willkommen-Grundlagen-guten-Gestaltung/393570/393572-4.html',
'only_matching': True,
}, {
# Status="NotFound", Message="Transcript not found"
'url': 'https://www.lynda.com/ASP-NET-tutorials/What-you-should-know/5034180/2811512-4.html',
'only_matching': True,
}]
def _raise_unavailable(self, video_id):
self.raise_login_required(
f'Video {video_id} is only available for members')
def _real_extract(self, url):
mobj = self._match_valid_url(url)
video_id = mobj.group('id')
course_id = mobj.group('course_id')
query = {
'videoId': video_id,
'type': 'video',
}
video = self._download_json(
'https://www.lynda.com/ajax/player', video_id,
'Downloading video JSON', fatal=False, query=query)
# Fallback scenario
if not video:
query['courseId'] = course_id
play = self._download_json(
f'https://www.lynda.com/ajax/course/{course_id}/{video_id}/play', video_id, 'Downloading play JSON')
if not play:
self._raise_unavailable(video_id)
formats = []
for formats_dict in play:
urls = formats_dict.get('urls')
if not isinstance(urls, dict):
continue
cdn = formats_dict.get('name')
for format_id, format_url in urls.items():
if not format_url:
continue
formats.append({
'url': format_url,
'format_id': f'{cdn}-{format_id}' if cdn else format_id,
'height': int_or_none(format_id),
})
conviva = self._download_json(
'https://www.lynda.com/ajax/player/conviva', video_id,
'Downloading conviva JSON', query=query)
return {
'id': video_id,
'title': conviva['VideoTitle'],
'description': conviva.get('VideoDescription'),
'release_year': int_or_none(conviva.get('ReleaseYear')),
'duration': int_or_none(conviva.get('Duration')),
'creator': conviva.get('Author'),
'formats': formats,
}
if 'Status' in video:
raise ExtractorError(
'lynda returned error: {}'.format(video['Message']), expected=True)
if video.get('HasAccess') is False:
self._raise_unavailable(video_id)
video_id = str(video.get('ID') or video_id)
duration = int_or_none(video.get('DurationInSeconds'))
title = video['Title']
formats = []
fmts = video.get('Formats')
if fmts:
formats.extend([{
'url': f['Url'],
'ext': f.get('Extension'),
'width': int_or_none(f.get('Width')),
'height': int_or_none(f.get('Height')),
'filesize': int_or_none(f.get('FileSize')),
'format_id': str(f.get('Resolution')) if f.get('Resolution') else None,
} for f in fmts if f.get('Url')])
prioritized_streams = video.get('PrioritizedStreams')
if prioritized_streams:
for prioritized_stream_id, prioritized_stream in prioritized_streams.items():
formats.extend([{
'url': video_url,
'height': int_or_none(format_id),
'format_id': f'{prioritized_stream_id}-{format_id}',
} for format_id, video_url in prioritized_stream.items()])
self._check_formats(formats, video_id)
subtitles = self.extract_subtitles(video_id)
return {
'id': video_id,
'title': title,
'duration': duration,
'subtitles': subtitles,
'formats': formats,
}
def _fix_subtitles(self, subs):
srt = ''
seq_counter = 0
for seq_current, seq_next in itertools.pairwise(subs):
m_current = re.match(self._TIMECODE_REGEX, seq_current['Timecode'])
if m_current is None:
continue
m_next = re.match(self._TIMECODE_REGEX, seq_next['Timecode'])
if m_next is None:
continue
appear_time = m_current.group('timecode')
disappear_time = m_next.group('timecode')
text = seq_current['Caption'].strip()
if text:
seq_counter += 1
srt += f'{seq_counter}\r\n{appear_time} --> {disappear_time}\r\n{text}\r\n\r\n'
if srt:
return srt
def _get_subtitles(self, video_id):
url = f'https://www.lynda.com/ajax/player?videoId={video_id}&type=transcript'
subs = self._download_webpage(
url, video_id, 'Downloading subtitles JSON', fatal=False)
if not subs or 'Status="NotFound"' in subs:
return {}
subs = self._parse_json(subs, video_id, fatal=False)
if not subs:
return {}
fixed_subs = self._fix_subtitles(subs)
if fixed_subs:
return {'en': [{'ext': 'srt', 'data': fixed_subs}]}
return {}
class LyndaCourseIE(LyndaBaseIE):
IE_NAME = 'lynda:course'
IE_DESC = 'lynda.com online courses'
# Course link equals to welcome/introduction video link of same course
# We will recognize it as course link
_VALID_URL = r'https?://(?:www|m)\.(?:lynda\.com|educourse\.ga)/(?P<coursepath>(?:[^/]+/){2,3}(?P<courseid>\d+))-2\.html'
_TESTS = [{
'url': 'https://www.lynda.com/Graphic-Design-tutorials/Grundlagen-guten-Gestaltung/393570-2.html',
'only_matching': True,
}, {
'url': 'https://www.lynda.com/de/Graphic-Design-tutorials/Grundlagen-guten-Gestaltung/393570-2.html',
'only_matching': True,
}]
def _real_extract(self, url):
mobj = self._match_valid_url(url)
course_path = mobj.group('coursepath')
course_id = mobj.group('courseid')
item_template = f'https://www.lynda.com/{course_path}/%s-4.html'
course = self._download_json(
f'https://www.lynda.com/ajax/player?courseId={course_id}&type=course',
course_id, 'Downloading course JSON', fatal=False)
if not course:
webpage = self._download_webpage(url, course_id)
entries = [
self.url_result(
item_template % video_id, ie=LyndaIE.ie_key(),
video_id=video_id)
for video_id in re.findall(
r'data-video-id=["\'](\d+)', webpage)]
return self.playlist_result(
entries, course_id,
self._og_search_title(webpage, fatal=False),
self._og_search_description(webpage))
if course.get('Status') == 'NotFound':
raise ExtractorError(
f'Course {course_id} does not exist', expected=True)
unaccessible_videos = 0
entries = []
# Might want to extract videos right here from video['Formats'] as it seems 'Formats' is not provided
# by single video API anymore
for chapter in course['Chapters']:
for video in chapter.get('Videos', []):
if video.get('HasAccess') is False:
unaccessible_videos += 1
continue
video_id = video.get('ID')
if video_id:
entries.append({
'_type': 'url_transparent',
'url': item_template % video_id,
'ie_key': LyndaIE.ie_key(),
'chapter': chapter.get('Title'),
'chapter_number': int_or_none(chapter.get('ChapterIndex')),
'chapter_id': str(chapter.get('ID')),
})
if unaccessible_videos > 0:
self.report_warning(
f'{unaccessible_videos} videos are only available for members (or paid members) '
f'and will not be downloaded. {self._ACCOUNT_CREDENTIALS_HINT}')
course_title = course.get('Title')
course_description = course.get('Description')
return self.playlist_result(entries, course_id, course_title, course_description)

View File

@ -1,121 +0,0 @@
import base64
from .common import InfoExtractor
from ..utils import (
merge_dicts,
parse_duration,
parse_iso8601,
parse_resolution,
try_get,
url_basename,
)
class MicrosoftStreamIE(InfoExtractor):
IE_NAME = 'microsoftstream'
IE_DESC = 'Microsoft Stream'
_VALID_URL = r'https?://(?:web|www|msit)\.microsoftstream\.com/video/(?P<id>[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12})'
_TESTS = [{
'url': 'https://web.microsoftstream.com/video/6e51d928-4f46-4f1c-b141-369925e37b62?list=user&userId=f5491e02-e8fe-4e34-b67c-ec2e79a6ecc0',
'only_matching': True,
}, {
'url': 'https://msit.microsoftstream.com/video/b60f5987-aabd-4e1c-a42f-c559d138f2ca',
'only_matching': True,
}]
def _get_all_subtitles(self, api_url, video_id, headers):
subtitles = {}
automatic_captions = {}
text_tracks = self._download_json(
f'{api_url}/videos/{video_id}/texttracks', video_id,
note='Downloading subtitles JSON', fatal=False, headers=headers,
query={'api-version': '1.4-private'}).get('value') or []
for track in text_tracks:
if not track.get('language') or not track.get('url'):
continue
sub_dict = automatic_captions if track.get('autoGenerated') else subtitles
sub_dict.setdefault(track['language'], []).append({
'ext': 'vtt',
'url': track.get('url'),
})
return {
'subtitles': subtitles,
'automatic_captions': automatic_captions,
}
def extract_all_subtitles(self, *args, **kwargs):
if (self.get_param('writesubtitles', False)
or self.get_param('writeautomaticsub', False)
or self.get_param('listsubtitles')):
return self._get_all_subtitles(*args, **kwargs)
return {}
def _real_extract(self, url):
video_id = self._match_id(url)
webpage = self._download_webpage(url, video_id)
if '<title>Microsoft Stream</title>' not in webpage:
self.raise_login_required(method='cookies')
access_token = self._html_search_regex(r'"AccessToken":"(.+?)"', webpage, 'access token')
api_url = self._html_search_regex(r'"ApiGatewayUri":"(.+?)"', webpage, 'api url')
headers = {'Authorization': f'Bearer {access_token}'}
video_data = self._download_json(
f'{api_url}/videos/{video_id}', video_id,
headers=headers, query={
'$expand': 'creator,tokens,status,liveEvent,extensions',
'api-version': '1.4-private',
})
video_id = video_data.get('id') or video_id
language = video_data.get('language')
thumbnails = []
for thumbnail_id in ('extraSmall', 'small', 'medium', 'large'):
thumbnail_url = try_get(video_data, lambda x: x['posterImage'][thumbnail_id]['url'], str)
if not thumbnail_url:
continue
thumb = {
'id': thumbnail_id,
'url': thumbnail_url,
}
thumb_name = url_basename(thumbnail_url)
thumb_name = str(base64.b64decode(thumb_name + '=' * (-len(thumb_name) % 4)))
thumb.update(parse_resolution(thumb_name))
thumbnails.append(thumb)
formats = []
for playlist in video_data['playbackUrls']:
if playlist['mimeType'] == 'application/vnd.apple.mpegurl':
formats.extend(self._extract_m3u8_formats(
playlist['playbackUrl'], video_id,
ext='mp4', entry_protocol='m3u8_native', m3u8_id='hls',
fatal=False, headers=headers))
elif playlist['mimeType'] == 'application/dash+xml':
formats.extend(self._extract_mpd_formats(
playlist['playbackUrl'], video_id, mpd_id='dash',
fatal=False, headers=headers))
elif playlist['mimeType'] == 'application/vnd.ms-sstr+xml':
formats.extend(self._extract_ism_formats(
playlist['playbackUrl'], video_id, ism_id='mss',
fatal=False, headers=headers))
formats = [merge_dicts(f, {'language': language}) for f in formats]
return {
'id': video_id,
'title': video_data['name'],
'description': video_data.get('description'),
'uploader': try_get(video_data, lambda x: x['creator']['name'], str),
'uploader_id': try_get(video_data, (lambda x: x['creator']['mail'],
lambda x: x['creator']['id']), str),
'thumbnails': thumbnails,
**self.extract_all_subtitles(api_url, video_id, headers),
'timestamp': parse_iso8601(video_data.get('created')),
'duration': parse_duration(try_get(video_data, lambda x: x['media']['duration'])),
'webpage_url': f'https://web.microsoftstream.com/video/{video_id}',
'view_count': try_get(video_data, lambda x: x['metrics']['views'], int),
'like_count': try_get(video_data, lambda x: x['metrics']['likes'], int),
'comment_count': try_get(video_data, lambda x: x['metrics']['comments'], int),
'formats': formats,
}

View File

@ -1,45 +0,0 @@
from .common import InfoExtractor
from ..utils import (
int_or_none,
parse_codecs,
)
class MinotoIE(InfoExtractor):
_VALID_URL = r'(?:minoto:|https?://(?:play|iframe|embed)\.minoto-video\.com/(?P<player_id>[0-9]+)/)(?P<id>[a-zA-Z0-9]+)'
def _real_extract(self, url):
mobj = self._match_valid_url(url)
player_id = mobj.group('player_id') or '1'
video_id = mobj.group('id')
video_data = self._download_json(f'http://play.minoto-video.com/{player_id}/{video_id}.js', video_id)
video_metadata = video_data['video-metadata']
formats = []
for fmt in video_data['video-files']:
fmt_url = fmt.get('url')
if not fmt_url:
continue
container = fmt.get('container')
if container == 'hls':
formats.extend(self._extract_m3u8_formats(fmt_url, video_id, 'mp4', m3u8_id='hls', fatal=False))
else:
fmt_profile = fmt.get('profile') or {}
formats.append({
'format_id': fmt_profile.get('name-short'),
'format_note': fmt_profile.get('name'),
'url': fmt_url,
'container': container,
'tbr': int_or_none(fmt.get('bitrate')),
'filesize': int_or_none(fmt.get('filesize')),
'width': int_or_none(fmt.get('width')),
'height': int_or_none(fmt.get('height')),
**parse_codecs(fmt.get('codecs')),
})
return {
'id': video_id,
'title': video_metadata['title'],
'description': video_metadata.get('description'),
'thumbnail': video_metadata.get('video-poster', {}).get('url'),
'formats': formats,
}

View File

@ -1,52 +0,0 @@
from .common import InfoExtractor
from ..utils import (
ExtractorError,
parse_duration,
)
class MojvideoIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?mojvideo\.com/video-(?P<display_id>[^/]+)/(?P<id>[a-f0-9]+)'
_TEST = {
'url': 'http://www.mojvideo.com/video-v-avtu-pred-mano-rdecelaska-alfi-nipic/3d1ed4497707730b2906',
'md5': 'f7fd662cc8ce2be107b0d4f2c0483ae7',
'info_dict': {
'id': '3d1ed4497707730b2906',
'display_id': 'v-avtu-pred-mano-rdecelaska-alfi-nipic',
'ext': 'mp4',
'title': 'V avtu pred mano rdečelaska - Alfi Nipič',
'thumbnail': r're:^http://.*\.jpg$',
'duration': 242,
},
}
def _real_extract(self, url):
mobj = self._match_valid_url(url)
video_id = mobj.group('id')
display_id = mobj.group('display_id')
# XML is malformed
playerapi = self._download_webpage(
f'http://www.mojvideo.com/playerapi.php?v={video_id}&t=1', display_id)
if '<error>true</error>' in playerapi:
error_desc = self._html_search_regex(
r'<errordesc>([^<]*)</errordesc>', playerapi, 'error description', fatal=False)
raise ExtractorError(f'{self.IE_NAME} said: {error_desc}', expected=True)
title = self._html_extract_title(playerapi)
video_url = self._html_search_regex(
r'<file>([^<]+)</file>', playerapi, 'video URL')
thumbnail = self._html_search_regex(
r'<preview>([^<]+)</preview>', playerapi, 'thumbnail', fatal=False)
duration = parse_duration(self._html_search_regex(
r'<duration>([^<]+)</duration>', playerapi, 'duration', fatal=False))
return {
'id': video_id,
'display_id': display_id,
'url': video_url,
'title': title,
'thumbnail': thumbnail,
'duration': duration,
}

View File

@ -12,7 +12,7 @@ from ..utils.traversal import find_element, traverse_obj
class MonstercatIE(InfoExtractor): class MonstercatIE(InfoExtractor):
_VALID_URL = r'https?://www\.monstercat\.com/release/(?P<id>\d+)' _VALID_URL = r'https?://www\.monstercat\.com/release/(?P<id>\d{12}|MC[A-Z]+\d+)'
_TESTS = [{ _TESTS = [{
'url': 'https://www.monstercat.com/release/742779548009', 'url': 'https://www.monstercat.com/release/742779548009',
'playlist_count': 20, 'playlist_count': 20,
@ -24,6 +24,28 @@ class MonstercatIE(InfoExtractor):
'album': 'The Secret Language of Trees', 'album': 'The Secret Language of Trees',
'album_artists': ['BT'], 'album_artists': ['BT'],
}, },
}, {
'url': 'https://www.monstercat.com/release/MCRAB001',
'playlist_count': 1,
'info_dict': {
'title': 'Crab Rave',
'id': 'MCRAB001',
'thumbnail': 'https://www.monstercat.com/release/MCRAB001/cover',
'release_date': '20180401',
'album': 'Crab Rave',
'album_artists': ['Noisestorm'],
},
}, {
'url': 'https://www.monstercat.com/release/MCEP209',
'playlist_count': 5,
'info_dict': {
'title': 'Somewhere New',
'id': 'MCEP209',
'thumbnail': 'https://www.monstercat.com/release/MCEP209/cover',
'release_date': '20210415',
'album': 'Somewhere New',
'album_artists': ['Bad Computer'],
},
}] }]
def _extract_tracks(self, table, album_meta): def _extract_tracks(self, table, album_meta):

View File

@ -1,289 +0,0 @@
import datetime as dt
import re
import urllib.parse
from .common import InfoExtractor
from ..utils import (
ExtractorError,
OnDemandPagedList,
remove_end,
str_to_int,
unified_strdate,
)
class MotherlessIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?motherless\.com/(?:g/[a-z0-9_]+/|G[VIG]?[A-F0-9]+/)?(?P<id>[A-F0-9]+)'
_TESTS = [{
'url': 'http://motherless.com/EE97006',
'md5': 'cb5e7438f7a3c4e886b7bccc1292a3bc',
'info_dict': {
'id': 'EE97006',
'ext': 'mp4',
'title': 'Dogging blond Brit getting glazed (comp)',
'categories': ['UK', 'slag', 'whore', 'dogging', 'cunt', 'cumhound', 'big tits', 'Pearl Necklace'],
'upload_date': '20230519',
'uploader_id': 'deathbird',
'thumbnail': r're:https?://.*\.jpg',
'age_limit': 18,
'comment_count': int,
'view_count': int,
'like_count': int,
},
'params': {
# Incomplete cert chains
'nocheckcertificate': True,
},
}, {
'url': 'http://motherless.com/532291B',
'md5': 'bc59a6b47d1f958e61fbd38a4d31b131',
'info_dict': {
'id': '532291B',
'ext': 'mp4',
'title': 'Amazing girl playing the omegle game, PERFECT!',
'categories': ['Amateur', 'webcam', 'omegle', 'pink', 'young', 'masturbate', 'teen',
'game', 'hairy'],
'upload_date': '20140622',
'uploader_id': 'Sulivana7x',
'thumbnail': r're:https?://.*\.jpg',
'age_limit': 18,
},
'skip': '404',
}, {
'url': 'http://motherless.com/g/cosplay/633979F',
'expected_exception': 'ExtractorError',
}, {
'url': 'http://motherless.com/8B4BBC1',
'info_dict': {
'id': '8B4BBC1',
'ext': 'mp4',
'title': 'VIDEO00441.mp4',
'categories': [],
'upload_date': '20160214',
'uploader_id': 'NMWildGirl',
'thumbnail': r're:https?://.*\.jpg',
'age_limit': 18,
'like_count': int,
'comment_count': int,
'view_count': int,
},
'params': {
'nocheckcertificate': True,
},
}, {
# see https://motherless.com/videos/recent for recent videos with
# uploaded date in "ago" format
'url': 'https://motherless.com/3C3E2CF',
'info_dict': {
'id': '3C3E2CF',
'ext': 'mp4',
'title': 'a/ Hot Teens',
'categories': list,
'upload_date': '20210104',
'uploader_id': 'anonymous',
'thumbnail': r're:https?://.*\.jpg',
'age_limit': 18,
'like_count': int,
'comment_count': int,
'view_count': int,
},
'params': {
'nocheckcertificate': True,
},
}]
def _real_extract(self, url):
video_id = self._match_id(url)
webpage = self._download_webpage(url, video_id)
if any(p in webpage for p in (
'<title>404 - MOTHERLESS.COM<',
">The page you're looking for cannot be found.<",
'<div class="error-page',
)):
raise ExtractorError(f'Video {video_id} does not exist', expected=True)
if '>The content you are trying to view is for friends only.' in webpage:
raise ExtractorError(f'Video {video_id} is for friends only', expected=True)
title = self._html_search_regex(
(r'(?s)<div[^>]+\bclass=["\']media-meta-title[^>]+>(.+?)</div>',
r'id="view-upload-title">\s+([^<]+)<'), webpage, 'title')
video_url = (self._html_search_regex(
(r'setup\(\{\s*["\']file["\']\s*:\s*(["\'])(?P<url>(?:(?!\1).)+)\1',
r'fileurl\s*=\s*(["\'])(?P<url>(?:(?!\1).)+)\1'),
webpage, 'video URL', default=None, group='url')
or f'http://cdn4.videos.motherlessmedia.com/videos/{video_id}.mp4?fs=opencloud')
age_limit = self._rta_search(webpage)
view_count = str_to_int(self._html_search_regex(
(r'>([\d,.]+)\s+Views<', r'<strong>Views</strong>\s+([^<]+)<'),
webpage, 'view count', fatal=False))
like_count = str_to_int(self._html_search_regex(
(r'>([\d,.]+)\s+Favorites<',
r'<strong>Favorited</strong>\s+([^<]+)<'),
webpage, 'like count', fatal=False))
upload_date = unified_strdate(self._search_regex(
r'class=["\']count[^>]+>(\d+\s+[a-zA-Z]{3}\s+\d{4})<', webpage,
'upload date', default=None))
if not upload_date:
uploaded_ago = self._search_regex(
r'>\s*(\d+[hd])\s+[aA]go\b', webpage, 'uploaded ago',
default=None)
if uploaded_ago:
delta = int(uploaded_ago[:-1])
_AGO_UNITS = {
'h': 'hours',
'd': 'days',
}
kwargs = {_AGO_UNITS.get(uploaded_ago[-1]): delta}
upload_date = (dt.datetime.now(dt.timezone.utc) - dt.timedelta(**kwargs)).strftime('%Y%m%d')
comment_count = len(re.findall(r'''class\s*=\s*['"]media-comment-contents\b''', webpage))
uploader_id = self._html_search_regex(
(r'''<span\b[^>]+\bclass\s*=\s*["']username\b[^>]*>([^<]+)</span>''',
r'''(?s)['"](?:media-meta-member|thumb-member-username)\b[^>]+>\s*<a\b[^>]+\bhref\s*=\s*['"]/m/([^"']+)'''),
webpage, 'uploader_id', fatal=False)
categories = self._html_search_meta('keywords', webpage, default='')
categories = [cat.strip() for cat in categories.split(',') if cat.strip()]
return {
'id': video_id,
'title': title,
'upload_date': upload_date,
'uploader_id': uploader_id,
'thumbnail': self._og_search_thumbnail(webpage),
'categories': categories,
'view_count': view_count,
'like_count': like_count,
'comment_count': comment_count,
'age_limit': age_limit,
'url': video_url,
}
class MotherlessPaginatedIE(InfoExtractor):
_EXTRA_QUERY = {}
_PAGE_SIZE = 60
def _correct_path(self, url, item_id):
raise NotImplementedError('This method must be implemented by subclasses')
def _correct_title(self, title, /):
return title.partition(' - Videos')[0] if title else None
def _extract_entries(self, webpage, base):
for mobj in re.finditer(r'href="[^"]*(?P<href>/[A-F0-9]+)"\s+title="(?P<title>[^"]+)',
webpage):
video_url = urllib.parse.urljoin(base, mobj.group('href'))
video_id = MotherlessIE.get_temp_id(video_url)
if video_id:
yield self.url_result(video_url, MotherlessIE, video_id, mobj.group('title'))
def _real_extract(self, url):
item_id = self._match_id(url)
real_url = self._correct_path(url, item_id)
webpage = self._download_webpage(real_url, item_id, 'Downloading page 1')
def get_page(idx):
page = idx + 1
current_page = webpage if not idx else self._download_webpage(
real_url, item_id, note=f'Downloading page {page}', query={'page': page, **self._EXTRA_QUERY})
yield from self._extract_entries(current_page, real_url)
return self.playlist_result(
OnDemandPagedList(get_page, self._PAGE_SIZE), item_id,
self._correct_title(self._html_extract_title(webpage)))
class MotherlessGroupIE(MotherlessPaginatedIE):
_VALID_URL = r'https?://(?:www\.)?motherless\.com/g[vifm]?/(?P<id>[a-z0-9_]+)/?(?:$|[#?])'
_TESTS = [{
'url': 'http://motherless.com/gv/movie_scenes',
'info_dict': {
'id': 'movie_scenes',
'title': 'Movie Scenes',
},
'playlist_mincount': 540,
}, {
'url': 'http://motherless.com/g/sex_must_be_funny',
'info_dict': {
'id': 'sex_must_be_funny',
'title': 'Sex must be funny',
},
'playlist_count': 0,
}, {
'url': 'https://motherless.com/gv/beautiful_cock',
'info_dict': {
'id': 'beautiful_cock',
'title': 'Beautiful Cock',
},
'playlist_mincount': 371,
}]
def _correct_path(self, url, item_id):
return urllib.parse.urljoin(url, f'/gv/{item_id}')
class MotherlessGalleryIE(MotherlessPaginatedIE):
_VALID_URL = r'https?://(?:www\.)?motherless\.com/G[VIG]?(?P<id>[A-F0-9]+)/?(?:$|[#?])'
_TESTS = [{
'url': 'https://motherless.com/GV338999F',
'info_dict': {
'id': '338999F',
'title': 'Random',
},
'playlist_mincount': 100,
}, {
'url': 'https://motherless.com/GVABD6213',
'info_dict': {
'id': 'ABD6213',
'title': 'Cuties',
},
'playlist_mincount': 1,
}, {
'url': 'https://motherless.com/GVBCF7622',
'info_dict': {
'id': 'BCF7622',
'title': 'Vintage',
},
'playlist_count': 0,
}, {
'url': 'https://motherless.com/G035DE2F',
'info_dict': {
'id': '035DE2F',
'title': 'General',
},
'playlist_mincount': 234,
}]
def _correct_title(self, title, /):
return remove_end(title, ' | MOTHERLESS.COM ™')
def _correct_path(self, url, item_id):
return urllib.parse.urljoin(url, f'/GV{item_id}')
class MotherlessUploaderIE(MotherlessPaginatedIE):
_VALID_URL = r'https?://(?:www\.)?motherless\.com/u/(?P<id>\w+)/?(?:$|[?#])'
_TESTS = [{
'url': 'https://motherless.com/u/Mrgo4hrs2023',
'info_dict': {
'id': 'Mrgo4hrs2023',
'title': "Mrgo4hrs2023's Uploads",
},
'playlist_mincount': 32,
}, {
'url': 'https://motherless.com/u/Happy_couple?t=v',
'info_dict': {
'id': 'Happy_couple',
'title': "Happy_couple's Uploads",
},
'playlist_mincount': 8,
}]
_EXTRA_QUERY = {'t': 'v'}
def _correct_path(self, url, item_id):
return urllib.parse.urljoin(url, f'/u/{item_id}?t=v')

View File

@ -1,43 +0,0 @@
from .jixie import JixieBaseIE
class MoviewPlayIE(JixieBaseIE):
_VALID_URL = r'https?://www\.moview\.id/play/\d+/(?P<id>[\w-]+)'
_TESTS = [
{
# drm hls, only use direct link
'url': 'https://www.moview.id/play/174/Candy-Monster',
'info_dict': {
'id': '146182',
'ext': 'mp4',
'display_id': 'Candy-Monster',
'uploader_id': 'Mo165qXUUf',
'duration': 528.2,
'title': 'Candy Monster',
'description': 'Mengapa Candy Monster ingin mengambil permen Chloe?',
'thumbnail': 'https://video.jixie.media/1034/146182/146182_1280x720.jpg',
},
}, {
# non-drm hls
'url': 'https://www.moview.id/play/75/Paris-Van-Java-Episode-16',
'info_dict': {
'id': '28210',
'ext': 'mp4',
'duration': 2595.666667,
'display_id': 'Paris-Van-Java-Episode-16',
'uploader_id': 'Mo165qXUUf',
'thumbnail': 'https://video.jixie.media/1003/28210/28210_1280x720.jpg',
'description': 'md5:2a5e18d98eef9b39d7895029cac96c63',
'title': 'Paris Van Java Episode 16',
},
},
]
def _real_extract(self, url):
display_id = self._match_id(url)
webpage = self._download_webpage(url, display_id)
video_id = self._search_regex(
r'video_id\s*=\s*"(?P<video_id>[^"]+)', webpage, 'video_id')
return self._extract_data_from_jixie_id(display_id, video_id, webpage)

View File

@ -1,38 +0,0 @@
from .common import InfoExtractor
class MoviezineIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?moviezine\.se/video/(?P<id>[^?#]+)'
_TEST = {
'url': 'http://www.moviezine.se/video/205866',
'info_dict': {
'id': '205866',
'ext': 'mp4',
'title': 'Oculus - Trailer 1',
'description': 'md5:40cc6790fc81d931850ca9249b40e8a4',
'thumbnail': r're:http://.*\.jpg',
},
}
def _real_extract(self, url):
mobj = self._match_valid_url(url)
video_id = mobj.group('id')
webpage = self._download_webpage(url, video_id)
jsplayer = self._download_webpage(f'http://www.moviezine.se/api/player.js?video={video_id}', video_id, 'Downloading js api player')
formats = [{
'format_id': 'sd',
'url': self._html_search_regex(r'file: "(.+?)",', jsplayer, 'file'),
'quality': 0,
'ext': 'mp4',
}]
return {
'id': video_id,
'title': self._search_regex(r'title: "(.+?)",', jsplayer, 'title'),
'thumbnail': self._search_regex(r'image: "(.+?)",', jsplayer, 'image'),
'formats': formats,
'description': self._og_search_description(webpage),
}

View File

@ -1,174 +0,0 @@
from .common import InfoExtractor
from ..utils import (
date_from_str,
format_field,
try_get,
unified_strdate,
)
class MusicdexBaseIE(InfoExtractor):
def _return_info(self, track_json, album_json, video_id):
return {
'id': str(video_id),
'title': track_json.get('name'),
'track': track_json.get('name'),
'description': track_json.get('description'),
'track_number': track_json.get('number'),
'url': format_field(track_json, 'url', 'https://www.musicdex.org/%s'),
'duration': track_json.get('duration'),
'genres': [genre.get('name') for genre in track_json.get('genres') or []],
'like_count': track_json.get('likes_count'),
'view_count': track_json.get('plays'),
'artists': [artist.get('name') for artist in track_json.get('artists') or []],
'album_artists': [artist.get('name') for artist in album_json.get('artists') or []],
'thumbnail': format_field(album_json, 'image', 'https://www.musicdex.org/%s'),
'album': album_json.get('name'),
'release_year': try_get(album_json, lambda x: date_from_str(unified_strdate(x['release_date'])).year),
'extractor_key': MusicdexSongIE.ie_key(),
'extractor': 'MusicdexSong',
}
class MusicdexSongIE(MusicdexBaseIE):
_VALID_URL = r'https?://(?:www\.)?musicdex\.org/track/(?P<id>\d+)'
_TESTS = [{
'url': 'https://www.musicdex.org/track/306/dual-existence',
'info_dict': {
'id': '306',
'ext': 'mp3',
'title': 'dual existence',
'description': '#NIPPONSEI @ IRC.RIZON.NET',
'track': 'dual existence',
'track_number': 1,
'duration': 266000,
'genres': ['Anime'],
'like_count': int,
'view_count': int,
'artists': ['fripSide'],
'album_artists': ['fripSide'],
'thumbnail': 'https://www.musicdex.org/storage/album/9iDIam1DHTVqUG4UclFIEq1WAFGXfPW4y0TtZa91.png',
'album': 'To Aru Kagaku no Railgun T OP2 Single - dual existence',
'release_year': 2020,
},
'params': {'skip_download': True},
}]
def _real_extract(self, url):
video_id = self._match_id(url)
data_json = self._download_json(
f'https://www.musicdex.org/secure/tracks/{video_id}?defaultRelations=true', video_id)['track']
return self._return_info(data_json, data_json.get('album') or {}, video_id)
class MusicdexAlbumIE(MusicdexBaseIE):
_VALID_URL = r'https?://(?:www\.)?musicdex\.org/album/(?P<id>\d+)'
_TESTS = [{
'url': 'https://www.musicdex.org/album/56/tenmon-and-eiichiro-yanagi-minori/ef-a-tale-of-memories-original-soundtrack-2-fortissimo',
'playlist_mincount': 28,
'info_dict': {
'id': '56',
'genres': ['OST'],
'view_count': int,
'artists': ['TENMON & Eiichiro Yanagi / minori'],
'title': 'ef - a tale of memories Original Soundtrack 2 ~fortissimo~',
'release_year': 2008,
'thumbnail': 'https://www.musicdex.org/storage/album/2rSHkyYBYfB7sbvElpEyTMcUn6toY7AohOgJuDlE.jpg',
},
}]
def _real_extract(self, url):
playlist_id = self._match_id(url)
data_json = self._download_json(
f'https://www.musicdex.org/secure/albums/{playlist_id}?defaultRelations=true', playlist_id)['album']
entries = [self._return_info(track, data_json, track['id'])
for track in data_json.get('tracks') or [] if track.get('id')]
return {
'_type': 'playlist',
'id': playlist_id,
'title': data_json.get('name'),
'description': data_json.get('description'),
'genres': [genre.get('name') for genre in data_json.get('genres') or []],
'view_count': data_json.get('plays'),
'artists': [artist.get('name') for artist in data_json.get('artists') or []],
'thumbnail': format_field(data_json, 'image', 'https://www.musicdex.org/%s'),
'release_year': try_get(data_json, lambda x: date_from_str(unified_strdate(x['release_date'])).year),
'entries': entries,
}
class MusicdexPageIE(MusicdexBaseIE): # XXX: Conventionally, base classes should end with BaseIE/InfoExtractor
def _entries(self, playlist_id):
next_page_url = self._API_URL % playlist_id
while next_page_url:
data_json = self._download_json(next_page_url, playlist_id)['pagination']
yield from data_json.get('data') or []
next_page_url = data_json.get('next_page_url')
class MusicdexArtistIE(MusicdexPageIE):
_VALID_URL = r'https?://(?:www\.)?musicdex\.org/artist/(?P<id>\d+)'
_API_URL = 'https://www.musicdex.org/secure/artists/%s/albums?page=1'
_TESTS = [{
'url': 'https://www.musicdex.org/artist/11/fripside',
'playlist_mincount': 28,
'info_dict': {
'id': '11',
'view_count': int,
'title': 'fripSide',
'thumbnail': 'https://www.musicdex.org/storage/artist/ZmOz0lN2vsweegB660em3xWffCjLPmTQHqJls5Xx.jpg',
},
}]
def _real_extract(self, url):
playlist_id = self._match_id(url)
data_json = self._download_json(f'https://www.musicdex.org/secure/artists/{playlist_id}', playlist_id)['artist']
entries = []
for album in self._entries(playlist_id):
entries.extend(self._return_info(track, album, track['id']) for track in album.get('tracks') or [] if track.get('id'))
return {
'_type': 'playlist',
'id': playlist_id,
'title': data_json.get('name'),
'view_count': data_json.get('plays'),
'thumbnail': format_field(data_json, 'image_small', 'https://www.musicdex.org/%s'),
'entries': entries,
}
class MusicdexPlaylistIE(MusicdexPageIE):
_VALID_URL = r'https?://(?:www\.)?musicdex\.org/playlist/(?P<id>\d+)'
_API_URL = 'https://www.musicdex.org/secure/playlists/%s/tracks?perPage=10000&page=1'
_TESTS = [{
'url': 'https://www.musicdex.org/playlist/9/test',
'playlist_mincount': 73,
'info_dict': {
'id': '9',
'view_count': int,
'title': 'Test',
'thumbnail': 'https://www.musicdex.org/storage/album/jXATI79f0IbQ2sgsKYOYRCW3zRwF3XsfHhzITCuJ.jpg',
'description': 'Test 123 123 21312 32121321321321312',
},
}]
def _real_extract(self, url):
playlist_id = self._match_id(url)
data_json = self._download_json(f'https://www.musicdex.org/secure/playlists/{playlist_id}', playlist_id)['playlist']
entries = [self._return_info(track, track.get('album') or {}, track['id'])
for track in self._entries(playlist_id) or [] if track.get('id')]
return {
'_type': 'playlist',
'id': playlist_id,
'title': data_json.get('name'),
'description': data_json.get('description'),
'view_count': data_json.get('plays'),
'thumbnail': format_field(data_json, 'image', 'https://www.musicdex.org/%s'),
'entries': entries,
}

View File

@ -1,64 +1,4 @@
from .common import InfoExtractor
from .fox import FOXIE from .fox import FOXIE
from ..utils import (
smuggle_url,
url_basename,
)
class NationalGeographicVideoIE(InfoExtractor):
IE_NAME = 'natgeo:video'
_VALID_URL = r'https?://video\.nationalgeographic\.com/.*?'
_TESTS = [
{
'url': 'http://video.nationalgeographic.com/video/news/150210-news-crab-mating-vin?source=featuredvideo',
'md5': '730855d559abbad6b42c2be1fa584917',
'info_dict': {
'id': '0000014b-70a1-dd8c-af7f-f7b559330001',
'ext': 'mp4',
'title': 'Mating Crabs Busted by Sharks',
'description': 'md5:16f25aeffdeba55aaa8ec37e093ad8b3',
'timestamp': 1423523799,
'upload_date': '20150209',
'uploader': 'NAGS',
},
'add_ie': ['ThePlatform'],
'skip': 'Redirects to main page',
},
{
'url': 'http://video.nationalgeographic.com/wild/when-sharks-attack/the-real-jaws',
'md5': '6a3105eb448c070503b3105fb9b320b5',
'info_dict': {
'id': 'ngc-I0IauNSWznb_UV008GxSbwY35BZvgi2e',
'ext': 'mp4',
'title': 'The Real Jaws',
'description': 'md5:8d3e09d9d53a85cd397b4b21b2c77be6',
'timestamp': 1433772632,
'upload_date': '20150608',
'uploader': 'NAGS',
},
'add_ie': ['ThePlatform'],
'skip': 'Redirects to main page',
},
]
def _real_extract(self, url):
name = url_basename(url)
webpage = self._download_webpage(url, name)
guid = self._search_regex(
r'id="(?:videoPlayer|player-container)"[^>]+data-guid="([^"]+)"',
webpage, 'guid')
return {
'_type': 'url_transparent',
'ie_key': 'ThePlatform',
'url': smuggle_url(
f'http://link.theplatform.com/s/ngs/media/guid/2423130747/{guid}?mbr=true',
{'force_smil_url': True}),
'id': guid,
}
class NationalGeographicTVIE(FOXIE): # XXX: Do not subclass from concrete IE class NationalGeographicTVIE(FOXIE): # XXX: Do not subclass from concrete IE

View File

@ -13,11 +13,9 @@ from ..utils import (
dict_get, dict_get,
int_or_none, int_or_none,
join_nonempty, join_nonempty,
merge_dicts,
parse_iso8601, parse_iso8601,
traverse_obj, traverse_obj,
try_get, try_get,
unified_timestamp,
update_url_query, update_url_query,
url_or_none, url_or_none,
) )
@ -284,142 +282,3 @@ class NaverLiveIE(NaverBaseIE):
}), get_all=False), }), get_all=False),
'is_live': True, 'is_live': True,
} }
class NaverNowIE(NaverBaseIE):
IE_NAME = 'navernow'
_VALID_URL = r'https?://now\.naver\.com/s/now\.(?P<id>\w+)'
_API_URL = 'https://apis.naver.com/now_web/oldnow_web/v4'
_TESTS = [{
'url': 'https://now.naver.com/s/now.4759?shareReplayId=26331132#replay=',
'md5': 'e05854162c21c221481de16b2944a0bc',
'info_dict': {
'id': '4759-26331132',
'title': '아이키X노제\r\n💖꽁냥꽁냥💖(1)',
'ext': 'mp4',
'thumbnail': r're:^https?://.*\.jpg',
'timestamp': 1650369600,
'upload_date': '20220419',
'uploader_id': 'now',
'view_count': int,
'uploader_url': 'https://now.naver.com/show/4759',
'uploader': '아이키의 떰즈업',
},
'params': {
'noplaylist': True,
},
}, {
'url': 'https://now.naver.com/s/now.4759?shareHightlight=26601461#highlight=',
'md5': '9f6118e398aa0f22b2152f554ea7851b',
'info_dict': {
'id': '4759-26601461',
'title': '아이키: 나 리정한테 흔들렸어,,, 질투 폭발하는 노제 여보😾 [아이키의 떰즈업]ㅣ네이버 NOW.',
'ext': 'mp4',
'thumbnail': r're:^https?://.*\.jpg',
'upload_date': '20220504',
'timestamp': 1651648311,
'uploader_id': 'now',
'view_count': int,
'uploader_url': 'https://now.naver.com/show/4759',
'uploader': '아이키의 떰즈업',
},
'params': {
'noplaylist': True,
},
}, {
'url': 'https://now.naver.com/s/now.4759',
'info_dict': {
'id': '4759',
'title': '아이키의 떰즈업',
},
'playlist_mincount': 101,
}, {
'url': 'https://now.naver.com/s/now.4759?shareReplayId=26331132#replay',
'info_dict': {
'id': '4759',
'title': '아이키의 떰즈업',
},
'playlist_mincount': 101,
}, {
'url': 'https://now.naver.com/s/now.4759?shareHightlight=26601461#highlight=',
'info_dict': {
'id': '4759',
'title': '아이키의 떰즈업',
},
'playlist_mincount': 101,
}, {
'url': 'https://now.naver.com/s/now.kihyunplay?shareReplayId=30573291#replay',
'only_matching': True,
}]
def _extract_replay(self, show_id, replay_id):
vod_info = self._download_json(f'{self._API_URL}/shows/now.{show_id}/vod/{replay_id}', replay_id)
in_key = self._download_json(f'{self._API_URL}/shows/now.{show_id}/vod/{replay_id}/inkey', replay_id)['inKey']
return merge_dicts({
'id': f'{show_id}-{replay_id}',
'title': traverse_obj(vod_info, ('episode', 'title')),
'timestamp': unified_timestamp(traverse_obj(vod_info, ('episode', 'start_time'))),
'thumbnail': vod_info.get('thumbnail_image_url'),
}, self._extract_video_info(replay_id, vod_info['video_id'], in_key))
def _extract_show_replays(self, show_id):
page_size = 15
page = 1
while True:
show_vod_info = self._download_json(
f'{self._API_URL}/vod-shows/now.{show_id}', show_id,
query={'page': page, 'page_size': page_size},
note=f'Downloading JSON vod list for show {show_id} - page {page}',
)['response']['result']
for v in show_vod_info.get('vod_list') or []:
yield self._extract_replay(show_id, v['id'])
if len(show_vod_info.get('vod_list') or []) < page_size:
break
page += 1
def _extract_show_highlights(self, show_id, highlight_id=None):
page_size = 10
page = 1
while True:
highlights_videos = self._download_json(
f'{self._API_URL}/shows/now.{show_id}/highlights/videos/', show_id,
query={'page': page, 'page_size': page_size},
note=f'Downloading JSON highlights for show {show_id} - page {page}')
for highlight in highlights_videos.get('results') or []:
if highlight_id and highlight.get('clip_no') != int(highlight_id):
continue
yield merge_dicts({
'id': f'{show_id}-{highlight["clip_no"]}',
'title': highlight.get('title'),
'timestamp': unified_timestamp(highlight.get('regdate')),
'thumbnail': highlight.get('thumbnail_url'),
}, self._extract_video_info(highlight['clip_no'], highlight['video_id'], highlight['video_inkey']))
if len(highlights_videos.get('results') or []) < page_size:
break
page += 1
def _extract_highlight(self, show_id, highlight_id):
try:
return next(self._extract_show_highlights(show_id, highlight_id))
except StopIteration:
raise ExtractorError(f'Unable to find highlight {highlight_id} for show {show_id}')
def _real_extract(self, url):
show_id = self._match_id(url)
qs = urllib.parse.parse_qs(urllib.parse.urlparse(url).query)
if not self._yes_playlist(show_id, qs.get('shareHightlight')):
return self._extract_highlight(show_id, qs['shareHightlight'][0])
elif not self._yes_playlist(show_id, qs.get('shareReplayId')):
return self._extract_replay(show_id, qs['shareReplayId'][0])
show_info = self._download_json(
f'{self._API_URL}/shows/now.{show_id}/', show_id,
note=f'Downloading JSON vod list for show {show_id}')
return self.playlist_result(
itertools.chain(self._extract_show_replays(show_id), self._extract_show_highlights(show_id)),
show_id, show_info.get('title'))

View File

@ -1,38 +0,0 @@
from .common import InfoExtractor
from .youtube import YoutubeIE
from ..utils import parse_iso8601, url_or_none
from ..utils.traversal import traverse_obj
class NerdCubedFeedIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?nerdcubed\.co\.uk/?(?:$|[#?])'
_TEST = {
'url': 'http://www.nerdcubed.co.uk/',
'info_dict': {
'id': 'nerdcubed-feed',
'title': 'nerdcubed.co.uk feed',
},
'playlist_mincount': 5500,
}
def _extract_video(self, feed_entry):
return self.url_result(
f'https://www.youtube.com/watch?v={feed_entry["id"]}', YoutubeIE,
**traverse_obj(feed_entry, {
'id': ('id', {str}),
'title': ('title', {str}),
'description': ('description', {str}),
'timestamp': ('publishedAt', {parse_iso8601}),
'channel': ('source', 'name', {str}),
'channel_id': ('source', 'id', {str}),
'channel_url': ('source', 'url', {str}),
'thumbnail': ('thumbnail', 'source', {url_or_none}),
}), url_transparent=True)
def _real_extract(self, url):
video_id = 'nerdcubed-feed'
feed = self._download_json('https://www.nerdcubed.co.uk/_/cdn/videos.json', video_id)
return self.playlist_result(
map(self._extract_video, traverse_obj(feed, ('videos', lambda _, v: v['id']))),
video_id, 'nerdcubed.co.uk feed')

View File

@ -1,281 +0,0 @@
import itertools
from .common import InfoExtractor, SearchInfoExtractor
from .dailymotion import DailymotionIE
from ..utils import smuggle_url, traverse_obj
class NetverseBaseIE(InfoExtractor):
_ENDPOINTS = {
'watch': 'watchvideo',
'video': 'watchvideo',
'webseries': 'webseries',
'season': 'webseason_videos',
}
def _call_api(self, slug, endpoint, query={}, season_id='', display_id=None):
return self._download_json(
f'https://api.netverse.id/medias/api/v2/{self._ENDPOINTS[endpoint]}/{slug}/{season_id}',
display_id or slug, query=query)
def _get_comments(self, video_id):
last_page_number = None
for i in itertools.count(1):
comment_data = self._download_json(
f'https://api.netverse.id/mediadetails/api/v3/videos/comments/{video_id}',
video_id, data=b'', fatal=False, query={'page': i},
note=f'Downloading JSON comment metadata page {i}') or {}
yield from traverse_obj(comment_data, ('response', 'comments', 'data', ..., {
'id': '_id',
'text': 'comment',
'author_id': 'customer_id',
'author': ('customer', 'name'),
'author_thumbnail': ('customer', 'profile_picture'),
}))
if not last_page_number:
last_page_number = traverse_obj(comment_data, ('response', 'comments', 'last_page'))
if i >= (last_page_number or 0):
break
class NetverseIE(NetverseBaseIE):
_VALID_URL = r'https?://(?:\w+\.)?netverse\.id/(?P<type>watch|video)/(?P<display_id>[^/?#&]+)'
_TESTS = [{
# Watch video
'url': 'https://www.netverse.id/watch/waktu-indonesia-bercanda-edisi-spesial-lebaran-2016',
'info_dict': {
'id': 'k4yhqUwINAGtmHx3NkL',
'title': 'Waktu Indonesia Bercanda - Edisi Spesial Lebaran 2016',
'ext': 'mp4',
'season': 'Season 2016',
'description': 'md5:d41d8cd98f00b204e9800998ecf8427e',
'thumbnail': r're:https?://s\d+\.dmcdn\.net/v/[^/]+/x1080',
'episode_number': 22,
'episode': 'Episode 22',
'uploader_id': 'x2ir3vq',
'age_limit': 0,
'tags': [],
'view_count': int,
'display_id': 'waktu-indonesia-bercanda-edisi-spesial-lebaran-2016',
'duration': 2990,
'upload_date': '20210722',
'timestamp': 1626919804,
'like_count': int,
'uploader': 'Net Prime',
},
}, {
# series
'url': 'https://www.netverse.id/watch/jadoo-seorang-model',
'info_dict': {
'id': 'x88izwc',
'title': 'Jadoo Seorang Model',
'ext': 'mp4',
'season': 'Season 2',
'description': 'md5:8a74f70812cca267e19ee0635f0af835',
'thumbnail': r're:https?://s\d+\.dmcdn\.net/v/[^/]+/x1080',
'episode_number': 2,
'episode': 'Episode 2',
'view_count': int,
'like_count': int,
'display_id': 'jadoo-seorang-model',
'uploader_id': 'x2ir3vq',
'duration': 635,
'timestamp': 1646372927,
'tags': ['PG069497-hellojadooseason2eps2'],
'upload_date': '20220304',
'uploader': 'Net Prime',
'age_limit': 0,
},
'skip': 'video get Geo-blocked for some country',
}, {
# non www host
'url': 'https://netverse.id/watch/tetangga-baru',
'info_dict': {
'id': 'k4CNGz7V0HJ7vfwZbXy',
'ext': 'mp4',
'title': 'Tetangga Baru',
'season': 'Season 1',
'description': 'md5:23fcf70e97d461d3029d25d59b2ccfb9',
'thumbnail': r're:https?://s\d+\.dmcdn\.net/v/[^/]+/x1080',
'episode_number': 1,
'episode': 'Episode 1',
'timestamp': 1624538169,
'view_count': int,
'upload_date': '20210624',
'age_limit': 0,
'uploader_id': 'x2ir3vq',
'like_count': int,
'uploader': 'Net Prime',
'tags': ['PG008534', 'tetangga', 'Baru'],
'display_id': 'tetangga-baru',
'duration': 1406,
},
}, {
# /video url
'url': 'https://www.netverse.id/video/pg067482-hellojadoo-season1',
'title': 'Namaku Choi Jadoo',
'info_dict': {
'id': 'x887jzz',
'ext': 'mp4',
'thumbnail': r're:https?://s\d+\.dmcdn\.net/v/[^/]+/x1080',
'season': 'Season 1',
'episode_number': 1,
'description': 'md5:d4f627b3e7a3f9acdc55f6cdd5ea41d5',
'title': 'Namaku Choi Jadoo',
'episode': 'Episode 1',
'age_limit': 0,
'like_count': int,
'view_count': int,
'tags': ['PG067482', 'PG067482-HelloJadoo-season1'],
'duration': 780,
'display_id': 'pg067482-hellojadoo-season1',
'uploader_id': 'x2ir3vq',
'uploader': 'Net Prime',
'timestamp': 1645764984,
'upload_date': '20220225',
},
'skip': 'This video get Geo-blocked for some country',
}, {
# video with comments
'url': 'https://netverse.id/video/episode-1-season-2016-ok-food',
'info_dict': {
'id': 'k6hetBPiQMljSxxvAy7',
'ext': 'mp4',
'thumbnail': r're:https?://s\d+\.dmcdn\.net/v/[^/]+/x1080',
'display_id': 'episode-1-season-2016-ok-food',
'like_count': int,
'description': '',
'duration': 1471,
'age_limit': 0,
'timestamp': 1642405848,
'episode_number': 1,
'season': 'Season 2016',
'uploader_id': 'x2ir3vq',
'title': 'Episode 1 - Season 2016 - Ok Food',
'upload_date': '20220117',
'tags': [],
'view_count': int,
'episode': 'Episode 1',
'uploader': 'Net Prime',
'comment_count': int,
},
'params': {
'getcomments': True,
},
}, {
# video with multiple page comment
'url': 'https://netverse.id/video/match-island-eps-1-fix',
'info_dict': {
'id': 'x8aznjc',
'ext': 'mp4',
'like_count': int,
'tags': ['Match-Island', 'Pd00111'],
'display_id': 'match-island-eps-1-fix',
'view_count': int,
'episode': 'Episode 1',
'uploader': 'Net Prime',
'duration': 4070,
'timestamp': 1653068165,
'description': 'md5:e9cf3b480ad18e9c33b999e3494f223f',
'age_limit': 0,
'title': 'Welcome To Match Island',
'upload_date': '20220520',
'episode_number': 1,
'thumbnail': r're:https?://s\d+\.dmcdn\.net/v/[^/]+/x1080',
'uploader_id': 'x2ir3vq',
'season': 'Season 1',
'comment_count': int,
},
'params': {
'getcomments': True,
},
}]
def _real_extract(self, url):
display_id, sites_type = self._match_valid_url(url).group('display_id', 'type')
program_json = self._call_api(display_id, sites_type)
videos = program_json['response']['videos']
return {
'_type': 'url_transparent',
'ie_key': DailymotionIE.ie_key(),
'url': smuggle_url(videos['dailymotion_url'], {'query': {'embedder': 'https://www.netverse.id'}}),
'display_id': display_id,
'title': videos.get('title'),
'season': videos.get('season_name'),
'thumbnail': traverse_obj(videos, ('program_detail', 'thumbnail_image')),
'description': traverse_obj(videos, ('program_detail', 'description')),
'episode_number': videos.get('episode_order'),
'__post_extractor': self.extract_comments(display_id),
}
class NetversePlaylistIE(NetverseBaseIE):
_VALID_URL = r'https?://(?:\w+\.)?netverse\.id/(?P<type>webseries)/(?P<display_id>[^/?#&]+)'
_TESTS = [{
# multiple season
'url': 'https://netverse.id/webseries/tetangga-masa-gitu',
'info_dict': {
'id': 'tetangga-masa-gitu',
'title': 'Tetangga Masa Gitu',
},
'playlist_count': 519,
}, {
# single season
'url': 'https://netverse.id/webseries/kelas-internasional',
'info_dict': {
'id': 'kelas-internasional',
'title': 'Kelas Internasional',
},
'playlist_count': 203,
}]
def parse_playlist(self, json_data, playlist_id):
slug_sample = traverse_obj(json_data, ('related', 'data', ..., 'slug'))[0]
for season in traverse_obj(json_data, ('seasons', ..., 'id')):
playlist_json = self._call_api(
slug_sample, 'season', display_id=playlist_id, season_id=season)
for current_page in range(playlist_json['response']['season_list']['last_page']):
playlist_json = self._call_api(slug_sample, 'season', query={'page': current_page + 1},
season_id=season, display_id=playlist_id)
for slug in traverse_obj(playlist_json, ('response', ..., 'data', ..., 'slug')):
yield self.url_result(f'https://www.netverse.id/video/{slug}', NetverseIE)
def _real_extract(self, url):
playlist_id, sites_type = self._match_valid_url(url).group('display_id', 'type')
playlist_data = self._call_api(playlist_id, sites_type)
return self.playlist_result(
self.parse_playlist(playlist_data['response'], playlist_id),
traverse_obj(playlist_data, ('response', 'webseries_info', 'slug')),
traverse_obj(playlist_data, ('response', 'webseries_info', 'title')))
class NetverseSearchIE(SearchInfoExtractor):
_SEARCH_KEY = 'netsearch'
_TESTS = [{
'url': 'netsearch10:tetangga',
'info_dict': {
'id': 'tetangga',
'title': 'tetangga',
},
'playlist_count': 10,
}]
def _search_results(self, query):
last_page = None
for i in itertools.count(1):
search_data = self._download_json(
'https://api.netverse.id/search/elastic/search', query,
query={'q': query, 'page': i}, note=f'Downloading page {i}')
videos = traverse_obj(search_data, ('response', 'data', ...))
for video in videos:
yield self.url_result(f'https://netverse.id/video/{video["slug"]}', NetverseIE)
last_page = last_page or traverse_obj(search_data, ('response', 'lastpage'))
if not videos or i >= (last_page or 0):
break

View File

@ -1,201 +0,0 @@
import functools
from .common import InfoExtractor
from ..utils import (
ExtractorError,
OnDemandPagedList,
UserNotLive,
filter_dict,
int_or_none,
parse_iso8601,
str_or_none,
url_or_none,
)
from ..utils.traversal import traverse_obj
class NuumBaseIE(InfoExtractor):
def _call_api(self, path, video_id, description, query={}):
response = self._download_json(
f'https://nuum.ru/api/v2/{path}', video_id, query=query,
note=f'Downloading {description} metadata',
errnote=f'Unable to download {description} metadata')
if error := response.get('error'):
raise ExtractorError(f'API returned error: {error!r}')
return response['result']
def _get_channel_info(self, channel_name):
return self._call_api(
'broadcasts/public', video_id=channel_name, description='channel',
query={
'with_extra': 'true',
'channel_name': channel_name,
'with_deleted': 'true',
})
def _parse_video_data(self, container, extract_formats=True):
stream = traverse_obj(container, ('media_container_streams', 0, {dict})) or {}
media = traverse_obj(stream, ('stream_media', 0, {dict})) or {}
media_url = traverse_obj(media, (
'media_meta', ('media_archive_url', 'media_url'), {url_or_none}), get_all=False)
video_id = str(container['media_container_id'])
is_live = media.get('media_status') == 'RUNNING'
formats, subtitles = None, None
headers = {'Referer': 'https://nuum.ru/'}
if extract_formats:
formats, subtitles = self._extract_m3u8_formats_and_subtitles(
media_url, video_id, 'mp4', live=is_live, headers=headers)
return filter_dict({
'id': video_id,
'is_live': is_live,
'formats': formats,
'subtitles': subtitles,
'http_headers': headers,
**traverse_obj(container, {
'title': ('media_container_name', {str}),
'description': ('media_container_description', {str}),
'timestamp': ('created_at', {parse_iso8601}),
'channel': ('media_container_channel', 'channel_name', {str}),
'channel_id': ('media_container_channel', 'channel_id', {str_or_none}),
}),
**traverse_obj(stream, {
'view_count': ('stream_total_viewers', {int_or_none}),
'concurrent_view_count': ('stream_current_viewers', {int_or_none}),
}),
**traverse_obj(media, {
'duration': ('media_duration', {int_or_none}),
'thumbnail': ('media_meta', ('media_preview_archive_url', 'media_preview_url'), {url_or_none}),
}, get_all=False),
})
class NuumMediaIE(NuumBaseIE):
IE_NAME = 'nuum:media'
_VALID_URL = r'https?://nuum\.ru/(?:streams|videos|clips)/(?P<id>[\d]+)'
_TESTS = [{
'url': 'https://nuum.ru/streams/1592713-7-days-to-die',
'only_matching': True,
}, {
'url': 'https://nuum.ru/videos/1567547-toxi-hurtz',
'md5': 'ce28837a5bbffe6952d7bfd3d39811b0',
'info_dict': {
'id': '1567547',
'ext': 'mp4',
'title': 'Toxi$ - Hurtz',
'description': '',
'timestamp': 1702631651,
'upload_date': '20231215',
'thumbnail': r're:^https?://.+\.jpg',
'view_count': int,
'concurrent_view_count': int,
'channel_id': '6911',
'channel': 'toxis',
'duration': 116,
},
}, {
'url': 'https://nuum.ru/clips/1552564-pro-misu',
'md5': 'b248ae1565b1e55433188f11beeb0ca1',
'info_dict': {
'id': '1552564',
'ext': 'mp4',
'title': 'Про Мису 🙃',
'timestamp': 1701971828,
'upload_date': '20231207',
'thumbnail': r're:^https?://.+\.jpg',
'view_count': int,
'concurrent_view_count': int,
'channel_id': '3320',
'channel': 'Misalelik',
'duration': 41,
},
}]
def _real_extract(self, url):
video_id = self._match_id(url)
video_data = self._call_api(f'media-containers/{video_id}', video_id, 'media')
return self._parse_video_data(video_data)
class NuumLiveIE(NuumBaseIE):
IE_NAME = 'nuum:live'
_VALID_URL = r'https?://nuum\.ru/channel/(?P<id>[^/#?]+)/?(?:$|[#?])'
_TESTS = [{
'url': 'https://nuum.ru/channel/mts_live',
'only_matching': True,
}]
def _real_extract(self, url):
channel = self._match_id(url)
channel_info = self._get_channel_info(channel)
if traverse_obj(channel_info, ('channel', 'channel_is_live')) is False:
raise UserNotLive(video_id=channel)
info = self._parse_video_data(channel_info['media_container'])
return {
'webpage_url': f'https://nuum.ru/streams/{info["id"]}',
'extractor_key': NuumMediaIE.ie_key(),
'extractor': NuumMediaIE.IE_NAME,
**info,
}
class NuumTabIE(NuumBaseIE):
IE_NAME = 'nuum:tab'
_VALID_URL = r'https?://nuum\.ru/channel/(?P<id>[^/#?]+)/(?P<type>streams|videos|clips)'
_TESTS = [{
'url': 'https://nuum.ru/channel/dankon_/clips',
'info_dict': {
'id': 'dankon__clips',
'title': 'Dankon_',
},
'playlist_mincount': 29,
}, {
'url': 'https://nuum.ru/channel/dankon_/videos',
'info_dict': {
'id': 'dankon__videos',
'title': 'Dankon_',
},
'playlist_mincount': 2,
}, {
'url': 'https://nuum.ru/channel/dankon_/streams',
'info_dict': {
'id': 'dankon__streams',
'title': 'Dankon_',
},
'playlist_mincount': 1,
}]
_PAGE_SIZE = 50
def _fetch_page(self, channel_id, tab_type, tab_id, page):
CONTAINER_TYPES = {
'clips': ['SHORT_VIDEO', 'REVIEW_VIDEO'],
'videos': ['LONG_VIDEO'],
'streams': ['SINGLE'],
}
media_containers = self._call_api(
'media-containers', video_id=tab_id, description=f'{tab_type} tab page {page + 1}',
query={
'limit': self._PAGE_SIZE,
'offset': page * self._PAGE_SIZE,
'channel_id': channel_id,
'media_container_status': 'STOPPED',
'media_container_type': CONTAINER_TYPES[tab_type],
})
for container in traverse_obj(media_containers, (..., {dict})):
metadata = self._parse_video_data(container, extract_formats=False)
yield self.url_result(f'https://nuum.ru/videos/{metadata["id"]}', NuumMediaIE, **metadata)
def _real_extract(self, url):
channel_name, tab_type = self._match_valid_url(url).group('id', 'type')
tab_id = f'{channel_name}_{tab_type}'
channel_data = self._get_channel_info(channel_name)['channel']
return self.playlist_result(OnDemandPagedList(functools.partial(
self._fetch_page, channel_data['channel_id'], tab_type, tab_id), self._PAGE_SIZE),
playlist_id=tab_id, playlist_title=channel_data.get('channel_name'))

View File

@ -1,41 +0,0 @@
from .common import InfoExtractor
from ..utils import js_to_json
class OnionStudiosIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?onionstudios\.com/(?:video(?:s/[^/]+-|/)|embed\?.*\bid=)(?P<id>\d+)(?!-)'
_EMBED_REGEX = [r'(?s)<(?:iframe|bulbs-video)[^>]+?src=(["\'])(?P<url>(?:https?:)?//(?:www\.)?onionstudios\.com/(?:embed.+?|video/\d+\.json))\1']
_TESTS = [{
'url': 'http://www.onionstudios.com/videos/hannibal-charges-forward-stops-for-a-cocktail-2937',
'md5': '5a118d466d62b5cd03647cf2c593977f',
'info_dict': {
'id': '3459881',
'ext': 'mp4',
'title': 'Hannibal charges forward, stops for a cocktail',
'description': 'md5:545299bda6abf87e5ec666548c6a9448',
'thumbnail': r're:^https?://.*\.jpg$',
'uploader': 'a.v. club',
'upload_date': '20150619',
'timestamp': 1434728546,
},
}, {
'url': 'http://www.onionstudios.com/embed?id=2855&autoplay=true',
'only_matching': True,
}, {
'url': 'http://www.onionstudios.com/video/6139.json',
'only_matching': True,
}]
def _real_extract(self, url):
video_id = self._match_id(url)
webpage = self._download_webpage(
'http://onionstudios.com/embed/dc94dc2899fe644c0e7241fa04c1b732.js',
video_id)
mcp_id = str(self._parse_json(self._search_regex(
r'window\.mcpMapping\s*=\s*({.+?});', webpage,
'MCP Mapping'), video_id, js_to_json)[video_id]['mcp_id'])
return self.url_result(
'http://kinja.com/ajax/inset/iframe?id=mcp-' + mcp_id,
'KinjaEmbed', mcp_id)

View File

@ -21,6 +21,7 @@ class OnsenIE(InfoExtractor):
IE_NAME = 'onsen' IE_NAME = 'onsen'
IE_DESC = 'インターネットラジオステーション<音泉>' IE_DESC = 'インターネットラジオステーション<音泉>'
_API_HEADERS = {'X-Client': 'onsen-web'}
_BASE_URL = 'https://www.onsen.ag' _BASE_URL = 'https://www.onsen.ag'
_HEADERS = {'Referer': f'{_BASE_URL}/'} _HEADERS = {'Referer': f'{_BASE_URL}/'}
_NETRC_MACHINE = 'onsen' _NETRC_MACHINE = 'onsen'
@ -71,6 +72,15 @@ class OnsenIE(InfoExtractor):
'playlist_mincount': 35, 'playlist_mincount': 35,
}] }]
@classmethod
def get_temp_id(cls, url):
import base64
import urllib.parse
if c := urllib.parse.parse_qs(urllib.parse.urlparse(url).query).get('c'):
return base64.urlsafe_b64decode(f'{c[-1]}===').decode()
return super().get_temp_id(url)
@staticmethod @staticmethod
def _get_encoded_id(program): def _get_encoded_id(program):
return base64.urlsafe_b64encode(str(program['id']).encode()).decode() return base64.urlsafe_b64encode(str(program['id']).encode()).decode()
@ -80,6 +90,7 @@ class OnsenIE(InfoExtractor):
f'{self._BASE_URL}/web_api/signin', None, 'Logging in', headers={ f'{self._BASE_URL}/web_api/signin', None, 'Logging in', headers={
'Accept': 'application/json', 'Accept': 'application/json',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
**self._API_HEADERS,
}, data=json.dumps({ }, data=json.dumps({
'session': { 'session': {
'email': username, 'email': username,
@ -94,7 +105,8 @@ class OnsenIE(InfoExtractor):
program_id = self._match_id(url) program_id = self._match_id(url)
try: try:
programs = self._download_json( programs = self._download_json(
f'{self._BASE_URL}/web_api/programs/{program_id}', program_id) f'{self._BASE_URL}/web_api/programs/{program_id}',
program_id, headers=self._API_HEADERS)
except ExtractorError as e: except ExtractorError as e:
if isinstance(e.cause, HTTPError) and e.cause.status == 404: if isinstance(e.cause, HTTPError) and e.cause.status == 404:
raise ExtractorError('Invalid URL', expected=True) raise ExtractorError('Invalid URL', expected=True)
@ -103,8 +115,10 @@ class OnsenIE(InfoExtractor):
query = {k: v[-1] for k, v in parse_qs(url).items() if v} query = {k: v[-1] for k, v in parse_qs(url).items() if v}
if 'c' not in query: if 'c' not in query:
entries = [ entries = [
self.url_result(update_url_query(url, {'c': self._get_encoded_id(program)}), OnsenIE) self.url_result(
for program in traverse_obj(programs, ('contents', lambda _, v: v['id'])) update_url_query(url, {'c': self._get_encoded_id(program)}),
OnsenIE, str_or_none(program['id']),
) for program in traverse_obj(programs, ('contents', lambda _, v: v['id']))
] ]
return self.playlist_result( return self.playlist_result(

View File

@ -1,72 +0,0 @@
import re
import urllib.parse
from .common import InfoExtractor
from ..utils import (
get_element_by_attribute,
qualities,
unescapeHTML,
)
class OraTVIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?(?:ora\.tv|unsafespeech\.com)/([^/]+/)*(?P<id>[^/\?#]+)'
_TESTS = [{
'url': 'https://www.ora.tv/larrykingnow/2015/12/16/vine-youtube-stars-zach-king-king-bach-on-their-viral-videos-0_36jupg6090pq',
'md5': 'fa33717591c631ec93b04b0e330df786',
'info_dict': {
'id': '50178',
'ext': 'mp4',
'title': 'Vine & YouTube Stars Zach King & King Bach On Their Viral Videos!',
'description': 'md5:ebbc5b1424dd5dba7be7538148287ac1',
},
}, {
'url': 'http://www.unsafespeech.com/video/2016/5/10/student-self-censorship-and-the-thought-police-on-university-campuses-0_6622bnkppw4d',
'only_matching': True,
}]
def _real_extract(self, url):
display_id = self._match_id(url)
webpage = self._download_webpage(url, display_id)
video_data = self._search_regex(
r'"(?:video|current)"\s*:\s*({[^}]+?})', webpage, 'current video')
m3u8_url = self._search_regex(
r'hls_stream"?\s*:\s*"([^"]+)', video_data, 'm3u8 url', None)
if m3u8_url:
formats = self._extract_m3u8_formats(
m3u8_url, display_id, 'mp4', 'm3u8_native',
m3u8_id='hls', fatal=False)
# similar to GameSpotIE
m3u8_path = urllib.parse.urlparse(m3u8_url).path
QUALITIES_RE = r'((,[a-z]+\d+)+,?)'
available_qualities = self._search_regex(
QUALITIES_RE, m3u8_path, 'qualities').strip(',').split(',')
http_path = m3u8_path[1:].split('/', 1)[1]
http_template = re.sub(QUALITIES_RE, r'%s', http_path)
http_template = http_template.replace('.csmil/master.m3u8', '')
http_template = urllib.parse.urljoin(
'http://videocdn-pmd.ora.tv/', http_template)
preference = qualities(
['mobile400', 'basic400', 'basic600', 'sd900', 'sd1200', 'sd1500', 'hd720', 'hd1080'])
for q in available_qualities:
formats.append({
'url': http_template % q,
'format_id': q,
'quality': preference(q),
})
else:
return self.url_result(self._search_regex(
r'"youtube_id"\s*:\s*"([^"]+)', webpage, 'youtube id'), 'Youtube')
return {
'id': self._search_regex(
r'"id"\s*:\s*(\d+)', video_data, 'video id', default=display_id),
'display_id': display_id,
'title': unescapeHTML(self._og_search_title(webpage)),
'description': get_element_by_attribute(
'class', 'video_txt_decription', webpage),
'thumbnail': self._proto_relative_url(self._search_regex(
r'"thumb"\s*:\s*"([^"]+)', video_data, 'thumbnail', None)),
'formats': formats,
}

View File

@ -1,99 +0,0 @@
from .common import InfoExtractor
from ..utils import parse_iso8601, smuggle_url, unsmuggle_url, url_or_none
from ..utils.traversal import traverse_obj
class PiramideTVIE(InfoExtractor):
_VALID_URL = r'https?://piramide\.tv/video/(?P<id>[\w-]+)'
_TESTS = [{
'url': 'https://piramide.tv/video/wWtBAORdJUTh',
'info_dict': {
'id': 'wWtBAORdJUTh',
'ext': 'mp4',
'title': 'md5:79f9c8183ea6a35c836923142cf0abcc',
'description': '',
'thumbnail': 'https://cdn.jwplayer.com/v2/media/W86PgQDn/thumbnails/B9gpIxkH.jpg',
'channel': 'León Picarón',
'channel_id': 'leonpicaron',
'timestamp': 1696460362,
'upload_date': '20231004',
},
}, {
'url': 'https://piramide.tv/video/wcYn6li79NgN',
'info_dict': {
'id': 'wcYn6li79NgN',
'ext': 'mp4',
'title': 'ACEPTO TENER UN BEBE CON MI NOVIA\u2026? | Parte 1',
'description': '',
'channel': 'ARTA GAME',
'channel_id': 'arta_game',
'thumbnail': 'https://cdn.jwplayer.com/v2/media/cnEdGp5X/thumbnails/rHAaWfP7.jpg',
'timestamp': 1703434976,
'upload_date': '20231224',
},
}]
def _extract_video(self, video_id):
video_data = self._download_json(
f'https://hermes.piramide.tv/video/data/{video_id}', video_id, fatal=False)
formats, subtitles = self._extract_m3u8_formats_and_subtitles(
f'https://cdn.piramide.tv/video/{video_id}/manifest.m3u8', video_id, fatal=False)
next_video = traverse_obj(video_data, ('video', 'next_video', 'id', {str}))
return next_video, {
'id': video_id,
'formats': formats,
'subtitles': subtitles,
**traverse_obj(video_data, ('video', {
'id': ('id', {str}),
'title': ('title', {str}),
'description': ('description', {str}),
'thumbnail': ('media', 'thumbnail', {url_or_none}),
'channel': ('channel', 'name', {str}),
'channel_id': ('channel', 'id', {str}),
'timestamp': ('date', {parse_iso8601}),
})),
}
def _entries(self, video_id):
visited = set()
while True:
visited.add(video_id)
next_video, info = self._extract_video(video_id)
yield info
if not next_video or next_video in visited:
break
video_id = next_video
def _real_extract(self, url):
url, smuggled_data = unsmuggle_url(url, {})
video_id = self._match_id(url)
if self._yes_playlist(video_id, video_id, smuggled_data):
return self.playlist_result(self._entries(video_id), video_id)
return self._extract_video(video_id)[1]
class PiramideTVChannelIE(InfoExtractor):
_VALID_URL = r'https?://piramide\.tv/channel/(?P<id>[\w-]+)'
_TESTS = [{
'url': 'https://piramide.tv/channel/thekalo',
'playlist_mincount': 10,
'info_dict': {
'id': 'thekalo',
},
}]
def _entries(self, channel_name):
videos = self._download_json(
f'https://hermes.piramide.tv/channel/list/{channel_name}/date/100000', channel_name)
for video in traverse_obj(videos, ('videos', lambda _, v: v['id'])):
yield self.url_result(smuggle_url(
f'https://piramide.tv/video/{video["id"]}', {'force_noplaylist': True}),
**traverse_obj(video, {
'id': ('id', {str}),
'title': ('title', {str}),
'description': ('description', {str}),
}))
def _real_extract(self, url):
channel_name = self._match_id(url)
return self.playlist_result(self._entries(channel_name), channel_name)

View File

@ -1,72 +0,0 @@
from .common import InfoExtractor
from ..utils import (
try_get,
unified_strdate,
)
class PlanetMarathiIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?planetmarathi\.com/titles/(?P<id>[^/#&?$]+)'
_TESTS = [{
'url': 'https://www.planetmarathi.com/titles/ek-unad-divas',
'playlist_mincount': 2,
'info_dict': {
'id': 'ek-unad-divas',
},
'playlist': [{
'info_dict': {
'id': 'ASSETS-MOVIE-ASSET-01_ek-unad-divas',
'ext': 'mp4',
'title': 'ek unad divas',
'alt_title': 'चित्रपट',
'description': 'md5:41c7ed6b041c2fea9820a3f3125bd881',
'episode_number': 1,
'duration': 5539,
'upload_date': '20210829',
},
}], # Trailer skipped
}, {
'url': 'https://www.planetmarathi.com/titles/baap-beep-baap-season-1',
'playlist_mincount': 10,
'info_dict': {
'id': 'baap-beep-baap-season-1',
},
'playlist': [{
'info_dict': {
'id': 'ASSETS-CHARACTER-PROFILE-SEASON-01-ASSET-01_baap-beep-baap-season-1',
'ext': 'mp4',
'title': 'Manohar Kanhere',
'alt_title': 'मनोहर कान्हेरे',
'description': 'md5:285ed45d5c0ab5522cac9a043354ebc6',
'season_number': 1,
'episode_number': 1,
'duration': 29,
'upload_date': '20210829',
},
}], # Trailers, Episodes, other Character profiles skipped
}]
def _real_extract(self, url):
playlist_id = self._match_id(url)
entries = []
json_data = self._download_json(
f'https://www.planetmarathi.com/api/v1/titles/{playlist_id}/assets', playlist_id)['assets']
for asset in json_data:
asset_title = asset['mediaAssetName']['en']
if asset_title == 'Movie':
asset_title = playlist_id.replace('-', ' ')
asset_id = f'{asset["sk"]}_{playlist_id}'.replace('#', '-')
formats, subtitles = self._extract_m3u8_formats_and_subtitles(asset['mediaAssetURL'], asset_id)
entries.append({
'id': asset_id,
'title': asset_title,
'alt_title': try_get(asset, lambda x: x['mediaAssetName']['mr']),
'description': try_get(asset, lambda x: x['mediaAssetDescription']['en']),
'season_number': asset.get('mediaAssetSeason'),
'episode_number': asset.get('mediaAssetIndexForAssetType'),
'duration': asset.get('mediaAssetDurationInSeconds'),
'upload_date': unified_strdate(asset.get('created')),
'formats': formats,
'subtitles': subtitles,
})
return self.playlist_result(entries, playlist_id=playlist_id)

View File

@ -1,100 +0,0 @@
import json
from .common import InfoExtractor
from ..networking import PUTRequest
from ..networking.exceptions import HTTPError
from ..utils import ExtractorError, clean_html, int_or_none
class PlayPlusTVIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?playplus\.(?:com|tv)/VOD/(?P<project_id>[0-9]+)/(?P<id>[0-9a-f]{32})'
_TEST = {
'url': 'https://www.playplus.tv/VOD/7572/db8d274a5163424e967f35a30ddafb8e',
'md5': 'd078cb89d7ab6b9df37ce23c647aef72',
'info_dict': {
'id': 'db8d274a5163424e967f35a30ddafb8e',
'ext': 'mp4',
'title': 'Capítulo 179 - Final',
'description': 'md5:01085d62d8033a1e34121d3c3cabc838',
'timestamp': 1529992740,
'upload_date': '20180626',
},
'skip': 'Requires account credential',
}
_NETRC_MACHINE = 'playplustv'
_GEO_COUNTRIES = ['BR']
_token = None
_profile_id = None
def _call_api(self, resource, video_id=None, query=None):
return self._download_json('https://api.playplus.tv/api/media/v2/get' + resource, video_id, headers={
'Authorization': 'Bearer ' + self._token,
}, query=query)
def _perform_login(self, username, password):
req = PUTRequest(
'https://api.playplus.tv/api/web/login', json.dumps({
'email': username,
'password': password,
}).encode(), {
'Content-Type': 'application/json; charset=utf-8',
})
try:
self._token = self._download_json(req, None)['token']
except ExtractorError as e:
if isinstance(e.cause, HTTPError) and e.cause.status == 401:
raise ExtractorError(self._parse_json(
e.cause.response.read(), None)['errorMessage'], expected=True)
raise
self._profile = self._call_api('Profiles')['list'][0]['_id']
def _real_initialize(self):
if not self._token:
self.raise_login_required(method='password')
def _real_extract(self, url):
project_id, media_id = self._match_valid_url(url).groups()
media = self._call_api(
'Media', media_id, {
'profileId': self._profile,
'projectId': project_id,
'mediaId': media_id,
})['obj']
title = media['title']
formats = []
for f in media.get('files', []):
f_url = f.get('url')
if not f_url:
continue
file_info = f.get('fileInfo') or {}
formats.append({
'url': f_url,
'width': int_or_none(file_info.get('width')),
'height': int_or_none(file_info.get('height')),
})
thumbnails = []
for thumb in media.get('thumbs', []):
thumb_url = thumb.get('url')
if not thumb_url:
continue
thumbnails.append({
'url': thumb_url,
'width': int_or_none(thumb.get('width')),
'height': int_or_none(thumb.get('height')),
})
return {
'id': media_id,
'title': title,
'formats': formats,
'thumbnails': thumbnails,
'description': clean_html(media.get('description')) or media.get('shortDescription'),
'timestamp': int_or_none(media.get('publishDate'), 1000),
'view_count': int_or_none(media.get('numberOfViews')),
'comment_count': int_or_none(media.get('numberOfComments')),
'tags': media.get('tags'),
}

View File

@ -1,79 +0,0 @@
from .common import InfoExtractor
from ..utils import (
dict_get,
float_or_none,
)
class PlaywireIE(InfoExtractor):
_VALID_URL = r'https?://(?:config|cdn)\.playwire\.com(?:/v2)?/(?P<publisher_id>\d+)/(?:videos/v2|embed|config)/(?P<id>\d+)'
_EMBED_REGEX = [r'<script[^>]+data-config=(["\'])(?P<url>(?:https?:)?//config\.playwire\.com/.+?)\1']
_TESTS = [{
'url': 'http://config.playwire.com/14907/videos/v2/3353705/player.json',
'md5': 'e6398701e3595888125729eaa2329ed9',
'info_dict': {
'id': '3353705',
'ext': 'mp4',
'title': 'S04_RM_UCL_Rus',
'thumbnail': r're:^https?://.*\.png$',
'duration': 145.94,
},
'skip': 'Invalid URL',
}, {
# m3u8 in f4m
'url': 'http://config.playwire.com/21772/videos/v2/4840492/zeus.json',
'info_dict': {
'id': '4840492',
'ext': 'mp4',
'title': 'ITV EL SHOW FULL',
},
'skip': 'Invalid URL',
}, {
# Multiple resolutions while bitrates missing
'url': 'http://cdn.playwire.com/11625/embed/85228.html',
'only_matching': True,
}, {
'url': 'http://config.playwire.com/12421/videos/v2/3389892/zeus.json',
'only_matching': True,
}, {
'url': 'http://cdn.playwire.com/v2/12342/config/1532636.json',
'only_matching': True,
}]
_WEBPAGE_TESTS = [{
'url': 'https://www.cinemablend.com/new/First-Joe-Dirt-2-Trailer-Teaser-Stupid-Greatness-70874.html',
'info_dict': {
'id': '3519514',
'ext': 'mp4',
'title': 'Joe Dirt 2 Beautiful Loser Teaser Trailer',
},
'skip': 'Site no longer embeds Playwire',
}]
def _real_extract(self, url):
mobj = self._match_valid_url(url)
publisher_id, video_id = mobj.group('publisher_id'), mobj.group('id')
player = self._download_json(
f'http://config.playwire.com/{publisher_id}/videos/v2/{video_id}/zeus.json',
video_id)
title = player['settings']['title']
duration = float_or_none(player.get('duration'), 1000)
content = player['content']
thumbnail = content.get('poster')
src = content['media']['f4m']
formats = self._extract_f4m_formats(src, video_id, m3u8_id='hls')
for a_format in formats:
if not dict_get(a_format, ['tbr', 'width', 'height']):
a_format['quality'] = 1 if '-hd.' in a_format['url'] else 0
return {
'id': video_id,
'title': title,
'thumbnail': thumbnail,
'duration': duration,
'formats': formats,
}

Some files were not shown because too many files have changed in this diff Show More