mirror of
https://github.com/yt-dlp/yt-dlp.git
synced 2026-06-20 09:44:23 +00:00
Compare commits
32 Commits
acf8ab7a6e
...
cb309b3293
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb309b3293 | ||
|
|
e47691215f | ||
|
|
a541df1ea5 | ||
|
|
7f7bdc974d | ||
|
|
821bef0f00 | ||
|
|
25056f0d2d | ||
|
|
2726572520 | ||
|
|
e578e265f7 | ||
|
|
3ba1534fa3 | ||
|
|
e85da3b985 | ||
|
|
a6791415e0 | ||
|
|
98c0beab97 | ||
|
|
3d1f8a4a0d | ||
|
|
aaa1c78956 | ||
|
|
0d8460b407 | ||
|
|
519a662aa2 | ||
|
|
1a1481e89b | ||
|
|
a75d66ae2c | ||
|
|
174afac7e3 | ||
|
|
7edb5ee870 | ||
|
|
37a8c6f42b | ||
|
|
83564f85db | ||
|
|
618b5e446c | ||
|
|
bc90c36b13 | ||
|
|
70050a6262 | ||
|
|
72ac620515 | ||
|
|
59bba1be7b | ||
|
|
10b54835cf | ||
|
|
1e4668e9df | ||
|
|
5faffa999f | ||
|
|
7aac95eae6 | ||
|
|
7fdc46d016 |
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
@ -195,7 +195,7 @@ jobs:
|
||||
UPDATE_TO: yt-dlp/yt-dlp@2025.09.05
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
fetch-depth: 0 # Needed for changelog
|
||||
persist-credentials: false
|
||||
@ -259,13 +259,13 @@ jobs:
|
||||
SKIP_ONEFILE_BUILD: ${{ (!matrix.onefile && '1') || '' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up QEMU
|
||||
if: matrix.qemu_platform
|
||||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
|
||||
with:
|
||||
image: tonistiigi/binfmt:qemu-v10.0.4-56@sha256:30cc9a4d03765acac9be2ed0afc23af1ad018aed2c28ea4be8c2eb9afe03fbd1
|
||||
cache-image: false
|
||||
@ -312,7 +312,7 @@ jobs:
|
||||
UPDATE_TO: yt-dlp/yt-dlp@2025.09.05
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@ -438,7 +438,7 @@ jobs:
|
||||
UPDATE_TO: yt-dlp/yt-dlp@2025.09.05
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
5
.github/workflows/challenge-tests.yml
vendored
5
.github/workflows/challenge-tests.yml
vendored
@ -36,17 +36,18 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
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:
|
||||
QJS_VERSION: '2025-04-26' # Earliest version with rope strings
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
allow-prereleases: true
|
||||
- name: Install Deno
|
||||
uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4
|
||||
with:
|
||||
|
||||
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
@ -31,17 +31,17 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
||||
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: none
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
||||
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
7
.github/workflows/core.yml
vendored
7
.github/workflows/core.yml
vendored
@ -45,7 +45,7 @@ jobs:
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
# 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:
|
||||
# atleast one of each CPython/PyPy tests must be in windows
|
||||
- os: windows-latest
|
||||
@ -58,11 +58,13 @@ jobs:
|
||||
python-version: '3.13'
|
||||
- os: windows-latest
|
||||
python-version: '3.14'
|
||||
- os: windows-latest
|
||||
python-version: '3.15'
|
||||
- os: windows-latest
|
||||
python-version: pypy-3.11
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
@ -71,6 +73,7 @@ jobs:
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
allow-prereleases: true
|
||||
|
||||
- name: Install test requirements (cpython)
|
||||
if: ${{ !startsWith(matrix.python-version, 'pypy') }}
|
||||
|
||||
4
.github/workflows/quick-test.yml
vendored
4
.github/workflows/quick-test.yml
vendored
@ -21,7 +21,7 @@ jobs:
|
||||
contents: read
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python 3.10
|
||||
@ -48,7 +48,7 @@ jobs:
|
||||
contents: read
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
|
||||
2
.github/workflows/release-nightly.yml
vendored
2
.github/workflows/release-nightly.yml
vendored
@ -16,7 +16,7 @@ jobs:
|
||||
outputs:
|
||||
commit: ${{ steps.check_for_new_commits.outputs.commit }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@ -84,7 +84,7 @@ jobs:
|
||||
head_sha: ${{ steps.get_target.outputs.head_sha }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: true # Needed to git-push the release commit
|
||||
@ -174,7 +174,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
fetch-depth: 0 # Needed for changelog
|
||||
persist-credentials: false
|
||||
@ -240,7 +240,7 @@ jobs:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
HEAD_SHA: ${{ needs.prepare.outputs.head_sha }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: true # Needed to git-push the release tag
|
||||
|
||||
4
.github/workflows/test-workflows.yml
vendored
4
.github/workflows/test-workflows.yml
vendored
@ -26,7 +26,7 @@ jobs:
|
||||
contents: read
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@ -78,7 +78,7 @@ jobs:
|
||||
actions: read # Needed by zizmorcore/zizmor-action if repository is private
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
2
.github/workflows/wiki.yml
vendored
2
.github/workflows/wiki.yml
vendored
@ -18,7 +18,7 @@ jobs:
|
||||
contents: write # Needed to git-push to the wiki
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
repository: yt-dlp/yt-dlp-wiki
|
||||
ref: master
|
||||
|
||||
@ -877,3 +877,12 @@ syphyr
|
||||
FriederHannenheim
|
||||
Peter-Devine
|
||||
SparseOrnament15
|
||||
AnAwesomGuy
|
||||
antorlovsky
|
||||
dlp-bot
|
||||
HarmfulBreeze
|
||||
jdesgats
|
||||
MemoKing34
|
||||
Suntooth
|
||||
Ventriduct
|
||||
vpertys
|
||||
|
||||
101
Changelog.md
101
Changelog.md
@ -4,6 +4,107 @@
|
||||
# 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
|
||||
|
||||
#### Extractor changes
|
||||
|
||||
47
README.md
47
README.md
@ -3,15 +3,12 @@
|
||||
|
||||
[](#readme)
|
||||
|
||||
[](#installation "Installation")
|
||||
[](#installation "Installation")
|
||||
[](https://github.com/yt-dlp/yt-dlp/blob/master/pyproject.toml "Python Version")
|
||||
[](https://pypi.org/project/yt-dlp "PyPI")
|
||||
[](Maintainers.md#maintainers "Donate")
|
||||
[](https://discord.gg/H5MNcFW63r "Discord")
|
||||
[](supportedsites.md "Supported Sites")
|
||||
[](LICENSE "License")
|
||||
[](https://github.com/yt-dlp/yt-dlp/actions "CI Status")
|
||||
[]([#](https://discord.gg/H5MNcFW63r "Discord")
|
||||
[](LICENSE "License")
|
||||
[](https://github.com/yt-dlp/yt-dlp/commits "Commit History")
|
||||
[](https://github.com/yt-dlp/yt-dlp/pulse/monthly "Last activity")
|
||||
|
||||
</div>
|
||||
<!-- MANPAGE: END EXCLUDED SECTION -->
|
||||
@ -497,7 +494,7 @@ Tip: Use `CTRL`+`F` (or `Command`+`F`) to search by keywords
|
||||
--max-filesize SIZE Abort download if filesize is larger than
|
||||
SIZE, e.g. 50k or 44.6M
|
||||
--date DATE Download only videos uploaded on this date.
|
||||
The date can be "YYYYMMDD" or in the format
|
||||
The date can be "YYYYMMDD" or in the format
|
||||
[now|today|yesterday][-N[day|week|month|year]].
|
||||
E.g. "--date today-2weeks" downloads only
|
||||
videos uploaded on the same day two weeks ago
|
||||
@ -834,8 +831,7 @@ Tip: Use `CTRL`+`F` (or `Command`+`F`) to search by keywords
|
||||
renegotiation
|
||||
--no-check-certificates Suppress HTTPS certificate validation
|
||||
--prefer-insecure Use an unencrypted connection to retrieve
|
||||
information about the video (Currently
|
||||
supported only for YouTube)
|
||||
information about the video
|
||||
--add-headers FIELD:VALUE Specify a custom HTTP header and its value,
|
||||
separated by a colon ":". You can use this
|
||||
option multiple times
|
||||
@ -1053,10 +1049,14 @@ Tip: Use `CTRL`+`F` (or `Command`+`F`) to search by keywords
|
||||
that of --use-postprocessor (default:
|
||||
after_move). The same syntax as the output
|
||||
template can be used to pass any field as
|
||||
arguments to the command. If no fields are
|
||||
passed, %(filepath,_filename|)q is appended
|
||||
to the end of the command. This option can
|
||||
be used multiple times
|
||||
arguments to the command; however, for
|
||||
security reasons the only allowed
|
||||
conversions are: "i"/"d" (signed integer
|
||||
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
|
||||
--convert-subs FORMAT Convert the subtitles to another format
|
||||
(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
|
||||
|
||||
* **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 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
|
||||
* `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 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~~
|
||||
* 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 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
|
||||
* (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 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 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.
|
||||
* 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.
|
||||
@ -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:
|
||||
|
||||
* `--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-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-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,-allow-unsafe-exec-expansion`
|
||||
* `--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 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
|
||||
>
|
||||
> **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
|
||||
|
||||
|
||||
@ -81,11 +81,11 @@ tomli==2.4.1 ; python_full_version < '3.11' \
|
||||
# via
|
||||
# build
|
||||
# hatchling
|
||||
trove-classifiers==2026.5.7.17 \
|
||||
--hash=sha256:5ec0800de5e2ddbd7c663cb4c0c15328f132dc168813897c18866c5c7b93db33 \
|
||||
--hash=sha256:a04a48f8f0a787cb996514d3969ac7608aa3c60cb15d073c1e02801e60533e80
|
||||
trove-classifiers==2026.5.22.10 \
|
||||
--hash=sha256:01fe864225726e03efb843827ecabfe319fc4dee8dd66d65b8996cb09be46e2c \
|
||||
--hash=sha256:5477e9974e91904fb2cfa4a7581ab6e2f30c2c38d847fd00ed866080748101d5
|
||||
# via hatchling
|
||||
zipp==3.23.1 ; python_full_version < '3.10.2' \
|
||||
--hash=sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc \
|
||||
--hash=sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110
|
||||
zipp==4.1.0 ; python_full_version < '3.10.2' \
|
||||
--hash=sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f \
|
||||
--hash=sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602
|
||||
# via importlib-metadata
|
||||
|
||||
@ -68,9 +68,9 @@ brotlicffi==1.2.0.1 ; implementation_name != 'cpython' \
|
||||
--hash=sha256:e015af99584c6db1490a69a210c765953e473e63adc2d891ac3062a737c9e851 \
|
||||
--hash=sha256:f2a5575653b0672638ba039b82fda56854934d7a6a24d4b8b5033f73ab43cbc1
|
||||
# via yt-dlp
|
||||
certifi==2026.4.22 \
|
||||
--hash=sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a \
|
||||
--hash=sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580
|
||||
certifi==2026.5.20 \
|
||||
--hash=sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897 \
|
||||
--hash=sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d
|
||||
# via
|
||||
# curl-cffi
|
||||
# requests
|
||||
@ -274,9 +274,9 @@ curl-cffi==0.15.0 ; implementation_name == 'cpython' \
|
||||
--hash=sha256:bda66404010e9ed743b1b83c20c86f24fe21a9a6873e17479d6e67e29d8ded28 \
|
||||
--hash=sha256:ea0c67652bf6893d34ee0f82c944f37e488f6147e9421bef1771cc6545b02ded
|
||||
# via yt-dlp
|
||||
idna==3.15 \
|
||||
--hash=sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8 \
|
||||
--hash=sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc
|
||||
idna==3.17 \
|
||||
--hash=sha256:466e48829084efe2548012b855df21540b96f2e20e51bd124c851536556a592c \
|
||||
--hash=sha256:5eb0cb53bc467c12eadcf6de83163ad8527cec9416f44b9b61b19caedad2b87f
|
||||
# via requests
|
||||
markdown-it-py==4.2.0 ; implementation_name == 'cpython' \
|
||||
--hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \
|
||||
|
||||
@ -68,9 +68,9 @@ brotlicffi==1.2.0.1 ; implementation_name != 'cpython' \
|
||||
--hash=sha256:e015af99584c6db1490a69a210c765953e473e63adc2d891ac3062a737c9e851 \
|
||||
--hash=sha256:f2a5575653b0672638ba039b82fda56854934d7a6a24d4b8b5033f73ab43cbc1
|
||||
# via yt-dlp
|
||||
certifi==2026.4.22 \
|
||||
--hash=sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a \
|
||||
--hash=sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580
|
||||
certifi==2026.5.20 \
|
||||
--hash=sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897 \
|
||||
--hash=sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d
|
||||
# via
|
||||
# requests
|
||||
# yt-dlp
|
||||
@ -174,9 +174,9 @@ charset-normalizer==3.4.7 \
|
||||
--hash=sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79 \
|
||||
--hash=sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464
|
||||
# via requests
|
||||
idna==3.15 \
|
||||
--hash=sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8 \
|
||||
--hash=sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc
|
||||
idna==3.17 \
|
||||
--hash=sha256:466e48829084efe2548012b855df21540b96f2e20e51bd124c851536556a592c \
|
||||
--hash=sha256:5eb0cb53bc467c12eadcf6de83163ad8527cec9416f44b9b61b19caedad2b87f
|
||||
# via requests
|
||||
mutagen==1.47.0 \
|
||||
--hash=sha256:719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99 \
|
||||
|
||||
@ -41,9 +41,9 @@ packaging==26.2 \
|
||||
# via
|
||||
# pytest
|
||||
# pytest-rerunfailures
|
||||
platformdirs==4.9.6 \
|
||||
--hash=sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a \
|
||||
--hash=sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917
|
||||
platformdirs==4.10.0 \
|
||||
--hash=sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7 \
|
||||
--hash=sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a
|
||||
# via
|
||||
# python-discovery
|
||||
# virtualenv
|
||||
@ -66,12 +66,12 @@ pytest==9.0.3 \
|
||||
--hash=sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9 \
|
||||
--hash=sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c
|
||||
# via pytest-rerunfailures
|
||||
pytest-rerunfailures==16.2 \
|
||||
--hash=sha256:5f5a32f15674a3d54f7598388fcd3cc1bc5c37284731a4704a44485dcdda5e23 \
|
||||
--hash=sha256:c22a53d2827becc76f057d4ded123c0e726523f2f0e5f0bb4efb31fd59e1f14e
|
||||
python-discovery==1.3.1 \
|
||||
--hash=sha256:62f6db28064c9613e7ca76cb3f00c38c839a07c31c00dfe7ed0986493d2150a6 \
|
||||
--hash=sha256:ed188687ebb3b82c01a17cd5ac62fc94d9f6487a7f1a0f9dfe89753fec91039c
|
||||
pytest-rerunfailures==16.3 \
|
||||
--hash=sha256:37c9b1231c8083e9f4e724f50f7a21241822f9516c15c700ebbf218d6452355c \
|
||||
--hash=sha256:6bdfb8ffb46c46072e6c16bdedee38b6c13eac620d9415ed5b63152cbf283170
|
||||
python-discovery==1.4.0 \
|
||||
--hash=sha256:26ed78d703e234879a66244c7d4114563fb13ec5cd30a2d1357e5fb4850782da \
|
||||
--hash=sha256:eb8bc7daad3c226c147e45bb4e970a1feb1bf4048ee178e6db59e197b8010ce3
|
||||
# via virtualenv
|
||||
pyyaml==6.0.3 \
|
||||
--hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \
|
||||
@ -132,25 +132,25 @@ pyyaml==6.0.3 \
|
||||
--hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \
|
||||
--hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0
|
||||
# via pre-commit
|
||||
ruff==0.15.13 \
|
||||
--hash=sha256:1c26d2f66163deeb6e08d8b39fbbe983ce3c71cea06a6d7591cfd1421793c629 \
|
||||
--hash=sha256:2471da9bd1068c8c064b5fd9c0c4b6dddffd6369cb1cd68b29993b1709ff1b21 \
|
||||
--hash=sha256:2e2e39bff6c341f4b577a21b801326fab0b11847f48fcaa83f00a113c9b3cb55 \
|
||||
--hash=sha256:4044f94208b3b05ba0fc4a4abd0558cf4d6459bd18325eead7fd8cc66f909b41 \
|
||||
--hash=sha256:444b580fc72fd6887e650acd3e575e18cdc79dbcf42fb4030b491057921f61f8 \
|
||||
--hash=sha256:6590d009e7cb7ebf36f83dbdd44a3fa48a0994ff6f1cdc1b08006abe58f98dc7 \
|
||||
--hash=sha256:7064884d442b7d477b4e7473d12da7f08851d2b1982763c5d3f388a19468a1a4 \
|
||||
--hash=sha256:768494eb08b9cee54e2fd27969966f74db5a57f6eaa7a90fcb3306af34dfc4bd \
|
||||
--hash=sha256:7ef823f817fcd191dc934e984be9cf4094f808effa16f2542ad8e821ba02bbf2 \
|
||||
--hash=sha256:8cb74dd33bb2f6613faf7fc03b660053b5ac4f80e706d5788c6335e2a8048d51 \
|
||||
--hash=sha256:9dbd6f94b434f896308e4d57fb7bfde0d02b99f7a64b3bdab0fdfa6a864203a5 \
|
||||
--hash=sha256:ae9c17e5eb4430c154e76abc25d79a318190f5a997f38fb6b114416c5319ffc9 \
|
||||
--hash=sha256:bf3259f3be4d181bda591da5db2571aed6853c6a048157756448020bc6c5cd22 \
|
||||
--hash=sha256:cc411dfebe5eebe55ce041c6ae080eb7668955e866daa2fbb16692a784f1c4ca \
|
||||
--hash=sha256:e8d9a8e08013542e94d3220bc5b62cc3e5ef87c5f74bff367d3fac14fab013e6 \
|
||||
--hash=sha256:f345a13937bd7f09f6f5d19fa0721b0c103e00e7f62bc67089a8e5e037719e0b \
|
||||
--hash=sha256:f9d89f17f7ba7fb2ed42921f0df75da797a9a5d71bc39049e2c687cf2baf44b7 \
|
||||
--hash=sha256:fb75f9a3a7e42ffe117d734494e6c5e5cb3565d66e12612cb63d0e572a41a5b6
|
||||
ruff==0.15.15 \
|
||||
--hash=sha256:2728b93d7b23a603ea2c0ac6eb73d760bd38ec9de35f35fb41e18f7a3fee7622 \
|
||||
--hash=sha256:29428ea79694afbe756d45fd59b36f22b6b020dc0443cf7de0173046236964b9 \
|
||||
--hash=sha256:3c8ceca6792f38196b8f589bc92eccd03eef286602da92e5dc05cc42ef6441b7 \
|
||||
--hash=sha256:3cdb1679e06a1f6b47bc384714ae96f6e2fb65ca441eb78c43d2ca554176ce1f \
|
||||
--hash=sha256:48decfa11d740de4889de623be1463308346312f2409a56e24aa280c86162dc4 \
|
||||
--hash=sha256:587a6278ed42059191c1a466e490bd7930fb50bd2e255398bc29616c895a61cb \
|
||||
--hash=sha256:7614ee79c69788cf6cedd568069ade9cecc22a1ad20494efe8d0c9ebb4b622d4 \
|
||||
--hash=sha256:77d955a431430c66f72dd94e379ad38a16daea3d25094872ac4edf9e797be530 \
|
||||
--hash=sha256:7aa77465b8ecaf1a27bea098d696f7fed5e1eccbd10b321b682d6de586ae5627 \
|
||||
--hash=sha256:8df0323902e15e24bc4bf246da830573d3cf3352bd0b9a164eab335d111ff4a4 \
|
||||
--hash=sha256:a5015088452ca0081387063649ec67f06d3d1d6b8b936a1f836b5e9657ecd48c \
|
||||
--hash=sha256:ac5a646d1f6a7dadd5d50842dae2c1f9862ac887ef5d1b1375e02def791fde6e \
|
||||
--hash=sha256:b8dff018130b46d8e5bf0f926ef6b60cf871d6d5ae45fc9334e09632daa741d6 \
|
||||
--hash=sha256:be582fcc0db438902c7792b08d6ddf6c9b9e21addaa10092c2c741cfb09e5a45 \
|
||||
--hash=sha256:cf93e5388f412e1b108b1f8b34a6e036b70fe8aff89393befad96fe48670311b \
|
||||
--hash=sha256:db5bd4d802415cca656dc1616070b725952d6ae95eb5d4831e49fbd94a38f75f \
|
||||
--hash=sha256:df0c1c084f5f4be9812f61518a45c440d3c30d69ce4bf6c5270e66d38338f02a \
|
||||
--hash=sha256:f5294aab6356c81600fcdea3a62bb1b924dfd5e91767c12318d3f68f86af57cd
|
||||
tomli==2.4.1 ; python_full_version < '3.11' \
|
||||
--hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \
|
||||
--hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \
|
||||
@ -208,7 +208,7 @@ typing-extensions==4.15.0 ; python_full_version < '3.11' \
|
||||
# via
|
||||
# exceptiongroup
|
||||
# virtualenv
|
||||
virtualenv==21.3.3 \
|
||||
--hash=sha256:7d5987d8369e098e41406efb780a3d4ca79280097293899e351a6407ee153ab3 \
|
||||
--hash=sha256:f5bda277e553b1c2b3c1a8debfc30496e1288cc93ce6b7b71b3280047e317328
|
||||
virtualenv==21.4.2 \
|
||||
--hash=sha256:38e6ee0a555615c0ea9da2ac7e9998fe8dc3b911dd33ad8eaad2020957653b0c \
|
||||
--hash=sha256:854210ca524a1a4d0d744734f4acbc721c3ffe163b85bbf5d56d14d5ae2f0fae
|
||||
# via pre-commit
|
||||
|
||||
@ -68,9 +68,9 @@ brotlicffi==1.2.0.1 ; implementation_name != 'cpython' \
|
||||
--hash=sha256:e015af99584c6db1490a69a210c765953e473e63adc2d891ac3062a737c9e851 \
|
||||
--hash=sha256:f2a5575653b0672638ba039b82fda56854934d7a6a24d4b8b5033f73ab43cbc1
|
||||
# via yt-dlp
|
||||
certifi==2026.4.22 \
|
||||
--hash=sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a \
|
||||
--hash=sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580
|
||||
certifi==2026.5.20 \
|
||||
--hash=sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897 \
|
||||
--hash=sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d
|
||||
# via
|
||||
# curl-cffi
|
||||
# requests
|
||||
@ -326,9 +326,9 @@ curl-cffi==0.15.0 ; implementation_name == 'cpython' \
|
||||
--hash=sha256:bda66404010e9ed743b1b83c20c86f24fe21a9a6873e17479d6e67e29d8ded28 \
|
||||
--hash=sha256:ea0c67652bf6893d34ee0f82c944f37e488f6147e9421bef1771cc6545b02ded
|
||||
# via yt-dlp
|
||||
idna==3.15 \
|
||||
--hash=sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8 \
|
||||
--hash=sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc
|
||||
idna==3.17 \
|
||||
--hash=sha256:466e48829084efe2548012b855df21540b96f2e20e51bd124c851536556a592c \
|
||||
--hash=sha256:5eb0cb53bc467c12eadcf6de83163ad8527cec9416f44b9b61b19caedad2b87f
|
||||
# via requests
|
||||
jeepney==0.9.0 \
|
||||
--hash=sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683 \
|
||||
|
||||
@ -74,9 +74,9 @@ brotlicffi==1.2.0.1 ; implementation_name != 'cpython' \
|
||||
--hash=sha256:e015af99584c6db1490a69a210c765953e473e63adc2d891ac3062a737c9e851 \
|
||||
--hash=sha256:f2a5575653b0672638ba039b82fda56854934d7a6a24d4b8b5033f73ab43cbc1
|
||||
# via yt-dlp
|
||||
certifi==2026.4.22 \
|
||||
--hash=sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a \
|
||||
--hash=sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580
|
||||
certifi==2026.5.20 \
|
||||
--hash=sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897 \
|
||||
--hash=sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d
|
||||
# via
|
||||
# curl-cffi
|
||||
# requests
|
||||
@ -184,9 +184,9 @@ charset-normalizer==3.4.7 \
|
||||
delocate==0.13.0 ; sys_platform == 'darwin' \
|
||||
--hash=sha256:11f7596f88984c33f76b27fe2eea7637d1ce369a9e0b6737bbc706b6426e862c \
|
||||
--hash=sha256:a93e67a9f56ee01a3f7096a042231d4ac37fecac873cd5ea34ea2b4f43a8fa13
|
||||
idna==3.15 \
|
||||
--hash=sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8 \
|
||||
--hash=sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc
|
||||
idna==3.17 \
|
||||
--hash=sha256:466e48829084efe2548012b855df21540b96f2e20e51bd124c851536556a592c \
|
||||
--hash=sha256:5eb0cb53bc467c12eadcf6de83163ad8527cec9416f44b9b61b19caedad2b87f
|
||||
# via requests
|
||||
macholib==1.16.4 ; sys_platform == 'darwin' \
|
||||
--hash=sha256:da1a3fa8266e30f0ce7e97c6a54eefaae8edd1e5f86f3eb8b95457cae90265ea \
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
pip==26.1.1 \
|
||||
--hash=sha256:99cb1c2899893b075ff56e4ed0af55669a955b49ad7fb8d8603ecdaf4ed653fb \
|
||||
--hash=sha256:d36762751d156a4ee895de8af39aa0abeeeb577f93a2eca6ab62467bbf0f8a78
|
||||
pip==26.1.2 \
|
||||
--hash=sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab \
|
||||
--hash=sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605
|
||||
|
||||
@ -5,25 +5,25 @@ pycodestyle==2.14.0 \
|
||||
--hash=sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783 \
|
||||
--hash=sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d
|
||||
# via autopep8
|
||||
ruff==0.15.13 \
|
||||
--hash=sha256:1c26d2f66163deeb6e08d8b39fbbe983ce3c71cea06a6d7591cfd1421793c629 \
|
||||
--hash=sha256:2471da9bd1068c8c064b5fd9c0c4b6dddffd6369cb1cd68b29993b1709ff1b21 \
|
||||
--hash=sha256:2e2e39bff6c341f4b577a21b801326fab0b11847f48fcaa83f00a113c9b3cb55 \
|
||||
--hash=sha256:4044f94208b3b05ba0fc4a4abd0558cf4d6459bd18325eead7fd8cc66f909b41 \
|
||||
--hash=sha256:444b580fc72fd6887e650acd3e575e18cdc79dbcf42fb4030b491057921f61f8 \
|
||||
--hash=sha256:6590d009e7cb7ebf36f83dbdd44a3fa48a0994ff6f1cdc1b08006abe58f98dc7 \
|
||||
--hash=sha256:7064884d442b7d477b4e7473d12da7f08851d2b1982763c5d3f388a19468a1a4 \
|
||||
--hash=sha256:768494eb08b9cee54e2fd27969966f74db5a57f6eaa7a90fcb3306af34dfc4bd \
|
||||
--hash=sha256:7ef823f817fcd191dc934e984be9cf4094f808effa16f2542ad8e821ba02bbf2 \
|
||||
--hash=sha256:8cb74dd33bb2f6613faf7fc03b660053b5ac4f80e706d5788c6335e2a8048d51 \
|
||||
--hash=sha256:9dbd6f94b434f896308e4d57fb7bfde0d02b99f7a64b3bdab0fdfa6a864203a5 \
|
||||
--hash=sha256:ae9c17e5eb4430c154e76abc25d79a318190f5a997f38fb6b114416c5319ffc9 \
|
||||
--hash=sha256:bf3259f3be4d181bda591da5db2571aed6853c6a048157756448020bc6c5cd22 \
|
||||
--hash=sha256:cc411dfebe5eebe55ce041c6ae080eb7668955e866daa2fbb16692a784f1c4ca \
|
||||
--hash=sha256:e8d9a8e08013542e94d3220bc5b62cc3e5ef87c5f74bff367d3fac14fab013e6 \
|
||||
--hash=sha256:f345a13937bd7f09f6f5d19fa0721b0c103e00e7f62bc67089a8e5e037719e0b \
|
||||
--hash=sha256:f9d89f17f7ba7fb2ed42921f0df75da797a9a5d71bc39049e2c687cf2baf44b7 \
|
||||
--hash=sha256:fb75f9a3a7e42ffe117d734494e6c5e5cb3565d66e12612cb63d0e572a41a5b6
|
||||
ruff==0.15.15 \
|
||||
--hash=sha256:2728b93d7b23a603ea2c0ac6eb73d760bd38ec9de35f35fb41e18f7a3fee7622 \
|
||||
--hash=sha256:29428ea79694afbe756d45fd59b36f22b6b020dc0443cf7de0173046236964b9 \
|
||||
--hash=sha256:3c8ceca6792f38196b8f589bc92eccd03eef286602da92e5dc05cc42ef6441b7 \
|
||||
--hash=sha256:3cdb1679e06a1f6b47bc384714ae96f6e2fb65ca441eb78c43d2ca554176ce1f \
|
||||
--hash=sha256:48decfa11d740de4889de623be1463308346312f2409a56e24aa280c86162dc4 \
|
||||
--hash=sha256:587a6278ed42059191c1a466e490bd7930fb50bd2e255398bc29616c895a61cb \
|
||||
--hash=sha256:7614ee79c69788cf6cedd568069ade9cecc22a1ad20494efe8d0c9ebb4b622d4 \
|
||||
--hash=sha256:77d955a431430c66f72dd94e379ad38a16daea3d25094872ac4edf9e797be530 \
|
||||
--hash=sha256:7aa77465b8ecaf1a27bea098d696f7fed5e1eccbd10b321b682d6de586ae5627 \
|
||||
--hash=sha256:8df0323902e15e24bc4bf246da830573d3cf3352bd0b9a164eab335d111ff4a4 \
|
||||
--hash=sha256:a5015088452ca0081387063649ec67f06d3d1d6b8b936a1f836b5e9657ecd48c \
|
||||
--hash=sha256:ac5a646d1f6a7dadd5d50842dae2c1f9862ac887ef5d1b1375e02def791fde6e \
|
||||
--hash=sha256:b8dff018130b46d8e5bf0f926ef6b60cf871d6d5ae45fc9334e09632daa741d6 \
|
||||
--hash=sha256:be582fcc0db438902c7792b08d6ddf6c9b9e21addaa10092c2c741cfb09e5a45 \
|
||||
--hash=sha256:cf93e5388f412e1b108b1f8b34a6e036b70fe8aff89393befad96fe48670311b \
|
||||
--hash=sha256:db5bd4d802415cca656dc1616070b725952d6ae95eb5d4831e49fbd94a38f75f \
|
||||
--hash=sha256:df0c1c084f5f4be9812f61518a45c440d3c30d69ce4bf6c5270e66d38338f02a \
|
||||
--hash=sha256:f5294aab6356c81600fcdea3a62bb1b924dfd5e91767c12318d3f68f86af57cd
|
||||
tomli==2.4.1 ; python_full_version < '3.11' \
|
||||
--hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \
|
||||
--hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \
|
||||
|
||||
@ -28,9 +28,9 @@ pytest==9.0.3 \
|
||||
--hash=sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9 \
|
||||
--hash=sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c
|
||||
# via pytest-rerunfailures
|
||||
pytest-rerunfailures==16.2 \
|
||||
--hash=sha256:5f5a32f15674a3d54f7598388fcd3cc1bc5c37284731a4704a44485dcdda5e23 \
|
||||
--hash=sha256:c22a53d2827becc76f057d4ded123c0e726523f2f0e5f0bb4efb31fd59e1f14e
|
||||
pytest-rerunfailures==16.3 \
|
||||
--hash=sha256:37c9b1231c8083e9f4e724f50f7a21241822f9516c15c700ebbf218d6452355c \
|
||||
--hash=sha256:6bdfb8ffb46c46072e6c16bdedee38b6c13eac620d9415ed5b63152cbf283170
|
||||
tomli==2.4.1 ; python_full_version < '3.11' \
|
||||
--hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \
|
||||
--hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \
|
||||
|
||||
@ -1,20 +1,20 @@
|
||||
uv==0.11.14 \
|
||||
--hash=sha256:078f2e63da89c8fcf6d578f02156045c5990c57d76464aab3f3f798d3fff95cd \
|
||||
--hash=sha256:0ea006a117b586b2681b6dfd9703a540d2ad2a136ec0f48d272767e599cc3dfb \
|
||||
--hash=sha256:29c12a562441fc2d604e6920c558cacce74a55f889468708683a79b35a6e18a1 \
|
||||
--hash=sha256:379e64b236cf55f762a8308d7efe4365d5296ba29f3a4868761bc45b4e915a71 \
|
||||
--hash=sha256:3b0759ca504e48dcd4fafb1a61ef69aeb24c5a60fbf5f504a7873c8db1b24718 \
|
||||
--hash=sha256:6a13e7e064563050c6606b3fd77091d427cdbdc5938b6f134baf8d8ec79bfdb7 \
|
||||
--hash=sha256:78411a883f230a710af19f2ac6e6f0ba8eae90f0e5af4605f923fd367539fff4 \
|
||||
--hash=sha256:78b51b117549ee4db7197ea5ece0848cecd443e464fb9dff9f254cdc1e4ed96f \
|
||||
--hash=sha256:9923da7c63d70de9fe71829503d7e7ebfd6304e804d7232aad5f716e190db25b \
|
||||
--hash=sha256:a1ddbe8a2ab160affc179e9c3a40913b23a08cdf55254e1f3829cc22a51a0d8d \
|
||||
--hash=sha256:b15bf7c146e38d7c938d3a207115d5fdd8ef764fe1f866c225b1bed27e88da1e \
|
||||
--hash=sha256:b384d873d0d18552c7524226125efd3965d921b7134c2f476c333771beb733e1 \
|
||||
--hash=sha256:d5c8f9ea36274ef2f9d24f0522085e280844172e901d9213f66a21b212266706 \
|
||||
--hash=sha256:dcdad43d52c130e3159e84ab1844e04d819d2c4a2495a687d27f80d560a3650e \
|
||||
--hash=sha256:ddda5c5e41097814adac535c74851bae55e8097b9afc79aeae7fcffd8d86c06d \
|
||||
--hash=sha256:e54326703f1eca83a6fd73275e0f398b16b7d3f81531bf58899c2869bc403f6c \
|
||||
--hash=sha256:e84069681c0334e07cbc7f114eb09d7fe1335e1db0297a66dbca80a1b393fe6d \
|
||||
--hash=sha256:f0a8b58b38e984241bca5d7a5a47bf9ffe1ca2ab392a640887db8a04c4a9ec95 \
|
||||
--hash=sha256:f3005a2db1e8d72e125630d4f22ac4ceddb2c033e1f9b94b7f3ea38ebac46dd6
|
||||
uv==0.11.17 \
|
||||
--hash=sha256:036d6e2940afe8b79637530b01b9241d8cfd174b07f1179a1ebbd42409c38ca3 \
|
||||
--hash=sha256:12b701fa32c5be3691759a73956e4462f30fa7b0dfa52ec66cb305bbb6ea4129 \
|
||||
--hash=sha256:1a817eeb3026f27a53d3f4b7855a5105f6787dd192140e201eda4d2b9a11b72e \
|
||||
--hash=sha256:1d1be74deec997db1dda05a7e67541c904d65cbfd72e455d3c0a2a1e4bf2cddf \
|
||||
--hash=sha256:283186700c3e65a4644a73a917232da7d3e4a94d25ea0377a44f5b263fa49577 \
|
||||
--hash=sha256:2ccd5487a4a192bc832ea04c867a26883757db8fdfe88bed85d8129c82f9e505 \
|
||||
--hash=sha256:37c915bfcf86f99c1c5be7c9ed21e0d80624067ba47bc8916a3cb0530bc94d27 \
|
||||
--hash=sha256:44ec1fe3af839f87370dcf0400c0cab917cc1ce697d563e860fc7d9ed72655e7 \
|
||||
--hash=sha256:58c07ffc272c847d29cd98ca5082fa4304a645f87c718ec900e3cca9026bd096 \
|
||||
--hash=sha256:659227cac719b618cc91e02be9e274ad5bd72d74fa278123e6373537e9f28216 \
|
||||
--hash=sha256:6d1a033cc68cabb4141d6c1e3b66ffc6e970b98ba42e210f33270251e0bd8697 \
|
||||
--hash=sha256:8426bfe315564d414cbc5ba5467595dc6348965e19acec742914f47da3ff269f \
|
||||
--hash=sha256:9da839e5a491c9a701d7d327a199cafc76ac27a03ac84fd2a8d4bf32c3af2448 \
|
||||
--hash=sha256:bf8f5ad959583dcd2c4ae445c754a97c05700246ff89259f3fd285c9c20f4c00 \
|
||||
--hash=sha256:ce16892a45134d20165c1ceababe06f3e9ce6a58902db1eff812c8c93626823f \
|
||||
--hash=sha256:e301d844eed9401f0f0351de12c55f1306ca05372acb0f28d35717c8ba663a22 \
|
||||
--hash=sha256:ec004b3c9bf9cb7756067ad1bd0bf64eb843e6fa2edbfbb3135ee152c14cea91 \
|
||||
--hash=sha256:f0bf483c0d9fa14283992d56061b498b9d3d4adebd285af8744dc33f64dadfba \
|
||||
--hash=sha256:f2e44dfbfc7778d0d90edc6738f237c91e5e37e4e3cfe94c8a312cec56a41485
|
||||
|
||||
@ -342,5 +342,30 @@
|
||||
"action": "add",
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
|
||||
@ -650,8 +650,8 @@ def update_requirements(
|
||||
modify_and_write_pyproject(pyproject_text, table_name=EXTRAS_TABLE, table=extras)
|
||||
|
||||
# Generate/upgrade final lockfile that includes pinned extras
|
||||
print(f'Running: uv lock {upgrade_arg}', file=sys.stderr)
|
||||
run_process('uv', 'lock', upgrade_arg, env=env)
|
||||
print('Running: uv lock', file=sys.stderr)
|
||||
run_process('uv', 'lock', env=env)
|
||||
|
||||
# Export bundle requirements; any updates to these are already recorded w/ uv.lock package diff
|
||||
for target_suffix, target in BUNDLE_TARGETS.items():
|
||||
|
||||
@ -69,9 +69,9 @@ deno = [
|
||||
pin = [
|
||||
"brotli==1.2.0 ; implementation_name == 'cpython' and sys_platform != 'ios'",
|
||||
"brotlicffi==1.2.0.1 ; implementation_name != 'cpython'",
|
||||
"certifi==2026.4.22",
|
||||
"certifi==2026.5.20",
|
||||
"charset-normalizer==3.4.7",
|
||||
"idna==3.15",
|
||||
"idna==3.17",
|
||||
"mutagen==1.47.0",
|
||||
"pycryptodomex==3.23.0",
|
||||
"requests==2.34.2",
|
||||
@ -80,7 +80,7 @@ pin = [
|
||||
"yt-dlp-ejs==0.8.0",
|
||||
]
|
||||
pin-curl-cffi = [
|
||||
"certifi==2026.4.22 ; implementation_name == 'cpython'",
|
||||
"certifi==2026.5.20 ; implementation_name == 'cpython'",
|
||||
"cffi==2.0.0 ; implementation_name == 'cpython'",
|
||||
"curl-cffi==0.15.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'",
|
||||
]
|
||||
pin-deno = [
|
||||
"deno==2.7.14",
|
||||
"deno==2.8.1",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
|
||||
@ -22,7 +22,6 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **4tube**
|
||||
- **56.com**
|
||||
- **7plus**
|
||||
- **8tracks**
|
||||
- **9c9media**
|
||||
- **9gag**: 9GAG
|
||||
- **9News**
|
||||
@ -51,7 +50,6 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **aenetworks:show**
|
||||
- **AeonCo**
|
||||
- **agalega:videos**
|
||||
- **AirTV**
|
||||
- **AitubeKZVideo**
|
||||
- **Alibaba**
|
||||
- **AliExpressLive**
|
||||
@ -60,8 +58,6 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **Allstar**
|
||||
- **AllstarProfile**
|
||||
- **AlphaPorno**
|
||||
- **Alsace20TV**
|
||||
- **Alsace20TVEmbed**
|
||||
- **altcensored**
|
||||
- **altcensored:channel**
|
||||
- **Alura**: [*alura*](## "netrc machine")
|
||||
@ -77,7 +73,6 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **AmericasTestKitchen**
|
||||
- **AmericasTestKitchenSeason**
|
||||
- **AmHistoryChannel**
|
||||
- **AnchorFMEpisode**
|
||||
- **anderetijden**: npo.nl, ntr.nl, omroepwnl.nl, zapp.nl and npo3.nl
|
||||
- **Angel**
|
||||
- **AnimalPlanet**
|
||||
@ -90,8 +85,6 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **Aparat**
|
||||
- **apple:music:connect**: Apple Music Connect
|
||||
- **ApplePodcasts**
|
||||
- **appletrailers**
|
||||
- **appletrailers:section**
|
||||
- **archive.org**: archive.org video and audio
|
||||
- **ArcPublishing**
|
||||
- **ARD**
|
||||
@ -111,7 +104,6 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **AsobiStage**: ASOBISTAGE (アソビステージ)
|
||||
- **AtresPlayer**: [*atresplayer*](## "netrc machine")
|
||||
- **AtScaleConfEvent**
|
||||
- **ATVAt**
|
||||
- **AudiMedia**
|
||||
- **AudioBoom**
|
||||
- **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:playlist**: Audius.co playlists
|
||||
- **audius:track**: Audius track ID or API link. Prepend with "audius:"
|
||||
- **AWAAN**
|
||||
- **awaan:live**
|
||||
- **awaan:season**
|
||||
- **awaan:video**
|
||||
- **axs.tv**
|
||||
- **AZMedien**: AZ Medien videos
|
||||
- **BaiduVideo**: 百度视频
|
||||
- **BanBye**
|
||||
@ -148,8 +135,6 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **BBVTVLive**: [*bbvtv*](## "netrc machine")
|
||||
- **BBVTVRecordings**: [*bbvtv*](## "netrc machine")
|
||||
- **BeaconTv**
|
||||
- **BeatBumpPlaylist**
|
||||
- **BeatBumpVideo**
|
||||
- **Beatport**
|
||||
- **Beeg**
|
||||
- **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:series**: BibelTV series playlist
|
||||
- **bibeltv:video**: BibelTV single video
|
||||
- **Bigflix**
|
||||
- **Bigo**
|
||||
- **Bild**: Bild.de
|
||||
- **BiliBili**
|
||||
@ -200,7 +184,6 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **blogger.com**
|
||||
- **Bloomberg**
|
||||
- **Bluesky**
|
||||
- **BokeCC**: CC视频
|
||||
- **BongaCams**
|
||||
- **Boosty**
|
||||
- **BostonGlobe**
|
||||
@ -229,12 +212,8 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **BusinessInsider**
|
||||
- **BuzzFeed**
|
||||
- **BYUtv**: (**Currently broken**)
|
||||
- **CaffeineTV**
|
||||
- **Callin**
|
||||
- **Caltrans**
|
||||
- **CAM4**
|
||||
- **Camdemy**
|
||||
- **CamdemyFolder**
|
||||
- **CamFMEpisode**
|
||||
- **CamFMShow**
|
||||
- **CamModels**
|
||||
@ -282,7 +261,6 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **ciscowebex**: Cisco Webex
|
||||
- **CJSW**
|
||||
- **Clipchamp**
|
||||
- **Clippit**
|
||||
- **ClipRs**: (**Currently broken**)
|
||||
- **CloserToTruth**: (**Currently broken**)
|
||||
- **CloudflareStream**
|
||||
@ -295,7 +273,6 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **ComedyCentral**
|
||||
- **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
|
||||
- **CONtv**
|
||||
- **CookingChannel**
|
||||
- **Corus**
|
||||
- **Coub**
|
||||
@ -370,7 +347,6 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **DouyuTV**: 斗鱼直播
|
||||
- **DPlay**
|
||||
- **DRBonanza**
|
||||
- **Drooble**
|
||||
- **Dropbox**
|
||||
- **Dropout**: [*dropout*](## "netrc machine")
|
||||
- **DropoutSeason**
|
||||
@ -381,8 +357,6 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **drtv:season**
|
||||
- **drtv:series**
|
||||
- **DTube**: (**Currently broken**)
|
||||
- **duboku**: www.duboku.io
|
||||
- **duboku:list**: www.duboku.io entire series
|
||||
- **Dumpert**
|
||||
- **Duoplay**
|
||||
- **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")
|
||||
- **EinsUndEinsTVLive**: [*1und1tv*](## "netrc machine")
|
||||
- **EinsUndEinsTVRecordings**: [*1und1tv*](## "netrc machine")
|
||||
- **eitb.tv**
|
||||
- **ElementorEmbed**
|
||||
- **Elonet**
|
||||
- **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")
|
||||
- **EWETVRecordings**: [*ewetv*](## "netrc machine")
|
||||
- **Expressen**
|
||||
- **EyedoTV**
|
||||
- **facebook**
|
||||
- **facebook:ads**
|
||||
- **facebook:reel**
|
||||
@ -473,7 +445,6 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **FrancaisFacile**
|
||||
- **FranceCulture**
|
||||
- **franceinfo**: franceinfo.fr (formerly francetvinfo.fr)
|
||||
- **FranceInter**
|
||||
- **francetv**
|
||||
- **francetv:site**
|
||||
- **Freesound**
|
||||
@ -483,13 +454,11 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **FrontendMasters**: [*frontendmasters*](## "netrc machine")
|
||||
- **FrontendMastersCourse**: [*frontendmasters*](## "netrc machine")
|
||||
- **FrontendMastersLesson**: [*frontendmasters*](## "netrc machine")
|
||||
- **FujiTVFODPlus7**
|
||||
- **Funk**
|
||||
- **Funker530**
|
||||
- **Fux**
|
||||
- **FuyinTV**
|
||||
- **Gab**
|
||||
- **GabTV**
|
||||
- **Gaia**: [*gaia*](## "netrc machine")
|
||||
- **GameDevTVDashboard**: [*gamedevtv*](## "netrc machine")
|
||||
- **GameJolt**
|
||||
@ -538,14 +507,10 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **Gofile**
|
||||
- **Golem**
|
||||
- **goodgame:stream**
|
||||
- **google:podcasts**
|
||||
- **google:podcasts:feed**
|
||||
- **GoogleDrive**
|
||||
- **GoogleDrive:Folder**
|
||||
- **GoPro**
|
||||
- **Goshgay**
|
||||
- **GoToStage**
|
||||
- **GPUTechConf**
|
||||
- **Graspop**
|
||||
- **Gronkh**
|
||||
- **gronkh:feed**
|
||||
@ -565,7 +530,6 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **history:player**
|
||||
- **history:topic**: History.com Topic
|
||||
- **HitRecord**
|
||||
- **hketv**: 香港教育局教育電視 (HKETV) Educational Television, Hong Kong Educational Bureau
|
||||
- **HollywoodReporter**
|
||||
- **HollywoodReporterPlaylist**
|
||||
- **Holodex**
|
||||
@ -593,7 +557,6 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **IdagioPlaylist**
|
||||
- **IdagioRecording**
|
||||
- **IdagioTrack**
|
||||
- **IdolPlus**
|
||||
- **iflix:episode**
|
||||
- **IflixSeries**
|
||||
- **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**)
|
||||
- **InstagramIOS**: IOS instagram:// URL
|
||||
- **Internazionale**
|
||||
- **InternetVideoArchive**
|
||||
- **InvestigationDiscovery**
|
||||
- **IPrima**: [*iprima*](## "netrc machine")
|
||||
- **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
|
||||
- **ivideon**: Ivideon TV
|
||||
- **Ivoox**
|
||||
- **IVXPlayer**
|
||||
- **iwara**: [*iwara*](## "netrc machine")
|
||||
- **iwara:playlist**: [*iwara*](## "netrc machine")
|
||||
- **iwara:user**: [*iwara*](## "netrc machine")
|
||||
- **Ixigua**
|
||||
- **Izlesene**
|
||||
- **Jamendo**
|
||||
- **JamendoAlbum**
|
||||
- **JeuxVideo**: (**Currently broken**)
|
||||
@ -674,11 +634,9 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **KickStarter**
|
||||
- **Kika**: KiKA.de
|
||||
- **KikaPlaylist**
|
||||
- **kinja:embed**
|
||||
- **KinoPoisk**
|
||||
- **Kommunetv**
|
||||
- **KompasVideo**
|
||||
- **Koo**: (**Currently broken**)
|
||||
- **KrasView**: Красвью (**Currently broken**)
|
||||
- **KTH**
|
||||
- **Ku6**
|
||||
@ -716,7 +674,6 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **Lemonde**
|
||||
- **Lenta**: (**Currently broken**)
|
||||
- **LePlaylist**
|
||||
- **LetvCloud**: 乐视云
|
||||
- **Libsyn**
|
||||
- **life**: Life.ru
|
||||
- **life:embed**
|
||||
@ -730,8 +687,6 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **ListenNotes**
|
||||
- **LiTV**
|
||||
- **LiveJournal**: (**Currently broken**)
|
||||
- **livestream**
|
||||
- **livestream:original**
|
||||
- **Livestreamfails**
|
||||
- **Lnk**
|
||||
- **loc**: Library of Congress
|
||||
@ -748,8 +703,6 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **LSMLTVEmbed**
|
||||
- **LSMReplay**
|
||||
- **Lumni**
|
||||
- **lynda**: [*lynda*](## "netrc machine") lynda.com videos
|
||||
- **lynda:course**: [*lynda*](## "netrc machine") lynda.com online courses
|
||||
- **maariv.co.il**
|
||||
- **MagellanTV**
|
||||
- **MagentaMusik**
|
||||
@ -799,11 +752,9 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **MicrosoftLearnPlaylist**
|
||||
- **MicrosoftLearnSession**
|
||||
- **MicrosoftMedius**
|
||||
- **microsoftstream**: Microsoft Stream
|
||||
- **minds**
|
||||
- **minds:channel**
|
||||
- **minds:group**
|
||||
- **Minoto**
|
||||
- **mir24.tv**
|
||||
- **mirrativ**
|
||||
- **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")
|
||||
- **MochaVideo**
|
||||
- **Mojevideo**: mojevideo.sk
|
||||
- **Mojvideo**
|
||||
- **Monstercat**
|
||||
- **monstersiren**: 塞壬唱片
|
||||
- **Motherless**
|
||||
- **MotherlessGallery**
|
||||
- **MotherlessGroup**
|
||||
- **MotherlessUploader**
|
||||
- **Motorsport**: motorsport.com (**Currently broken**)
|
||||
- **MovieFap**
|
||||
- **moviepilot**: Moviepilot trailer
|
||||
- **MoviewPlay**
|
||||
- **Moviezine**
|
||||
- **MovingImage**
|
||||
- **MSN**
|
||||
- **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**)
|
||||
- **MuseAI**
|
||||
- **MuseScore**
|
||||
- **MusicdexAlbum**
|
||||
- **MusicdexArtist**
|
||||
- **MusicdexPlaylist**
|
||||
- **MusicdexSong**
|
||||
- **Mux**
|
||||
- **Mx3**
|
||||
- **Mx3Neo**
|
||||
@ -871,11 +811,9 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **NascarClassics**
|
||||
- **Nate**
|
||||
- **NateProgram**
|
||||
- **natgeo:video**
|
||||
- **NationalGeographicTV**
|
||||
- **Naver**
|
||||
- **Naver:live**
|
||||
- **navernow**
|
||||
- **nba**: (**Currently broken**)
|
||||
- **nba:channel**: (**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:video**: [*watchnebula*](## "netrc machine")
|
||||
- **NekoHacker**
|
||||
- **NerdCubedFeed**
|
||||
- **Nest**
|
||||
- **NestClip**
|
||||
- **NetAppCollection**
|
||||
@ -915,9 +852,6 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **NetPlusTV**: [*netplus*](## "netrc machine")
|
||||
- **NetPlusTVLive**: [*netplus*](## "netrc machine")
|
||||
- **NetPlusTVRecordings**: [*netplus*](## "netrc machine")
|
||||
- **Netverse**
|
||||
- **NetversePlaylist**
|
||||
- **NetverseSearch**: "netsearch:" prefix
|
||||
- **Netzkino**
|
||||
- **Newgrounds**: [*newgrounds*](## "netrc machine")
|
||||
- **Newgrounds:playlist**
|
||||
@ -993,9 +927,6 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **nts.live**
|
||||
- **ntv.ru**
|
||||
- **NubilesPorn**: [*nubiles-porn*](## "netrc machine")
|
||||
- **nuum:live**
|
||||
- **nuum:media**
|
||||
- **nuum:tab**
|
||||
- **Nuvid**
|
||||
- **NYTimes**
|
||||
- **NYTimesArticle**
|
||||
@ -1020,14 +951,12 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **onet.tv**
|
||||
- **onet.tv:channel**
|
||||
- **OnetMVP**
|
||||
- **OnionStudios**
|
||||
- **onsen**: [*onsen*](## "netrc machine") インターネットラジオステーション<音泉>
|
||||
- **Opencast**
|
||||
- **OpencastPlaylist**
|
||||
- **openrec**
|
||||
- **openrec:capture**
|
||||
- **openrec:movie**
|
||||
- **OraTV**
|
||||
- **orf:fm4:story**: fm4.orf.at stories
|
||||
- **orf:iptv**: iptv.ORF.at
|
||||
- **orf:on**
|
||||
@ -1079,24 +1008,17 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **Pinkbike**
|
||||
- **Pinterest**
|
||||
- **PinterestCollection**
|
||||
- **PiramideTV**
|
||||
- **PiramideTVChannel**
|
||||
- **PlanetMarathi**
|
||||
- **Platzi**: [*platzi*](## "netrc machine")
|
||||
- **PlatziCourse**: [*platzi*](## "netrc machine")
|
||||
- **play.tv**: [*goplay*](## "netrc machine") PLAY (formerly goplay.be)
|
||||
- **player.sky.it**
|
||||
- **PlayerFm**
|
||||
- **playeur**
|
||||
- **PlayPlusTV**: [*playplustv*](## "netrc machine")
|
||||
- **PlaySuisse**: [*playsuisse*](## "netrc machine")
|
||||
- **Playtvak**: Playtvak.cz, iDNES.cz and Lidovky.cz
|
||||
- **PlayVids**
|
||||
- **Playwire**
|
||||
- **pluralsight**: [*pluralsight*](## "netrc machine")
|
||||
- **pluralsight:course**
|
||||
- **PlutoTV**: (**Currently broken**)
|
||||
- **PlVideo**: Платформа
|
||||
- **PlyrEmbed**
|
||||
- **PodbayFM**
|
||||
- **PodbayFMChannel**
|
||||
@ -1133,7 +1055,6 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **PremiershipRugby**
|
||||
- **PressTV**
|
||||
- **ProjectVeritas**: (**Currently broken**)
|
||||
- **prosiebensat1**: ProSiebenSat.1 Digital
|
||||
- **PRXAccount**
|
||||
- **PRXSeries**
|
||||
- **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**
|
||||
- **puhutv**
|
||||
- **puhutv:serie**
|
||||
- **Puls4**
|
||||
- **Pyvideo**
|
||||
- **QDance**: [*qdance*](## "netrc machine")
|
||||
- **QingTing**
|
||||
@ -1162,8 +1082,6 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **Radio1Be**
|
||||
- **radiocanada**
|
||||
- **radiocanada:audiovideo**
|
||||
- **RadioComercial**
|
||||
- **RadioComercialPlaylist**
|
||||
- **radiofrance**
|
||||
- **RadioFranceLive**
|
||||
- **RadioFrancePodcast**
|
||||
@ -1203,7 +1121,6 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **RedBullEmbed**
|
||||
- **RedBullTV**
|
||||
- **RedBullTVRrnContent**
|
||||
- **redcdnlivx**
|
||||
- **Reddit**: [*reddit*](## "netrc machine")
|
||||
- **RedGifs**
|
||||
- **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**)
|
||||
- **Reuters**: (**Currently broken**)
|
||||
- **ReverbNation**
|
||||
- **RheinMainTV**
|
||||
- **RideHome**
|
||||
- **RinseFM**
|
||||
- **RinseFMArtistPlaylist**
|
||||
- **RMCDecouverte**
|
||||
- **RockstarGames**: (**Currently broken**)
|
||||
- **Rokfin**: [*rokfin*](## "netrc machine")
|
||||
- **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:watch**
|
||||
- **Scrolller**
|
||||
- **sejm**
|
||||
- **sejm**: (**Currently broken**)
|
||||
- **Sen**
|
||||
- **SenalColombiaLive**: (**Currently broken**)
|
||||
- **senate.gov**
|
||||
- **senate.gov:isvp**
|
||||
- **SendtoNews**: (**Currently broken**)
|
||||
- **Servus**
|
||||
- **Sexu**: (**Currently broken**)
|
||||
- **SeznamZpravy**
|
||||
@ -1315,7 +1229,6 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **Shahid**: [*shahid*](## "netrc machine")
|
||||
- **ShahidShow**
|
||||
- **SharePoint**
|
||||
- **ShareVideosEmbed**
|
||||
- **ShemarooMe**
|
||||
- **Shiey**
|
||||
- **ShowRoomLive**
|
||||
@ -1345,15 +1258,14 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **smotrim:live**
|
||||
- **smotrim:playlist**
|
||||
- **SnapchatSpotlight**
|
||||
- **Snotr**
|
||||
- **SoftWhiteUnderbelly**: [*softwhiteunderbelly*](## "netrc machine")
|
||||
- **Sohu**
|
||||
- **SohuV**
|
||||
- **SonyLIV**: [*sonyliv*](## "netrc machine")
|
||||
- **SonyLIVSeries**
|
||||
- **soop**: [*afreecatv*](## "netrc machine") sooplive.co.kr
|
||||
- **soop:catchstory**: [*afreecatv*](## "netrc machine") sooplive.co.kr catch story
|
||||
- **soop:live**: [*afreecatv*](## "netrc machine") sooplive.co.kr livestreams
|
||||
- **soop**: [*afreecatv*](## "netrc machine") sooplive.com
|
||||
- **soop:catchstory**: [*afreecatv*](## "netrc machine") sooplive.com catch story
|
||||
- **soop:live**: [*afreecatv*](## "netrc machine") sooplive.com livestreams
|
||||
- **soop:user**: [*afreecatv*](## "netrc machine")
|
||||
- **soundcloud**: [*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**
|
||||
- **Spreaker**
|
||||
- **SpreakerShow**
|
||||
- **SpringboardPlatform**
|
||||
- **SproutVideo**
|
||||
- **sr:mediathek**: Saarländischer Rundfunk
|
||||
- **SRGSSR**
|
||||
@ -1391,14 +1302,11 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **StacommuLive**: [*stacommu*](## "netrc machine")
|
||||
- **StacommuVOD**: [*stacommu*](## "netrc machine")
|
||||
- **StagePlusVODConcert**: [*stageplus*](## "netrc machine")
|
||||
- **stanfordoc**: Stanford Open ClassRoom
|
||||
- **startrek**: STAR TREK
|
||||
- **startv**
|
||||
- **Steam**
|
||||
- **SteamCommunity**
|
||||
- **SteamCommunityBroadcast**
|
||||
- **Stitcher**
|
||||
- **StitcherShow**
|
||||
- **StoryFire**
|
||||
- **StoryFireSeries**
|
||||
- **StoryFireUser**
|
||||
@ -1406,7 +1314,6 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **Streamable**
|
||||
- **StreamCZ**
|
||||
- **StreetVoice**
|
||||
- **StretchInternet**
|
||||
- **Stripchat**
|
||||
- **stv:player**
|
||||
- **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:play**: SVT Play and Öppet arkiv
|
||||
- **svt:play:series**
|
||||
- **SwearnetEpisode**
|
||||
- **Syfy**
|
||||
- **SYVDK**
|
||||
- **SztvHu**
|
||||
- **t-online.de**: (**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**
|
||||
- **TeleTask**: (**Currently broken**)
|
||||
- **Telewebion**: (**Currently broken**)
|
||||
- **Tempo**
|
||||
- **TennisTV**: [*tennistv*](## "netrc machine")
|
||||
- **TF1**
|
||||
- **TFO**: (**Currently broken**)
|
||||
@ -1476,7 +1380,6 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **TheGuardianPodcast**
|
||||
- **TheGuardianPodcastPlaylist**
|
||||
- **TheHighWire**
|
||||
- **TheHoleTv**
|
||||
- **TheIntercept**
|
||||
- **ThePlatform**
|
||||
- **ThePlatformFeed**
|
||||
@ -1510,11 +1413,7 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **toutiao**: 今日头条
|
||||
- **Toypics**: Toypics video (**Currently broken**)
|
||||
- **ToypicsUser**: Toypics user profile (**Currently broken**)
|
||||
- **TrailerAddict**: (**Currently broken**)
|
||||
- **TravelChannel**
|
||||
- **Triller**: [*triller*](## "netrc machine")
|
||||
- **TrillerShort**
|
||||
- **TrillerUser**: [*triller*](## "netrc machine")
|
||||
- **Trovo**
|
||||
- **TrovoChannelClip**: All Clips of a trovo.live channel; "trovoclip:" 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:vod**
|
||||
- **tvp:vod:series**
|
||||
- **TVPlayer**
|
||||
- **TVPlayHome**
|
||||
- **tvw**
|
||||
- **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:course**: [*udemy*](## "netrc machine")
|
||||
- **UDNEmbed**: 聯合影音
|
||||
- **UFCArabia**: [*ufcarabia*](## "netrc machine")
|
||||
- **UFCTV**: [*ufctv*](## "netrc machine")
|
||||
- **ukcolumn**: (**Currently broken**)
|
||||
- **UKTVPlay**
|
||||
- **UlizaPlayer**
|
||||
- **UlizaPortal**: ulizaportal.jp
|
||||
- **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**
|
||||
- **Vevo**
|
||||
- **VevoPlaylist**
|
||||
- **VGTV**: VGTV, BTTV, FTV, Aftenposten and Aftonbladet
|
||||
- **VGTV**: VGTV, BTTV, FTV, Aftenposten and Aftonbladet (**Currently broken**)
|
||||
- **vh1.com**
|
||||
- **vhx:embed**: [*vimeo*](## "netrc machine")
|
||||
- **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.sky.it**
|
||||
- **video.sky.it:live**
|
||||
- **VideoDetective**
|
||||
- **videofy.me**: (**Currently broken**)
|
||||
- **VideoKen**
|
||||
- **VideoKenCategory**
|
||||
- **VideoKenPlayer**
|
||||
- **VideoKenPlaylist**
|
||||
- **VideoKenTopic**
|
||||
- **videomore**
|
||||
- **videomore:season**
|
||||
- **videomore:video**
|
||||
- **VideoPress**
|
||||
- **Vidflex**
|
||||
- **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:user**: [*vimeo*](## "netrc machine")
|
||||
- **vimeo:watchlater**: [*vimeo*](## "netrc machine") Vimeo watch later list, ":vimeowatchlater" keyword (requires authentication)
|
||||
- **Vimm:recording**
|
||||
- **Vimm:stream**
|
||||
- **ViMP**
|
||||
- **ViMP:Playlist**
|
||||
- **Viously**
|
||||
@ -1683,7 +1568,6 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **VKPlayLive**
|
||||
- **vm.tiktok**
|
||||
- **Vocaroo**
|
||||
- **VODPl**
|
||||
- **VODPlatform**
|
||||
- **voicy**: (**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")
|
||||
- **VTXTVLive**: [*vtxtv*](## "netrc machine")
|
||||
- **VTXTVRecordings**: [*vtxtv*](## "netrc machine")
|
||||
- **VuClip**
|
||||
- **VVVVID**
|
||||
- **VVVVIDShow**
|
||||
- **Walla**
|
||||
- **WalyTV**: [*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**
|
||||
- **WatchESPN**
|
||||
- **WDR**
|
||||
- **wdr:mobile**: (**Currently broken**)
|
||||
- **WDRElefant**
|
||||
- **WDRPage**
|
||||
- **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")
|
||||
- **WeverseMoment**: [*weverse*](## "netrc machine")
|
||||
- **WeVidi**
|
||||
- **Weyyak**
|
||||
- **whowatch**
|
||||
- **Whyp**
|
||||
- **wikimedia.org**
|
||||
@ -1778,7 +1657,6 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **Xinpianchang**: 新片场
|
||||
- **XMinus**: (**Currently broken**)
|
||||
- **XNXX**
|
||||
- **Xstream**
|
||||
- **XVideos**
|
||||
- **xvideos:quickies**
|
||||
- **XXXYMovies**
|
||||
@ -1837,8 +1715,6 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **ZattooRecordings**: [*zattoo*](## "netrc machine")
|
||||
- **zdf**
|
||||
- **zdf:channel**
|
||||
- **Zee5**: [*zee5*](## "netrc machine")
|
||||
- **zee5:series**
|
||||
- **ZeeNews**: (**Currently broken**)
|
||||
- **ZenPorn**
|
||||
- **ZetlandDKArticle**
|
||||
|
||||
@ -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,
|
||||
}],
|
||||
{},
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@ -6,11 +6,11 @@ import sys
|
||||
import unittest
|
||||
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__))))
|
||||
|
||||
|
||||
from yt_dlp.globals import all_plugins_loaded
|
||||
|
||||
import contextlib
|
||||
import copy
|
||||
import json
|
||||
|
||||
@ -8,8 +8,15 @@ import unittest
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
import http.cookiejar
|
||||
import http.server
|
||||
import ipaddress
|
||||
import pytest
|
||||
import json
|
||||
import tempfile
|
||||
import threading
|
||||
|
||||
from test.helper import FakeYDL
|
||||
from yt_dlp.networking.common import HTTPHeaderDict
|
||||
from yt_dlp.downloader.external import (
|
||||
Aria2cFD,
|
||||
AxelFD,
|
||||
@ -75,34 +82,114 @@ class TestWgetFD(unittest.TestCase):
|
||||
def test_make_cmd(self):
|
||||
with FakeYDL() as 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))
|
||||
self.assertIn('--load-cookies', downloader._make_cmd('test', TEST_INFO))
|
||||
assert '--load-cookies' in downloader._make_cmd('test', TEST_INFO)
|
||||
|
||||
|
||||
class TestCurlFD(unittest.TestCase):
|
||||
def test_make_cmd(self):
|
||||
class HTTPTestHandler(http.server.BaseHTTPRequestHandler):
|
||||
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:
|
||||
downloader = CurlFD(ydl, {})
|
||||
self.assertNotIn('--cookie', downloader._make_cmd('test', TEST_INFO))
|
||||
# Test cookie header is added
|
||||
ydl.cookiejar.set_cookie(http.cookiejar.Cookie(**TEST_COOKIE))
|
||||
self.assertIn('--cookie', downloader._make_cmd('test', TEST_INFO))
|
||||
self.assertIn('test=ytdlp', downloader._make_cmd('test', TEST_INFO))
|
||||
downloader = downloader_cls(ydl, {})
|
||||
|
||||
with HTTPTestServer(('localhost', 0), HTTPTestHandler) as server_a:
|
||||
second_addr = server_a.address + 1
|
||||
if not second_addr.is_loopback:
|
||||
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):
|
||||
def test_make_cmd(self):
|
||||
with FakeYDL() as 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))
|
||||
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')
|
||||
|
||||
@ -20,7 +20,12 @@ LAZY_EXTRACTORS = 'yt_dlp/extractor/lazy_extractors.py'
|
||||
class TestExecution(unittest.TestCase):
|
||||
def run_yt_dlp(self, exe=(sys.executable, 'yt_dlp/__main__.py'), opts=('--version', )):
|
||||
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)
|
||||
self.assertEqual(returncode, 0)
|
||||
return stdout.strip(), stderr.strip()
|
||||
|
||||
@ -11,7 +11,10 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
import subprocess
|
||||
|
||||
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 (
|
||||
ExecPP,
|
||||
FFmpegThumbnailsConvertorPP,
|
||||
@ -91,6 +94,51 @@ class TestExec(unittest.TestCase):
|
||||
self.assertEqual(pp.parse_cmd('echo {}', 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):
|
||||
def setUp(self):
|
||||
|
||||
@ -16,7 +16,6 @@ from yt_dlp.extractor import (
|
||||
CeskaTelevizeIE,
|
||||
DailymotionIE,
|
||||
DemocracynowIE,
|
||||
LyndaIE,
|
||||
RaiPlayIE,
|
||||
RTVEALaCartaIE,
|
||||
TedTalkIE,
|
||||
@ -250,20 +249,6 @@ class TestCeskaTelevizeSubtitles(BaseTestSubtitles):
|
||||
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
|
||||
@unittest.skip('IE broken')
|
||||
class TestNPOSubtitles(BaseTestSubtitles):
|
||||
|
||||
@ -327,6 +327,12 @@ class TestUtil(unittest.TestCase):
|
||||
with self.assertRaises(_UnsafeExtensionError):
|
||||
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):
|
||||
self.assertEqual(replace_extension('abc.ext', 'temp'), 'abc.temp')
|
||||
self.assertEqual(replace_extension('abc.ext', 'temp', 'ext'), 'abc.temp')
|
||||
@ -345,6 +351,12 @@ class TestUtil(unittest.TestCase):
|
||||
with self.assertRaises(_UnsafeExtensionError):
|
||||
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):
|
||||
self.assertEqual(subtitles_filename('abc.ext', 'en', 'vtt'), '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)
|
||||
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):
|
||||
assert extract_basic_auth('http://:foo.bar') == ('http://:foo.bar', None)
|
||||
assert extract_basic_auth('http://foo.bar') == ('http://foo.bar', None)
|
||||
|
||||
28
test/testdata/mpd/supplemental_codecs.mpd
vendored
Normal file
28
test/testdata/mpd/supplemental_codecs.mpd
vendored
Normal 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
154
uv.lock
generated
@ -137,11 +137,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2026.4.22"
|
||||
version = "2026.5.20"
|
||||
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 = [
|
||||
{ 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]]
|
||||
@ -458,15 +458,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "deno"
|
||||
version = "2.7.14"
|
||||
version = "2.8.1"
|
||||
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 = [
|
||||
{ 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/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/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/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/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/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/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/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/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/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]]
|
||||
@ -526,11 +526,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.15"
|
||||
version = "3.17"
|
||||
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 = [
|
||||
{ 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]]
|
||||
@ -643,20 +643,20 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pip"
|
||||
version = "26.1.1"
|
||||
version = "26.1.2"
|
||||
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 = [
|
||||
{ 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]]
|
||||
name = "platformdirs"
|
||||
version = "4.9.6"
|
||||
version = "4.10.0"
|
||||
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 = [
|
||||
{ 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]]
|
||||
@ -825,28 +825,28 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pytest-rerunfailures"
|
||||
version = "16.2"
|
||||
version = "16.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "packaging" },
|
||||
{ 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 = [
|
||||
{ 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]]
|
||||
name = "python-discovery"
|
||||
version = "1.3.1"
|
||||
version = "1.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "filelock" },
|
||||
{ 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 = [
|
||||
{ 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]]
|
||||
@ -952,27 +952,27 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.15.13"
|
||||
version = "0.15.15"
|
||||
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 = [
|
||||
{ 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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]]
|
||||
@ -1053,11 +1053,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "trove-classifiers"
|
||||
version = "2026.5.7.17"
|
||||
version = "2026.5.22.10"
|
||||
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 = [
|
||||
{ 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]]
|
||||
@ -1080,33 +1080,33 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "uv"
|
||||
version = "0.11.14"
|
||||
version = "0.11.17"
|
||||
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 = [
|
||||
{ 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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]]
|
||||
name = "virtualenv"
|
||||
version = "21.3.3"
|
||||
version = "21.4.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "distlib" },
|
||||
@ -1115,9 +1115,9 @@ dependencies = [
|
||||
{ name = "python-discovery" },
|
||||
{ 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 = [
|
||||
{ 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]]
|
||||
@ -1290,9 +1290,9 @@ requires-dist = [
|
||||
{ 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 == '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 == '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 = "implementation_name == 'cpython' and extra == 'pin-curl-cffi'", specifier = "==2.0.0" },
|
||||
{ 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 == 'pin-curl-cffi'", specifier = "==0.15.0" },
|
||||
{ name = "deno", marker = "extra == 'deno'", specifier = ">=2.6.6" },
|
||||
{ name = "deno", marker = "extra == 'pin-deno'", specifier = "==2.7.14" },
|
||||
{ name = "idna", marker = "extra == 'pin'", specifier = "==3.15" },
|
||||
{ name = "deno", marker = "extra == 'pin-deno'", specifier = "==2.8.1" },
|
||||
{ name = "idna", marker = "extra == 'pin'", specifier = "==3.17" },
|
||||
{ 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 = "mdurl", marker = "implementation_name == 'cpython' and extra == 'pin-curl-cffi'", specifier = "==0.1.2" },
|
||||
@ -1364,9 +1364,9 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "zipp"
|
||||
version = "3.23.1"
|
||||
version = "4.1.0"
|
||||
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 = [
|
||||
{ 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" },
|
||||
]
|
||||
|
||||
@ -112,6 +112,7 @@ from .utils import (
|
||||
RejectedVideoReached,
|
||||
SameFileError,
|
||||
UnavailableVideoError,
|
||||
UnsafeExecExpansionError,
|
||||
UserNotLive,
|
||||
YoutubeDLError,
|
||||
age_restricted,
|
||||
@ -826,9 +827,14 @@ class YoutubeDL:
|
||||
for pp_def_raw in self.params.get('postprocessors', []):
|
||||
pp_def = dict(pp_def_raw)
|
||||
when = pp_def.pop('when', 'post_process')
|
||||
self.add_post_processor(
|
||||
get_postprocessor(pp_def.pop('key'))(self, **pp_def),
|
||||
when=when)
|
||||
# Handle errors for ExecPP command validation
|
||||
try:
|
||||
self.add_post_processor(
|
||||
get_postprocessor(pp_def.pop('key'))(self, **pp_def),
|
||||
when=when)
|
||||
except UnsafeExecExpansionError as e:
|
||||
self.report_error(e)
|
||||
raise
|
||||
|
||||
def preload_download_archive(fn):
|
||||
"""Preload the archive, if any is specified"""
|
||||
@ -1254,7 +1260,7 @@ class YoutubeDL:
|
||||
info_dict.pop('__pending_error', None)
|
||||
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
|
||||
@param sanitize Whether to sanitize the output as a filename
|
||||
"""
|
||||
@ -1305,6 +1311,9 @@ class YoutubeDL:
|
||||
(?:&(?P<replacement>.*?))?
|
||||
(?:\|(?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):
|
||||
if field == ':':
|
||||
@ -1429,6 +1438,25 @@ class YoutubeDL:
|
||||
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'
|
||||
|
||||
# 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 ''
|
||||
str_fmt = f'{fmt[:-1]}s'
|
||||
if value is None:
|
||||
@ -3377,7 +3405,9 @@ class YoutubeDL:
|
||||
self.report_warning(
|
||||
f'Cannot write internet shortcut file because the actual URL of "{info_dict["webpage_url"]}" is unknown')
|
||||
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):
|
||||
return False
|
||||
if self.params.get('overwrites', True) and os.path.exists(linkfn):
|
||||
|
||||
@ -44,6 +44,7 @@ from .utils import (
|
||||
GeoUtils,
|
||||
PlaylistEntries,
|
||||
SameFileError,
|
||||
UnsafeExecExpansionError,
|
||||
download_range_func,
|
||||
expand_path,
|
||||
float_or_none,
|
||||
@ -618,7 +619,7 @@ def validate_options(opts):
|
||||
warnings.append(
|
||||
'Using allow-unsafe-ext opens you up to potential attacks. '
|
||||
'Use with great care!')
|
||||
_UnsafeExtensionError.sanitize_extension = lambda x, prepend=False: x
|
||||
_UnsafeExtensionError._enabled = False
|
||||
|
||||
return warnings, deprecation_warnings
|
||||
|
||||
@ -1077,7 +1078,7 @@ def main(argv=None):
|
||||
IN_CLI.value = True
|
||||
try:
|
||||
_exit(*variadic(_real_main(argv)))
|
||||
except (CookieLoadError, DownloadError):
|
||||
except (CookieLoadError, DownloadError, UnsafeExecExpansionError):
|
||||
_exit(1)
|
||||
except SameFileError as e:
|
||||
_exit(f'ERROR: {e}')
|
||||
|
||||
@ -1,21 +1,21 @@
|
||||
import enum
|
||||
import functools
|
||||
import json
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from .fragment import FragmentFD
|
||||
from ..networking import Request
|
||||
from ..postprocessor.ffmpeg import EXT_TO_OUT_FORMATS, FFmpegPostProcessor
|
||||
from ..utils import (
|
||||
DownloadError,
|
||||
Popen,
|
||||
RetryManager,
|
||||
_configuration_args,
|
||||
_get_exe_version_output,
|
||||
check_executable,
|
||||
classproperty,
|
||||
cli_bool_option,
|
||||
@ -23,9 +23,9 @@ from ..utils import (
|
||||
cli_valueless_option,
|
||||
determine_ext,
|
||||
encodeArgument,
|
||||
find_available_port,
|
||||
remove_end,
|
||||
traverse_obj,
|
||||
version_tuple,
|
||||
)
|
||||
|
||||
|
||||
@ -136,7 +136,7 @@ class ExternalFD(FragmentFD):
|
||||
self._cookies_tempfile = tmp_cookies.name
|
||||
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
|
||||
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
|
||||
|
||||
def _call_downloader(self, tmpfilename, info_dict):
|
||||
@ -195,12 +195,38 @@ class ExternalFD(FragmentFD):
|
||||
class CurlFD(ExternalFD):
|
||||
AVAILABLE_OPT = '-V'
|
||||
_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):
|
||||
cmd = [self.exe, '--location', '-o', tmpfilename, '--compressed']
|
||||
cookie_header = self.ydl.cookiejar.get_cookie_header(info_dict['url'])
|
||||
if cookie_header:
|
||||
cmd += ['--cookie', cookie_header]
|
||||
|
||||
if self._curl_version >= self._MIN_VERSION_FOR_STDIN_COOKIES:
|
||||
# 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:
|
||||
for key, val in info_dict['http_headers'].items():
|
||||
cmd += ['--header', f'{key}: {val}']
|
||||
@ -222,6 +248,16 @@ class CurlFD(ExternalFD):
|
||||
cmd += ['--', info_dict['url']]
|
||||
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):
|
||||
AVAILABLE_OPT = '-V'
|
||||
@ -244,8 +280,7 @@ class WgetFD(ExternalFD):
|
||||
|
||||
def _make_cmd(self, tmpfilename, info_dict):
|
||||
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:
|
||||
for key, val in info_dict['http_headers'].items():
|
||||
cmd += ['--header', f'{key}: {val}']
|
||||
@ -268,41 +303,19 @@ class WgetFD(ExternalFD):
|
||||
|
||||
class Aria2cFD(ExternalFD):
|
||||
AVAILABLE_OPT = '-v'
|
||||
SUPPORTED_PROTOCOLS = ('http', 'https', 'ftp', 'ftps', 'dash_frag_urls', 'm3u8_frag_urls')
|
||||
|
||||
@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)
|
||||
SUPPORTED_PROTOCOLS = ('http', 'https', 'ftp', 'ftps')
|
||||
|
||||
@staticmethod
|
||||
def _aria2c_filename(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):
|
||||
cmd = [self.exe, '-c', '--no-conf',
|
||||
'--console-log-level=warn', '--summary-interval=0', '--download-result=hide',
|
||||
'--http-accept-gzip=true', '--file-allocation=none', '-x16', '-j16', '-s16']
|
||||
if 'fragments' in info_dict:
|
||||
cmd += ['--allow-overwrite=true', '--allow-piece-length-change=true']
|
||||
else:
|
||||
cmd += ['--min-split-size', '1M']
|
||||
'--http-accept-gzip=true', '--file-allocation=none', '-x16', '-j16', '-s16',
|
||||
'--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:
|
||||
for key, val in info_dict['http_headers'].items():
|
||||
cmd += ['--header', f'{key}: {val}']
|
||||
@ -314,12 +327,6 @@ class Aria2cFD(ExternalFD):
|
||||
cmd += self._bool_option('--show-console-readout', 'noprogress', 'false', 'true', '=')
|
||||
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.
|
||||
# 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.
|
||||
@ -329,106 +336,17 @@ class Aria2cFD(ExternalFD):
|
||||
dn = os.path.dirname(tmpfilename)
|
||||
if dn:
|
||||
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 += ['--uri-selector=inorder']
|
||||
url_list_file = f'{tmpfilename}.frag.urls'
|
||||
url_list = []
|
||||
for frag_index, fragment in enumerate(info_dict['fragments']):
|
||||
fragment_filename = f'{os.path.basename(tmpfilename)}-Frag{frag_index}'
|
||||
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']]
|
||||
cmd += [
|
||||
'--out',
|
||||
self._aria2c_filename(os.path.basename(tmpfilename)),
|
||||
'--auto-file-renaming=false',
|
||||
'--',
|
||||
info_dict['url'],
|
||||
]
|
||||
|
||||
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):
|
||||
AVAILABLE_OPT = '--version'
|
||||
@ -521,10 +439,11 @@ class FFmpegFD(ExternalFD):
|
||||
args.extend(['-cookies', ''.join(
|
||||
f'{cookie.name}={cookie.value}; path={cookie.path}; domain={cookie.domain};\r\n'
|
||||
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:
|
||||
# [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:
|
||||
args += ['-ss', str(start_time)]
|
||||
|
||||
@ -54,7 +54,6 @@ from .agora import (
|
||||
WyborczaPodcastIE,
|
||||
WyborczaVideoIE,
|
||||
)
|
||||
from .airtv import AirTVIE
|
||||
from .aitube import AitubeKZVideoIE
|
||||
from .alibaba import AlibabaIE
|
||||
from .aliexpress import AliExpressLiveIE
|
||||
@ -65,10 +64,6 @@ from .allstar import (
|
||||
AllstarProfileIE,
|
||||
)
|
||||
from .alphaporno import AlphaPornoIE
|
||||
from .alsace20tv import (
|
||||
Alsace20TVEmbedIE,
|
||||
Alsace20TVIE,
|
||||
)
|
||||
from .altcensored import (
|
||||
AltCensoredChannelIE,
|
||||
AltCensoredIE,
|
||||
@ -93,7 +88,6 @@ from .americastestkitchen import (
|
||||
AmericasTestKitchenIE,
|
||||
AmericasTestKitchenSeasonIE,
|
||||
)
|
||||
from .anchorfm import AnchorFMEpisodeIE
|
||||
from .angel import AngelIE
|
||||
from .antenna import (
|
||||
Ant1NewsGrArticleIE,
|
||||
@ -106,10 +100,6 @@ from .apa import APAIE
|
||||
from .aparat import AparatIE
|
||||
from .appleconnect import AppleConnectIE
|
||||
from .applepodcasts import ApplePodcastsIE
|
||||
from .appletrailers import (
|
||||
AppleTrailersIE,
|
||||
AppleTrailersSectionIE,
|
||||
)
|
||||
from .archiveorg import (
|
||||
ArchiveOrgIE,
|
||||
YoutubeWebArchiveIE,
|
||||
@ -140,7 +130,6 @@ from .asobichannel import (
|
||||
from .asobistage import AsobiStageIE
|
||||
from .atresplayer import AtresPlayerIE
|
||||
from .atscaleconf import AtScaleConfEventIE
|
||||
from .atvat import ATVAtIE
|
||||
from .audimedia import AudiMediaIE
|
||||
from .audioboom import AudioBoomIE
|
||||
from .audiodraft import (
|
||||
@ -157,13 +146,6 @@ from .audius import (
|
||||
AudiusProfileIE,
|
||||
AudiusTrackIE,
|
||||
)
|
||||
from .awaan import (
|
||||
AWAANIE,
|
||||
AWAANLiveIE,
|
||||
AWAANSeasonIE,
|
||||
AWAANVideoIE,
|
||||
)
|
||||
from .axs import AxsIE
|
||||
from .azmedien import AZMedienIE
|
||||
from .baidu import BaiduVideoIE
|
||||
from .banbye import (
|
||||
@ -190,10 +172,6 @@ from .bbc import (
|
||||
BBCCoUkPlaylistIE,
|
||||
)
|
||||
from .beacon import BeaconTvIE
|
||||
from .beatbump import (
|
||||
BeatBumpPlaylistIE,
|
||||
BeatBumpVideoIE,
|
||||
)
|
||||
from .beatport import BeatportIE
|
||||
from .beeg import BeegIE
|
||||
from .behindkink import BehindKinkIE
|
||||
@ -210,7 +188,6 @@ from .bibeltv import (
|
||||
BibelTVSeriesIE,
|
||||
BibelTVVideoIE,
|
||||
)
|
||||
from .bigflix import BigflixIE
|
||||
from .bigo import BigoIE
|
||||
from .bild import BildIE
|
||||
from .bilibili import (
|
||||
@ -255,7 +232,6 @@ from .blerp import BlerpIE
|
||||
from .blogger import BloggerIE
|
||||
from .bloomberg import BloombergIE
|
||||
from .bluesky import BlueskyIE
|
||||
from .bokecc import BokeCCIE
|
||||
from .bongacams import BongaCamsIE
|
||||
from .boosty import BoostyIE
|
||||
from .bostonglobe import BostonGlobeIE
|
||||
@ -288,14 +264,8 @@ from .businessinsider import BusinessInsiderIE
|
||||
from .buzzfeed import BuzzFeedIE
|
||||
from .byutv import BYUtvIE
|
||||
from .c56 import C56IE
|
||||
from .caffeinetv import CaffeineTVIE
|
||||
from .callin import CallinIE
|
||||
from .caltrans import CaltransIE
|
||||
from .cam4 import CAM4IE
|
||||
from .camdemy import (
|
||||
CamdemyFolderIE,
|
||||
CamdemyIE,
|
||||
)
|
||||
from .camfm import (
|
||||
CamFMEpisodeIE,
|
||||
CamFMShowIE,
|
||||
@ -371,7 +341,6 @@ from .ciscolive import (
|
||||
from .ciscowebex import CiscoWebexIE
|
||||
from .cjsw import CJSWIE
|
||||
from .clipchamp import ClipchampIE
|
||||
from .clippit import ClippitIE
|
||||
from .cliprs import ClipRsIE
|
||||
from .closertotruth import CloserToTruthIE
|
||||
from .cloudflarestream import CloudflareStreamIE
|
||||
@ -395,7 +364,6 @@ from .commonprotocols import (
|
||||
ViewSourceIE,
|
||||
)
|
||||
from .condenast import CondeNastIE
|
||||
from .contv import CONtvIE
|
||||
from .corus import CorusIE
|
||||
from .coub import CoubIE
|
||||
from .cozytv import CozyTVIE
|
||||
@ -510,7 +478,6 @@ from .dplay import (
|
||||
)
|
||||
from .drbonanza import DRBonanzaIE
|
||||
from .dreisat import DreiSatIE
|
||||
from .drooble import DroobleIE
|
||||
from .dropbox import DropboxIE
|
||||
from .dropout import (
|
||||
DropoutIE,
|
||||
@ -525,10 +492,6 @@ from .drtv import (
|
||||
DRTVSeriesIE,
|
||||
)
|
||||
from .dtube import DTubeIE
|
||||
from .duboku import (
|
||||
DubokuIE,
|
||||
DubokuPlaylistIE,
|
||||
)
|
||||
from .dumpert import DumpertIE
|
||||
from .duoplay import DuoplayIE
|
||||
from .dvtv import DVTVIE
|
||||
@ -546,8 +509,6 @@ from .eggs import (
|
||||
EggsArtistIE,
|
||||
EggsIE,
|
||||
)
|
||||
from .eighttracks import EightTracksIE
|
||||
from .eitb import EitbIE
|
||||
from .elementorembed import ElementorEmbedIE
|
||||
from .elonet import ElonetIE
|
||||
from .elpais import ElPaisIE
|
||||
@ -591,7 +552,6 @@ from .europeantour import EuropeanTourIE
|
||||
from .eurosport import EurosportIE
|
||||
from .euscreen import EUScreenIE
|
||||
from .expressen import ExpressenIE
|
||||
from .eyedotv import EyedoTVIE
|
||||
from .facebook import (
|
||||
FacebookAdsIE,
|
||||
FacebookIE,
|
||||
@ -655,7 +615,6 @@ from .foxnews import (
|
||||
from .foxsports import FoxSportsIE
|
||||
from .fptplay import FptplayIE
|
||||
from .francaisfacile import FrancaisFacileIE
|
||||
from .franceinter import FranceInterIE
|
||||
from .francetv import (
|
||||
FranceTVIE,
|
||||
FranceTVInfoIE,
|
||||
@ -672,14 +631,10 @@ from .frontendmasters import (
|
||||
FrontendMastersIE,
|
||||
FrontendMastersLessonIE,
|
||||
)
|
||||
from .fujitv import FujiTVFODPlus7IE
|
||||
from .funk import FunkIE
|
||||
from .funker530 import Funker530IE
|
||||
from .fuyintv import FuyinTVIE
|
||||
from .gab import (
|
||||
GabIE,
|
||||
GabTVIE,
|
||||
)
|
||||
from .gab import GabIE
|
||||
from .gaia import GaiaIE
|
||||
from .gamedevtv import GameDevTVDashboardIE
|
||||
from .gamejolt import (
|
||||
@ -743,16 +698,10 @@ from .googledrive import (
|
||||
GoogleDriveFolderIE,
|
||||
GoogleDriveIE,
|
||||
)
|
||||
from .googlepodcasts import (
|
||||
GooglePodcastsFeedIE,
|
||||
GooglePodcastsIE,
|
||||
)
|
||||
from .googlesearch import GoogleSearchIE
|
||||
from .goplay import GoPlayIE
|
||||
from .gopro import GoProIE
|
||||
from .goshgay import GoshgayIE
|
||||
from .gotostage import GoToStageIE
|
||||
from .gputechconf import GPUTechConfIE
|
||||
from .graspop import GraspopIE
|
||||
from .gronkh import (
|
||||
GronkhFeedIE,
|
||||
@ -769,7 +718,6 @@ from .hgtv import HGTVComShowIE
|
||||
from .hidive import HiDiveIE
|
||||
from .historicfilms import HistoricFilmsIE
|
||||
from .hitrecord import HitRecordIE
|
||||
from .hketv import HKETVIE
|
||||
from .hollywoodreporter import (
|
||||
HollywoodReporterIE,
|
||||
HollywoodReporterPlaylistIE,
|
||||
@ -818,7 +766,6 @@ from .idagio import (
|
||||
IdagioRecordingIE,
|
||||
IdagioTrackIE,
|
||||
)
|
||||
from .idolplus import IdolPlusIE
|
||||
from .ign import (
|
||||
IGNIE,
|
||||
IGNArticleIE,
|
||||
@ -851,7 +798,6 @@ from .instagram import (
|
||||
InstagramUserIE,
|
||||
)
|
||||
from .internazionale import InternazionaleIE
|
||||
from .internetvideoarchive import InternetVideoArchiveIE
|
||||
from .iprima import (
|
||||
IPrimaCNNIE,
|
||||
IPrimaIE,
|
||||
@ -886,7 +832,6 @@ from .iwara import (
|
||||
IwaraUserIE,
|
||||
)
|
||||
from .ixigua import IxiguaIE
|
||||
from .izlesene import IzleseneIE
|
||||
from .jamendo import (
|
||||
JamendoAlbumIE,
|
||||
JamendoIE,
|
||||
@ -939,11 +884,9 @@ from .kika import (
|
||||
KikaIE,
|
||||
KikaPlaylistIE,
|
||||
)
|
||||
from .kinja import KinjaEmbedIE
|
||||
from .kinopoisk import KinoPoiskIE
|
||||
from .kommunetv import KommunetvIE
|
||||
from .kompas import KompasVideoIE
|
||||
from .koo import KooIE
|
||||
from .krasview import KrasViewIE
|
||||
from .kth import KTHIE
|
||||
from .ku6 import Ku6IE
|
||||
@ -991,7 +934,6 @@ from .lecturio import (
|
||||
from .leeco import (
|
||||
LeIE,
|
||||
LePlaylistIE,
|
||||
LetvCloudIE,
|
||||
)
|
||||
from .lefigaro import (
|
||||
LeFigaroVideoEmbedIE,
|
||||
@ -1020,11 +962,6 @@ from .liputan6 import Liputan6IE
|
||||
from .listennotes import ListenNotesIE
|
||||
from .litv import LiTVIE
|
||||
from .livejournal import LiveJournalIE
|
||||
from .livestream import (
|
||||
LivestreamIE,
|
||||
LivestreamOriginalIE,
|
||||
LivestreamShortenerIE,
|
||||
)
|
||||
from .livestreamfails import LivestreamfailsIE
|
||||
from .lnk import LnkIE
|
||||
from .locipo import (
|
||||
@ -1048,10 +985,6 @@ from .lsm import (
|
||||
LSMReplayIE,
|
||||
)
|
||||
from .lumni import LumniIE
|
||||
from .lynda import (
|
||||
LyndaCourseIE,
|
||||
LyndaIE,
|
||||
)
|
||||
from .maariv import MaarivIE
|
||||
from .magellantv import MagellanTVIE
|
||||
from .magentamusik import MagentaMusikIE
|
||||
@ -1117,13 +1050,11 @@ from .microsoftembed import (
|
||||
MicrosoftLearnSessionIE,
|
||||
MicrosoftMediusIE,
|
||||
)
|
||||
from .microsoftstream import MicrosoftStreamIE
|
||||
from .minds import (
|
||||
MindsChannelIE,
|
||||
MindsGroupIE,
|
||||
MindsIE,
|
||||
)
|
||||
from .minoto import MinotoIE
|
||||
from .mir24tv import Mir24TvIE
|
||||
from .mirrativ import (
|
||||
MirrativIE,
|
||||
@ -1157,18 +1088,9 @@ from .mlb import (
|
||||
from .mlssoccer import MLSSoccerIE
|
||||
from .mocha import MochaVideoIE
|
||||
from .mojevideo import MojevideoIE
|
||||
from .mojvideo import MojvideoIE
|
||||
from .monstercat import MonstercatIE
|
||||
from .motherless import (
|
||||
MotherlessGalleryIE,
|
||||
MotherlessGroupIE,
|
||||
MotherlessIE,
|
||||
MotherlessUploaderIE,
|
||||
)
|
||||
from .motorsport import MotorsportIE
|
||||
from .moviepilot import MoviepilotIE
|
||||
from .moview import MoviewPlayIE
|
||||
from .moviezine import MoviezineIE
|
||||
from .movingimage import MovingImageIE
|
||||
from .msn import MSNIE
|
||||
from .mtv import MTVIE
|
||||
@ -1179,12 +1101,6 @@ from .murrtube import (
|
||||
)
|
||||
from .museai import MuseAIIE
|
||||
from .musescore import MuseScoreIE
|
||||
from .musicdex import (
|
||||
MusicdexAlbumIE,
|
||||
MusicdexArtistIE,
|
||||
MusicdexPlaylistIE,
|
||||
MusicdexSongIE,
|
||||
)
|
||||
from .mux import MuxIE
|
||||
from .mx3 import (
|
||||
Mx3IE,
|
||||
@ -1212,14 +1128,10 @@ from .nate import (
|
||||
NateIE,
|
||||
NateProgramIE,
|
||||
)
|
||||
from .nationalgeographic import (
|
||||
NationalGeographicTVIE,
|
||||
NationalGeographicVideoIE,
|
||||
)
|
||||
from .nationalgeographic import NationalGeographicTVIE
|
||||
from .naver import (
|
||||
NaverIE,
|
||||
NaverLiveIE,
|
||||
NaverNowIE,
|
||||
)
|
||||
from .nba import (
|
||||
NBAIE,
|
||||
@ -1257,7 +1169,6 @@ from .nebula import (
|
||||
NebulaSubscriptionsIE,
|
||||
)
|
||||
from .nekohacker import NekoHackerIE
|
||||
from .nerdcubed import NerdCubedFeedIE
|
||||
from .nest import (
|
||||
NestClipIE,
|
||||
NestIE,
|
||||
@ -1275,11 +1186,6 @@ from .neteasemusic import (
|
||||
NetEaseMusicProgramIE,
|
||||
NetEaseMusicSingerIE,
|
||||
)
|
||||
from .netverse import (
|
||||
NetverseIE,
|
||||
NetversePlaylistIE,
|
||||
NetverseSearchIE,
|
||||
)
|
||||
from .netzkino import NetzkinoIE
|
||||
from .newgrounds import (
|
||||
NewgroundsIE,
|
||||
@ -1389,11 +1295,6 @@ from .ntvcojp import NTVCoJpCUIE
|
||||
from .ntvde import NTVDeIE
|
||||
from .ntvru import NTVRuIE
|
||||
from .nubilesporn import NubilesPornIE
|
||||
from .nuum import (
|
||||
NuumLiveIE,
|
||||
NuumMediaIE,
|
||||
NuumTabIE,
|
||||
)
|
||||
from .nuvid import NuvidIE
|
||||
from .nytimes import (
|
||||
NYTimesArticleIE,
|
||||
@ -1426,7 +1327,6 @@ from .onet import (
|
||||
OnetMVPIE,
|
||||
OnetPlIE,
|
||||
)
|
||||
from .onionstudios import OnionStudiosIE
|
||||
from .onsen import OnsenIE
|
||||
from .opencast import (
|
||||
OpencastIE,
|
||||
@ -1437,7 +1337,6 @@ from .openrec import (
|
||||
OpenRecIE,
|
||||
OpenRecMovieIE,
|
||||
)
|
||||
from .ora import OraTVIE
|
||||
from .orf import (
|
||||
ORFIPTVIE,
|
||||
ORFONIE,
|
||||
@ -1511,26 +1410,18 @@ from .pinterest import (
|
||||
PinterestCollectionIE,
|
||||
PinterestIE,
|
||||
)
|
||||
from .piramidetv import (
|
||||
PiramideTVChannelIE,
|
||||
PiramideTVIE,
|
||||
)
|
||||
from .planetmarathi import PlanetMarathiIE
|
||||
from .platzi import (
|
||||
PlatziCourseIE,
|
||||
PlatziIE,
|
||||
)
|
||||
from .playerfm import PlayerFmIE
|
||||
from .playplustv import PlayPlusTVIE
|
||||
from .playsuisse import PlaySuisseIE
|
||||
from .playtvak import PlaytvakIE
|
||||
from .playwire import PlaywireIE
|
||||
from .pluralsight import (
|
||||
PluralsightCourseIE,
|
||||
PluralsightIE,
|
||||
)
|
||||
from .plutotv import PlutoTVIE
|
||||
from .plvideo import PlVideoIE
|
||||
from .plyr import PlyrEmbedIE
|
||||
from .podbayfm import (
|
||||
PodbayFMChannelIE,
|
||||
@ -1574,7 +1465,6 @@ from .prankcast import (
|
||||
from .premiershiprugby import PremiershipRugbyIE
|
||||
from .presstv import PressTVIE
|
||||
from .projectveritas import ProjectVeritasIE
|
||||
from .prosiebensat1 import ProSiebenSat1IE
|
||||
from .prx import (
|
||||
PRXAccountIE,
|
||||
PRXSeriesIE,
|
||||
@ -1586,7 +1476,6 @@ from .puhutv import (
|
||||
PuhuTVIE,
|
||||
PuhuTVSerieIE,
|
||||
)
|
||||
from .puls4 import Puls4IE
|
||||
from .pyvideo import PyvideoIE
|
||||
from .qdance import QDanceIE
|
||||
from .qingting import QingTingIE
|
||||
@ -1610,10 +1499,6 @@ from .radiocanada import (
|
||||
RadioCanadaAudioVideoIE,
|
||||
RadioCanadaIE,
|
||||
)
|
||||
from .radiocomercial import (
|
||||
RadioComercialIE,
|
||||
RadioComercialPlaylistIE,
|
||||
)
|
||||
from .radiode import RadioDeIE
|
||||
from .radiofrance import (
|
||||
FranceCultureIE,
|
||||
@ -1678,7 +1563,6 @@ from .redbulltv import (
|
||||
RedBullTVRrnContentIE,
|
||||
)
|
||||
from .reddit import RedditIE
|
||||
from .redge import RedCDNLivxIE
|
||||
from .redgifs import (
|
||||
RedGifsIE,
|
||||
RedGifsSearchIE,
|
||||
@ -1692,13 +1576,11 @@ from .rentv import (
|
||||
from .restudy import RestudyIE
|
||||
from .reuters import ReutersIE
|
||||
from .reverbnation import ReverbNationIE
|
||||
from .rheinmaintv import RheinMainTVIE
|
||||
from .ridehome import RideHomeIE
|
||||
from .rinsefm import (
|
||||
RinseFMArtistPlaylistIE,
|
||||
RinseFMIE,
|
||||
)
|
||||
from .rmcdecouverte import RMCDecouverteIE
|
||||
from .rockstargames import RockstarGamesIE
|
||||
from .rokfin import (
|
||||
RokfinChannelIE,
|
||||
@ -1815,7 +1697,6 @@ from .senategov import (
|
||||
SenateGovIE,
|
||||
SenateISVPIE,
|
||||
)
|
||||
from .sendtonews import SendtoNewsIE
|
||||
from .servus import ServusIE
|
||||
from .sevenplus import SevenPlusIE
|
||||
from .sexu import SexuIE
|
||||
@ -1828,7 +1709,6 @@ from .shahid import (
|
||||
ShahidShowIE,
|
||||
)
|
||||
from .sharepoint import SharePointIE
|
||||
from .sharevideos import ShareVideosEmbedIE
|
||||
from .shemaroome import ShemarooMeIE
|
||||
from .shiey import ShieyIE
|
||||
from .showroomlive import ShowRoomLiveIE
|
||||
@ -1873,7 +1753,6 @@ from .smotrim import (
|
||||
SmotrimPlaylistIE,
|
||||
)
|
||||
from .snapchat import SnapchatSpotlightIE
|
||||
from .snotr import SnotrIE
|
||||
from .softwhiteunderbelly import SoftWhiteUnderbellyIE
|
||||
from .sohu import (
|
||||
SohuIE,
|
||||
@ -1923,7 +1802,6 @@ from .spreaker import (
|
||||
SpreakerIE,
|
||||
SpreakerShowIE,
|
||||
)
|
||||
from .springboardplatform import SpringboardPlatformIE
|
||||
from .sproutvideo import (
|
||||
SproutVideoIE,
|
||||
VidsIoIE,
|
||||
@ -1940,7 +1818,6 @@ from .stacommu import (
|
||||
TheaterComplexTownVODIE,
|
||||
)
|
||||
from .stageplus import StagePlusVODConcertIE
|
||||
from .stanfordoc import StanfordOpenClassroomIE
|
||||
from .startrek import StarTrekIE
|
||||
from .startv import StarTVIE
|
||||
from .steam import (
|
||||
@ -1948,10 +1825,6 @@ from .steam import (
|
||||
SteamCommunityIE,
|
||||
SteamIE,
|
||||
)
|
||||
from .stitcher import (
|
||||
StitcherIE,
|
||||
StitcherShowIE,
|
||||
)
|
||||
from .storyfire import (
|
||||
StoryFireIE,
|
||||
StoryFireSeriesIE,
|
||||
@ -1961,7 +1834,6 @@ from .streaks import StreaksIE
|
||||
from .streamable import StreamableIE
|
||||
from .streamcz import StreamCZIE
|
||||
from .streetvoice import StreetVoiceIE
|
||||
from .stretchinternet import StretchInternetIE
|
||||
from .stripchat import StripchatIE
|
||||
from .stv import STVPlayerIE
|
||||
from .subsplash import (
|
||||
@ -1979,8 +1851,6 @@ from .svt import (
|
||||
SVTPlayIE,
|
||||
SVTSeriesIE,
|
||||
)
|
||||
from .swearnet import SwearnetEpisodeIE
|
||||
from .syvdk import SYVDKIE
|
||||
from .sztvhu import SztvHuIE
|
||||
from .tagesschau import TagesschauIE
|
||||
from .taptap import (
|
||||
@ -2039,10 +1909,6 @@ from .telequebec import (
|
||||
)
|
||||
from .teletask import TeleTaskIE
|
||||
from .telewebion import TelewebionIE
|
||||
from .tempo import (
|
||||
IVXPlayerIE,
|
||||
TempoIE,
|
||||
)
|
||||
from .tencent import (
|
||||
IflixEpisodeIE,
|
||||
IflixSeriesIE,
|
||||
@ -2068,7 +1934,6 @@ from .theguardian import (
|
||||
TheGuardianPodcastPlaylistIE,
|
||||
)
|
||||
from .thehighwire import TheHighWireIE
|
||||
from .theholetv import TheHoleTvIE
|
||||
from .theintercept import TheInterceptIE
|
||||
from .theplatform import (
|
||||
ThePlatformFeedIE,
|
||||
@ -2120,12 +1985,6 @@ from .toypics import (
|
||||
ToypicsIE,
|
||||
ToypicsUserIE,
|
||||
)
|
||||
from .traileraddict import TrailerAddictIE
|
||||
from .triller import (
|
||||
TrillerIE,
|
||||
TrillerShortIE,
|
||||
TrillerUserIE,
|
||||
)
|
||||
from .trovo import (
|
||||
TrovoChannelClipIE,
|
||||
TrovoChannelVodIE,
|
||||
@ -2208,7 +2067,6 @@ from .tvplay import (
|
||||
TVPlayHomeIE,
|
||||
TVPlayIE,
|
||||
)
|
||||
from .tvplayer import TVPlayerIE
|
||||
from .tvw import (
|
||||
TvwIE,
|
||||
TvwNewsIE,
|
||||
@ -2248,12 +2106,8 @@ from .udemy import (
|
||||
UdemyIE,
|
||||
)
|
||||
from .udn import UDNEmbedIE
|
||||
from .ufctv import (
|
||||
UFCTVIE,
|
||||
UFCArabiaIE,
|
||||
)
|
||||
from .ufctv import UFCTVIE
|
||||
from .ukcolumn import UkColumnIE
|
||||
from .uktvplay import UKTVPlayIE
|
||||
from .uliza import (
|
||||
UlizaPlayerIE,
|
||||
UlizaPortalIE,
|
||||
@ -2283,7 +2137,6 @@ from .ustudio import (
|
||||
UstudioEmbedIE,
|
||||
UstudioIE,
|
||||
)
|
||||
from .utreon import UtreonIE
|
||||
from .varzesh3 import Varzesh3IE
|
||||
from .vbox7 import Vbox7IE
|
||||
from .veo import VeoIE
|
||||
@ -2308,20 +2161,7 @@ from .videocampus_sachsen import (
|
||||
VideocampusSachsenIE,
|
||||
ViMPPlaylistIE,
|
||||
)
|
||||
from .videodetective import VideoDetectiveIE
|
||||
from .videofyme import VideofyMeIE
|
||||
from .videoken import (
|
||||
VideoKenCategoryIE,
|
||||
VideoKenIE,
|
||||
VideoKenPlayerIE,
|
||||
VideoKenPlaylistIE,
|
||||
VideoKenTopicIE,
|
||||
)
|
||||
from .videomore import (
|
||||
VideomoreIE,
|
||||
VideomoreSeasonIE,
|
||||
VideomoreVideoIE,
|
||||
)
|
||||
from .videoken import VideoKenPlayerIE
|
||||
from .videopress import VideoPressIE
|
||||
from .vidflex import VidflexIE
|
||||
from .vidio import (
|
||||
@ -2351,10 +2191,6 @@ from .vimeo import (
|
||||
VimeoUserIE,
|
||||
VimeoWatchLaterIE,
|
||||
)
|
||||
from .vimm import (
|
||||
VimmIE,
|
||||
VimmRecordingIE,
|
||||
)
|
||||
from .viously import ViouslyIE
|
||||
from .viqeo import ViqeoIE
|
||||
from .visir import VisirIE
|
||||
@ -2372,7 +2208,6 @@ from .vk import (
|
||||
VKWallPostIE,
|
||||
)
|
||||
from .vocaroo import VocarooIE
|
||||
from .vodpl import VODPlIE
|
||||
from .vodplatform import VODPlatformIE
|
||||
from .voicy import (
|
||||
VoicyChannelIE,
|
||||
@ -2404,11 +2239,6 @@ from .vtv import (
|
||||
VTVIE,
|
||||
VTVGoIE,
|
||||
)
|
||||
from .vuclip import VuClipIE
|
||||
from .vvvvid import (
|
||||
VVVVIDIE,
|
||||
VVVVIDShowIE,
|
||||
)
|
||||
from .walla import WallaIE
|
||||
from .washingtonpost import (
|
||||
WashingtonPostArticleIE,
|
||||
@ -2418,7 +2248,6 @@ from .wat import WatIE
|
||||
from .wdr import (
|
||||
WDRIE,
|
||||
WDRElefantIE,
|
||||
WDRMobileIE,
|
||||
WDRPageIE,
|
||||
)
|
||||
from .webcamerapl import WebcameraplIE
|
||||
@ -2445,7 +2274,6 @@ from .weverse import (
|
||||
WeverseMomentIE,
|
||||
)
|
||||
from .wevidi import WeVidiIE
|
||||
from .weyyak import WeyyakIE
|
||||
from .whowatch import WhoWatchIE
|
||||
from .whyp import WhypIE
|
||||
from .wikimedia import WikimediaIE
|
||||
@ -2494,7 +2322,6 @@ from .ximalaya import (
|
||||
from .xinpianchang import XinpianchangIE
|
||||
from .xminus import XMinusIE
|
||||
from .xnxx import XNXXIE
|
||||
from .xstream import XstreamIE
|
||||
from .xvideos import (
|
||||
XVideosIE,
|
||||
XVideosQuickiesIE,
|
||||
@ -2618,10 +2445,6 @@ from .zdf import (
|
||||
ZDFIE,
|
||||
ZDFChannelIE,
|
||||
)
|
||||
from .zee5 import (
|
||||
Zee5IE,
|
||||
Zee5SeriesIE,
|
||||
)
|
||||
from .zeenews import ZeeNewsIE
|
||||
from .zenporn import ZenPornIE
|
||||
from .zetland import ZetlandDKArticleIE
|
||||
|
||||
@ -407,7 +407,7 @@ class AbemaTVIE(AbemaTVBaseIE):
|
||||
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('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)
|
||||
|
||||
info.update({
|
||||
@ -415,6 +415,7 @@ class AbemaTVIE(AbemaTVBaseIE):
|
||||
'title': title,
|
||||
'description': description,
|
||||
'formats': formats,
|
||||
'subtitles': subtitles,
|
||||
'is_live': is_live,
|
||||
'availability': availability,
|
||||
})
|
||||
|
||||
@ -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')),
|
||||
}
|
||||
@ -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)
|
||||
@ -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')),
|
||||
}
|
||||
@ -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('\'', '''))
|
||||
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'])
|
||||
@ -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,
|
||||
}
|
||||
@ -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)
|
||||
@ -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,
|
||||
}
|
||||
@ -420,10 +420,10 @@ class BandcampWeeklyIE(BandcampIE): # XXX: Do not subclass from concrete IE
|
||||
'info_dict': {
|
||||
'id': '224',
|
||||
'ext': 'mp3',
|
||||
'title': 'Bandcamp Weekly, 2017-04-04',
|
||||
'title': 'Magic Moments, 2017-04-04',
|
||||
'description': 'md5:5d48150916e8e02d030623a48512c874',
|
||||
'thumbnail': 'https://f4.bcbits.com/img/9982549_0.jpg',
|
||||
'series': 'Bandcamp Weekly',
|
||||
'series': 'Magic Moments',
|
||||
'episode_id': '224',
|
||||
'release_timestamp': 1491264000,
|
||||
'release_date': '20170404',
|
||||
@ -440,10 +440,10 @@ class BandcampWeeklyIE(BandcampIE): # XXX: Do not subclass from concrete IE
|
||||
def _real_extract(self, url):
|
||||
show_id = self._match_id(url)
|
||||
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',
|
||||
data=json.dumps({'id': show_id}).encode(),
|
||||
headers={'Content-Type': 'application/json'})
|
||||
data=json.dumps({'item_id': int(show_id), 'item_type': 'radio'}).encode(),
|
||||
headers={'Content-Type': 'application/json'})['tracklist']
|
||||
audio_data = show_data['compiledTrack']
|
||||
|
||||
stream_url = audio_data['streamUrl']
|
||||
|
||||
@ -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_)
|
||||
@ -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,
|
||||
}
|
||||
@ -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),
|
||||
}
|
||||
@ -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),
|
||||
}
|
||||
@ -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 Elon’s Starlink',
|
||||
'description': 'Or, why the government doesn’t 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 Elon’s 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': 'Let’s 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,
|
||||
}
|
||||
@ -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)
|
||||
@ -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,
|
||||
}
|
||||
@ -2970,6 +2970,8 @@ class InfoExtractor:
|
||||
content_type = representation_attrib.get('contentType', mime_type.split('/')[0])
|
||||
|
||||
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
|
||||
if mime_type == 'application/x-rawcc':
|
||||
codecs = {'scodec': codec_str}
|
||||
@ -3025,7 +3027,7 @@ class InfoExtractor:
|
||||
'asr': int_or_none(representation_attrib.get('audioSamplingRate')),
|
||||
'fps': int_or_none(representation_attrib.get('frameRate')),
|
||||
'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,
|
||||
'container': mimetype2ext(mime_type) + '_dash',
|
||||
**codecs,
|
||||
|
||||
@ -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')),
|
||||
}
|
||||
@ -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]
|
||||
@ -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)
|
||||
@ -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'),
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
@ -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')),
|
||||
}
|
||||
@ -12,14 +12,6 @@ class FootyRoomIE(InfoExtractor):
|
||||
},
|
||||
'playlist_count': 2,
|
||||
'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):
|
||||
@ -38,13 +30,6 @@ class FootyRoomIE(InfoExtractor):
|
||||
payload = video.get('payload')
|
||||
if not payload:
|
||||
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)
|
||||
if streamable_url:
|
||||
entries.append(self.url_result(
|
||||
|
||||
@ -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',
|
||||
}],
|
||||
}
|
||||
@ -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', ),
|
||||
}
|
||||
@ -1,70 +1,13 @@
|
||||
import re
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
clean_html,
|
||||
int_or_none,
|
||||
parse_codecs,
|
||||
parse_duration,
|
||||
str_to_int,
|
||||
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):
|
||||
_VALID_URL = r'https?://(?:www\.)?gab\.com/[^/]+/posts/(?P<id>\d+)'
|
||||
_TESTS = [{
|
||||
|
||||
@ -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]))
|
||||
@ -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,
|
||||
}
|
||||
@ -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',
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
@ -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')),
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
import base64
|
||||
import urllib.parse
|
||||
|
||||
from .bokecc import BokeCCBaseIE
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
ExtractorError,
|
||||
determine_ext,
|
||||
@ -10,7 +10,7 @@ from ..utils import (
|
||||
)
|
||||
|
||||
|
||||
class InfoQIE(BokeCCBaseIE):
|
||||
class InfoQIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://(?:www\.)?infoq\.com/(?:[^/]+/)+(?P<id>[^/]+)'
|
||||
|
||||
_TESTS = [{
|
||||
@ -117,14 +117,10 @@ class InfoQIE(BokeCCBaseIE):
|
||||
video_title = self._html_extract_title(webpage)
|
||||
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 = (
|
||||
self._extract_rtmp_video(webpage)
|
||||
+ self._extract_http_video(webpage)
|
||||
+ self._extract_http_audio(webpage, video_id))
|
||||
formats = (
|
||||
self._extract_rtmp_video(webpage)
|
||||
+ self._extract_http_video(webpage)
|
||||
+ self._extract_http_audio(webpage, video_id))
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
|
||||
@ -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'),
|
||||
}
|
||||
@ -29,6 +29,12 @@ class IwaraBaseIE(InfoExtractor):
|
||||
self.to_screen(f'{token_type} token has expired')
|
||||
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):
|
||||
username, password = self._get_login_info()
|
||||
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)
|
||||
if not user_token or self._is_token_expired(user_token, 'User'):
|
||||
response = self._download_json(
|
||||
'https://api.iwara.tv/user/login', None, note='Logging in',
|
||||
response = self._call_api(
|
||||
'user/login', None, note='Logging in',
|
||||
headers={'Content-Type': 'application/json'}, data=json.dumps({
|
||||
'email': username,
|
||||
'password': password,
|
||||
@ -60,8 +66,8 @@ class IwaraBaseIE(InfoExtractor):
|
||||
return # user has not passed credentials
|
||||
|
||||
if not IwaraBaseIE._MEDIATOKEN or self._is_token_expired(IwaraBaseIE._MEDIATOKEN, 'Media'):
|
||||
IwaraBaseIE._MEDIATOKEN = self._download_json(
|
||||
'https://api.iwara.tv/user/token', None, note='Fetching media token',
|
||||
IwaraBaseIE._MEDIATOKEN = self._call_api(
|
||||
'user/token', None, note='Fetching media token',
|
||||
data=b'', headers={
|
||||
'Authorization': f'Bearer {IwaraBaseIE._USERTOKEN}',
|
||||
'Content-Type': 'application/json',
|
||||
@ -147,11 +153,11 @@ class IwaraIE(IwaraBaseIE):
|
||||
q = urllib.parse.parse_qs(up.query)
|
||||
paths = up.path.rstrip('/').split('/')
|
||||
# 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'])
|
||||
|
||||
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:
|
||||
yield traverse_obj(fmt, {
|
||||
'format_id': 'name',
|
||||
@ -164,8 +170,8 @@ class IwaraIE(IwaraBaseIE):
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
username, _ = self._get_login_info()
|
||||
video_data = self._download_json(
|
||||
f'https://api.iwara.tv/video/{video_id}', video_id,
|
||||
video_data = self._call_api(
|
||||
f'video/{video_id}', video_id,
|
||||
expected_status=lambda x: True, headers=self._get_media_token())
|
||||
errmsg = video_data.get('message')
|
||||
# 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):
|
||||
videos = self._download_json(
|
||||
'https://api.iwara.tv/videos', playlist_id,
|
||||
videos = self._call_api(
|
||||
'videos', playlist_id,
|
||||
note=f'Downloading page {page}',
|
||||
query={
|
||||
'page': page,
|
||||
@ -251,8 +257,8 @@ class IwaraUserIE(IwaraBaseIE):
|
||||
|
||||
def _real_extract(self, url):
|
||||
playlist_id = self._match_id(url)
|
||||
user_info = self._download_json(
|
||||
f'https://api.iwara.tv/profile/{playlist_id}', playlist_id,
|
||||
user_info = self._call_api(
|
||||
f'profile/{playlist_id}', playlist_id,
|
||||
note='Requesting user info')
|
||||
user_id = traverse_obj(user_info, ('user', 'id'))
|
||||
|
||||
@ -277,8 +283,8 @@ class IwaraPlaylistIE(IwaraBaseIE):
|
||||
}]
|
||||
|
||||
def _entries(self, playlist_id, first_page, page):
|
||||
videos = self._download_json(
|
||||
'https://api.iwara.tv/videos', playlist_id, f'Downloading page {page}',
|
||||
videos = self._call_api(
|
||||
'videos', playlist_id, note=f'Downloading page {page}',
|
||||
query={'page': page, 'limit': self._PER_PAGE},
|
||||
headers=self._get_media_token()) if page else first_page
|
||||
for x in traverse_obj(videos, ('results', ..., 'id')):
|
||||
@ -286,9 +292,11 @@ class IwaraPlaylistIE(IwaraBaseIE):
|
||||
|
||||
def _real_extract(self, url):
|
||||
playlist_id = self._match_id(url)
|
||||
page_0 = self._download_json(
|
||||
f'https://api.iwara.tv/playlist/{playlist_id}?page=0&limit={self._PER_PAGE}', playlist_id,
|
||||
note='Requesting playlist info', headers=self._get_media_token())
|
||||
page_0 = self._call_api(
|
||||
f'playlist/{playlist_id}', playlist_id,
|
||||
note='Requesting playlist info',
|
||||
query={'page': 0, 'limit': self._PER_PAGE},
|
||||
headers=self._get_media_token())
|
||||
|
||||
return self.playlist_result(
|
||||
OnDemandPagedList(
|
||||
|
||||
@ -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,
|
||||
}
|
||||
@ -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': 'Don’t 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')),
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
@ -1,9 +1,6 @@
|
||||
import base64
|
||||
import datetime as dt
|
||||
import hashlib
|
||||
import re
|
||||
import time
|
||||
import urllib.parse
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..compat import compat_ord
|
||||
@ -14,8 +11,6 @@ from ..utils import (
|
||||
int_or_none,
|
||||
orderedSet,
|
||||
parse_iso8601,
|
||||
str_or_none,
|
||||
url_basename,
|
||||
urshift,
|
||||
)
|
||||
|
||||
@ -248,114 +243,3 @@ class LePlaylistIE(InfoExtractor):
|
||||
|
||||
return self.playlist_result(entries, playlist_id, playlist_title=title,
|
||||
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,
|
||||
}
|
||||
|
||||
@ -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))
|
||||
@ -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)
|
||||
@ -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,
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
@ -12,7 +12,7 @@ from ..utils.traversal import find_element, traverse_obj
|
||||
|
||||
|
||||
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 = [{
|
||||
'url': 'https://www.monstercat.com/release/742779548009',
|
||||
'playlist_count': 20,
|
||||
@ -24,6 +24,28 @@ class MonstercatIE(InfoExtractor):
|
||||
'album': 'The Secret Language of Trees',
|
||||
'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):
|
||||
|
||||
@ -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')
|
||||
@ -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)
|
||||
@ -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),
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
@ -1,64 +1,4 @@
|
||||
from .common import InfoExtractor
|
||||
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
|
||||
|
||||
@ -13,11 +13,9 @@ from ..utils import (
|
||||
dict_get,
|
||||
int_or_none,
|
||||
join_nonempty,
|
||||
merge_dicts,
|
||||
parse_iso8601,
|
||||
traverse_obj,
|
||||
try_get,
|
||||
unified_timestamp,
|
||||
update_url_query,
|
||||
url_or_none,
|
||||
)
|
||||
@ -284,142 +282,3 @@ class NaverLiveIE(NaverBaseIE):
|
||||
}), get_all=False),
|
||||
'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'))
|
||||
|
||||
@ -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')
|
||||
@ -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
|
||||
@ -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'))
|
||||
@ -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)
|
||||
@ -21,6 +21,7 @@ class OnsenIE(InfoExtractor):
|
||||
IE_NAME = 'onsen'
|
||||
IE_DESC = 'インターネットラジオステーション<音泉>'
|
||||
|
||||
_API_HEADERS = {'X-Client': 'onsen-web'}
|
||||
_BASE_URL = 'https://www.onsen.ag'
|
||||
_HEADERS = {'Referer': f'{_BASE_URL}/'}
|
||||
_NETRC_MACHINE = 'onsen'
|
||||
@ -71,6 +72,15 @@ class OnsenIE(InfoExtractor):
|
||||
'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
|
||||
def _get_encoded_id(program):
|
||||
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={
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
**self._API_HEADERS,
|
||||
}, data=json.dumps({
|
||||
'session': {
|
||||
'email': username,
|
||||
@ -94,7 +105,8 @@ class OnsenIE(InfoExtractor):
|
||||
program_id = self._match_id(url)
|
||||
try:
|
||||
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:
|
||||
if isinstance(e.cause, HTTPError) and e.cause.status == 404:
|
||||
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}
|
||||
if 'c' not in query:
|
||||
entries = [
|
||||
self.url_result(update_url_query(url, {'c': self._get_encoded_id(program)}), OnsenIE)
|
||||
for program in traverse_obj(programs, ('contents', lambda _, v: v['id']))
|
||||
self.url_result(
|
||||
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(
|
||||
|
||||
@ -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,
|
||||
}
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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'),
|
||||
}
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user