mirror of
https://github.com/yt-dlp/yt-dlp.git
synced 2026-08-15 08:39:09 +03:00
Compare commits
34
Commits
498e51f5da
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d6b8c8cd1 | ||
|
|
15941bd48f | ||
|
|
d798b9b8fb | ||
|
|
fdcc954df4 | ||
|
|
07591f601e | ||
|
|
aaf7405ba3 | ||
|
|
1f1101d0dc | ||
|
|
a8be438aac | ||
|
|
69ea200067 | ||
|
|
93ceb95cdf | ||
|
|
aefce1eea4 | ||
|
|
d9813a3da6 | ||
|
|
6a188aed91 | ||
|
|
59d9ae606a | ||
|
|
1328586f72 | ||
|
|
b3854cc41b | ||
|
|
981190089f | ||
|
|
fdec00e0bf | ||
|
|
997fa14084 | ||
|
|
b6590aaa1e | ||
|
|
bed3d58c5c | ||
|
|
8c1f07d813 | ||
|
|
e8de28e23c | ||
|
|
1472d10980 | ||
|
|
6694ef8299 | ||
|
|
fa383a9efa | ||
|
|
5aa335ecd9 | ||
|
|
26654a359d | ||
|
|
161dd9fd05 | ||
|
|
ac4c955ea9 | ||
|
|
ad9a6f25f6 | ||
|
|
84db5246fa | ||
|
|
d120d841dd | ||
|
|
40dd052c03 |
@@ -18,3 +18,7 @@ paths:
|
||||
ignore:
|
||||
# SC1090 "Can't follow non-constant source": ignore when using `source` to activate venv
|
||||
- '.+SC1090.+'
|
||||
.github/workflows/label-handler.yml:
|
||||
ignore:
|
||||
# https://github.com/rhysd/actionlint/issues/657
|
||||
- 'unexpected key "queue" for "concurrency" section.+'
|
||||
|
||||
@@ -200,7 +200,7 @@ jobs:
|
||||
fetch-depth: 0 # Needed for changelog
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
@@ -265,7 +265,7 @@ jobs:
|
||||
|
||||
- name: Set up QEMU
|
||||
if: matrix.qemu_platform
|
||||
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
|
||||
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
|
||||
with:
|
||||
image: tonistiigi/binfmt:qemu-v10.0.4-56@sha256:30cc9a4d03765acac9be2ed0afc23af1ad018aed2c28ea4be8c2eb9afe03fbd1
|
||||
cache-image: false
|
||||
@@ -442,7 +442,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
with:
|
||||
python-version: ${{ matrix.python_version }}
|
||||
architecture: ${{ matrix.arch }}
|
||||
|
||||
@@ -43,45 +43,61 @@ jobs:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
allow-prereleases: true
|
||||
|
||||
- name: Install Deno
|
||||
uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4
|
||||
with:
|
||||
deno-version: '2.3.0' # minimum supported version
|
||||
|
||||
- name: Install Bun
|
||||
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
||||
with:
|
||||
bun-version: '1.2.11' # minimum supported version
|
||||
no-cache: true
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: '22.0' # minimum supported version
|
||||
|
||||
- name: Install QuickJS (Linux)
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
shell: bash
|
||||
run: |
|
||||
wget "https://bellard.org/quickjs/binary_releases/quickjs-linux-x86_64-${QJS_VERSION}.zip" -O quickjs.zip
|
||||
unzip quickjs.zip qjs
|
||||
sudo install qjs /usr/local/bin/qjs
|
||||
wget "https://bellard.org/quickjs/binary_releases/quickjs-linux-x86_64-${QJS_VERSION}.zip" -O quickjs.zip
|
||||
unzip quickjs.zip qjs
|
||||
sudo install qjs /usr/local/bin/qjs
|
||||
|
||||
- name: Install QuickJS (Windows)
|
||||
if: matrix.os == 'windows-latest'
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
$PSNativeCommandUseErrorActionPreference = $true
|
||||
Invoke-WebRequest "https://bellard.org/quickjs/binary_releases/quickjs-win-x86_64-${Env:QJS_VERSION}.zip" -OutFile quickjs.zip
|
||||
unzip quickjs.zip
|
||||
- name: Install test requirements
|
||||
shell: bash
|
||||
$ErrorActionPreference = "Stop"
|
||||
$PSNativeCommandUseErrorActionPreference = $true
|
||||
Invoke-WebRequest "https://bellard.org/quickjs/binary_releases/quickjs-win-x86_64-${Env:QJS_VERSION}.zip" -OutFile quickjs.zip
|
||||
unzip quickjs.zip
|
||||
|
||||
- name: Install test requirements (cpython)
|
||||
if: ${{ !startsWith(matrix.python-version, 'pypy') }}
|
||||
run: |
|
||||
python -m pip install -U --require-hashes -r "bundle/requirements/pip.txt"
|
||||
python -m pip install -U --require-hashes -r "bundle/requirements/test.txt"
|
||||
python -m pip install -U --require-hashes -r "bundle/requirements/default.txt"
|
||||
|
||||
- name: Install test requirements (PyPy)
|
||||
if: ${{ startsWith(matrix.python-version, 'pypy') }}
|
||||
run: |
|
||||
# Upgrading/downgrading pip on Windows with actions/setup-python's PyPy can cause breakage
|
||||
# See https://github.com/actions/setup-python/issues/1348
|
||||
python -m pip install -U --require-hashes -r "bundle/requirements/test.txt"
|
||||
python -m pip install -U --require-hashes -r "bundle/requirements/default.txt"
|
||||
|
||||
- name: Run tests
|
||||
timeout-minutes: 15
|
||||
shell: bash
|
||||
|
||||
@@ -36,12 +36,12 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: none
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
allow-prereleases: true
|
||||
@@ -85,7 +85,8 @@ jobs:
|
||||
- name: Install test requirements (PyPy)
|
||||
if: ${{ startsWith(matrix.python-version, 'pypy') }}
|
||||
run: |
|
||||
python -m pip install -U --require-hashes -r "bundle/requirements/pip.txt"
|
||||
# Upgrading/downgrading pip on Windows with actions/setup-python's PyPy can cause breakage
|
||||
# See https://github.com/actions/setup-python/issues/1348
|
||||
python -m pip install -U --require-hashes -r "bundle/requirements/test.txt"
|
||||
python -m pip install -U --require-hashes -r "bundle/requirements/default.txt"
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
name: Label Handler
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
pull_request_target:
|
||||
types: [labeled] # zizmor: ignore[dangerous-triggers]
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: label
|
||||
cancel-in-progress: false
|
||||
queue: max
|
||||
|
||||
env:
|
||||
GH_TELEMETRY: "false"
|
||||
LLM_MESSAGE: >-
|
||||
This contribution has been determined to be in violation of yt-dlp's
|
||||
[**NO AI / NO LLM POLICY**](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#no-ai--no-llm-policy).
|
||||
Repeat or flagrant violations of this policy will result in a permanent ban from this repository.
|
||||
BAD_MESSAGE: >-
|
||||
This contribution has been determined to be in violation of yt-dlp's
|
||||
[policy against supporting sites that are primarily used for
|
||||
piracy](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#is-the-website-primarily-used-for-piracy).
|
||||
Please consult the [yt-dlp wiki
|
||||
FAQ](https://github.com/yt-dlp/yt-dlp/wiki/FAQ#why-is-there-a-rule-against-websites-primarily-used-for-piracy)
|
||||
if you have questions.
|
||||
|
||||
jobs:
|
||||
issue-llm:
|
||||
name: Issue (ai-policy-violation)
|
||||
if: github.event.issue.state == 'open' && contains(github.event.issue.labels.*.name, 'ai-policy-violation')
|
||||
permissions:
|
||||
issues: write # Needed to comment on, close, and lock issues
|
||||
runs-on: ubuntu-slim
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
steps:
|
||||
- name: Comment, close and lock issue
|
||||
run: |
|
||||
gh issue unlock "${ISSUE_NUMBER}" --repo "${GITHUB_REPOSITORY}" || true
|
||||
gh issue close "${ISSUE_NUMBER}" --repo "${GITHUB_REPOSITORY}" --reason "not planned" --comment "${LLM_MESSAGE}"
|
||||
gh issue lock "${ISSUE_NUMBER}" --repo "${GITHUB_REPOSITORY}"
|
||||
|
||||
issue-bad:
|
||||
name: Issue (piracy/illegal)
|
||||
if: github.event.issue.state == 'open' && contains(github.event.issue.labels.*.name, 'piracy/illegal')
|
||||
permissions:
|
||||
issues: write # Needed to comment on, close, and lock issues
|
||||
runs-on: ubuntu-slim
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
steps:
|
||||
- name: Comment, close and lock issue
|
||||
run: |
|
||||
gh issue unlock "${ISSUE_NUMBER}" --repo "${GITHUB_REPOSITORY}" || true
|
||||
gh issue close "${ISSUE_NUMBER}" --repo "${GITHUB_REPOSITORY}" --reason "not planned" --comment "${BAD_MESSAGE}"
|
||||
gh issue lock "${ISSUE_NUMBER}" --repo "${GITHUB_REPOSITORY}"
|
||||
|
||||
pr-llm:
|
||||
name: PR (ai-policy-violation)
|
||||
if: github.event.pull_request.state == 'open' && contains(github.event.pull_request.labels.*.name, 'ai-policy-violation')
|
||||
permissions:
|
||||
pull-requests: write # Needed to comment on, close, and lock PRs
|
||||
runs-on: ubuntu-slim
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
steps:
|
||||
- name: Comment, close and lock PR
|
||||
run: |
|
||||
gh pr unlock "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" || true
|
||||
gh pr close "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --comment "${LLM_MESSAGE}"
|
||||
gh pr lock "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}"
|
||||
|
||||
pr-bad:
|
||||
name: PR (piracy/illegal)
|
||||
if: github.event.pull_request.state == 'open' && contains(github.event.pull_request.labels.*.name, 'piracy/illegal')
|
||||
permissions:
|
||||
pull-requests: write # Needed to comment on, close, and lock PRs
|
||||
runs-on: ubuntu-slim
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
steps:
|
||||
- name: Comment, close and lock PR
|
||||
run: |
|
||||
gh pr unlock "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" || true
|
||||
gh pr close "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --comment "${BAD_MESSAGE}"
|
||||
gh pr lock "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}"
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- name: Install test requirements
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
with:
|
||||
python-version: '3.14'
|
||||
- name: Install dev dependencies
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
run: echo "head=$(git rev-parse HEAD)" | tee -a "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Cache nightly commit hash
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
env:
|
||||
SEGMENT_DOWNLOAD_TIMEOUT_MINS: 1
|
||||
with:
|
||||
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
persist-credentials: true # Needed to git-push the release commit
|
||||
ssh-key: ${{ secrets.RELEASE_KEY }}
|
||||
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
with:
|
||||
python-version: "3.13" # Keep this in sync with test-workflows.yml
|
||||
|
||||
@@ -178,7 +178,7 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0 # Needed for changelog
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
@@ -250,7 +250,7 @@ jobs:
|
||||
path: artifact
|
||||
pattern: build-*
|
||||
merge-multiple: true
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
@@ -280,7 +280,7 @@ jobs:
|
||||
fi
|
||||
printf '\n\n%s\n\n%s%s%s\n\n---\n' \
|
||||
"#### A description of the various files is in the [README](https://github.com/${REPOSITORY}#release-files)" \
|
||||
"The zipimport Unix executable contains code licensed under ISC and MIT. " \
|
||||
"The zipimport Unix executable and release tarball contain code licensed under ISC and MIT. " \
|
||||
"The PyInstaller-bundled executables are subject to these and other licenses, all of which are compiled in " \
|
||||
"[THIRD_PARTY_LICENSES.txt](https://github.com/${BASE_REPO}/blob/${HEAD_SHA}/THIRD_PARTY_LICENSES.txt)" >> ./RELEASE_NOTES
|
||||
python ./devscripts/make_changelog.py -vv --collapsible >> ./RELEASE_NOTES
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
with:
|
||||
python-version: "3.13" # Keep this in sync with release.yml's prepare job
|
||||
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run zizmor
|
||||
uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6
|
||||
uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7
|
||||
with:
|
||||
advanced-security: false
|
||||
persona: pedantic
|
||||
|
||||
@@ -886,3 +886,16 @@ MemoKing34
|
||||
Suntooth
|
||||
Ventriduct
|
||||
vpertys
|
||||
billauer
|
||||
BOplaid
|
||||
dialmaster
|
||||
dreammu
|
||||
FranciscoPombal
|
||||
HybridDog
|
||||
longcharmroeun
|
||||
Masterjun3
|
||||
noseb13eds
|
||||
Sec-Wayne
|
||||
selfhoster1312
|
||||
tcely
|
||||
tewhalen
|
||||
|
||||
@@ -4,6 +4,101 @@
|
||||
# To create a release, dispatch the https://github.com/yt-dlp/yt-dlp/actions/workflows/release.yml workflow on master
|
||||
-->
|
||||
|
||||
### 2026.07.04
|
||||
|
||||
#### Important changes
|
||||
- **The minimum *recommended* Python version has been raised to 3.11**
|
||||
- Since Python 3.10 will reach its end-of-life in October 2026, support for it will be dropped soon. [Read more](https://github.com/yt-dlp/yt-dlp/issues/16916)
|
||||
- **The official Windows release binaries will soon require Windows 10 or later.** [Read more](https://github.com/yt-dlp/yt-dlp/issues/16917)
|
||||
- Security: [[CVE-2026-55404](https://nvd.nist.gov/vuln/detail/CVE-2026-55404)] [Downstream command injection via improper sanitization of --write-link output](https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-6v4j-43gg-vj32)
|
||||
- Shortcut file data is now properly validated and sanitized when the `--write-link` options are used
|
||||
|
||||
#### Core changes
|
||||
- [Always include warnings in debug output](https://github.com/yt-dlp/yt-dlp/commit/d23e6f5a387d5933bc24e1eb5437da8fd563c1f0) ([#17059](https://github.com/yt-dlp/yt-dlp/issues/17059)) by [bashonly](https://github.com/bashonly)
|
||||
- [Fix `allow-unsafe-ext` compat option](https://github.com/yt-dlp/yt-dlp/commit/e47691215f75fe7e9684080d17fadf340c9a8450) ([#16920](https://github.com/yt-dlp/yt-dlp/issues/16920)) by [bashonly](https://github.com/bashonly)
|
||||
- [Raise minimum recommended Python version to 3.11](https://github.com/yt-dlp/yt-dlp/commit/7b03011294c0210802ffc901390006c39152b999) ([#17034](https://github.com/yt-dlp/yt-dlp/issues/17034)) by [bashonly](https://github.com/bashonly)
|
||||
- [Validate and escape values in `--write-link` output](https://github.com/yt-dlp/yt-dlp/commit/b6590aaa1e3808155d69c9a79a797ae484163789) by [bashonly](https://github.com/bashonly)
|
||||
- **cookies**: [Use `'` instead of `"` for SQL string quoting](https://github.com/yt-dlp/yt-dlp/commit/7a569456f24fd9afcda09ae55c10d3bbd03f46ef) ([#17078](https://github.com/yt-dlp/yt-dlp/issues/17078)) by [Grub4K](https://github.com/Grub4K)
|
||||
- **utils**
|
||||
- [Deprecate `make_dir` in favor of `make_parent_dirs`](https://github.com/yt-dlp/yt-dlp/commit/b05b408d10ebf8f4c47c0db1236eb713b6ad7ab6) ([#16931](https://github.com/yt-dlp/yt-dlp/issues/16931)) by [doe1080](https://github.com/doe1080)
|
||||
- `HTTPHeaderDict`: [Fix `__ior__`](https://github.com/yt-dlp/yt-dlp/commit/cb309b3293c9919cfb55f5d9ffa2c8c109a5f1eb) ([#16930](https://github.com/yt-dlp/yt-dlp/issues/16930)) by [doe1080](https://github.com/doe1080)
|
||||
- `parse_duration`: [Return `int` when appropriate](https://github.com/yt-dlp/yt-dlp/commit/e584a65f2a0feee0c6c363b3309e9ebd6065f6b4) ([#13899](https://github.com/yt-dlp/yt-dlp/issues/13899)) by [doe1080](https://github.com/doe1080)
|
||||
- `parse_resolution`: [Support fps suffixes](https://github.com/yt-dlp/yt-dlp/commit/1249676e98aecf2131901f10f0230fb5e1bdc17e) ([#17073](https://github.com/yt-dlp/yt-dlp/issues/17073)) by [doe1080](https://github.com/doe1080)
|
||||
- `pkcs1pad`: [Fix invalid PKCS#1 v1.5 padding bytes](https://github.com/yt-dlp/yt-dlp/commit/25a05fc0e24ebda38578e87c3f6772a149522786) ([#17035](https://github.com/yt-dlp/yt-dlp/issues/17035)) by [doe1080](https://github.com/doe1080)
|
||||
- `qualities`: [Avoid repeated index lookups](https://github.com/yt-dlp/yt-dlp/commit/3cf981af60dd0b1b6c40807fb542d68ddb0a5f84) ([#17025](https://github.com/yt-dlp/yt-dlp/issues/17025)) by [doe1080](https://github.com/doe1080)
|
||||
- `random_user_agent`: [Bump version range 143-149 => 144-150](https://github.com/yt-dlp/yt-dlp/commit/d120d841dde18839f28cba4a8b0df6d3c4fc6761) ([#17117](https://github.com/yt-dlp/yt-dlp/issues/17117)) by [bashonly](https://github.com/bashonly), [dlp-bot](https://github.com/dlp-bot)
|
||||
|
||||
#### Extractor changes
|
||||
- [Always include `id` in request error output](https://github.com/yt-dlp/yt-dlp/commit/af8a3f34370c82f323ed30074e3fe741ebcc98ca) ([#17068](https://github.com/yt-dlp/yt-dlp/issues/17068)) by [bashonly](https://github.com/bashonly), [HybridDog](https://github.com/HybridDog)
|
||||
- [Fix request logging](https://github.com/yt-dlp/yt-dlp/commit/c13e2f8a20fc1fafb31cba4e6287c874bc0c0cc0) ([#17072](https://github.com/yt-dlp/yt-dlp/issues/17072)) by [bashonly](https://github.com/bashonly)
|
||||
- **arte**: [Fix playlist support](https://github.com/yt-dlp/yt-dlp/commit/16bdcc525e6a550781d65d6fed92a37800ad95e1) ([#13191](https://github.com/yt-dlp/yt-dlp/issues/13191)) by [1100101](https://github.com/1100101)
|
||||
- **bandcamp**: weekly: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/a541df1ea5a593abf3ceaf94ed806e4b52a2c459) ([#16925](https://github.com/yt-dlp/yt-dlp/issues/16925)) by [bashonly](https://github.com/bashonly) (With fixes in [9055188](https://github.com/yt-dlp/yt-dlp/commit/9055188250348c3e6e29eee53e5fb3dc2c951977))
|
||||
- **bilibili**: [Fix API extraction](https://github.com/yt-dlp/yt-dlp/commit/e8de28e23c1ecb4a12b2c3dec188c07e998c412c) ([#13730](https://github.com/yt-dlp/yt-dlp/issues/13730)) by [grqz](https://github.com/grqz)
|
||||
- **instagram**
|
||||
- [Detect when cookies are invalidated](https://github.com/yt-dlp/yt-dlp/commit/ac4c955ea93f35a3a426903141436a6468bddf9f) ([#17126](https://github.com/yt-dlp/yt-dlp/issues/17126)) by [bashonly](https://github.com/bashonly)
|
||||
- [Rework extractor](https://github.com/yt-dlp/yt-dlp/commit/f49b551a0c4c25358d2afaeda4ee63989d2d56ab) ([#17075](https://github.com/yt-dlp/yt-dlp/issues/17075)) by [bashonly](https://github.com/bashonly) (With fixes in [8b8e3e3](https://github.com/yt-dlp/yt-dlp/commit/8b8e3e3cb4d3ba0dedf7b1fd00ce68f07da7e588))
|
||||
- **linkedin**: [Remove broken login support](https://github.com/yt-dlp/yt-dlp/commit/a5e0f87140c6ea73ad3f34914339e873681b4dca) ([#17039](https://github.com/yt-dlp/yt-dlp/issues/17039)) by [0xvd](https://github.com/0xvd)
|
||||
- **mgtv**: [Fix VIP stream extraction](https://github.com/yt-dlp/yt-dlp/commit/6b67e1f2b77feb6d97852be08bfc2e0ab1c8aef0) ([#16982](https://github.com/yt-dlp/yt-dlp/issues/16982)) by [longcharmroeun](https://github.com/longcharmroeun)
|
||||
- **mxplayer**
|
||||
- [Fix extractors](https://github.com/yt-dlp/yt-dlp/commit/01f4f06fdd1e1e088981fa4af3422806aefa0c2a) ([#16988](https://github.com/yt-dlp/yt-dlp/issues/16988)) by [0xvd](https://github.com/0xvd)
|
||||
- [Rework extractors](https://github.com/yt-dlp/yt-dlp/commit/55a58debec7fa5bbaa119dfbc874fb84dd48c76e) ([#17018](https://github.com/yt-dlp/yt-dlp/issues/17018)) by [doe1080](https://github.com/doe1080)
|
||||
- **niconico**
|
||||
- [Fix error detection](https://github.com/yt-dlp/yt-dlp/commit/c4f94545c9d3ce356f2f3149c8fde2134073cee2) ([#16991](https://github.com/yt-dlp/yt-dlp/issues/16991)) by [doe1080](https://github.com/doe1080)
|
||||
- [Support shorts](https://github.com/yt-dlp/yt-dlp/commit/4af5541bb56b08d462f32773dfa15c207de13b74) ([#16992](https://github.com/yt-dlp/yt-dlp/issues/16992)) by [doe1080](https://github.com/doe1080)
|
||||
- **omnyfm**: [Add extractors](https://github.com/yt-dlp/yt-dlp/commit/fa383a9efa02c2f89aba315b17e47fb6bf9e5bee) ([#15942](https://github.com/yt-dlp/yt-dlp/issues/15942)) by [doe1080](https://github.com/doe1080)
|
||||
- **openrec**: [Rework extractors](https://github.com/yt-dlp/yt-dlp/commit/5aa335ecd9d12251b63b5afb23e166ea63cd7271) ([#16857](https://github.com/yt-dlp/yt-dlp/issues/16857)) by [doe1080](https://github.com/doe1080) (With fixes in [6694ef8](https://github.com/yt-dlp/yt-dlp/commit/6694ef82993396847e5a66802634c12944c42a5e))
|
||||
- **patreon**: [Support new URL format](https://github.com/yt-dlp/yt-dlp/commit/707537a03946fbc5707e22be429545c670cd8ec2) ([#16926](https://github.com/yt-dlp/yt-dlp/issues/16926)) by [0xvd](https://github.com/0xvd)
|
||||
- **peertube**: [Support password-protected videos](https://github.com/yt-dlp/yt-dlp/commit/a2483524fbf9c1f5406774622d8d048430b320e9) ([#16873](https://github.com/yt-dlp/yt-dlp/issues/16873)) by [selfhoster1312](https://github.com/selfhoster1312)
|
||||
- **periscope**: [Improve metadata extraction](https://github.com/yt-dlp/yt-dlp/commit/2b27a203f7573cb491c8bef77cb4d944cee6f8cf) ([#16084](https://github.com/yt-dlp/yt-dlp/issues/16084)) by [doe1080](https://github.com/doe1080)
|
||||
- **reddit**: [Remove broken login support](https://github.com/yt-dlp/yt-dlp/commit/917dad55e5b4dbf88d61541477d1830740ab3115) ([#17038](https://github.com/yt-dlp/yt-dlp/issues/17038)) by [0xvd](https://github.com/0xvd)
|
||||
- **soundcloud**
|
||||
- [Extract `uploader_url` for playlists](https://github.com/yt-dlp/yt-dlp/commit/f69e64a954524518add538648f6327611349ead4) ([#16842](https://github.com/yt-dlp/yt-dlp/issues/16842)) by [noseb13eds](https://github.com/noseb13eds)
|
||||
- [Extract comments](https://github.com/yt-dlp/yt-dlp/commit/785e507ef06b709f7f68744f29c16a5cdb8942f2) ([#16938](https://github.com/yt-dlp/yt-dlp/issues/16938)) by [0xvd](https://github.com/0xvd)
|
||||
- [Improve metadata extraction](https://github.com/yt-dlp/yt-dlp/commit/8bdfbfd4461a643e5c37a232b0efd7bcd86a3091) ([#17088](https://github.com/yt-dlp/yt-dlp/issues/17088)) by [noseb13eds](https://github.com/noseb13eds)
|
||||
- **streaks**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/26654a359d8d8dbe3c95b8fc6b0b09aca9a17fba) ([#16413](https://github.com/yt-dlp/yt-dlp/issues/16413)) by [doe1080](https://github.com/doe1080)
|
||||
- **svt**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/c84b2c6736ac5fac6c3eb9a0cbbeee4d6ed84fdd) ([#16288](https://github.com/yt-dlp/yt-dlp/issues/16288)) by [billauer](https://github.com/billauer), [dirkf](https://github.com/dirkf)
|
||||
- **telewebion**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/24aecad5df6090ef9c2a9deff27a7182c5164f07) ([#16986](https://github.com/yt-dlp/yt-dlp/issues/16986)) by [BOplaid](https://github.com/BOplaid)
|
||||
- **trovo**: [Remove dead extractors](https://github.com/yt-dlp/yt-dlp/commit/acc995cf9137918568857d39b68a94498726543a) ([#16353](https://github.com/yt-dlp/yt-dlp/issues/16353)) by [doe1080](https://github.com/doe1080)
|
||||
- **truth**: [Support share URLs](https://github.com/yt-dlp/yt-dlp/commit/e7b5d68f372499167fbf9711de42d3180a92e4de) ([#16096](https://github.com/yt-dlp/yt-dlp/issues/16096)) by [InvalidUsernameException](https://github.com/InvalidUsernameException)
|
||||
- **tviplayer**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/9ae7df9a22b29e2f81825230c9bba7d444190de0) ([#16527](https://github.com/yt-dlp/yt-dlp/issues/16527)) by [FranciscoPombal](https://github.com/FranciscoPombal)
|
||||
- **unsupported**: [Update unsupported sites](https://github.com/yt-dlp/yt-dlp/commit/5678b282e2a17a8181e682a9681461b9c82ff008) ([#17085](https://github.com/yt-dlp/yt-dlp/issues/17085)) by [bashonly](https://github.com/bashonly)
|
||||
- **wrestleuniverse**: vod: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/498e51f5da47539f3b4cc52ff85be5f33e7e9d2f) ([#16685](https://github.com/yt-dlp/yt-dlp/issues/16685)) by [0xvd](https://github.com/0xvd), [bashonly](https://github.com/bashonly), [Sec-Wayne](https://github.com/Sec-Wayne), [tewhalen](https://github.com/tewhalen)
|
||||
- **youtube**
|
||||
- [Fix `extract_relative_time` for abbreviated units](https://github.com/yt-dlp/yt-dlp/commit/6a24c96f7f61e5e651466cc3d4c6a30982318efe) ([#16687](https://github.com/yt-dlp/yt-dlp/issues/16687)) by [dialmaster](https://github.com/dialmaster)
|
||||
- [Fix detection of forced preroll wait time](https://github.com/yt-dlp/yt-dlp/commit/3c279b33cb6d1133624a468e71560c6a75039586) ([#17062](https://github.com/yt-dlp/yt-dlp/issues/17062)) by [bashonly](https://github.com/bashonly)
|
||||
- [Fix minor issues](https://github.com/yt-dlp/yt-dlp/commit/57528faa361314a69d47aead8f44c6a5380ca66a) ([#17060](https://github.com/yt-dlp/yt-dlp/issues/17060)) by [doe1080](https://github.com/doe1080)
|
||||
- [Support live adaptive formats](https://github.com/yt-dlp/yt-dlp/commit/8c1f07d813175cbcf84651f27a827bad84fd8184) ([#16771](https://github.com/yt-dlp/yt-dlp/issues/16771)) by [dreammu](https://github.com/dreammu)
|
||||
- tab
|
||||
- [Fix flat extraction of collaborators](https://github.com/yt-dlp/yt-dlp/commit/a75ba96fa48522738b91a907773a6fa9efe6e2d4) ([#17045](https://github.com/yt-dlp/yt-dlp/issues/17045)) by [bashonly](https://github.com/bashonly)
|
||||
- [Fix flat playlist metadata extraction](https://github.com/yt-dlp/yt-dlp/commit/ad6b5f4b3552472c17b3955a3f1525296bed6137) ([#16965](https://github.com/yt-dlp/yt-dlp/issues/16965)) by [bashonly](https://github.com/bashonly)
|
||||
- [Fix metadata extraction](https://github.com/yt-dlp/yt-dlp/commit/d6c411bcd0a0519a0db3b330df2530c8213eb9f0) ([#16976](https://github.com/yt-dlp/yt-dlp/issues/16976)) by [bashonly](https://github.com/bashonly)
|
||||
- [Fix pagination](https://github.com/yt-dlp/yt-dlp/commit/b23046bbc8e53f32a3853dc33138f2986f3aed06) ([#16948](https://github.com/yt-dlp/yt-dlp/issues/16948)) by [bashonly](https://github.com/bashonly)
|
||||
- **zan**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/ad9a6f25f69344ebc060f7be223e5c19fc03e9b5) ([#16086](https://github.com/yt-dlp/yt-dlp/issues/16086)) by [doe1080](https://github.com/doe1080)
|
||||
- **zdf**: [Detect livestreams](https://github.com/yt-dlp/yt-dlp/commit/cfee151fcd9e1a0f4e5af9ca8440f1253cb70b88) ([#16954](https://github.com/yt-dlp/yt-dlp/issues/16954)) by [InvalidUsernameException](https://github.com/InvalidUsernameException)
|
||||
- **zoom**: clips: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/4a6296248fb6218d77221da4e5421c84bbc792d4) ([#17005](https://github.com/yt-dlp/yt-dlp/issues/17005)) by [Bnyro](https://github.com/Bnyro)
|
||||
|
||||
#### Downloader changes
|
||||
- **external**
|
||||
- [Fix resuming downloads with aria2c](https://github.com/yt-dlp/yt-dlp/commit/500e54cf860e4807d259bfe6a7abb47e51364a3b) ([#11698](https://github.com/yt-dlp/yt-dlp/issues/11698)) by [tcely](https://github.com/tcely)
|
||||
- `curl`: [Support development versions](https://github.com/yt-dlp/yt-dlp/commit/7937a139cf3e1e1ccd1b277f18ab73fc6dec06a7) ([#16922](https://github.com/yt-dlp/yt-dlp/issues/16922)) by [syphyr](https://github.com/syphyr)
|
||||
- **mhtml**: [Fix storyboard content-length calculation](https://github.com/yt-dlp/yt-dlp/commit/8902f6ba8c7baba8fb43fb08ea1a9ddfef77e998) ([#13998](https://github.com/yt-dlp/yt-dlp/issues/13998)) by [Masterjun3](https://github.com/Masterjun3)
|
||||
|
||||
#### Misc. changes
|
||||
- **build**
|
||||
- [Update 17 dependencies](https://github.com/yt-dlp/yt-dlp/commit/8a5f1dd3bd39ad2e917755a29b313a51b5446871) ([#17014](https://github.com/yt-dlp/yt-dlp/issues/17014)) by [dlp-bot](https://github.com/dlp-bot)
|
||||
- [Update 6 dependencies](https://github.com/yt-dlp/yt-dlp/commit/40dd052c03c2d1b5c180f393d9344be2bd718ba3) ([#17086](https://github.com/yt-dlp/yt-dlp/issues/17086)) by [dlp-bot](https://github.com/dlp-bot)
|
||||
- **ci**
|
||||
- [Add label handler workflow](https://github.com/yt-dlp/yt-dlp/commit/1472d1098020d46ea1f15940b80d515ad13eb420) ([#17128](https://github.com/yt-dlp/yt-dlp/issues/17128)) by [bashonly](https://github.com/bashonly)
|
||||
- [Bump actions/checkout v6.0.3 => v7.0.0](https://github.com/yt-dlp/yt-dlp/commit/da99b21b2d6e32690d1871afc9e9779701dd7f8c) ([#17015](https://github.com/yt-dlp/yt-dlp/issues/17015)) by [dlp-bot](https://github.com/dlp-bot)
|
||||
- [Update 5 actions in 8 workflows](https://github.com/yt-dlp/yt-dlp/commit/84db5246faab5bffb8909de60c75e7c692b5dd79) ([#17024](https://github.com/yt-dlp/yt-dlp/issues/17024)) by [dlp-bot](https://github.com/dlp-bot)
|
||||
- **cleanup**
|
||||
- [Fix invalid info dict fields](https://github.com/yt-dlp/yt-dlp/commit/98036ccd4fd9a6b15e025bbc164e27286e342be7) ([#17007](https://github.com/yt-dlp/yt-dlp/issues/17007)) by [seproDev](https://github.com/seproDev)
|
||||
- [Fix minor mistakes](https://github.com/yt-dlp/yt-dlp/commit/b0472c3bce71eb08996a8d70c4c1b1df8cad4042) ([#17083](https://github.com/yt-dlp/yt-dlp/issues/17083)) by [doe1080](https://github.com/doe1080)
|
||||
- [Remove RTSP and MMS support](https://github.com/yt-dlp/yt-dlp/commit/bed3d58c5c1ff088584c5dac3c16eea4ff01c7b5) ([#17030](https://github.com/yt-dlp/yt-dlp/issues/17030)) by [seproDev](https://github.com/seproDev)
|
||||
- [Replace dead example/test URL](https://github.com/yt-dlp/yt-dlp/commit/c102b2096525218a6918a46b415fb167786f9656) ([#17061](https://github.com/yt-dlp/yt-dlp/issues/17061)) by [bashonly](https://github.com/bashonly)
|
||||
- Miscellaneous: [997fa14](https://github.com/yt-dlp/yt-dlp/commit/997fa140840a08df3938b40da470c78049fef1f6) by [bashonly](https://github.com/bashonly), [seproDev](https://github.com/seproDev)
|
||||
- **docs**: [Make "No AI / No LLM Policy" abundantly clear](https://github.com/yt-dlp/yt-dlp/commit/249aa5d6e667308fbf95ae5cfb40eba8177a802c) ([#16285](https://github.com/yt-dlp/yt-dlp/issues/16285)) by [bashonly](https://github.com/bashonly)
|
||||
|
||||
### 2026.06.09
|
||||
|
||||
#### Important changes
|
||||
|
||||
+7
-1
@@ -78,6 +78,13 @@ Core Maintainers are responsible for reviewing and merging contributions, publis
|
||||
* Improved/fixed/added ArdMediathek, DRTV, Floatplane, MagentaMusik, Naver, Nebula, OnDemandKorea, Vbox7 etc
|
||||
|
||||
|
||||
## Maintainers
|
||||
|
||||
Maintainers are stewards of the project's codebase who can review and merge pull requests.
|
||||
|
||||
- [doe1080](https://github.com/doe1080)
|
||||
|
||||
|
||||
## Triage Maintainers
|
||||
|
||||
Triage Maintainers are frequent contributors who can manage issues and pull requests.
|
||||
@@ -86,5 +93,4 @@ Triage Maintainers are frequent contributors who can manage issues and pull requ
|
||||
- [garret1317](https://github.com/garret1317)
|
||||
- [pzhlkj6612](https://github.com/pzhlkj6612)
|
||||
- [DTrombett](https://github.com/dtrombett)
|
||||
- [doe1080](https://github.com/doe1080)
|
||||
- [grqz](https://github.com/grqz)
|
||||
|
||||
@@ -142,11 +142,11 @@ While yt-dlp is licensed under the [Unlicense](LICENSE), many of the release fil
|
||||
|
||||
Most notably, the PyInstaller-bundled executables include GPLv3+ licensed code, and as such the combined work is licensed under [GPLv3+](https://www.gnu.org/licenses/gpl-3.0.html).
|
||||
|
||||
The zipimport Unix executable (`yt-dlp`) contains [ISC](https://github.com/meriyah/meriyah/blob/main/LICENSE.md) licensed code from [`meriyah`](https://github.com/meriyah/meriyah) and [MIT](https://github.com/davidbonnet/astring/blob/main/LICENSE) licensed code from [`astring`](https://github.com/davidbonnet/astring).
|
||||
The zipimport Unix executable (`yt-dlp`) and release tarball (`yt-dlp.tar.gz`) contain [ISC](https://github.com/meriyah/meriyah/blob/main/LICENSE.md) licensed code from [`meriyah`](https://github.com/meriyah/meriyah) and [MIT](https://github.com/davidbonnet/astring/blob/main/LICENSE) licensed code from [`astring`](https://github.com/davidbonnet/astring).
|
||||
|
||||
See [THIRD_PARTY_LICENSES.txt](THIRD_PARTY_LICENSES.txt) for more details.
|
||||
|
||||
The git repository, the source tarball (`yt-dlp.tar.gz`), the PyPI source distribution and the PyPI built distribution (wheel) only contain code licensed under the [Unlicense](LICENSE).
|
||||
The git repository, the PyPI source distribution and the PyPI built distribution (wheel) only contain code licensed under the [Unlicense](LICENSE).
|
||||
|
||||
<!-- MANPAGE: END EXCLUDED SECTION -->
|
||||
|
||||
@@ -245,7 +245,6 @@ The following provide support for impersonating browser requests. This may be re
|
||||
### Deprecated
|
||||
|
||||
* [**rtmpdump**](http://rtmpdump.mplayerhq.hu) - For downloading `rtmp` streams. ffmpeg can be used instead with `--downloader ffmpeg`. Licensed under [GPLv2+](http://rtmpdump.mplayerhq.hu)
|
||||
* [**mplayer**](http://mplayerhq.hu/design7/info.html) or [**mpv**](https://mpv.io) - For downloading `rstp`/`mms` streams. ffmpeg can be used instead with `--downloader ffmpeg`. Licensed under [GPLv2+](https://github.com/mpv-player/mpv/blob/master/Copyright)
|
||||
|
||||
To use or redistribute the dependencies, you must agree to their respective licensing terms.
|
||||
|
||||
@@ -403,7 +402,7 @@ Tip: Use `CTRL`+`F` (or `Command`+`F`) to search by keywords
|
||||
(default)
|
||||
--live-from-start Download livestreams from the start.
|
||||
Currently experimental and only supported
|
||||
for YouTube, Twitch, and TVer
|
||||
for YouTube, Twitch, TVer, and mellow-fan
|
||||
--no-live-from-start Download livestreams from the current time
|
||||
(default)
|
||||
--wait-for-video MIN[-MAX] Wait for scheduled streams to become
|
||||
@@ -625,16 +624,16 @@ Tip: Use `CTRL`+`F` (or `Command`+`F`) to search by keywords
|
||||
"*10:15-inf" --download-sections "intro"
|
||||
--downloader [PROTO:]NAME Name or path of the external downloader to
|
||||
use (optionally) prefixed by the protocols
|
||||
(http, ftp, m3u8, dash, rstp, rtmp, mms) to
|
||||
use it for. Currently supports native,
|
||||
aria2c, axel, curl, ffmpeg, httpie, wget.
|
||||
You can use this option multiple times to
|
||||
set different downloaders for different
|
||||
protocols. E.g. --downloader aria2c
|
||||
--downloader "dash,m3u8:native" will use
|
||||
aria2c for http/ftp downloads, and the
|
||||
native downloader for dash/m3u8 downloads
|
||||
(Alias: --external-downloader)
|
||||
(http, ftp, m3u8, dash, rtmp) to use it for.
|
||||
Currently supports native, aria2c, axel,
|
||||
curl, ffmpeg, httpie, wget. You can use this
|
||||
option multiple times to set different
|
||||
downloaders for different protocols. E.g.
|
||||
--downloader aria2c --downloader
|
||||
"dash,m3u8:native" will use aria2c for
|
||||
http/ftp downloads, and the native
|
||||
downloader for dash/m3u8 downloads (Alias:
|
||||
--external-downloader)
|
||||
--downloader-args NAME:ARGS Give these arguments to the external
|
||||
downloader. Specify the downloader name and
|
||||
the arguments separated by a colon ":". For
|
||||
@@ -1587,7 +1586,7 @@ Also filtering work for comparisons `=` (equals), `^=` (starts with), `$=` (ends
|
||||
- `acodec`: Name of the audio codec in use
|
||||
- `vcodec`: Name of the video codec in use
|
||||
- `container`: Name of the container format
|
||||
- `protocol`: The protocol that will be used for the actual download, lower-case (`http`, `https`, `rtsp`, `rtmp`, `rtmpe`, `mms`, `f4m`, `ism`, `http_dash_segments`, `m3u8`, or `m3u8_native`)
|
||||
- `protocol`: The protocol that will be used for the actual download, lower-case (`http`, `https`, `rtmp`, `rtmpe`, `f4m`, `ism`, `http_dash_segments`, `m3u8`, or `m3u8_native`)
|
||||
- `language`: Language code
|
||||
- `dynamic_range`: The dynamic range of the video
|
||||
- `format_id`: A short description of the format
|
||||
@@ -1615,7 +1614,7 @@ The available fields are:
|
||||
- `lang`: The language preference as determined by the extractor (e.g. original language preferred over audio description)
|
||||
- `quality`: The quality of the format
|
||||
- `source`: The preference of the source
|
||||
- `proto`: Protocol used for download (`https`/`ftps` > `http`/`ftp` > `m3u8_native`/`m3u8` > `http_dash_segments`> `websocket_frag` > `mms`/`rtsp` > `f4f`/`f4m`)
|
||||
- `proto`: Protocol used for download (`https`/`ftps` > `http`/`ftp` > `m3u8_native`/`m3u8` > `http_dash_segments`> `websocket_frag` > `f4f`/`f4m`)
|
||||
- `vcodec`: Video Codec (`av01` > `vp9.2` > `vp9` > `h265` > `h264` > `vp8` > `h263` > `theora` > other)
|
||||
- `acodec`: Audio Codec (`flac`/`alac` > `wav`/`aiff` > `opus` > `vorbis` > `aac` > `mp4a` > `mp3` > `ac4` > `eac3` > `ac3` > `dts` > other)
|
||||
- `codec`: Equivalent to `vcodec,acodec`
|
||||
@@ -1860,7 +1859,7 @@ The following extractors use this feature:
|
||||
#### youtube
|
||||
* `lang`: Prefer translated metadata (`title`, `description` etc) of this language code (case-sensitive). By default, the video primary language metadata is preferred, with a fallback to `en` translated. See [youtube/_base.py](https://github.com/yt-dlp/yt-dlp/blob/415b4c9f955b1a0391204bd24a7132590e7b3bdb/yt_dlp/extractor/youtube/_base.py#L402-L409) for the list of supported content language codes
|
||||
* `skip`: One or more of `hls`, `dash` or `translated_subs` to skip extraction of the m3u8 manifests, dash manifests and [auto-translated subtitles](https://github.com/yt-dlp/yt-dlp/issues/4090#issuecomment-1158102032) respectively
|
||||
* `player_client`: Clients to extract video data from. The currently available clients are `web`, `web_safari`, `web_embedded`, `web_music`, `web_creator`, `mweb`, `ios`, `android`, `android_vr`, `tv`, `tv_downgraded`, and `tv_simply`. By default, `android_vr,web_safari` is used. If no JavaScript runtime/engine is available, then only `android_vr` is used. If logged-in cookies are passed to yt-dlp, then `tv_downgraded,web_safari` is used for free accounts and `tv_downgraded,web_creator` is used for premium accounts. The `web_music` client is added for `music.youtube.com` URLs when logged-in cookies are used. The `web_embedded` client is added for age-restricted videos but only successfully works around the age-restriction sometimes (e.g. if the video is embeddable), and may be added as a fallback if `android_vr` is unable to access a video. The `web_creator` client is added for age-restricted videos if account age-verification is required. Some clients, such as `web_creator` and `web_music`, require a `po_token` for their formats to be downloadable. Some clients, such as `web_creator`, will only work with authentication. Not all clients support authentication via cookies. You can use `default` for the default clients, or you can use `all` for all clients (not recommended). You can prefix a client with `-` to exclude it, e.g. `youtube:player_client=default,-web_safari`
|
||||
* `player_client`: Clients to extract video data from. The currently available clients are `web`, `web_safari`, `web_embedded`, `web_music`, `web_creator`, `mweb`, `ios`, `visionos`, `android`, `android_vr`, `tv`, `tv_downgraded`, and `tv_simply`. By default, `visionos,android_vr,web` is used. If no JavaScript runtime/engine is available, then `web` is omitted. If logged-in cookies are passed to yt-dlp, then `tv_downgraded,web` is used for free accounts and `tv_downgraded,web_creator,web` is used for premium accounts. The `web_music` client is added for `music.youtube.com` URLs when logged-in cookies are used. The `web_embedded` client is added for age-restricted videos but only successfully works around the age-restriction sometimes (e.g. if the video is embeddable). The `tv_downgraded` client may be added as a fallback if `android_vr` or `visionos` is unable to access a video. The `web_creator` client is added for age-restricted videos if account age-verification is required. Some clients, such as `web_creator` and `web_music`, require a `po_token` for their formats to be downloadable. Some clients, such as `web_creator`, will only work with authentication. Not all clients support authentication via cookies. You can use `default` for the default clients, or you can use `all` for all clients (not recommended). You can prefix a client with `-` to exclude it, e.g. `youtube:player_client=default,-web`
|
||||
* `player_skip`: Skip some network requests that are generally needed for robust extraction. One or more of `configs` (skip client configs), `webpage` (skip initial webpage), `js` (skip js player), `initial_data` (skip initial data/next ep request). While these options can help reduce the number of requests needed or avoid some rate-limiting, they could cause issues such as missing formats or metadata. See [#860](https://github.com/yt-dlp/yt-dlp/pull/860) and [#12826](https://github.com/yt-dlp/yt-dlp/issues/12826) for more details
|
||||
* `webpage_skip`: Skip extraction of embedded webpage data. One or both of `player_response`, `initial_data`. These options are for testing purposes and don't skip any network requests. Neither is skipped by default; however, if a `player_js_version` value other than `actual` is used, then `webpage_skip=player_response` is implied
|
||||
* `webpage_client`: Client to use for the video webpage request. One of `web` or `web_safari` (default)
|
||||
@@ -1871,7 +1870,7 @@ The following extractors use this feature:
|
||||
* `max_comments`: Limit the amount of comments to gather. Comma-separated list of integers representing `max-comments,max-parents,max-replies,max-replies-per-thread,max-depth`. Default is `all,all,all,all,all`
|
||||
* A `max-depth` value of `1` will discard all replies, regardless of the `max-replies` or `max-replies-per-thread` values given
|
||||
* E.g. `all,all,1000,10,2` will get a maximum of 1000 replies total, with up to 10 replies per thread, and only 2 levels of depth (i.e. top-level comments plus their immediate replies). `1000,all,100` will get a maximum of 1000 comments, with a maximum of 100 replies total
|
||||
* `formats`: Change the types of formats to return. `dashy` (convert HTTP to DASH), `duplicate` (identical content but different URLs or protocol; includes `dashy`), `incomplete` (cannot be downloaded completely - live dash, live adaptive https, and post-live m3u8), `missing_pot` (include formats that require a PO Token but are missing one)
|
||||
* `formats`: Change the types of formats to return. `dashy` (convert HTTP to DASH), `duplicate` (identical content but different URLs or protocol; includes `dashy`), `incomplete` (cannot be downloaded completely - live and post-live dash, post-live m3u8, and live adaptive https without --live-from-start), `missing_pot` (include formats that require a PO Token but are missing one)
|
||||
* `innertube_host`: Innertube API host to use for all API requests; e.g. `studio.youtube.com`, `youtubei.googleapis.com`. Note that cookies exported from one subdomain will not work on others
|
||||
* `innertube_key`: Innertube API key to use for all API requests. By default, no API key is used
|
||||
* `raise_incomplete_data`: `Incomplete Data Received` raises an error instead of reporting a warning
|
||||
@@ -1969,13 +1968,19 @@ The following extractors use this feature:
|
||||
#### sonylivseries
|
||||
* `sort_order`: Episode sort order for series extraction - one of `asc` (ascending, oldest first) or `desc` (descending, newest first). Default is `asc`
|
||||
|
||||
#### streaks
|
||||
* `api_key`: API key for the `X-Streaks-Api-Key` header
|
||||
|
||||
#### tver
|
||||
* `backend`: Backend API to use for extraction - one of `streaks` (default) or `brightcove` (deprecated)
|
||||
|
||||
#### vimeo
|
||||
* `client`: Client to extract video data from. The currently available clients are `android`, `ios`, `macos` and `web`. Only one client can be used. The `macos` client is used by default, but the `web` client is used when logged-in. The `web` client only works with account cookies or login credentials. The `android` and `ios` clients only work with previously cached OAuth tokens
|
||||
* `client`: Client to extract video data from. The currently available clients are `android`, `macos_basic`, and `web`. Only one client can be used. The `macos_basic` client is used by default, but the `web` client is used when logged-in. The `web` client only works with account cookies or login credentials. The `android` client only works with previously cached OAuth tokens
|
||||
* `original_format_policy`: Policy for when to try extracting original formats. One of `always`, `never`, or `auto`. The default `auto` policy tries to avoid exceeding the web client's API rate-limit by only making an extra request when Vimeo publicizes the video's downloadability
|
||||
|
||||
#### zan
|
||||
* `split_angles`: Split multi-angle streams into separate angle formats. Forces re-encoding of the video stream during download, and requires ffmpeg. Either `true` or `false` (default)
|
||||
|
||||
**Note**: These options may be changed/removed in the future without concern for backward compatibility
|
||||
|
||||
<!-- MANPAGE: MOVE "INSTALLATION" SECTION HERE -->
|
||||
|
||||
+891
-528
File diff suppressed because it is too large
Load Diff
@@ -38,11 +38,16 @@ def main():
|
||||
f'--name={name}',
|
||||
'--icon=devscripts/logo.ico',
|
||||
'--upx-exclude=vcruntime140.dll',
|
||||
# setuptools and packaging are PyInstaller runtime dependencies,
|
||||
# but would be collected due to cffi's imports if we don't exclude
|
||||
'--exclude-module=setuptools',
|
||||
'--exclude-module=packaging',
|
||||
# Ref: https://github.com/yt-dlp/yt-dlp/issues/13311
|
||||
# https://github.com/pyinstaller/pyinstaller/issues/9149
|
||||
'--exclude-module=pkg_resources',
|
||||
'--noconfirm',
|
||||
'--additional-hooks-dir=yt_dlp/__pyinstaller',
|
||||
'--add-data=THIRD_PARTY_LICENSES.txt:.',
|
||||
*opts,
|
||||
'yt_dlp/__main__.py',
|
||||
]
|
||||
|
||||
@@ -68,9 +68,9 @@ brotlicffi==1.2.0.1 ; implementation_name != 'cpython' \
|
||||
--hash=sha256:e015af99584c6db1490a69a210c765953e473e63adc2d891ac3062a737c9e851 \
|
||||
--hash=sha256:f2a5575653b0672638ba039b82fda56854934d7a6a24d4b8b5033f73ab43cbc1
|
||||
# via yt-dlp
|
||||
certifi==2026.5.20 \
|
||||
--hash=sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897 \
|
||||
--hash=sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d
|
||||
certifi==2026.6.17 \
|
||||
--hash=sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432 \
|
||||
--hash=sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db
|
||||
# via
|
||||
# curl-cffi
|
||||
# requests
|
||||
@@ -286,9 +286,9 @@ mdurl==0.1.2 ; implementation_name == 'cpython' \
|
||||
--hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \
|
||||
--hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba
|
||||
# via markdown-it-py
|
||||
mutagen==1.47.0 \
|
||||
--hash=sha256:719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99 \
|
||||
--hash=sha256:edd96f50c5907a9539d8e5bba7245f62c9f520aef333d13392a79a4f70aca719
|
||||
mutagen==1.48.1 \
|
||||
--hash=sha256:4f077fe87d3fc7fba259aa63d8c026b18382ca6a42ef37c61e16f1b1b5b82fe7 \
|
||||
--hash=sha256:8f95637ab9f6f305cec6bd1294e197debe207998e3e068596563c74f86b0a173
|
||||
# via yt-dlp
|
||||
pycparser==3.0 ; implementation_name != 'PyPy' \
|
||||
--hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \
|
||||
|
||||
@@ -68,9 +68,9 @@ brotlicffi==1.2.0.1 ; implementation_name != 'cpython' \
|
||||
--hash=sha256:e015af99584c6db1490a69a210c765953e473e63adc2d891ac3062a737c9e851 \
|
||||
--hash=sha256:f2a5575653b0672638ba039b82fda56854934d7a6a24d4b8b5033f73ab43cbc1
|
||||
# via yt-dlp
|
||||
certifi==2026.5.20 \
|
||||
--hash=sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897 \
|
||||
--hash=sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d
|
||||
certifi==2026.6.17 \
|
||||
--hash=sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432 \
|
||||
--hash=sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db
|
||||
# via
|
||||
# requests
|
||||
# yt-dlp
|
||||
@@ -178,9 +178,9 @@ idna==3.18 \
|
||||
--hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \
|
||||
--hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848
|
||||
# via requests
|
||||
mutagen==1.47.0 \
|
||||
--hash=sha256:719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99 \
|
||||
--hash=sha256:edd96f50c5907a9539d8e5bba7245f62c9f520aef333d13392a79a4f70aca719
|
||||
mutagen==1.48.1 \
|
||||
--hash=sha256:4f077fe87d3fc7fba259aa63d8c026b18382ca6a42ef37c61e16f1b1b5b82fe7 \
|
||||
--hash=sha256:8f95637ab9f6f305cec6bd1294e197debe207998e3e068596563c74f86b0a173
|
||||
# via yt-dlp
|
||||
pycryptodomex==3.23.0 \
|
||||
--hash=sha256:02d87b80778c171445d67e23d1caef279bf4b25c3597050ccd2e13970b57fd51 \
|
||||
|
||||
+25
-25
@@ -62,9 +62,9 @@ pygments==2.20.0 \
|
||||
--hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \
|
||||
--hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176
|
||||
# via pytest
|
||||
pytest==9.1.0 \
|
||||
--hash=sha256:41dd9148c08072446394cefd3d79701701335a9f4cae69ba92e39f6c7f5c061c \
|
||||
--hash=sha256:8ebb0e7888bdf2bdfc602ec51f8f62d50200af37356c74e503c79a94f5c81f32
|
||||
pytest==9.1.1 \
|
||||
--hash=sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313 \
|
||||
--hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c
|
||||
# via pytest-rerunfailures
|
||||
pytest-rerunfailures==16.3 \
|
||||
--hash=sha256:37c9b1231c8083e9f4e724f50f7a21241822f9516c15c700ebbf218d6452355c \
|
||||
@@ -132,25 +132,25 @@ pyyaml==6.0.3 \
|
||||
--hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \
|
||||
--hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0
|
||||
# via pre-commit
|
||||
ruff==0.15.17 \
|
||||
--hash=sha256:0e01a84ddbc8c16c23055ba3924476850f1bbc1917cebbb9376665a63e74260d \
|
||||
--hash=sha256:25805a226d741c47d274a35ad5c10a7dde175fcddfa511d7cf3da0a21eb3eab7 \
|
||||
--hash=sha256:2ec446937fd16c8c4de2674a209cc5af64d9c6f17d21fbf1151054fa0bcf5219 \
|
||||
--hash=sha256:382fc0521025f5a8ad447d8bdd523545d0d7646adb718eb1c2dac5065ec27c0f \
|
||||
--hash=sha256:456d41fcd1b2777ad63f09a6e7121d43f7b688bbc76a800c10f7f8fb1f912c3f \
|
||||
--hash=sha256:596065960ab1ff593f744220c9fe6580eda00a95003cffa9f4048bb5b1bf0392 \
|
||||
--hash=sha256:6769e5fa1710b179b92e0bfa5a51735b35baea9013dadb06d5f44cbcf9547084 \
|
||||
--hash=sha256:6ba0c1e4f95bcb3869d0d30cbd5917071ef2e28665abfec970cdab0492c713ed \
|
||||
--hash=sha256:6eccbe50a038b503e7140b441aa9c7fc8c1f36edf23ebef9f4165c2f28f568b7 \
|
||||
--hash=sha256:81647960f10bff57d2e51cadd0c3950fe598400c852863a038720ef5b8cca91e \
|
||||
--hash=sha256:84fe9f653152f8f294f9f7e03bf3a453d8b4a27f7a59c78c8666167f2b17b96c \
|
||||
--hash=sha256:8c0fe88a7676e7a05b73174d4d4a59cb2ac21ff8263583f87a81a6018475a978 \
|
||||
--hash=sha256:b1a04bcc94ae6194e9db05d16ad31f298a7194bfbcb08258bbe589cee1d587b8 \
|
||||
--hash=sha256:b8461180b22420b1bdc289909410930761629fddf2a5aaf60fae1ab26cedc4c4 \
|
||||
--hash=sha256:d9feddb927fc68bd295f5eebc587a7e42cfaf9b65f60ca4a2386febff575da8f \
|
||||
--hash=sha256:ecfc3c7878fff94633ab0348524e093f9ce3243080416dd7d14f8ba400174719 \
|
||||
--hash=sha256:f3be1fbb34bcdfd146240d8fb92a709d4c2c8191348580a3c044ec60fa0b4456 \
|
||||
--hash=sha256:f6ad73b14c2d18a3bf8ad7cb6974294d7f613a7898604826058e6ac64918ef4d
|
||||
ruff==0.15.19 \
|
||||
--hash=sha256:08143f0685ae278b30727ea72e90c61e5bd9c31b91aac4f5bb989538f73d24b8 \
|
||||
--hash=sha256:27c15b2a241dd4d995557949a094fe78b8ad99122a38ccae1595849bcc947b3f \
|
||||
--hash=sha256:4171b6613effa9363cd46dd4f75bd1827b6d1b946b5e278ed0c600d305379445 \
|
||||
--hash=sha256:4d190d8f62a0b94aba8f721116538a9ee29b1e74d26650846ba9b99f0ae21c40 \
|
||||
--hash=sha256:5a2c86ba6870dd415a9d9eb8be94d7924ebec6a26ffc7958ec7ca29d4bff967d \
|
||||
--hash=sha256:6a498f82e0f4d8904c4e0aea5139cdfac1f39d19a3c51d491292f63a36e83b2e \
|
||||
--hash=sha256:6c6b607466e47349332eb1d9be52fb1467423fc07c217341af41cd0f3f0573be \
|
||||
--hash=sha256:82b432bc087264aea70fd25ac198918b70bd9e2aa0db4297b0bb91bbfbbc63ce \
|
||||
--hash=sha256:8530a09d03b3a8c994f8b559a7dcdabc690bcd3f78ef276c38c83166798ebf56 \
|
||||
--hash=sha256:87bf21fb3875fe69f0eacc825411657e2e85589cce633c35c0adf1113649c62b \
|
||||
--hash=sha256:8f47f0f92952af2557212bb10cf3e695cd4cf28b2c6e42cdb18ec6c9ebfa19da \
|
||||
--hash=sha256:922d1eb283161564759bd49f507e91dc6112c15da8bd5b84ed714e086243cf86 \
|
||||
--hash=sha256:c6c754515be7b76afe6e7e62df7776709571bcfc1631183828afcf3bafa869e3 \
|
||||
--hash=sha256:d48caa34488fb521fd0ef4aea2b0e8fe758298df044138f0d67b687a6a0d07ed \
|
||||
--hash=sha256:ed03b7862d68f0a8771d50ee129980cbf1b113f96e250b73954bc292f689e0bb \
|
||||
--hash=sha256:edc27f7172a93b32b102687009d6a588508815072141543ae603a8b9b0823063 \
|
||||
--hash=sha256:efeca47ee3f9d4a7162655a3b8e6ee4a878646044233978d4d2c1ff8cdd914f0 \
|
||||
--hash=sha256:f9b229cb3ef56ecc2c1c8ebeca64b7a7740ccaef40a9eb097e78dde5a8560b83
|
||||
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.5.0 \
|
||||
--hash=sha256:8f7c38605023688c89789f566959006af6d61c99eeeb9e58342eb780c5761e5e \
|
||||
--hash=sha256:98847aadf5e2037e0e4d2e19528eb3aca6f23906422e59a510bff231a6d32fce
|
||||
virtualenv==21.5.1 \
|
||||
--hash=sha256:55aa670b67bbfb991b03fda39bd3276d92c419d702376e98c5df1c9989a26783 \
|
||||
--hash=sha256:dca3bf98275a59c652b69d68e73433e597d977c2da9198882479d1a7188009c8
|
||||
# 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.5.20 \
|
||||
--hash=sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897 \
|
||||
--hash=sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d
|
||||
certifi==2026.6.17 \
|
||||
--hash=sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432 \
|
||||
--hash=sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db
|
||||
# via
|
||||
# curl-cffi
|
||||
# requests
|
||||
@@ -339,9 +339,9 @@ mdurl==0.1.2 ; implementation_name == 'cpython' \
|
||||
--hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \
|
||||
--hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba
|
||||
# via markdown-it-py
|
||||
mutagen==1.47.0 \
|
||||
--hash=sha256:719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99 \
|
||||
--hash=sha256:edd96f50c5907a9539d8e5bba7245f62c9f520aef333d13392a79a4f70aca719
|
||||
mutagen==1.48.1 \
|
||||
--hash=sha256:4f077fe87d3fc7fba259aa63d8c026b18382ca6a42ef37c61e16f1b1b5b82fe7 \
|
||||
--hash=sha256:8f95637ab9f6f305cec6bd1294e197debe207998e3e068596563c74f86b0a173
|
||||
# via yt-dlp
|
||||
pycparser==3.0 ; implementation_name != 'PyPy' \
|
||||
--hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \
|
||||
|
||||
@@ -74,9 +74,9 @@ brotlicffi==1.2.0.1 ; implementation_name != 'cpython' \
|
||||
--hash=sha256:e015af99584c6db1490a69a210c765953e473e63adc2d891ac3062a737c9e851 \
|
||||
--hash=sha256:f2a5575653b0672638ba039b82fda56854934d7a6a24d4b8b5033f73ab43cbc1
|
||||
# via yt-dlp
|
||||
certifi==2026.5.20 \
|
||||
--hash=sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897 \
|
||||
--hash=sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d
|
||||
certifi==2026.6.17 \
|
||||
--hash=sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432 \
|
||||
--hash=sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db
|
||||
# via
|
||||
# curl-cffi
|
||||
# requests
|
||||
@@ -202,9 +202,9 @@ mdurl==0.1.2 ; implementation_name == 'cpython' \
|
||||
--hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \
|
||||
--hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba
|
||||
# via markdown-it-py
|
||||
mutagen==1.47.0 \
|
||||
--hash=sha256:719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99 \
|
||||
--hash=sha256:edd96f50c5907a9539d8e5bba7245f62c9f520aef333d13392a79a4f70aca719
|
||||
mutagen==1.48.1 \
|
||||
--hash=sha256:4f077fe87d3fc7fba259aa63d8c026b18382ca6a42ef37c61e16f1b1b5b82fe7 \
|
||||
--hash=sha256:8f95637ab9f6f305cec6bd1294e197debe207998e3e068596563c74f86b0a173
|
||||
# via yt-dlp
|
||||
packaging==26.2 \
|
||||
--hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \
|
||||
|
||||
@@ -5,25 +5,25 @@ pycodestyle==2.14.0 \
|
||||
--hash=sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783 \
|
||||
--hash=sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d
|
||||
# via autopep8
|
||||
ruff==0.15.17 \
|
||||
--hash=sha256:0e01a84ddbc8c16c23055ba3924476850f1bbc1917cebbb9376665a63e74260d \
|
||||
--hash=sha256:25805a226d741c47d274a35ad5c10a7dde175fcddfa511d7cf3da0a21eb3eab7 \
|
||||
--hash=sha256:2ec446937fd16c8c4de2674a209cc5af64d9c6f17d21fbf1151054fa0bcf5219 \
|
||||
--hash=sha256:382fc0521025f5a8ad447d8bdd523545d0d7646adb718eb1c2dac5065ec27c0f \
|
||||
--hash=sha256:456d41fcd1b2777ad63f09a6e7121d43f7b688bbc76a800c10f7f8fb1f912c3f \
|
||||
--hash=sha256:596065960ab1ff593f744220c9fe6580eda00a95003cffa9f4048bb5b1bf0392 \
|
||||
--hash=sha256:6769e5fa1710b179b92e0bfa5a51735b35baea9013dadb06d5f44cbcf9547084 \
|
||||
--hash=sha256:6ba0c1e4f95bcb3869d0d30cbd5917071ef2e28665abfec970cdab0492c713ed \
|
||||
--hash=sha256:6eccbe50a038b503e7140b441aa9c7fc8c1f36edf23ebef9f4165c2f28f568b7 \
|
||||
--hash=sha256:81647960f10bff57d2e51cadd0c3950fe598400c852863a038720ef5b8cca91e \
|
||||
--hash=sha256:84fe9f653152f8f294f9f7e03bf3a453d8b4a27f7a59c78c8666167f2b17b96c \
|
||||
--hash=sha256:8c0fe88a7676e7a05b73174d4d4a59cb2ac21ff8263583f87a81a6018475a978 \
|
||||
--hash=sha256:b1a04bcc94ae6194e9db05d16ad31f298a7194bfbcb08258bbe589cee1d587b8 \
|
||||
--hash=sha256:b8461180b22420b1bdc289909410930761629fddf2a5aaf60fae1ab26cedc4c4 \
|
||||
--hash=sha256:d9feddb927fc68bd295f5eebc587a7e42cfaf9b65f60ca4a2386febff575da8f \
|
||||
--hash=sha256:ecfc3c7878fff94633ab0348524e093f9ce3243080416dd7d14f8ba400174719 \
|
||||
--hash=sha256:f3be1fbb34bcdfd146240d8fb92a709d4c2c8191348580a3c044ec60fa0b4456 \
|
||||
--hash=sha256:f6ad73b14c2d18a3bf8ad7cb6974294d7f613a7898604826058e6ac64918ef4d
|
||||
ruff==0.15.19 \
|
||||
--hash=sha256:08143f0685ae278b30727ea72e90c61e5bd9c31b91aac4f5bb989538f73d24b8 \
|
||||
--hash=sha256:27c15b2a241dd4d995557949a094fe78b8ad99122a38ccae1595849bcc947b3f \
|
||||
--hash=sha256:4171b6613effa9363cd46dd4f75bd1827b6d1b946b5e278ed0c600d305379445 \
|
||||
--hash=sha256:4d190d8f62a0b94aba8f721116538a9ee29b1e74d26650846ba9b99f0ae21c40 \
|
||||
--hash=sha256:5a2c86ba6870dd415a9d9eb8be94d7924ebec6a26ffc7958ec7ca29d4bff967d \
|
||||
--hash=sha256:6a498f82e0f4d8904c4e0aea5139cdfac1f39d19a3c51d491292f63a36e83b2e \
|
||||
--hash=sha256:6c6b607466e47349332eb1d9be52fb1467423fc07c217341af41cd0f3f0573be \
|
||||
--hash=sha256:82b432bc087264aea70fd25ac198918b70bd9e2aa0db4297b0bb91bbfbbc63ce \
|
||||
--hash=sha256:8530a09d03b3a8c994f8b559a7dcdabc690bcd3f78ef276c38c83166798ebf56 \
|
||||
--hash=sha256:87bf21fb3875fe69f0eacc825411657e2e85589cce633c35c0adf1113649c62b \
|
||||
--hash=sha256:8f47f0f92952af2557212bb10cf3e695cd4cf28b2c6e42cdb18ec6c9ebfa19da \
|
||||
--hash=sha256:922d1eb283161564759bd49f507e91dc6112c15da8bd5b84ed714e086243cf86 \
|
||||
--hash=sha256:c6c754515be7b76afe6e7e62df7776709571bcfc1631183828afcf3bafa869e3 \
|
||||
--hash=sha256:d48caa34488fb521fd0ef4aea2b0e8fe758298df044138f0d67b687a6a0d07ed \
|
||||
--hash=sha256:ed03b7862d68f0a8771d50ee129980cbf1b113f96e250b73954bc292f689e0bb \
|
||||
--hash=sha256:edc27f7172a93b32b102687009d6a588508815072141543ae603a8b9b0823063 \
|
||||
--hash=sha256:efeca47ee3f9d4a7162655a3b8e6ee4a878646044233978d4d2c1ff8cdd914f0 \
|
||||
--hash=sha256:f9b229cb3ef56ecc2c1c8ebeca64b7a7740ccaef40a9eb097e78dde5a8560b83
|
||||
tomli==2.4.1 ; python_full_version < '3.11' \
|
||||
--hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \
|
||||
--hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \
|
||||
|
||||
@@ -24,9 +24,9 @@ pygments==2.20.0 \
|
||||
--hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \
|
||||
--hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176
|
||||
# via pytest
|
||||
pytest==9.1.0 \
|
||||
--hash=sha256:41dd9148c08072446394cefd3d79701701335a9f4cae69ba92e39f6c7f5c061c \
|
||||
--hash=sha256:8ebb0e7888bdf2bdfc602ec51f8f62d50200af37356c74e503c79a94f5c81f32
|
||||
pytest==9.1.1 \
|
||||
--hash=sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313 \
|
||||
--hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c
|
||||
# via pytest-rerunfailures
|
||||
pytest-rerunfailures==16.3 \
|
||||
--hash=sha256:37c9b1231c8083e9f4e724f50f7a21241822f9516c15c700ebbf218d6452355c \
|
||||
|
||||
+20
-20
@@ -1,20 +1,20 @@
|
||||
uv==0.11.21 \
|
||||
--hash=sha256:00193e4e077c27ee3d66da356744dbf0b3aa59356dfbd9a9efb1dc8469af8ad7 \
|
||||
--hash=sha256:05e2f2e0fbf7c423f8287011ba0d2d69464f26a5f13b33df05cd491fbe5a910a \
|
||||
--hash=sha256:083882c73373a16de4c136d54e3386a52388dead5048a07505e25578b157182f \
|
||||
--hash=sha256:0e7916874f125a6f6af4cddd95f892ef19a4bb65c146afea7e544b0f98c63d02 \
|
||||
--hash=sha256:4530761c565f3a519a68f36628ee51f2b467b66573e2023e9073641219b60d23 \
|
||||
--hash=sha256:48c36eb170a5e7a668c1d13d2c8edeb017a3e6484c224f1521b540a6bda9e50b \
|
||||
--hash=sha256:4ed98ff3165bf7b339692d0df918b87e6d36eb0bed5183466330d27d5730d57b \
|
||||
--hash=sha256:525ef0eb56ff982357a321eca953307d824ab6f58473630c69521e8085f12b0a \
|
||||
--hash=sha256:66906cfa7c29c2cf4ea5117cf5614b0b83078ff669e664e2187071fcb24c85c1 \
|
||||
--hash=sha256:870f48082df673016f465b068f40ad5aa7d2d3cfbcfb4e73724630684003a2ab \
|
||||
--hash=sha256:88668a27959df9188ff72b0314f6b14f6acf6090964bb0748974239183ecb51c \
|
||||
--hash=sha256:88d8283f6ea9f0cdbb7717e6e08e916c32a8b8b7e11c72fcc6426a4c4eeb89e0 \
|
||||
--hash=sha256:9c11169a049ec8bf9ddc6a9f55fba9a240942ec8005faaaf4393f00ff7a4c16e \
|
||||
--hash=sha256:a00c78f3eea6db7967d98a505b01b7d80354517c7ff34f51701949f39c7b53e6 \
|
||||
--hash=sha256:af08e0d8f43da43bc68930aee56ca5f38ccfbc79d45b6e8a7d5051f1e975684f \
|
||||
--hash=sha256:b756dd2b368d7cc4aeb48249d06e1250bfcf81f0313ff7d7ec2ccafcd3ee4c93 \
|
||||
--hash=sha256:b9ecdefa81db7e966d1655988cad6f840316228381dd69131ebc4ae9362bbccd \
|
||||
--hash=sha256:d956ba9470d5267cc0ea3d7572cac3bf045bc78adad5b031b5558c6df13d2e19 \
|
||||
--hash=sha256:f64a851e429e6afb96f3a0b688995757ed3697bf1078509e2da8220ffc9805cd
|
||||
uv==0.11.24 \
|
||||
--hash=sha256:047d763d20d71968c00f4afec40b0e75d9da7e3693f725b9f502d84a25256893 \
|
||||
--hash=sha256:0e100b9cbc59beff2730840ac989b1f100cc03c90514e7cab6992a1f3f13784e \
|
||||
--hash=sha256:3176668ea8a2318d775c0d9188661c61ccc36790220724b1d360fcc8945d520e \
|
||||
--hash=sha256:356435577fae11fafe7a067ee3269cccefd35b031b83a3a36c87fe9d192bffba \
|
||||
--hash=sha256:38f38c9449aafd71dc0fa35e66a8a547ee48947b2f2f94084893c2afe6058cfa \
|
||||
--hash=sha256:438f8291fb9daea30a4bd333299b82e6ef755578302745a021162f328cfcfc68 \
|
||||
--hash=sha256:48a6123f71b801e0e0b8a38520b011632ad81e0a043445044ce5b1a7b1cec7b6 \
|
||||
--hash=sha256:6ecdad43e870f88d3772d9d37e877259ae35ec374d51589805cdcf6196205829 \
|
||||
--hash=sha256:79757f90b7765996366b80e77cad13555c7f7e1282ca8d8b686ee21773ab0b77 \
|
||||
--hash=sha256:8346b0086d213b80430f3bb4477379a8a4fa550b03447d23c84eee85c2e52bc4 \
|
||||
--hash=sha256:8602a1b6300a3a948afacc62e1cb933c8394c27966db85ed7e29483300b69dc4 \
|
||||
--hash=sha256:9312b6fd44361674e9c847a828ded792493766697816261e05848a024fe34129 \
|
||||
--hash=sha256:bf568c62dddd55ad9c85a16ffdfbcc7746be9c3159ed644e4f9e6f5e44905cbb \
|
||||
--hash=sha256:c4ab221c0a949f8006a7582786dae384706b2f82016a0db60baa7cc696042705 \
|
||||
--hash=sha256:c8ec3caf656645f58b53cb9aee9aa95cfc65c82ba2d7f1362bfd2660d1484307 \
|
||||
--hash=sha256:d6a49543c659c0cf1ff4c944955d97e69dbce4b76e3d284f5a92cea19133ebb6 \
|
||||
--hash=sha256:e499579f557abf17b8ffd1490d3e827748aea096f6eaa66737e729e046449f08 \
|
||||
--hash=sha256:e7e78c18686202c8b8715bebb83bfaf58f82d7fb848b6a5ae4e925a9fac3de4c \
|
||||
--hash=sha256:ed0c9a9d7909f0e48a9dafe666ca9ebefe2a1534e51ed05c0a7de7406465f868
|
||||
|
||||
@@ -367,5 +367,24 @@
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"action": "add",
|
||||
"when": "7b03011294c0210802ffc901390006c39152b999",
|
||||
"short": "[priority] **The minimum *recommended* Python version has been raised to 3.11**\n - Since Python 3.10 will reach its end-of-life in October 2026, support for it will be dropped soon. [Read more](https://github.com/yt-dlp/yt-dlp/issues/16916)"
|
||||
},
|
||||
{
|
||||
"action": "add",
|
||||
"when": "7b03011294c0210802ffc901390006c39152b999",
|
||||
"short": "[priority] **The official Windows release binaries will soon require Windows 10 or later.** [Read more](https://github.com/yt-dlp/yt-dlp/issues/16917)"
|
||||
},
|
||||
{
|
||||
"action": "add",
|
||||
"when": "b6590aaa1e3808155d69c9a79a797ae484163789",
|
||||
"short": "[priority] Security: [[CVE-2026-55404](https://nvd.nist.gov/vuln/detail/CVE-2026-55404)] [Downstream command injection via improper sanitization of --write-link output](https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-6v4j-43gg-vj32)\n - Shortcut file data is now properly validated and sanitized when the `--write-link` options are used"
|
||||
},
|
||||
{
|
||||
"action": "remove",
|
||||
"when": "a8be438aac1b90c3888e974056d967b8be90fa7e"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -10,7 +10,7 @@ HEADER = '''THIRD-PARTY LICENSES
|
||||
This file aggregates license texts of third-party components included with the yt-dlp PyInstaller-bundled executables.
|
||||
yt-dlp itself is licensed under the Unlicense (see LICENSE file).
|
||||
Source code for bundled third-party components is available from the original projects.
|
||||
If you cannot obtain it, the maintainers will provide it as per license obligation; maintainer emails are listed in pyproject.toml.'''
|
||||
If you cannot obtain it, the maintainers will provide it as per license obligation: email maintainers@yt-dlp.org'''
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -47,6 +47,13 @@ DEPENDENCIES: list[Dependency] = [
|
||||
license_url='https://raw.githubusercontent.com/libffi/libffi/refs/heads/master/LICENSE',
|
||||
project_url='https://sourceware.org/libffi/',
|
||||
),
|
||||
Dependency(
|
||||
name='OpenSSL 1.x',
|
||||
license='OpenSSL',
|
||||
license_url='https://raw.githubusercontent.com/openssl/openssl/refs/tags/OpenSSL_1_1_1t/LICENSE',
|
||||
comment='Only included in `yt-dlp.exe` and `yt-dlp_x86.exe` Windows builds',
|
||||
project_url='https://www.openssl.org/',
|
||||
),
|
||||
Dependency(
|
||||
name='OpenSSL 3.0+',
|
||||
license='Apache-2.0',
|
||||
@@ -123,43 +130,79 @@ DEPENDENCIES: list[Dependency] = [
|
||||
name='libintl',
|
||||
license='LGPL-2.1-or-later',
|
||||
license_url='https://raw.githubusercontent.com/autotools-mirror/gettext/refs/heads/master/gettext-runtime/intl/COPYING.LIB',
|
||||
comment='Only included in macOS builds',
|
||||
comment='Only included in Linux builds',
|
||||
project_url='https://www.gnu.org/software/gettext/',
|
||||
),
|
||||
Dependency(
|
||||
name='libidn2',
|
||||
license='LGPL-3.0-or-later',
|
||||
license_url='https://gitlab.com/libidn/libidn2/-/raw/master/COPYING.LESSERv3',
|
||||
comment='Only included in macOS builds',
|
||||
comment='Only included in Linux builds',
|
||||
project_url='https://www.gnu.org/software/libidn/',
|
||||
),
|
||||
Dependency(
|
||||
name='libidn2 (Unicode character data files)',
|
||||
license='Unicode-TOU AND Unicode-DFS-2016',
|
||||
license_url='https://gitlab.com/libidn/libidn2/-/raw/master/COPYING.unicode',
|
||||
comment='Only included in macOS builds',
|
||||
comment='Only included in Linux builds',
|
||||
project_url='https://www.gnu.org/software/libidn/',
|
||||
),
|
||||
Dependency(
|
||||
name='libunistring',
|
||||
license='LGPL-3.0-or-later',
|
||||
license_url='https://gitweb.git.savannah.gnu.org/gitweb/?p=libunistring.git;a=blob_plain;f=COPYING.LIB;hb=HEAD',
|
||||
comment='Only included in macOS builds',
|
||||
comment='Only included in Linux builds',
|
||||
project_url='https://www.gnu.org/software/libunistring/',
|
||||
),
|
||||
|
||||
# Non-Python dependencies of curl_cffi
|
||||
Dependency(
|
||||
name='librtmp',
|
||||
license='LGPL-2.1-or-later',
|
||||
# No official repo URL
|
||||
license_url='https://gist.githubusercontent.com/seproDev/31d8c691ccddebe37b8b379307cb232d/raw/053408e98547ea8c7d9ba3a80c965f33e163b881/librtmp_COPYING.txt',
|
||||
comment='Only included in macOS builds',
|
||||
project_url='https://rtmpdump.mplayerhq.hu/',
|
||||
name='curl-impersonate',
|
||||
license='MIT',
|
||||
license_url='https://raw.githubusercontent.com/lexiforest/curl-impersonate/refs/heads/main/LICENSE',
|
||||
comment='Not included in `yt-dlp_x86.exe` Windows builds',
|
||||
project_url='https://github.com/lexiforest/curl-impersonate',
|
||||
),
|
||||
Dependency(
|
||||
name='curl',
|
||||
license='curl',
|
||||
license_url='https://raw.githubusercontent.com/curl/curl/refs/heads/master/LICENSES/curl.txt',
|
||||
comment='Not included in `yt-dlp_x86.exe` Windows builds',
|
||||
project_url='https://curl.se/',
|
||||
),
|
||||
Dependency(
|
||||
name='BoringSSL',
|
||||
license='Apache-2.0',
|
||||
license_url='https://raw.githubusercontent.com/google/boringssl/refs/heads/main/LICENSE',
|
||||
comment='Not included in `yt-dlp_x86.exe` Windows builds',
|
||||
project_url='https://boringssl.googlesource.com/boringssl',
|
||||
),
|
||||
Dependency(
|
||||
name='nghttp2',
|
||||
license='MIT',
|
||||
license_url='https://raw.githubusercontent.com/nghttp2/nghttp2/refs/heads/master/COPYING',
|
||||
comment='Not included in `yt-dlp_x86.exe` Windows builds',
|
||||
project_url='https://nghttp2.org/',
|
||||
),
|
||||
Dependency(
|
||||
name='ngtcp2',
|
||||
license='MIT',
|
||||
license_url='https://raw.githubusercontent.com/ngtcp2/ngtcp2/refs/heads/main/COPYING',
|
||||
comment='Not included in `yt-dlp_x86.exe` Windows builds',
|
||||
project_url='https://nghttp2.org/ngtcp2/',
|
||||
),
|
||||
Dependency(
|
||||
name='nghttp3',
|
||||
license='MIT',
|
||||
license_url='https://raw.githubusercontent.com/ngtcp2/nghttp3/refs/heads/main/COPYING',
|
||||
comment='Not included in `yt-dlp_x86.exe` Windows builds',
|
||||
project_url='https://nghttp2.org/nghttp3/',
|
||||
),
|
||||
Dependency(
|
||||
name='zstd',
|
||||
license='BSD-3-Clause',
|
||||
license_url='https://raw.githubusercontent.com/facebook/zstd/refs/heads/dev/LICENSE',
|
||||
comment='Only included in macOS builds',
|
||||
comment='Not included in `yt-dlp_x86.exe` Windows builds',
|
||||
project_url='https://facebook.github.io/zstd/',
|
||||
),
|
||||
|
||||
@@ -174,28 +217,74 @@ DEPENDENCIES: list[Dependency] = [
|
||||
name='curl_cffi',
|
||||
license='MIT',
|
||||
license_url='https://raw.githubusercontent.com/lexiforest/curl_cffi/refs/heads/main/LICENSE',
|
||||
comment='Not included in `yt-dlp_x86` and `yt-dlp_musllinux_aarch64` builds',
|
||||
comment='Not included in `yt-dlp_x86.exe` Windows builds',
|
||||
project_url='https://curl-cffi.readthedocs.io/',
|
||||
),
|
||||
# curl_cffi vendored code:
|
||||
# - https://github.com/lexiforest/curl_cffi/blob/v0.15.0/curl_cffi/_asyncio_selector.py
|
||||
Dependency(
|
||||
name='Tornado',
|
||||
license='Apache-2.0',
|
||||
license_url='https://raw.githubusercontent.com/tornadoweb/tornado/master/LICENSE',
|
||||
comment='Not included in `yt-dlp_x86.exe` Windows builds',
|
||||
project_url='http://www.tornadoweb.org/',
|
||||
),
|
||||
# curl_cffi vendored code:
|
||||
# - https://github.com/lexiforest/curl_cffi/blob/v0.15.0/curl_cffi/requests/cookies.py
|
||||
# - https://github.com/lexiforest/curl_cffi/blob/v0.15.0/curl_cffi/requests/headers.py
|
||||
Dependency(
|
||||
name='httpx',
|
||||
license='BSD-3-Clause',
|
||||
license_url='https://github.com/encode/httpx/raw/master/LICENSE.md',
|
||||
comment='Not included in `yt-dlp_x86.exe` Windows builds',
|
||||
project_url='https://www.python-httpx.org/',
|
||||
),
|
||||
# Dependency of curl_cffi
|
||||
Dependency(
|
||||
name='curl-impersonate',
|
||||
name='rich',
|
||||
license='MIT',
|
||||
license_url='https://raw.githubusercontent.com/lexiforest/curl-impersonate/refs/heads/main/LICENSE',
|
||||
comment='Not included in `yt-dlp_x86` and `yt-dlp_musllinux_aarch64` builds',
|
||||
project_url='https://github.com/lexiforest/curl-impersonate',
|
||||
license_url='https://raw.githubusercontent.com/Textualize/rich/refs/heads/main/LICENSE',
|
||||
comment='Not included in `yt-dlp_x86.exe` Windows builds',
|
||||
project_url='https://rich.readthedocs.io/',
|
||||
),
|
||||
# Dependency of rich
|
||||
Dependency(
|
||||
name='pygments',
|
||||
license='BSD-2-Clause',
|
||||
license_url='https://raw.githubusercontent.com/pygments/pygments/refs/heads/master/LICENSE',
|
||||
comment='Not included in `yt-dlp_x86.exe` Windows builds',
|
||||
project_url='http://pygments.org/',
|
||||
),
|
||||
# Dependency of rich
|
||||
Dependency(
|
||||
name='markdown-it-py',
|
||||
license='MIT',
|
||||
license_url='https://raw.githubusercontent.com/executablebooks/markdown-it-py/refs/heads/master/LICENSE',
|
||||
comment='Not included in `yt-dlp_x86.exe` Windows builds',
|
||||
project_url='https://markdown-it-py.readthedocs.io/',
|
||||
),
|
||||
# Dependency of markdown-it-py
|
||||
Dependency(
|
||||
name='mdurl',
|
||||
license='MIT',
|
||||
license_url='https://raw.githubusercontent.com/executablebooks/mdurl/refs/heads/master/LICENSE',
|
||||
comment='Not included in `yt-dlp_x86.exe` Windows builds',
|
||||
project_url='https://github.com/executablebooks/mdurl',
|
||||
),
|
||||
# Dependency of cryptography and curl_cffi
|
||||
Dependency(
|
||||
name='cffi',
|
||||
license='MIT-0', # Technically does not need to be included
|
||||
license_url='https://raw.githubusercontent.com/python-cffi/cffi/refs/heads/main/LICENSE',
|
||||
comment='Not included in `yt-dlp_x86.exe` Windows builds',
|
||||
project_url='https://cffi.readthedocs.io/',
|
||||
),
|
||||
# Dependecy of cffi
|
||||
# Dependency of cffi
|
||||
Dependency(
|
||||
name='pycparser',
|
||||
license='BSD-3-Clause',
|
||||
license_url='https://raw.githubusercontent.com/eliben/pycparser/refs/heads/main/LICENSE',
|
||||
comment='Not included in `yt-dlp_x86.exe` Windows builds',
|
||||
project_url='https://github.com/eliben/pycparser',
|
||||
),
|
||||
Dependency(
|
||||
@@ -276,12 +365,14 @@ DEPENDENCIES: list[Dependency] = [
|
||||
name='Meriyah',
|
||||
license='ISC',
|
||||
license_url='https://raw.githubusercontent.com/meriyah/meriyah/refs/heads/main/LICENSE.md',
|
||||
comment='Also included in `yt-dlp` zipimport Unix executables and `yt-dlp.tar.gz` release tarballs',
|
||||
project_url='https://github.com/meriyah/meriyah',
|
||||
),
|
||||
Dependency(
|
||||
name='Astring',
|
||||
license='MIT',
|
||||
license_url='https://raw.githubusercontent.com/davidbonnet/astring/refs/heads/main/LICENSE',
|
||||
comment='Also included in `yt-dlp` zipimport Unix executables and `yt-dlp.tar.gz` release tarballs',
|
||||
project_url='https://github.com/davidbonnet/astring/',
|
||||
),
|
||||
]
|
||||
|
||||
+3
-3
@@ -69,10 +69,10 @@ deno = [
|
||||
pin = [
|
||||
"brotli==1.2.0 ; implementation_name == 'cpython' and sys_platform != 'ios'",
|
||||
"brotlicffi==1.2.0.1 ; implementation_name != 'cpython'",
|
||||
"certifi==2026.5.20",
|
||||
"certifi==2026.6.17",
|
||||
"charset-normalizer==3.4.7",
|
||||
"idna==3.18",
|
||||
"mutagen==1.47.0",
|
||||
"mutagen==1.48.1",
|
||||
"pycryptodomex==3.23.0",
|
||||
"requests==2.34.2",
|
||||
"urllib3==2.7.0",
|
||||
@@ -80,7 +80,7 @@ pin = [
|
||||
"yt-dlp-ejs==0.8.0",
|
||||
]
|
||||
pin-curl-cffi = [
|
||||
"certifi==2026.5.20 ; implementation_name == 'cpython'",
|
||||
"certifi==2026.6.17 ; 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'",
|
||||
|
||||
+32
-28
@@ -42,7 +42,7 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **AcFunVideo**
|
||||
- **ADN**: [*animationdigitalnetwork*](## "netrc machine") Animation Digital Network
|
||||
- **ADNSeason**: [*animationdigitalnetwork*](## "netrc machine") Animation Digital Network
|
||||
- **AdobeConnect**
|
||||
- **AdobeConnect**: (**Currently broken**)
|
||||
- **adobetv**
|
||||
- **AdultSwim**
|
||||
- **aenetworks**: A+E Networks: A&E, Lifetime, History.com, FYI Network and History Vault
|
||||
@@ -326,7 +326,7 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **DeuxMNews**
|
||||
- **DHM**: Filmarchiv - Deutsches Historisches Museum (**Currently broken**)
|
||||
- **DigitalConcertHall**: [*digitalconcerthall*](## "netrc machine") DigitalConcertHall extractor
|
||||
- **DigitallySpeaking**
|
||||
- **DigitallySpeaking**: (**Currently broken**)
|
||||
- **Digiteka**
|
||||
- **Digiview**
|
||||
- **DiscogsReleasePlaylist**
|
||||
@@ -430,7 +430,7 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **Flickr**
|
||||
- **Floatplane**
|
||||
- **FloatplaneChannel**
|
||||
- **Folketinget**: Folketinget (ft.dk; Danish parliament)
|
||||
- **Folketinget**: Folketinget (ft.dk; Danish parliament) (**Currently broken**)
|
||||
- **FoodNetwork**
|
||||
- **FootyRoom**
|
||||
- **Formula1**
|
||||
@@ -504,7 +504,6 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **GoDiscovery**
|
||||
- **GodResource**
|
||||
- **GodTube**: (**Currently broken**)
|
||||
- **Gofile**
|
||||
- **Golem**
|
||||
- **goodgame:stream**
|
||||
- **GoogleDrive**
|
||||
@@ -620,7 +619,7 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **Kakao**
|
||||
- **Kaltura**
|
||||
- **KankaNews**: (**Currently broken**)
|
||||
- **Karaoketv**
|
||||
- **Karaoketv**: (**Currently broken**)
|
||||
- **Katsomo**: (**Currently broken**)
|
||||
- **KelbyOne**: (**Currently broken**)
|
||||
- **Kenh14Playlist**
|
||||
@@ -679,10 +678,10 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **life:embed**
|
||||
- **likee**
|
||||
- **likee:user**
|
||||
- **LinkedIn**: [*linkedin*](## "netrc machine")
|
||||
- **linkedin:events**: [*linkedin*](## "netrc machine")
|
||||
- **linkedin:learning**: [*linkedin*](## "netrc machine")
|
||||
- **linkedin:learning:course**: [*linkedin*](## "netrc machine")
|
||||
- **LinkedIn**
|
||||
- **linkedin:events**
|
||||
- **linkedin:learning**
|
||||
- **linkedin:learning:course**
|
||||
- **Liputan6**
|
||||
- **ListenNotes**
|
||||
- **LiTV**
|
||||
@@ -743,6 +742,12 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **megatvcom**: megatv.com videos
|
||||
- **megatvcom:embed**: megatv.com embedded videos
|
||||
- **Meipai**: 美拍
|
||||
- **mellowfan**: [*mellowfan*](## "netrc machine") mellow-fan
|
||||
- **mellowfan:capture**: [*mellowfan*](## "netrc machine")
|
||||
- **mellowfan:channel**: [*mellowfan*](## "netrc machine")
|
||||
- **mellowfan:channel:search**: [*mellowfan*](## "netrc machine")
|
||||
- **mellowfan:movie**: [*mellowfan*](## "netrc machine")
|
||||
- **mellowfan:playlist**: [*mellowfan*](## "netrc machine")
|
||||
- **MelonVOD**
|
||||
- **Metacritic**
|
||||
- **mewatch**
|
||||
@@ -797,8 +802,9 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **Mx3**
|
||||
- **Mx3Neo**
|
||||
- **Mx3Volksmusik**
|
||||
- **Mxplayer**
|
||||
- **MxplayerShow**
|
||||
- **mxplayer**: Amazon MX Player
|
||||
- **mxplayer:season**
|
||||
- **mxplayer:show**
|
||||
- **MySpace**
|
||||
- **MySpace:album**
|
||||
- **MySpass**
|
||||
@@ -939,8 +945,11 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **Odnoklassniki**
|
||||
- **OfTV**
|
||||
- **OfTVPlaylist**
|
||||
- **OktoberfestTV**
|
||||
- **OktoberfestTV**: (**Currently broken**)
|
||||
- **OlympicsReplay**
|
||||
- **Omnyfm**
|
||||
- **OmnyfmPlaylist**
|
||||
- **OmnyfmShow**
|
||||
- **on24**: ON24
|
||||
- **OnDemandChinaEpisode**
|
||||
- **OnDemandKorea**
|
||||
@@ -954,9 +963,6 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **onsen**: [*onsen*](## "netrc machine") インターネットラジオステーション<音泉>
|
||||
- **Opencast**
|
||||
- **OpencastPlaylist**
|
||||
- **openrec**
|
||||
- **openrec:capture**
|
||||
- **openrec:movie**
|
||||
- **orf:fm4:story**: fm4.orf.at stories
|
||||
- **orf:iptv**: iptv.ORF.at
|
||||
- **orf:on**
|
||||
@@ -1014,7 +1020,7 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **player.sky.it**
|
||||
- **PlayerFm**
|
||||
- **PlaySuisse**: [*playsuisse*](## "netrc machine")
|
||||
- **Playtvak**: Playtvak.cz, iDNES.cz and Lidovky.cz
|
||||
- **Playtvak**: Playtvak.cz, iDNES.cz and Lidovky.cz (**Currently broken**)
|
||||
- **PlayVids**
|
||||
- **pluralsight**: [*pluralsight*](## "netrc machine")
|
||||
- **pluralsight:course**
|
||||
@@ -1121,7 +1127,7 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **RedBullEmbed**
|
||||
- **RedBullTV**
|
||||
- **RedBullTVRrnContent**
|
||||
- **Reddit**: [*reddit*](## "netrc machine")
|
||||
- **Reddit**
|
||||
- **RedGifs**
|
||||
- **RedGifsSearch**: Redgifs search
|
||||
- **RedGifsUser**: Redgifs user
|
||||
@@ -1153,7 +1159,7 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **rtl.lu:article**
|
||||
- **rtl.lu:tele-vod**
|
||||
- **rtl.nl**: rtl.nl and rtlxl.nl
|
||||
- **rtl2**
|
||||
- **rtl2**: (**Currently broken**)
|
||||
- **RTLLuLive**
|
||||
- **RTLLuRadio**
|
||||
- **RTNews**
|
||||
@@ -1231,7 +1237,7 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **SharePoint**
|
||||
- **ShemarooMe**
|
||||
- **Shiey**
|
||||
- **ShowRoomLive**
|
||||
- **ShowRoomLive**: (**Currently broken**)
|
||||
- **ShugiinItvLive**: 衆議院インターネット審議中継
|
||||
- **ShugiinItvLiveRoom**: 衆議院インターネット審議中継 (中継)
|
||||
- **ShugiinItvVod**: 衆議院インターネット審議中継 (ビデオライブラリ)
|
||||
@@ -1369,7 +1375,7 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **TeleQuebecSquat**
|
||||
- **TeleQuebecVideo**
|
||||
- **TeleTask**: (**Currently broken**)
|
||||
- **Telewebion**: (**Currently broken**)
|
||||
- **Telewebion**
|
||||
- **TennisTV**: [*tennistv*](## "netrc machine")
|
||||
- **TF1**
|
||||
- **TFO**: (**Currently broken**)
|
||||
@@ -1414,10 +1420,6 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **Toypics**: Toypics video (**Currently broken**)
|
||||
- **ToypicsUser**: Toypics user profile (**Currently broken**)
|
||||
- **TravelChannel**
|
||||
- **Trovo**
|
||||
- **TrovoChannelClip**: All Clips of a trovo.live channel; "trovoclip:" prefix
|
||||
- **TrovoChannelVod**: All VODs of a trovo.live channel; "trovovod:" prefix
|
||||
- **TrovoVod**
|
||||
- **TrtCocukVideo**
|
||||
- **TrtWorld**
|
||||
- **TrueID**
|
||||
@@ -1591,7 +1593,7 @@ 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")
|
||||
- **Walla**
|
||||
- **Walla**: (**Currently broken**)
|
||||
- **WalyTV**: [*walytv*](## "netrc machine")
|
||||
- **WalyTVLive**: [*walytv*](## "netrc machine")
|
||||
- **WalyTVRecordings**: [*walytv*](## "netrc machine")
|
||||
@@ -1621,7 +1623,7 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **WeverseMediaTab**: [*weverse*](## "netrc machine")
|
||||
- **WeverseMoment**: [*weverse*](## "netrc machine")
|
||||
- **WeVidi**
|
||||
- **whowatch**
|
||||
- **whowatch**: (**Currently broken**)
|
||||
- **Whyp**
|
||||
- **wikimedia.org**
|
||||
- **Wimbledon**
|
||||
@@ -1636,8 +1638,8 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **WorldStarHipHop**
|
||||
- **wppilot**
|
||||
- **wppilot:channels**
|
||||
- **WrestleUniversePPV**: [*wrestleuniverse*](## "netrc machine")
|
||||
- **WrestleUniverseVOD**: [*wrestleuniverse*](## "netrc machine")
|
||||
- **wrestleuniverse:ppv**: [*wrestleuniverse*](## "netrc machine")
|
||||
- **wrestleuniverse:vod**: [*wrestleuniverse*](## "netrc machine")
|
||||
- **WSJ**: Wall Street Journal
|
||||
- **WSJArticle**
|
||||
- **WWE**
|
||||
@@ -1708,6 +1710,7 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **YoutubeYtBe**: [*youtube*](## "netrc machine") youtu.be
|
||||
- **Zaiko**
|
||||
- **ZaikoETicket**
|
||||
- **zan**: Z-aN
|
||||
- **Zapiks**
|
||||
- **Zattoo**: [*zattoo*](## "netrc machine")
|
||||
- **ZattooLive**: [*zattoo*](## "netrc machine")
|
||||
@@ -1730,5 +1733,6 @@ The only reliable way to check if a site is supported is to try it.
|
||||
- **zingmp3:user**
|
||||
- **zingmp3:week-chart**
|
||||
- **zoom**
|
||||
- **zoom:clips**
|
||||
- **Zype**
|
||||
- **generic**: Generic downloader that works on some sites
|
||||
|
||||
+25
-3
@@ -134,7 +134,10 @@ from yt_dlp.utils import (
|
||||
xpath_text,
|
||||
xpath_with_ns,
|
||||
)
|
||||
from yt_dlp.utils._utils import _UnsafeExtensionError
|
||||
from yt_dlp.utils._utils import (
|
||||
_UnsafeExtensionError,
|
||||
_desktop_entry_localestring,
|
||||
)
|
||||
from yt_dlp.utils.networking import (
|
||||
HTTPHeaderDict,
|
||||
escape_rfc3986,
|
||||
@@ -689,8 +692,6 @@ class TestUtil(unittest.TestCase):
|
||||
self.assertEqual(url_or_none('//foo.de'), '//foo.de')
|
||||
self.assertEqual(url_or_none('s3://foo.de'), None)
|
||||
self.assertEqual(url_or_none('rtmpte://foo.de'), 'rtmpte://foo.de')
|
||||
self.assertEqual(url_or_none('mms://foo.de'), 'mms://foo.de')
|
||||
self.assertEqual(url_or_none('rtspu://foo.de'), 'rtspu://foo.de')
|
||||
self.assertEqual(url_or_none('ftps://foo.de'), 'ftps://foo.de')
|
||||
self.assertEqual(url_or_none('ws://foo.de'), 'ws://foo.de')
|
||||
self.assertEqual(url_or_none('wss://foo.de'), 'wss://foo.de')
|
||||
@@ -1950,6 +1951,27 @@ Line 1
|
||||
self.assertEqual(
|
||||
iri_to_uri('http://导航.中国/'),
|
||||
'http://xn--fet810g.xn--fiqs8s/')
|
||||
self.assertEqual(
|
||||
iri_to_uri('file://example.org/run.exe', allowed_schemes=('file',)),
|
||||
'file://example.org/run.exe')
|
||||
self.assertRaises(ValueError, iri_to_uri, 'file://example.org/run.exe')
|
||||
|
||||
def test_desktop_entry_localestring(self):
|
||||
self.assertEqual(
|
||||
_desktop_entry_localestring('A B'),
|
||||
'A\\sB')
|
||||
self.assertEqual(
|
||||
_desktop_entry_localestring('A\nB'),
|
||||
'A\\nB')
|
||||
self.assertEqual(
|
||||
_desktop_entry_localestring('A\tB'),
|
||||
'A\\tB')
|
||||
self.assertEqual(
|
||||
_desktop_entry_localestring('A\rB'),
|
||||
'A\\rB')
|
||||
self.assertEqual(
|
||||
_desktop_entry_localestring('A\\B'),
|
||||
'A\\\\B')
|
||||
|
||||
def test_clean_podcast_url(self):
|
||||
self.assertEqual(clean_podcast_url('https://www.podtrac.com/pts/redirect.mp3/chtbl.com/track/5899E/traffic.megaphone.fm/HSW7835899191.mp3'), 'https://traffic.megaphone.fm/HSW7835899191.mp3')
|
||||
|
||||
@@ -137,11 +137,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2026.5.20"
|
||||
version = "2026.6.17"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
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" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" }
|
||||
wheels = [
|
||||
{ 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" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -595,11 +595,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "mutagen"
|
||||
version = "1.47.0"
|
||||
version = "1.48.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/81/e6/64bc71b74eef4b68e61eb921dcf72dabd9e4ec4af1e11891bbd312ccbb77/mutagen-1.47.0.tar.gz", hash = "sha256:719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99", size = 1274186, upload-time = "2023-09-03T16:33:33.411Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/df/70/1675da133ea92227da41bf5b24e1c66be597ff736a1533ade41da986852f/mutagen-1.48.1.tar.gz", hash = "sha256:8f95637ab9f6f305cec6bd1294e197debe207998e3e068596563c74f86b0a173", size = 1276978, upload-time = "2026-06-25T09:47:32.443Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/7a/620f945b96be1f6ee357d211d5bf74ab1b7fe72a9f1525aafbfe3aee6875/mutagen-1.47.0-py3-none-any.whl", hash = "sha256:edd96f50c5907a9539d8e5bba7245f62c9f520aef333d13392a79a4f70aca719", size = 194391, upload-time = "2023-09-03T16:33:29.955Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/d8/a29e4e3991765e7ce4ed1f7e4074fe1ba9da03e0048639734de60f9cadb9/mutagen-1.48.1-py3-none-any.whl", hash = "sha256:4f077fe87d3fc7fba259aa63d8c026b18382ca6a42ef37c61e16f1b1b5b82fe7", size = 195706, upload-time = "2026-06-25T09:47:30.296Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -803,7 +803,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.1.0"
|
||||
version = "9.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
@@ -814,9 +814,9 @@ dependencies = [
|
||||
{ name = "pygments" },
|
||||
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/84/0e/b5858858d74958632c49b72cb25a3976ff9f632397626715be71c89d3971/pytest-9.1.0.tar.gz", hash = "sha256:41dd9148c08072446394cefd3d79701701335a9f4cae69ba92e39f6c7f5c061c", size = 1634181, upload-time = "2026-06-13T18:52:45.983Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/5a/ba30a81239b909821b3153e303e7def45178bf353da4f72380e6c5e8793b/pytest-9.1.0-py3-none-any.whl", hash = "sha256:8ebb0e7888bdf2bdfc602ec51f8f62d50200af37356c74e503c79a94f5c81f32", size = 386453, upload-time = "2026-06-13T18:52:44.045Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -948,27 +948,27 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.15.17"
|
||||
version = "0.15.19"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8c/a9/3abdf488f1bf3d24c699415e454ed554a6350d5d89ce183be1ee0a3361ac/ruff-0.15.17.tar.gz", hash = "sha256:2ec446937fd16c8c4de2674a209cc5af64d9c6f17d21fbf1151054fa0bcf5219", size = 4743346, upload-time = "2026-06-11T17:54:47.663Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d5/e6/15800dfde183a1a106594016c912b4c12d050a301989d1aca6cb63759fe8/ruff-0.15.19.tar.gz", hash = "sha256:edc27f7172a93b32b102687009d6a588508815072141543ae603a8b9b0823063", size = 4772071, upload-time = "2026-06-24T01:10:46.942Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/db/4d/e11259f5da07cb6afb2d074c31bf09da9671993f7329d4f15d2fdc458301/ruff-0.15.17-py3-none-linux_armv6l.whl", hash = "sha256:d9feddb927fc68bd295f5eebc587a7e42cfaf9b65f60ca4a2386febff575da8f", size = 10856677, upload-time = "2026-06-11T17:54:49.533Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/3e/772d679e1a0dc058e58875bd2c0cb713a0530877b4a76fee3c7966df0d49/ruff-0.15.17-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:25805a226d741c47d274a35ad5c10a7dde175fcddfa511d7cf3da0a21eb3eab7", size = 11223443, upload-time = "2026-06-11T17:55:00.573Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/58/bd41f7688b2fd5623012605130ed70e60aa7f2244baa3d5066bdd61530c8/ruff-0.15.17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f6ad73b14c2d18a3bf8ad7cb6974294d7f613a7898604826058e6ac64918ef4d", size = 10566458, upload-time = "2026-06-11T17:55:07.52Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/5b/733371013fcf1ec339e477ece6ab42bfe10bdd9bba8ee88a9516aa56bfc0/ruff-0.15.17-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ba0c1e4f95bcb3869d0d30cbd5917071ef2e28665abfec970cdab0492c713ed", size = 10914483, upload-time = "2026-06-11T17:55:05.501Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/cc/6f24251cc0252f7239391ccb85833f320efad14ebe5b443943f37ced6332/ruff-0.15.17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:81647960f10bff57d2e51cadd0c3950fe598400c852863a038720ef5b8cca91e", size = 10647497, upload-time = "2026-06-11T17:54:57.733Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/dd/0d10c17ce1a1624d6fc3156309c3f834fdb5dfaad026ec90c85684f3990e/ruff-0.15.17-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e01a84ddbc8c16c23055ba3924476850f1bbc1917cebbb9376665a63e74260d", size = 11416967, upload-time = "2026-06-11T17:54:51.461Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/91/556bfb156f6144f355e831c23db00b2fc4120f86b3ce81cc5f7fd2df51f3/ruff-0.15.17-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fe9f653152f8f294f9f7e03bf3a453d8b4a27f7a59c78c8666167f2b17b96c", size = 12335770, upload-time = "2026-06-11T17:54:45.793Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/82/8b5999aa13355e926f06d9f42a32dcca862f623bf0363785ff89d607dffd/ruff-0.15.17-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c0fe88a7676e7a05b73174d4d4a59cb2ac21ff8263583f87a81a6018475a978", size = 11575441, upload-time = "2026-06-11T17:54:32.661Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/93/f10377bb04109ca0e8cbc483ff1982c54b6d418210041776f93e8cdc7fa9/ruff-0.15.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecfc3c7878fff94633ab0348524e093f9ce3243080416dd7d14f8ba400174719", size = 11557614, upload-time = "2026-06-11T17:54:34.698Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/a6/eeeae7f7d5493df41649ab3db92f086b2d0a30199e4efdf8e3dd7a033f24/ruff-0.15.17-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:b8461180b22420b1bdc289909410930761629fddf2a5aaf60fae1ab26cedc4c4", size = 11544450, upload-time = "2026-06-11T17:54:39.042Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/88/5991ce565129a24dd4a00db1254b3b5db2e53018cbe4018ea5a89738e727/ruff-0.15.17-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6eccbe50a038b503e7140b441aa9c7fc8c1f36edf23ebef9f4165c2f28f568b7", size = 10892524, upload-time = "2026-06-11T17:55:09.432Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/1d/0fdd248313425f55223968af04b0a42125466a8d88d21c1d99c6af0a51e8/ruff-0.15.17-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:382fc0521025f5a8ad447d8bdd523545d0d7646adb718eb1c2dac5065ec27c0f", size = 10659573, upload-time = "2026-06-11T17:54:36.824Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/0e/072e8260deb9461062ce9311ced27a8e541229a6ffd483013dd37661e43e/ruff-0.15.17-py3-none-musllinux_1_2_i686.whl", hash = "sha256:456d41fcd1b2777ad63f09a6e7121d43f7b688bbc76a800c10f7f8fb1f912c3f", size = 11127818, upload-time = "2026-06-11T17:55:03.124Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/b4/55060a34163121498014696b5f656db5b8c6963768f227dbf0d76b311073/ruff-0.15.17-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b1a04bcc94ae6194e9db05d16ad31f298a7194bfbcb08258bbe589cee1d587b8", size = 11655901, upload-time = "2026-06-11T17:54:53.562Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/71/9b29d6b87cef468d697f43c6a91e3fae4a80185779d7d5a4ef27d173439f/ruff-0.15.17-py3-none-win32.whl", hash = "sha256:596065960ab1ff593f744220c9fe6580eda00a95003cffa9f4048bb5b1bf0392", size = 10925574, upload-time = "2026-06-11T17:54:55.723Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/b2/8fc77f3723228836fa5d12497eb71c808f83782e10d058d2b15cfa14640b/ruff-0.15.17-py3-none-win_amd64.whl", hash = "sha256:6769e5fa1710b179b92e0bfa5a51735b35baea9013dadb06d5f44cbcf9547084", size = 12058788, upload-time = "2026-06-11T17:54:41.042Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/c7/c53e8dbff9c9dc4b7928773421ae294a5d28fcb8dcda1a089579d3a7e510/ruff-0.15.17-py3-none-win_arm64.whl", hash = "sha256:f3be1fbb34bcdfd146240d8fb92a709d4c2c8191348580a3c044ec60fa0b4456", size = 11355275, upload-time = "2026-06-11T17:54:43.635Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/4c/9ded7626c39a0440c575bf69e2bf500d443388272c842662c59852ee7fcd/ruff-0.15.19-py3-none-linux_armv6l.whl", hash = "sha256:922d1eb283161564759bd49f507e91dc6112c15da8bd5b84ed714e086243cf86", size = 10950859, upload-time = "2026-06-24T01:10:38.491Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/ef/c211505ece1d00ef493d58e54e3b6383c946a21e9874774eb531f2512cf3/ruff-0.15.19-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4d190d8f62a0b94aba8f721116538a9ee29b1e74d26650846ba9b99f0ae21c40", size = 11294529, upload-time = "2026-06-24T01:10:36.481Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/93/78d462e7d39968e58094dc57be7d09ffb14ce37da5b68ed70338a35a1f21/ruff-0.15.19-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5a2c86ba6870dd415a9d9eb8be94d7924ebec6a26ffc7958ec7ca29d4bff967d", size = 10641416, upload-time = "2026-06-24T01:10:48.923Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/c4/5cb66cfd1f865d5cca908b86c93ac785e7f572193d3c7426079ca6643e24/ruff-0.15.19-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82b432bc087264aea70fd25ac198918b70bd9e2aa0db4297b0bb91bbfbbc63ce", size = 11015582, upload-time = "2026-06-24T01:10:30.089Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/9f/8ecfaec10cf5eecd28fbc00ff4fb867db90a1be54bf3d39ebf93f893cd52/ruff-0.15.19-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8530a09d03b3a8c994f8b559a7dcdabc690bcd3f78ef276c38c83166798ebf56", size = 10744059, upload-time = "2026-06-24T01:10:32.48Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/6b/983249d04562bc2d590edd75f32455cdb473affb3ba4bc8d883e939c697d/ruff-0.15.19-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87bf21fb3875fe69f0eacc825411657e2e85589cce633c35c0adf1113649c62b", size = 11568461, upload-time = "2026-06-24T01:10:17.435Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/39/bc7794f127b18f492a3b4ee82bba5a900c985ff13b72b46f46e3c171ba34/ruff-0.15.19-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f9b229cb3ef56ecc2c1c8ebeca64b7a7740ccaef40a9eb097e78dde5a8560b83", size = 12429690, upload-time = "2026-06-24T01:10:40.638Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/3b/0de6859e698ed11c8a49e765196c8d333599b6a546c0715df39b6ba1aa2e/ruff-0.15.19-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c754515be7b76afe6e7e62df7776709571bcfc1631183828afcf3bafa869e3", size = 11693067, upload-time = "2026-06-24T01:10:25.681Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/3d/0b1f30f84bee9ae6ae8d349c2ba8b6f4b040966744efdd3acc804ae7c024/ruff-0.15.19-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a498f82e0f4d8904c4e0aea5139cdfac1f39d19a3c51d491292f63a36e83b2e", size = 11616911, upload-time = "2026-06-24T01:10:44.809Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/eb/c90bd3dfc12eed9032c2c1bfe05105b93a1b2c8bce555db6308315b853ce/ruff-0.15.19-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:d48caa34488fb521fd0ef4aea2b0e8fe758298df044138f0d67b687a6a0d07ed", size = 11649343, upload-time = "2026-06-24T01:10:23.472Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/91/01caa13602a2f12fae5edbe8caf78b3c1e6db1293132aee6959eecce095c/ruff-0.15.19-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4171b6613effa9363cd46dd4f75bd1827b6d1b946b5e278ed0c600d305379445", size = 10977610, upload-time = "2026-06-24T01:10:50.892Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/51/acb817922feab9ecbb3201377d4dbe7a25f1395e46545820061973f03468/ruff-0.15.19-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:27c15b2a241dd4d995557949a094fe78b8ad99122a38ccae1595849bcc947b3f", size = 10744900, upload-time = "2026-06-24T01:10:42.726Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/bc/5c8ca46b8a7a3f2b16cfbec88721d772b1c93912904e8f8c2e49470fea63/ruff-0.15.19-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ed03b7862d68f0a8771d50ee129980cbf1b113f96e250b73954bc292f689e0bb", size = 11293560, upload-time = "2026-06-24T01:10:21.262Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/e0/4a888cbe4d5523b3f77a2b1fa043f46cfeba1b32eac35dcfadee0578fa8a/ruff-0.15.19-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:08143f0685ae278b30727ea72e90c61e5bd9c31b91aac4f5bb989538f73d24b8", size = 11696533, upload-time = "2026-06-24T01:10:53.046Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/43/c34b2fcd79262a85161764a97aaca89c3e4f574340ab61430cefa2bdd2c1/ruff-0.15.19-py3-none-win32.whl", hash = "sha256:8f47f0f92952af2557212bb10cf3e695cd4cf28b2c6e42cdb18ec6c9ebfa19da", size = 10986299, upload-time = "2026-06-24T01:10:55.185Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/e8/15fd23e02b2442b56b2026b455977bc3057aa34b26e6323d1e99e8531a9f/ruff-0.15.19-py3-none-win_amd64.whl", hash = "sha256:efeca47ee3f9d4a7162655a3b8e6ee4a878646044233978d4d2c1ff8cdd914f0", size = 12123473, upload-time = "2026-06-24T01:10:27.74Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/66/9a73695e31eaee04f35d8475998bf8ab354465f9c638936d76111603dcc5/ruff-0.15.19-py3-none-win_arm64.whl", hash = "sha256:6c6b607466e47349332eb1d9be52fb1467423fc07c217341af41cd0f3f0573be", size = 11376779, upload-time = "2026-06-24T01:10:34.465Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1076,33 +1076,33 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "uv"
|
||||
version = "0.11.21"
|
||||
version = "0.11.24"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f3/f9/f45bb1c251962ee614afd58ccd3dc06ada7869d04987efc2858a81cc4e0f/uv-0.11.21.tar.gz", hash = "sha256:083882c73373a16de4c136d54e3386a52388dead5048a07505e25578b157182f", size = 4259001, upload-time = "2026-06-11T18:18:26.468Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/42/48/eb2ab279d7e4f007b08b778062432afd1b1d97bd84f101e0e0e4c8a2c21a/uv-0.11.24.tar.gz", hash = "sha256:8602a1b6300a3a948afacc62e1cb933c8394c27966db85ed7e29483300b69dc4", size = 4296256, upload-time = "2026-06-23T21:14:24.878Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/a5/1c863b931f3aba6e07547929b8cb45875038de00678bfd2fbabcd76faeef/uv-0.11.21-py3-none-linux_armv6l.whl", hash = "sha256:48c36eb170a5e7a668c1d13d2c8edeb017a3e6484c224f1521b540a6bda9e50b", size = 23747368, upload-time = "2026-06-11T18:19:21.724Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/8c/66d22f9152a014fbb17b1308394efe274e860b8beb4933f051396f96dd9f/uv-0.11.21-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:88d8283f6ea9f0cdbb7717e6e08e916c32a8b8b7e11c72fcc6426a4c4eeb89e0", size = 22992460, upload-time = "2026-06-11T18:18:33.543Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/f7/31d62c17837c9ae79cc6d5351fc5d54e8926e78b0315b4b6c187e0d1d50d/uv-0.11.21-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9c11169a049ec8bf9ddc6a9f55fba9a240942ec8005faaaf4393f00ff7a4c16e", size = 21762931, upload-time = "2026-06-11T18:18:41.155Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/04/c5503fc1015095db71c280526f45537f3bb06855ce281ff1761b85d149bf/uv-0.11.21-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:00193e4e077c27ee3d66da356744dbf0b3aa59356dfbd9a9efb1dc8469af8ad7", size = 23716032, upload-time = "2026-06-11T18:19:17.03Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/ac/46132335772fcdc38e5b5ec76701a8df8e3707605909b5fed46783689501/uv-0.11.21-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:870f48082df673016f465b068f40ad5aa7d2d3cfbcfb4e73724630684003a2ab", size = 23330010, upload-time = "2026-06-11T18:19:00.825Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/d4/cfa1ea36706c32006dea9bf0a819b56c22af8270ea3a2b57562ce96c2d45/uv-0.11.21-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:af08e0d8f43da43bc68930aee56ca5f38ccfbc79d45b6e8a7d5051f1e975684f", size = 23339731, upload-time = "2026-06-11T18:18:52.395Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/c5/b34d3cdf05a069c583ef368e6db90242f842d7eb26b246981b3ca8799c27/uv-0.11.21-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4530761c565f3a519a68f36628ee51f2b467b66573e2023e9073641219b60d23", size = 24657820, upload-time = "2026-06-11T18:19:25.62Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/b9/89b4e3909111c14311d4a1551afb37f0669587dc1f4ae7e26ec5baea6c09/uv-0.11.21-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66906cfa7c29c2cf4ea5117cf5614b0b83078ff669e664e2187071fcb24c85c1", size = 25744586, upload-time = "2026-06-11T18:19:09.311Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/7b/51d53d9fb1aaf38a613c2d20b40583ee2aa47fc000724a00aecbd5e61431/uv-0.11.21-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:525ef0eb56ff982357a321eca953307d824ab6f58473630c69521e8085f12b0a", size = 24990030, upload-time = "2026-06-11T18:18:29.618Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/70/3347f736911b73df1f31c0823d6502891f3c49fdeb157fe8060b18c08d1c/uv-0.11.21-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9ecdefa81db7e966d1655988cad6f840316228381dd69131ebc4ae9362bbccd", size = 25110133, upload-time = "2026-06-11T18:19:13.307Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/b5/b92538042d78550626ec7ac98b525bcb81ded8605c7ca9d6e35a1454ba71/uv-0.11.21-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:4ed98ff3165bf7b339692d0df918b87e6d36eb0bed5183466330d27d5730d57b", size = 23755172, upload-time = "2026-06-11T18:18:19.189Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/1a/5c8993f95d4384baeaf00b96df0111af3c941a34e4466cde0d52b0b6ad99/uv-0.11.21-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:0e7916874f125a6f6af4cddd95f892ef19a4bb65c146afea7e544b0f98c63d02", size = 24468447, upload-time = "2026-06-11T18:19:04.572Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/2c/d4db24f9aeab8fce106633cd0388df4c0cf9f0991a2b5d9f58d061a031f7/uv-0.11.21-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:05e2f2e0fbf7c423f8287011ba0d2d69464f26a5f13b33df05cd491fbe5a910a", size = 24564716, upload-time = "2026-06-11T18:19:29.559Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/53/c61711e81f9f8d34dd020340ace968499b2539d3bb4ac09d39339df54a9d/uv-0.11.21-py3-none-musllinux_1_1_i686.whl", hash = "sha256:b756dd2b368d7cc4aeb48249d06e1250bfcf81f0313ff7d7ec2ccafcd3ee4c93", size = 23917742, upload-time = "2026-06-11T18:18:57.187Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/21/210a5562a6a0eddfbe4890eb48e67f167be0307e75f029ca46b8f6386e5d/uv-0.11.21-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:88668a27959df9188ff72b0314f6b14f6acf6090964bb0748974239183ecb51c", size = 25330418, upload-time = "2026-06-11T18:18:37.383Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/3c/81979463de0278facaa59ed3940b9c62f25a68d737d1a6f11cc3f922fba3/uv-0.11.21-py3-none-win32.whl", hash = "sha256:a00c78f3eea6db7967d98a505b01b7d80354517c7ff34f51701949f39c7b53e6", size = 22633520, upload-time = "2026-06-11T18:18:44.992Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/51/e682e060813424467f14ae964dd7022f8fc537fea5803b5aab0ba1eca9cc/uv-0.11.21-py3-none-win_amd64.whl", hash = "sha256:d956ba9470d5267cc0ea3d7572cac3bf045bc78adad5b031b5558c6df13d2e19", size = 25291878, upload-time = "2026-06-11T18:18:23.832Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/ef/8b1d92f9501963ef8694bb17ad80ba9926d049240d2da0a4f879aa37f3e2/uv-0.11.21-py3-none-win_arm64.whl", hash = "sha256:f64a851e429e6afb96f3a0b688995757ed3697bf1078509e2da8220ffc9805cd", size = 23715885, upload-time = "2026-06-11T18:18:48.596Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/9d/56e94909139172b7842f767579167684519a34736ae9bbf4485e550f5f5f/uv-0.11.24-py3-none-linux_armv6l.whl", hash = "sha256:d6a49543c659c0cf1ff4c944955d97e69dbce4b76e3d284f5a92cea19133ebb6", size = 24261376, upload-time = "2026-06-23T21:13:30.652Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/d0/dbef6f7ddf8e7d41d0d121b00d3654145c2fb63ed15442e0297e79d33d5b/uv-0.11.24-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e499579f557abf17b8ffd1490d3e827748aea096f6eaa66737e729e046449f08", size = 23120267, upload-time = "2026-06-23T21:13:33.681Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/af/d2e563185aeccdc19bea6881d10a2cba470ae8be680fc7dad889f053d002/uv-0.11.24-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c8ec3caf656645f58b53cb9aee9aa95cfc65c82ba2d7f1362bfd2660d1484307", size = 22138947, upload-time = "2026-06-23T21:13:41.975Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/1b/b2c1d9533c3d17389bb9375aaf1a183622384101c574393d45b8906063ad/uv-0.11.24-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:e7e78c18686202c8b8715bebb83bfaf58f82d7fb848b6a5ae4e925a9fac3de4c", size = 24000643, upload-time = "2026-06-23T21:13:44.49Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/5d/1693c6169057f4ef95125f50e2e228aa5b599f34917612895a295cc39e88/uv-0.11.24-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:38f38c9449aafd71dc0fa35e66a8a547ee48947b2f2f94084893c2afe6058cfa", size = 23763002, upload-time = "2026-06-23T21:13:47.696Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/3f/8c3784616845f93eeffa40caf09dce0a2d1ce73257e9fe1864fa82c7ec85/uv-0.11.24-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8346b0086d213b80430f3bb4477379a8a4fa550b03447d23c84eee85c2e52bc4", size = 23784726, upload-time = "2026-06-23T21:13:50.1Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/47/f53fb657f007d67ade9d6d4e2a42493b84178cfafa6dfce10b3752afbf90/uv-0.11.24-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79757f90b7765996366b80e77cad13555c7f7e1282ca8d8b686ee21773ab0b77", size = 25105365, upload-time = "2026-06-23T21:13:52.871Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/a3/b265bd83029624ae06239075729d811d75fc5d367afced4d9896439037e8/uv-0.11.24-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0e100b9cbc59beff2730840ac989b1f100cc03c90514e7cab6992a1f3f13784e", size = 26074124, upload-time = "2026-06-23T21:13:55.743Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/41/e13bbca48db26a562e3e281c886c3683c17c951a6b51b1dd535f746862f6/uv-0.11.24-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3176668ea8a2318d775c0d9188661c61ccc36790220724b1d360fcc8945d520e", size = 25206336, upload-time = "2026-06-23T21:13:58.383Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/ba/91397c8802500976acb452b93decdfc5d22a903f561fa6e472697d997f86/uv-0.11.24-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ecdad43e870f88d3772d9d37e877259ae35ec374d51589805cdcf6196205829", size = 25333933, upload-time = "2026-06-23T21:14:01.208Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/3b/90fe0e4c1c58297c43d69caedf7ae4d1400d127a96348003758be2f6f7d0/uv-0.11.24-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:48a6123f71b801e0e0b8a38520b011632ad81e0a043445044ce5b1a7b1cec7b6", size = 24114275, upload-time = "2026-06-23T21:14:03.733Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/35/e15587b5eea12fc6b25cbd4d48fbd848d281f872db60f47bb150bb8bf60b/uv-0.11.24-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:c4ab221c0a949f8006a7582786dae384706b2f82016a0db60baa7cc696042705", size = 24651510, upload-time = "2026-06-23T21:14:06.584Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/47/bb23736f7d3970eae35a3656108ec8cd5eb8d6dcbd329c3070257c0c93bc/uv-0.11.24-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:bf568c62dddd55ad9c85a16ffdfbcc7746be9c3159ed644e4f9e6f5e44905cbb", size = 24723805, upload-time = "2026-06-23T21:14:09.218Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/73/fa71b09aef80edcf82efa5ecd00ca9e4a30bae5a7fd5b08632a8dee513e7/uv-0.11.24-py3-none-musllinux_1_1_i686.whl", hash = "sha256:438f8291fb9daea30a4bd333299b82e6ef755578302745a021162f328cfcfc68", size = 24399230, upload-time = "2026-06-23T21:14:11.775Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/fe/e1d780c3d277adb315f861b05a9aad30506f019b60c3d41beba1078c3796/uv-0.11.24-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:356435577fae11fafe7a067ee3269cccefd35b031b83a3a36c87fe9d192bffba", size = 25523356, upload-time = "2026-06-23T21:14:14.387Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/8f/877584dc86866b93d23f092da202027a17cb80d76e6296fa3aa75a9fa870/uv-0.11.24-py3-none-win32.whl", hash = "sha256:9312b6fd44361674e9c847a828ded792493766697816261e05848a024fe34129", size = 22984622, upload-time = "2026-06-23T21:14:17.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/a3/d95fda2e0bc9ac693a3f90eae673ce2c2d2f8a4c6e87d3a47dbeba9bc34d/uv-0.11.24-py3-none-win_amd64.whl", hash = "sha256:ed0c9a9d7909f0e48a9dafe666ca9ebefe2a1534e51ed05c0a7de7406465f868", size = 25675683, upload-time = "2026-06-23T21:14:19.668Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/f4/2860fef80fa82a33b4f05f744be5620699a5c3c99239499deedeea57be4a/uv-0.11.24-py3-none-win_arm64.whl", hash = "sha256:047d763d20d71968c00f4afec40b0e75d9da7e3693f725b9f502d84a25256893", size = 24140983, upload-time = "2026-06-23T21:14:22.43Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "virtualenv"
|
||||
version = "21.5.0"
|
||||
version = "21.5.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "distlib" },
|
||||
@@ -1111,9 +1111,9 @@ dependencies = [
|
||||
{ name = "python-discovery" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cd/0e/933bacb37b57ae7928b0030eef205a3dbb3e37afdbdde5be2e113318958f/virtualenv-21.5.0.tar.gz", hash = "sha256:98847aadf5e2037e0e4d2e19528eb3aca6f23906422e59a510bff231a6d32fce", size = 4577424, upload-time = "2026-06-13T20:36:45.066Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/a5/81f987504738e6defeed61ec1c47e2aefab3c35d8eeb87e1b3f38cf28254/virtualenv-21.5.1.tar.gz", hash = "sha256:dca3bf98275a59c652b69d68e73433e597d977c2da9198882479d1a7188009c8", size = 4578798, upload-time = "2026-06-16T16:23:58.603Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/87/b0667ede418386ab631e48924b845d326f366d61e6bd08fe68a748fae4d4/virtualenv-21.5.0-py3-none-any.whl", hash = "sha256:8f7c38605023688c89789f566959006af6d61c99eeeb9e58342eb780c5761e5e", size = 4557937, upload-time = "2026-06-13T20:36:42.967Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl", hash = "sha256:55aa670b67bbfb991b03fda39bd3276d92c419d702376e98c5df1c9989a26783", size = 4558820, upload-time = "2026-06-16T16:23:56.963Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1286,9 +1286,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.5.20" },
|
||||
{ name = "certifi", marker = "implementation_name == 'cpython' and extra == 'pin-curl-cffi'", specifier = "==2026.6.17" },
|
||||
{ name = "certifi", marker = "extra == 'default'" },
|
||||
{ name = "certifi", marker = "extra == 'pin'", specifier = "==2026.5.20" },
|
||||
{ name = "certifi", marker = "extra == 'pin'", specifier = "==2026.6.17" },
|
||||
{ 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" },
|
||||
@@ -1302,7 +1302,7 @@ requires-dist = [
|
||||
{ name = "markdown-it-py", marker = "implementation_name == 'cpython' and extra == 'pin-curl-cffi'", specifier = "==4.2.0" },
|
||||
{ name = "mdurl", marker = "implementation_name == 'cpython' and extra == 'pin-curl-cffi'", specifier = "==0.1.2" },
|
||||
{ name = "mutagen", marker = "extra == 'default'" },
|
||||
{ name = "mutagen", marker = "extra == 'pin'", specifier = "==1.47.0" },
|
||||
{ name = "mutagen", marker = "extra == 'pin'", specifier = "==1.48.1" },
|
||||
{ name = "pycparser", marker = "implementation_name != 'PyPy' and platform_python_implementation != 'PyPy' and extra == 'pin-secretstorage'", specifier = "==3.0" },
|
||||
{ name = "pycparser", marker = "implementation_name == 'cpython' and extra == 'pin-curl-cffi'", specifier = "==3.0" },
|
||||
{ name = "pycryptodomex", marker = "extra == 'default'" },
|
||||
|
||||
+14
-5
@@ -170,7 +170,12 @@ from .utils import (
|
||||
write_json_file,
|
||||
write_string,
|
||||
)
|
||||
from .utils._utils import _UnsafeExtensionError, _YDLLogger, _ProgressState
|
||||
from .utils._utils import (
|
||||
_UnsafeExtensionError,
|
||||
_YDLLogger,
|
||||
_ProgressState,
|
||||
_desktop_entry_localestring,
|
||||
)
|
||||
from .utils.networking import (
|
||||
HTTPHeaderDict,
|
||||
clean_headers,
|
||||
@@ -480,7 +485,7 @@ class YoutubeDL:
|
||||
geo_bypass_country
|
||||
external_downloader: A dictionary of protocol keys and the executable of the
|
||||
external downloader to use for it. The allowed protocols
|
||||
are default|http|ftp|m3u8|dash|rtsp|rtmp|mms.
|
||||
are default|http|ftp|m3u8|dash|rtmp.
|
||||
Set the value to 'native' to use the native downloader
|
||||
compat_opts: Compatibility options. See "Differences in default behavior".
|
||||
The following options do not work when used through the API:
|
||||
@@ -1140,7 +1145,7 @@ class YoutubeDL:
|
||||
self.params['logger'].warning(message)
|
||||
elif self.params.get('no_warnings'):
|
||||
if self.params.get('verbose'):
|
||||
self.to_stderr(f'[debug:warning] {message}', only_once=only_once)
|
||||
self.to_stderr(f'[debug] WARNING: {message}', only_once=only_once)
|
||||
else:
|
||||
self.to_stderr(f'{self._format_err("WARNING:", self.Styles.WARNING)} {message}', only_once)
|
||||
|
||||
@@ -3406,10 +3411,13 @@ class YoutubeDL:
|
||||
|
||||
# Write internet shortcut files
|
||||
def _write_link_file(link_type):
|
||||
# iri_to_uri converts to ascii, percent-escapes unsafe characters & validates scheme
|
||||
# See https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-6v4j-43gg-vj32
|
||||
url = try_get(info_dict['webpage_url'], iri_to_uri)
|
||||
if not url:
|
||||
self.report_warning(
|
||||
f'Cannot write internet shortcut file because the actual URL of "{info_dict["webpage_url"]}" is unknown')
|
||||
f'Cannot write internet shortcut file because the actual URL '
|
||||
f'of "{info_dict["webpage_url"]}" is unknown or disallowed')
|
||||
return True
|
||||
linkfn = replace_extension(
|
||||
self.prepare_filename(info_dict, 'link'), link_type,
|
||||
@@ -3425,7 +3433,8 @@ class YoutubeDL:
|
||||
newline='\r\n' if link_type == 'url' else '\n') as linkfile:
|
||||
template_vars = {'url': url}
|
||||
if link_type == 'desktop':
|
||||
template_vars['filename'] = linkfn[:-(len(link_type) + 1)]
|
||||
# See https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-6v4j-43gg-vj32
|
||||
template_vars['filename'] = _desktop_entry_localestring(linkfn[:-(len(link_type) + 1)])
|
||||
linkfile.write(LINK_TEMPLATES[link_type] % template_vars)
|
||||
except OSError:
|
||||
self.report_error(f'Cannot write internet shortcut {linkfn}')
|
||||
|
||||
+1
-1
@@ -296,7 +296,7 @@ def validate_options(opts):
|
||||
default_step = start if op or limit else 0
|
||||
return lambda n: min(float(start) + float(step or default_step) * n, float(limit or 'inf'))
|
||||
|
||||
for key, expr in opts.retry_sleep.items():
|
||||
for key, expr in list(opts.retry_sleep.items()):
|
||||
if not expr:
|
||||
del opts.retry_sleep[key]
|
||||
continue
|
||||
|
||||
@@ -32,7 +32,6 @@ from .ism import IsmFD
|
||||
from .mhtml import MhtmlFD
|
||||
from .niconico import NiconicoLiveFD
|
||||
from .rtmp import RtmpFD
|
||||
from .rtsp import RtspFD
|
||||
from .websocket import WebSocketFragmentFD
|
||||
from .youtube_live_chat import YoutubeLiveChatFD
|
||||
from .bunnycdn import BunnyCdnFD
|
||||
@@ -44,8 +43,6 @@ PROTOCOL_MAP = {
|
||||
'rtmp_ffmpeg': FFmpegFD,
|
||||
'm3u8_native': HlsFD,
|
||||
'm3u8': FFmpegFD,
|
||||
'mms': RtspFD,
|
||||
'rtsp': RtspFD,
|
||||
'f4m': F4mFD,
|
||||
'http_dash_segments': DashSegmentsFD,
|
||||
'http_dash_segments_generator': DashSegmentsFD,
|
||||
|
||||
@@ -375,7 +375,7 @@ class HttpieFD(ExternalFD):
|
||||
|
||||
|
||||
class FFmpegFD(ExternalFD):
|
||||
SUPPORTED_PROTOCOLS = ('http', 'https', 'ftp', 'ftps', 'm3u8', 'm3u8_native', 'rtsp', 'rtmp', 'rtmp_ffmpeg', 'mms', 'http_dash_segments')
|
||||
SUPPORTED_PROTOCOLS = ('http', 'https', 'ftp', 'ftps', 'm3u8', 'm3u8_native', 'rtmp', 'rtmp_ffmpeg', 'http_dash_segments')
|
||||
SUPPORTED_FEATURES = (Features.TO_STDOUT, Features.MULTIPLE_FORMATS)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -300,7 +300,7 @@ class HlsFD(FragmentFD):
|
||||
|
||||
# We only download the first fragment during the test
|
||||
if self.params.get('test', False):
|
||||
fragments = [fragments[0] if fragments else None]
|
||||
fragments = fragments[:1]
|
||||
|
||||
if real_downloader:
|
||||
info_dict['fragments'] = fragments
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
from .common import FileDownloader
|
||||
from ..utils import check_executable
|
||||
|
||||
|
||||
class RtspFD(FileDownloader):
|
||||
def real_download(self, filename, info_dict):
|
||||
url = info_dict['url']
|
||||
self.report_destination(filename)
|
||||
tmpfilename = self.temp_name(filename)
|
||||
|
||||
if check_executable('mplayer', ['-h']):
|
||||
args = [
|
||||
'mplayer', '-really-quiet', '-vo', 'null', '-vc', 'dummy',
|
||||
'-dumpstream', '-dumpfile', tmpfilename, url]
|
||||
elif check_executable('mpv', ['-h']):
|
||||
args = [
|
||||
'mpv', '-really-quiet', '--vo=null', '--stream-dump=' + tmpfilename, url]
|
||||
else:
|
||||
self.report_error('MMS or RTSP download detected but neither "mplayer" nor "mpv" could be run. Please install one')
|
||||
return False
|
||||
|
||||
self._debug_cmd(args)
|
||||
|
||||
retval = subprocess.call(args)
|
||||
if retval == 0:
|
||||
fsize = os.path.getsize(tmpfilename)
|
||||
self.to_screen(f'\r[{args[0]}] {fsize} bytes')
|
||||
self.try_rename(tmpfilename, filename)
|
||||
self._hook_progress({
|
||||
'downloaded_bytes': fsize,
|
||||
'total_bytes': fsize,
|
||||
'filename': filename,
|
||||
'status': 'finished',
|
||||
}, info_dict)
|
||||
return True
|
||||
else:
|
||||
self.to_stderr('\n')
|
||||
self.report_error('%s exited with code %d' % (args[0], retval))
|
||||
return False
|
||||
@@ -106,7 +106,6 @@ from .archiveorg import (
|
||||
)
|
||||
from .arcpublishing import ArcPublishingIE
|
||||
from .ard import (
|
||||
ARDIE,
|
||||
ARDAudiothekIE,
|
||||
ARDAudiothekPlaylistIE,
|
||||
ARDBetaMediathekIE,
|
||||
@@ -359,7 +358,6 @@ from .commonmistakes import (
|
||||
UnicodeBOMIE,
|
||||
)
|
||||
from .commonprotocols import (
|
||||
MmsIE,
|
||||
RtmpIE,
|
||||
ViewSourceIE,
|
||||
)
|
||||
@@ -1314,6 +1312,11 @@ from .oftv import (
|
||||
)
|
||||
from .oktoberfesttv import OktoberfestTVIE
|
||||
from .olympics import OlympicsReplayIE
|
||||
from .omnyfm import (
|
||||
OmnyfmIE,
|
||||
OmnyfmPlaylistIE,
|
||||
OmnyfmShowIE,
|
||||
)
|
||||
from .on24 import On24IE
|
||||
from .ondemandkorea import (
|
||||
OnDemandKoreaIE,
|
||||
@@ -1335,8 +1338,11 @@ from .opencast import (
|
||||
)
|
||||
from .openrec import (
|
||||
OpenRecCaptureIE,
|
||||
OpenRecChannelIE,
|
||||
OpenRecChannelSearchIE,
|
||||
OpenRecIE,
|
||||
OpenRecMovieIE,
|
||||
OpenRecPlaylistIE,
|
||||
)
|
||||
from .orf import (
|
||||
ORFIPTVIE,
|
||||
@@ -1712,7 +1718,10 @@ from .shahid import (
|
||||
from .sharepoint import SharePointIE
|
||||
from .shemaroome import ShemarooMeIE
|
||||
from .shiey import ShieyIE
|
||||
from .showroomlive import ShowRoomLiveIE
|
||||
from .showroomlive import (
|
||||
ShowRoomLiveIE,
|
||||
ShowRoomVodIE,
|
||||
)
|
||||
from .sibnet import SibnetEmbedIE
|
||||
from .simplecast import (
|
||||
SimplecastEpisodeIE,
|
||||
@@ -2394,6 +2403,7 @@ from .zaiko import (
|
||||
ZaikoETicketIE,
|
||||
ZaikoIE,
|
||||
)
|
||||
from .zan import ZanIE
|
||||
from .zapiks import ZapiksIE
|
||||
from .zattoo import (
|
||||
BBVTVIE,
|
||||
|
||||
@@ -4,6 +4,7 @@ from .common import InfoExtractor
|
||||
|
||||
|
||||
class AdobeConnectIE(InfoExtractor):
|
||||
_WORKING = False
|
||||
_VALID_URL = r'https?://\w+\.adobeconnect\.com/(?P<id>[\w-]+)'
|
||||
|
||||
def _real_extract(self, url):
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import json
|
||||
import itertools
|
||||
import re
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..networking.exceptions import HTTPError
|
||||
from ..utils import (
|
||||
ExtractorError,
|
||||
clean_html,
|
||||
int_or_none,
|
||||
try_get,
|
||||
unified_strdate,
|
||||
unified_timestamp,
|
||||
urljoin,
|
||||
)
|
||||
|
||||
|
||||
@@ -17,7 +21,7 @@ class AmericasTestKitchenIE(InfoExtractor):
|
||||
'md5': 'b861c3e365ac38ad319cfd509c30577f',
|
||||
'info_dict': {
|
||||
'id': '5b400b9ee338f922cb06450c',
|
||||
'title': 'Japanese Suppers',
|
||||
'title': 'Weeknight Japanese Suppers',
|
||||
'ext': 'mp4',
|
||||
'display_id': 'weeknight-japanese-suppers',
|
||||
'description': 'md5:64e606bfee910627efc4b5f050de92b3',
|
||||
@@ -106,23 +110,44 @@ class AmericasTestKitchenIE(InfoExtractor):
|
||||
|
||||
|
||||
class AmericasTestKitchenSeasonIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://(?:www\.)?(?P<show>americastestkitchen|(?P<cooks>cooks(?:country|illustrated)))\.com(?:(?:/(?P<show2>cooks(?:country|illustrated)))?(?:/?$|(?<!ated)(?<!ated\.com)/episodes/browse/season_(?P<season>\d+)))'
|
||||
_VALID_URL = r'''(?x)
|
||||
https?://(?:www\.)?(?P<domain>americastestkitchen|cookscountry|cooksillustrated)\.com
|
||||
(?:/(?P<path>cookscountry|cooksillustrated))?
|
||||
(?:/episodes(?:/browse)?/season[-_](?P<season>\d+))?
|
||||
/?(?:[?#]|$)'''
|
||||
_SHOWS = {
|
||||
'americastestkitchen': ('', 'America\'s Test Kitchen'),
|
||||
'cookscountry': ('/cookscountry', 'Cook\'s Country'),
|
||||
'cooksillustrated': ('/cooksillustrated', 'Cook\'s Illustrated'),
|
||||
}
|
||||
_TESTS = [{
|
||||
# ATK Season
|
||||
'url': 'https://www.americastestkitchen.com/episodes/browse/season_1',
|
||||
'url': 'https://www.americastestkitchen.com/episodes/season-1',
|
||||
'info_dict': {
|
||||
'id': 'season_1',
|
||||
'title': 'Season 1',
|
||||
},
|
||||
'playlist_count': 13,
|
||||
'playlist_mincount': 13,
|
||||
}, {
|
||||
# Latest ATK Season (new URL scheme)
|
||||
'url': 'https://www.americastestkitchen.com/episodes/season-26',
|
||||
'info_dict': {
|
||||
'id': 'season_26',
|
||||
'title': 'Season 26',
|
||||
},
|
||||
'playlist_count': 26,
|
||||
}, {
|
||||
# Cooks Country Season
|
||||
'url': 'https://www.americastestkitchen.com/cookscountry/episodes/browse/season_12',
|
||||
'url': 'https://www.americastestkitchen.com/cookscountry/episodes/season-12',
|
||||
'info_dict': {
|
||||
'id': 'season_12',
|
||||
'title': 'Season 12',
|
||||
},
|
||||
'playlist_count': 13,
|
||||
'playlist_mincount': 13,
|
||||
}, {
|
||||
# Old-style URL (redirects to the new season page)
|
||||
'url': 'https://www.americastestkitchen.com/episodes/browse/season_1',
|
||||
'only_matching': True,
|
||||
}, {
|
||||
# America's Test Kitchen Series
|
||||
'url': 'https://www.americastestkitchen.com/',
|
||||
@@ -130,7 +155,7 @@ class AmericasTestKitchenSeasonIE(InfoExtractor):
|
||||
'id': 'americastestkitchen',
|
||||
'title': 'America\'s Test Kitchen',
|
||||
},
|
||||
'playlist_count': 558,
|
||||
'playlist_mincount': 558,
|
||||
}, {
|
||||
# Cooks Country Series
|
||||
'url': 'https://www.americastestkitchen.com/cookscountry',
|
||||
@@ -138,7 +163,7 @@ class AmericasTestKitchenSeasonIE(InfoExtractor):
|
||||
'id': 'cookscountry',
|
||||
'title': 'Cook\'s Country',
|
||||
},
|
||||
'playlist_count': 199,
|
||||
'playlist_mincount': 199,
|
||||
}, {
|
||||
'url': 'https://www.americastestkitchen.com/cookscountry/',
|
||||
'only_matching': True,
|
||||
@@ -157,59 +182,46 @@ class AmericasTestKitchenSeasonIE(InfoExtractor):
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
season_number, show1, show = self._match_valid_url(url).group('season', 'show', 'show2')
|
||||
show_path = ('/' + show) if show else ''
|
||||
show = show or show1
|
||||
season_number = int_or_none(season_number)
|
||||
domain, url_path, season = self._match_valid_url(url).group('domain', 'path', 'season')
|
||||
show_path, title = self._SHOWS[url_path or domain]
|
||||
season = int_or_none(season)
|
||||
|
||||
slug, title = {
|
||||
'americastestkitchen': ('atk', 'America\'s Test Kitchen'),
|
||||
'cookscountry': ('cco', 'Cook\'s Country'),
|
||||
'cooksillustrated': ('cio', 'Cook\'s Illustrated'),
|
||||
}[show]
|
||||
if season:
|
||||
playlist_id = f'season_{season}'
|
||||
playlist_title = f'Season {season}'
|
||||
|
||||
facet_filters = [
|
||||
'search_document_klass:episode',
|
||||
'search_show_slug:' + slug,
|
||||
]
|
||||
|
||||
if season_number:
|
||||
playlist_id = f'season_{season_number}'
|
||||
playlist_title = f'Season {season_number}'
|
||||
facet_filters.append('search_season_list:' + playlist_title)
|
||||
def entries():
|
||||
yield from self._season_entries(show_path, season)
|
||||
else:
|
||||
playlist_id = show
|
||||
playlist_id = url_path or domain
|
||||
playlist_title = title
|
||||
|
||||
season_search = self._download_json(
|
||||
f'https://y1fnzxui30-dsn.algolia.net/1/indexes/everest_search_{slug}_season_desc_production',
|
||||
playlist_id, headers={
|
||||
'Origin': 'https://www.americastestkitchen.com',
|
||||
'X-Algolia-API-Key': '8d504d0099ed27c1b73708d22871d805',
|
||||
'X-Algolia-Application-Id': 'Y1FNZXUI30',
|
||||
}, query={
|
||||
'facetFilters': json.dumps(facet_filters),
|
||||
'attributesToRetrieve': f'description,search_{slug}_episode_number,search_document_date,search_url,title,search_atk_episode_season',
|
||||
'attributesToHighlight': '',
|
||||
'hitsPerPage': 1000,
|
||||
})
|
||||
|
||||
def entries():
|
||||
for episode in (season_search.get('hits') or []):
|
||||
search_url = episode.get('search_url') # always formatted like '/episode/123-title-of-episode'
|
||||
if not search_url:
|
||||
continue
|
||||
yield {
|
||||
'_type': 'url',
|
||||
'url': f'https://www.americastestkitchen.com{show_path or ""}{search_url}',
|
||||
'id': try_get(episode, lambda e: e['objectID'].split('_')[-1]),
|
||||
'title': episode.get('title'),
|
||||
'description': episode.get('description'),
|
||||
'timestamp': unified_timestamp(episode.get('search_document_date')),
|
||||
'season_number': season_number,
|
||||
'episode_number': int_or_none(episode.get(f'search_{slug}_episode_number')),
|
||||
'ie_key': AmericasTestKitchenIE.ie_key(),
|
||||
}
|
||||
def entries():
|
||||
for season_number in itertools.count(1):
|
||||
try:
|
||||
yield from self._season_entries(show_path, season_number)
|
||||
except ExtractorError as e:
|
||||
if isinstance(e.cause, HTTPError) and e.cause.status == 404:
|
||||
break
|
||||
raise
|
||||
|
||||
return self.playlist_result(
|
||||
entries(), playlist_id, playlist_title)
|
||||
|
||||
def _season_entries(self, show_path, season_number):
|
||||
webpage = self._download_webpage(
|
||||
f'https://www.americastestkitchen.com{show_path}/episodes/season-{season_number}',
|
||||
f'season-{season_number}', f'Downloading season {season_number} webpage')
|
||||
seen = set()
|
||||
for episode in re.finditer(
|
||||
r'<a [^>]*\bhref="(?P<path>/(?:cookscountry/|cooksillustrated/)?episode/(?P<id>\d+)-[^"]+)"[^>]*>\s*<h3[^>]*>(?P<title>[^<]+)</h3>',
|
||||
webpage):
|
||||
path = episode.group('path')
|
||||
if path in seen:
|
||||
continue
|
||||
seen.add(path)
|
||||
yield self.url_result(
|
||||
urljoin('https://www.americastestkitchen.com', path),
|
||||
AmericasTestKitchenIE, episode.group('id'),
|
||||
clean_html(episode.group('title')),
|
||||
season_number=season_number)
|
||||
|
||||
@@ -1,39 +1,22 @@
|
||||
import time
|
||||
|
||||
from .common import InfoExtractor
|
||||
from .applepodcasts import AppleBaseIE
|
||||
from ..utils import (
|
||||
ExtractorError,
|
||||
extract_attributes,
|
||||
float_or_none,
|
||||
jwt_decode_hs256,
|
||||
jwt_encode,
|
||||
parse_resolution,
|
||||
qualities,
|
||||
unified_strdate,
|
||||
update_url,
|
||||
url_or_none,
|
||||
urljoin,
|
||||
)
|
||||
from ..utils.traversal import (
|
||||
find_element,
|
||||
require,
|
||||
traverse_obj,
|
||||
)
|
||||
|
||||
|
||||
class AppleConnectIE(InfoExtractor):
|
||||
class AppleConnectIE(AppleBaseIE):
|
||||
IE_NAME = 'apple:music:connect'
|
||||
IE_DESC = 'Apple Music Connect'
|
||||
|
||||
_BASE_URL = 'https://music.apple.com'
|
||||
_QUALITIES = {
|
||||
'provisionalUploadVideo': None,
|
||||
'sdVideo': 480,
|
||||
'sdVideoWithPlusAudio': 480,
|
||||
'sd480pVideo': 480,
|
||||
'720pHdVideo': 720,
|
||||
'1080pHdVideo': 1080,
|
||||
}
|
||||
_VALID_URL = r'https?://music\.apple\.com/[\w-]+/post/(?P<id>\d+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://music.apple.com/us/post/1018290019',
|
||||
@@ -59,29 +42,16 @@ class AppleConnectIE(InfoExtractor):
|
||||
},
|
||||
}]
|
||||
|
||||
_jwt = None
|
||||
|
||||
@staticmethod
|
||||
def _jwt_is_expired(token):
|
||||
return jwt_decode_hs256(token)['exp'] - time.time() < 120
|
||||
|
||||
def _get_token(self, webpage, video_id):
|
||||
if self._jwt and not self._jwt_is_expired(self._jwt):
|
||||
return self._jwt
|
||||
|
||||
js_url = traverse_obj(webpage, (
|
||||
{find_element(tag='script', attr='crossorigin', value='', html=True)},
|
||||
{extract_attributes}, 'src', {urljoin(self._BASE_URL)}, {require('JS URL')}))
|
||||
js = self._download_webpage(
|
||||
js_url, video_id, 'Downloading token JS', 'Unable to download token JS')
|
||||
|
||||
header = jwt_encode({}, '', headers={'alg': 'ES256', 'kid': 'WebPlayKid'}).split('.')[0]
|
||||
self._jwt = self._search_regex(
|
||||
fr'(["\'])(?P<jwt>{header}(?:\.[\w-]+){{2}})\1', js, 'JSON Web Token', group='jwt')
|
||||
if self._jwt_is_expired(self._jwt):
|
||||
raise ExtractorError('The fetched token is already expired')
|
||||
|
||||
return self._jwt
|
||||
_BASE_URL = 'https://music.apple.com'
|
||||
_JWT_KEY_ID = 'WebPlayKid'
|
||||
_QUALITIES = {
|
||||
'provisionalUploadVideo': None,
|
||||
'sdVideo': 480,
|
||||
'sdVideoWithPlusAudio': 480,
|
||||
'sd480pVideo': 480,
|
||||
'720pHdVideo': 720,
|
||||
'1080pHdVideo': 1080,
|
||||
}
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
|
||||
@@ -1,15 +1,55 @@
|
||||
import time
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
ExtractorError,
|
||||
clean_html,
|
||||
clean_podcast_url,
|
||||
int_or_none,
|
||||
jwt_decode_hs256,
|
||||
jwt_encode,
|
||||
parse_iso8601,
|
||||
try_call,
|
||||
update_url,
|
||||
url_or_none,
|
||||
urljoin,
|
||||
)
|
||||
from ..utils.traversal import traverse_obj
|
||||
|
||||
|
||||
class ApplePodcastsIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://podcasts\.apple\.com/(?:[^/]+/)?podcast(?:/[^/]+){1,2}.*?\bi=(?P<id>\d+)'
|
||||
class AppleBaseIE(InfoExtractor):
|
||||
"""Subclasses must set _BASE_URL and _JWT_KEY_ID"""
|
||||
|
||||
_jwt_cache = {}
|
||||
|
||||
@staticmethod
|
||||
def _jwt_is_expired(token):
|
||||
return jwt_decode_hs256(token)['exp'] - time.time() < 120
|
||||
|
||||
def _get_token(self, webpage, episode_id):
|
||||
if self._jwt_cache.get(self._BASE_URL) and not self._jwt_is_expired(self._jwt_cache[self._BASE_URL]):
|
||||
return self._jwt
|
||||
|
||||
js_path = self._search_regex(
|
||||
r'<script [^>]*\bsrc="(/assets/index~[0-9a-f]+\.js)">', webpage, 'JS asset path')
|
||||
js_code = self._download_webpage(
|
||||
urljoin(self._BASE_URL, js_path), episode_id,
|
||||
'Downloading JS asset', 'Unable to download JS asset')
|
||||
|
||||
header = jwt_encode({}, '', headers={'typ': 'JWT', 'alg': 'ES256', 'kid': self._JWT_KEY_ID}).split('.')[0]
|
||||
self._jwt_cache[self._BASE_URL] = self._search_regex(
|
||||
fr'(["\'])(?P<jwt>{header}(?:\.[\w-]+){{2}})\1', js_code, 'JSON Web Token', group='jwt')
|
||||
if self._jwt_is_expired(self._jwt_cache[self._BASE_URL]):
|
||||
raise ExtractorError('The fetched token is already expired')
|
||||
|
||||
return self._jwt_cache[self._BASE_URL]
|
||||
|
||||
|
||||
class ApplePodcastsIE(AppleBaseIE):
|
||||
IE_NAME = 'apple:podcasts'
|
||||
IE_DESC = 'Apple Podcasts'
|
||||
|
||||
_VALID_URL = r'https?://podcasts\.apple\.com/(?P<country>[^/?#]+/)?podcast(?:/[^/?#]+){1,2}/?\?(?:[^#]+&)?i=(?P<id>\d+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://podcasts.apple.com/us/podcast/urbana-podcast-724-by-david-penn/id1531349107?i=1000748574256',
|
||||
'md5': 'f8a6f92735d0cfbd5e6a7294151e28d8',
|
||||
@@ -23,7 +63,7 @@ class ApplePodcastsIE(InfoExtractor):
|
||||
'timestamp': 1770400801,
|
||||
'duration': 3602,
|
||||
'series': 'Urbana Radio Show',
|
||||
'thumbnail': 're:.+[.](png|jpe?g|webp)',
|
||||
'thumbnail': r're:https://.+/.+\.jpg',
|
||||
},
|
||||
}, {
|
||||
'url': 'https://podcasts.apple.com/us/podcast/207-whitney-webb-returns/id1135137367?i=1000482637777',
|
||||
@@ -39,7 +79,7 @@ class ApplePodcastsIE(InfoExtractor):
|
||||
'timestamp': 1593932400,
|
||||
'duration': 5369,
|
||||
'series': 'The Tim Dillon Show',
|
||||
'thumbnail': 're:.+[.](png|jpe?g|webp)',
|
||||
'thumbnail': r're:https://.+/.+\.jpg',
|
||||
},
|
||||
}, {
|
||||
'url': 'https://podcasts.apple.com/podcast/207-whitney-webb-returns/id1135137367?i=1000482637777',
|
||||
@@ -52,15 +92,55 @@ class ApplePodcastsIE(InfoExtractor):
|
||||
'only_matching': True,
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
episode_id = self._match_id(url)
|
||||
webpage = self._download_webpage(url, episode_id)
|
||||
_BASE_URL = 'https://podcasts.apple.com'
|
||||
_JWT_KEY_ID = 'C4J7GBP74H'
|
||||
|
||||
def _extract_podcast_from_api(self, webpage, episode_id, country_code):
|
||||
data = self._download_json(
|
||||
f'https://amp-api.podcasts.apple.com/v1/catalog/{country_code or "us"}/podcast-episodes/{episode_id}',
|
||||
episode_id, headers={
|
||||
'Authorization': f'Bearer {self._get_token(webpage, episode_id)}',
|
||||
'Origin': self._BASE_URL,
|
||||
},
|
||||
query={
|
||||
# XXX: if video is available, try adding the params 'with=entitlements,hlsVideo'
|
||||
'extend': 'fullDescription',
|
||||
'include': 'podcast',
|
||||
'l': 'en-US',
|
||||
})['data'][0]
|
||||
|
||||
thumb_info = traverse_obj(data, ('attributes', 'artwork', {
|
||||
'url': ('url', {url_or_none}),
|
||||
'h': ('height', {int_or_none}),
|
||||
'w': ('width', {int_or_none}),
|
||||
}))
|
||||
|
||||
return {
|
||||
'id': episode_id,
|
||||
**traverse_obj(data, {
|
||||
'title': ('attributes', 'name', {str}),
|
||||
'description': ('attributes', 'fullDescription', {clean_html}),
|
||||
'url': ('attributes', 'assetUrl', {clean_podcast_url}, {update_url(scheme='https')}),
|
||||
'timestamp': ('attributes', 'releaseDateTime', {parse_iso8601}),
|
||||
'duration': ('attributes', 'durationInMilliseconds', {int_or_none(scale=1000)}),
|
||||
'episode': ('attributes', 'name', {str}),
|
||||
'episode_number': ('attributes', 'episodeNumber', {int_or_none}),
|
||||
'series': ('relationships', 'podcast', 'data', 0, 'attributes', 'name', {str}),
|
||||
}),
|
||||
'thumbnail': try_call(lambda: thumb_info.pop('url').format(f='jpg', **thumb_info)),
|
||||
'vcodec': 'none',
|
||||
}
|
||||
|
||||
def _extract_podcast_from_webpage(self, webpage, episode_id):
|
||||
server_data = self._search_json(
|
||||
r'<script [^>]*\bid=["\']serialized-server-data["\'][^>]*>', webpage,
|
||||
'server data', episode_id)['data'][0]['data']
|
||||
'server data', episode_id, default=None)
|
||||
model_data = traverse_obj(server_data, (
|
||||
'headerButtonItems', lambda _, v: v['$kind'] == 'share' and v['modelType'] == 'EpisodeLockup',
|
||||
'data', 0, 'data', 'headerButtonItems',
|
||||
lambda _, v: v['$kind'] == 'share' and v['modelType'] == 'EpisodeLockup',
|
||||
'model', {dict}, any))
|
||||
if not model_data:
|
||||
return None
|
||||
|
||||
return {
|
||||
'id': episode_id,
|
||||
@@ -77,3 +157,12 @@ class ApplePodcastsIE(InfoExtractor):
|
||||
'thumbnail': self._og_search_thumbnail(webpage),
|
||||
'vcodec': 'none',
|
||||
}
|
||||
|
||||
def _real_extract(self, url):
|
||||
episode_id, country_code = self._match_valid_url(url).group('id', 'country')
|
||||
# Webpage may be unavailable, see https://github.com/yt-dlp/yt-dlp/issues/17266
|
||||
webpage = self._download_webpage(url, episode_id, expected_status=500)
|
||||
|
||||
return (
|
||||
self._extract_podcast_from_webpage(webpage, episode_id)
|
||||
or self._extract_podcast_from_api(webpage, episode_id, country_code))
|
||||
|
||||
@@ -11,15 +11,12 @@ from ..utils import (
|
||||
join_nonempty,
|
||||
jwt_decode_hs256,
|
||||
make_archive_id,
|
||||
parse_duration,
|
||||
parse_iso8601,
|
||||
remove_start,
|
||||
str_or_none,
|
||||
unified_strdate,
|
||||
update_url,
|
||||
update_url_query,
|
||||
url_or_none,
|
||||
xpath_text,
|
||||
)
|
||||
from ..utils.traversal import traverse_obj, value
|
||||
|
||||
@@ -119,118 +116,6 @@ class ARDMediathekBaseIE(InfoExtractor):
|
||||
return formats
|
||||
|
||||
|
||||
class ARDIE(InfoExtractor):
|
||||
_VALID_URL = r'(?P<mainurl>https?://(?:www\.)?daserste\.de/(?:[^/?#&]+/)+(?P<id>[^/?#&]+))\.html'
|
||||
_TESTS = [{
|
||||
# available till 7.12.2023
|
||||
'url': 'https://www.daserste.de/information/talk/maischberger/videos/maischberger-video-424.html',
|
||||
'md5': '94812e6438488fb923c361a44469614b',
|
||||
'info_dict': {
|
||||
'id': 'maischberger-video-424',
|
||||
'display_id': 'maischberger-video-424',
|
||||
'ext': 'mp4',
|
||||
'duration': 4452.0,
|
||||
'title': 'maischberger am 07.12.2022',
|
||||
'upload_date': '20221207',
|
||||
'thumbnail': r're:^https?://.*\.jpg$',
|
||||
},
|
||||
}, {
|
||||
'url': 'https://www.daserste.de/information/politik-weltgeschehen/morgenmagazin/videosextern/dominik-kahun-aus-der-nhl-direkt-zur-weltmeisterschaft-100.html',
|
||||
'only_matching': True,
|
||||
}, {
|
||||
'url': 'https://www.daserste.de/information/nachrichten-wetter/tagesthemen/videosextern/tagesthemen-17736.html',
|
||||
'only_matching': True,
|
||||
}, {
|
||||
'url': 'https://www.daserste.de/unterhaltung/serie/in-aller-freundschaft-die-jungen-aerzte/videos/diversity-tag-sanam-afrashteh100.html',
|
||||
'only_matching': True,
|
||||
}, {
|
||||
'url': 'http://www.daserste.de/information/reportage-dokumentation/dokus/videos/die-story-im-ersten-mission-unter-falscher-flagge-100.html',
|
||||
'only_matching': True,
|
||||
}, {
|
||||
'url': 'https://www.daserste.de/unterhaltung/serie/in-aller-freundschaft-die-jungen-aerzte/Drehpause-100.html',
|
||||
'only_matching': True,
|
||||
}, {
|
||||
'url': 'https://www.daserste.de/unterhaltung/film/filmmittwoch-im-ersten/videos/making-ofwendezeit-video-100.html',
|
||||
'only_matching': True,
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
mobj = self._match_valid_url(url)
|
||||
display_id = mobj.group('id')
|
||||
|
||||
player_url = mobj.group('mainurl') + '~playerXml.xml'
|
||||
doc = self._download_xml(player_url, display_id)
|
||||
video_node = doc.find('./video')
|
||||
upload_date = unified_strdate(xpath_text(
|
||||
video_node, './broadcastDate'))
|
||||
thumbnail = xpath_text(video_node, './/teaserImage//variant/url')
|
||||
|
||||
formats = []
|
||||
for a in video_node.findall('.//asset'):
|
||||
file_name = xpath_text(a, './fileName', default=None)
|
||||
if not file_name:
|
||||
continue
|
||||
format_type = a.attrib.get('type')
|
||||
format_url = url_or_none(file_name)
|
||||
if format_url:
|
||||
ext = determine_ext(file_name)
|
||||
if ext == 'm3u8':
|
||||
formats.extend(self._extract_m3u8_formats(
|
||||
format_url, display_id, 'mp4', entry_protocol='m3u8_native',
|
||||
m3u8_id=format_type or 'hls', fatal=False))
|
||||
continue
|
||||
elif ext == 'f4m':
|
||||
formats.extend(self._extract_f4m_formats(
|
||||
update_url_query(format_url, {'hdcore': '3.7.0'}),
|
||||
display_id, f4m_id=format_type or 'hds', fatal=False))
|
||||
continue
|
||||
f = {
|
||||
'format_id': format_type,
|
||||
'width': int_or_none(xpath_text(a, './frameWidth')),
|
||||
'height': int_or_none(xpath_text(a, './frameHeight')),
|
||||
'vbr': int_or_none(xpath_text(a, './bitrateVideo')),
|
||||
'abr': int_or_none(xpath_text(a, './bitrateAudio')),
|
||||
'vcodec': xpath_text(a, './codecVideo'),
|
||||
'tbr': int_or_none(xpath_text(a, './totalBitrate')),
|
||||
}
|
||||
server_prefix = xpath_text(a, './serverPrefix', default=None)
|
||||
if server_prefix:
|
||||
f.update({
|
||||
'url': server_prefix,
|
||||
'playpath': file_name,
|
||||
})
|
||||
else:
|
||||
if not format_url:
|
||||
continue
|
||||
f['url'] = format_url
|
||||
formats.append(f)
|
||||
|
||||
_SUB_FORMATS = (
|
||||
('./dataTimedText', 'ttml'),
|
||||
('./dataTimedTextNoOffset', 'ttml'),
|
||||
('./dataTimedTextVtt', 'vtt'),
|
||||
)
|
||||
|
||||
subtitles = {}
|
||||
for subsel, subext in _SUB_FORMATS:
|
||||
for node in video_node.findall(subsel):
|
||||
subtitles.setdefault('de', []).append({
|
||||
'url': node.attrib['url'],
|
||||
'ext': subext,
|
||||
})
|
||||
|
||||
return {
|
||||
'id': xpath_text(video_node, './videoId', default=display_id),
|
||||
'formats': formats,
|
||||
'subtitles': subtitles,
|
||||
'display_id': display_id,
|
||||
'title': video_node.find('./title').text,
|
||||
'duration': parse_duration(video_node.find('./duration').text),
|
||||
'upload_date': upload_date,
|
||||
'thumbnail': thumbnail,
|
||||
}
|
||||
|
||||
|
||||
class ARDBetaMediathekIE(InfoExtractor):
|
||||
IE_NAME = 'ARDMediathek'
|
||||
_VALID_URL = r'''(?x)https?://
|
||||
|
||||
@@ -127,7 +127,7 @@ class BilibiliBaseIE(InfoExtractor):
|
||||
'format_note': ('quality', {format_names.get}),
|
||||
'duration': ('timelength', {float_or_none(scale=1000)}),
|
||||
}),
|
||||
**parse_resolution(format_names.get(play_info.get('quality'))),
|
||||
**parse_resolution(traverse_obj(play_info, ('quality', {format_names.get}))),
|
||||
})
|
||||
return formats
|
||||
|
||||
@@ -166,8 +166,58 @@ class BilibiliBaseIE(InfoExtractor):
|
||||
params['w_rid'] = hashlib.md5(f'{query}{self._get_wbi_key(video_id)}'.encode()).hexdigest()
|
||||
return params
|
||||
|
||||
def _download_playinfo(self, bvid, cid, headers=None, query=None):
|
||||
params = {'bvid': bvid, 'cid': cid, 'fnval': 4048, **(query or {})}
|
||||
@staticmethod
|
||||
@functools.cache
|
||||
def __screen_dimensions():
|
||||
dims, prefs = zip(
|
||||
((1920, 1080), 18),
|
||||
((1366, 768), 18),
|
||||
((1536, 864), 17),
|
||||
((1280, 720), 8),
|
||||
((2560, 1440), 7),
|
||||
((1440, 900), 5),
|
||||
((1600, 900), 5),
|
||||
strict=True)
|
||||
return random.choices(dims, weights=prefs)[0]
|
||||
|
||||
@property
|
||||
def _dm_params(self):
|
||||
def get_wh(width=1920, height=1080):
|
||||
res0, res1 = width, height
|
||||
rnd = math.floor(114 * random.random())
|
||||
return [2 * res0 + 2 * res1 + 3 * rnd, 4 * res0 - res1 + rnd, rnd]
|
||||
|
||||
def get_of(scroll_top=10, scroll_left=10):
|
||||
res0, res1 = scroll_top, scroll_left
|
||||
rnd = math.floor(514 * random.random())
|
||||
return [3 * res0 + 2 * res1 + rnd, 4 * res0 - 4 * res1 + 2 * rnd, rnd]
|
||||
|
||||
# Source: https://s1.hdslb.com/bfs/seed/jinkela/short/user-fingerprint/bili-user-fingerprint.min.js
|
||||
# function window.__biliUserFp__.queryUserLog
|
||||
# .dm_img_list and .dm_img_inter.ds are more troublesome as they come from mousemove/click events.
|
||||
# Leave them empty for now, since they should allow playing the video without any mousemove/click.
|
||||
return {
|
||||
'dm_img_list': '[]',
|
||||
'dm_img_str': base64.b64encode(
|
||||
''.join(random.choices(string.printable, k=random.randint(16, 64))).encode())[:-2].decode(),
|
||||
'dm_cover_img_str': base64.b64encode(
|
||||
''.join(random.choices(string.printable, k=random.randint(32, 128))).encode())[:-2].decode(),
|
||||
# Bilibili expects dm_img_inter to be a compact JSON (without spaces)
|
||||
'dm_img_inter': json.dumps({
|
||||
'ds': [],
|
||||
'wh': get_wh(*self.__screen_dimensions()),
|
||||
'of': get_of(random.randint(0, 100), 0),
|
||||
}, separators=(',', ':')),
|
||||
}
|
||||
|
||||
def _download_playinfo(self, bvid, cid, headers=None, query=None, fatal=True):
|
||||
params = {
|
||||
'bvid': bvid,
|
||||
'cid': cid,
|
||||
'fnval': 4048,
|
||||
**self._dm_params,
|
||||
**(query or {}),
|
||||
}
|
||||
if self.is_logged_in:
|
||||
params.pop('try_look', None)
|
||||
if qn := params.get('qn'):
|
||||
@@ -175,9 +225,24 @@ class BilibiliBaseIE(InfoExtractor):
|
||||
else:
|
||||
note = f'Downloading video formats for cid {cid}'
|
||||
|
||||
return self._download_json(
|
||||
playurl_raw = self._download_json(
|
||||
'https://api.bilibili.com/x/player/wbi/playurl', bvid,
|
||||
query=self._sign_wbi(params, bvid), headers=headers, note=note)['data']
|
||||
query=self._sign_wbi(params, bvid), headers=headers, note=note)
|
||||
code = traverse_obj(playurl_raw, ('code', {lambda x: x * -1}))
|
||||
if code == 0:
|
||||
return playurl_raw['data']
|
||||
else:
|
||||
msg = join_nonempty(
|
||||
'Unable to download video info', code,
|
||||
traverse_obj(playurl_raw, ('message', {str})),
|
||||
delim=': ')
|
||||
expected = code in (401, 352)
|
||||
if expected:
|
||||
msg += ', please wait and try later'
|
||||
if fatal:
|
||||
raise ExtractorError(msg, expected=expected)
|
||||
else:
|
||||
self.report_warning(msg)
|
||||
|
||||
def json2srt(self, json_data):
|
||||
srt_data = ''
|
||||
@@ -298,7 +363,7 @@ class BilibiliBaseIE(InfoExtractor):
|
||||
'title': f'{metainfo.get("title")} - {next(iter(edges.values())).get("title")}',
|
||||
'formats': self.extract_formats(play_info),
|
||||
'description': f'{json.dumps(edges, ensure_ascii=False)}\n{metainfo.get("description", "")}',
|
||||
'duration': float_or_none(play_info.get('timelength'), scale=1000),
|
||||
'duration': traverse_obj(play_info, ('timelength', {float_or_none(scale=1000)})),
|
||||
'subtitles': self.extract_subtitles(video_id, cid),
|
||||
}
|
||||
|
||||
@@ -662,7 +727,10 @@ class BiliBiliIE(BilibiliBaseIE):
|
||||
if not self._match_valid_url(urlh.url):
|
||||
return self.url_result(urlh.url)
|
||||
|
||||
headers['Referer'] = url
|
||||
headers.update({
|
||||
'Referer': 'https://www.bilibili.com/',
|
||||
'Origin': 'https://www.bilibili.com',
|
||||
})
|
||||
|
||||
initial_state = self._search_json(r'window\.__INITIAL_STATE__\s*=', webpage, 'initial state', video_id, default=None)
|
||||
if not initial_state:
|
||||
@@ -758,13 +826,12 @@ class BiliBiliIE(BilibiliBaseIE):
|
||||
duration=traverse_obj(initial_state, ('videoData', 'duration', {int_or_none})),
|
||||
__post_extractor=self.extract_comments(aid))
|
||||
|
||||
play_info = None
|
||||
if self.is_logged_in:
|
||||
play_info = traverse_obj(
|
||||
self._search_json(r'window\.__playinfo__\s*=', webpage, 'play info', video_id, default=None),
|
||||
('data', {dict}))
|
||||
if not play_info:
|
||||
play_info = self._download_playinfo(video_id, cid, headers=headers, query={'try_look': 1})
|
||||
play_info = traverse_obj(
|
||||
self._search_json(r'window\.__playinfo__\s*=', webpage, 'play info', video_id, default=None),
|
||||
('data', {dict}))
|
||||
if not self.is_logged_in or not play_info:
|
||||
if dl_play_info := self._download_playinfo(video_id, cid, headers=headers, query={'try_look': 1}, fatal=False):
|
||||
play_info = dl_play_info
|
||||
formats = self.extract_formats(play_info)
|
||||
|
||||
if video_data.get('is_upower_exclusive'):
|
||||
@@ -819,13 +886,13 @@ class BiliBiliIE(BilibiliBaseIE):
|
||||
'subtitles': self.extract_subtitles(video_id, cid) if idx == 0 else None,
|
||||
'__post_extractor': self.extract_comments(aid) if idx == 0 else None,
|
||||
} for idx, fragment in enumerate(formats[0]['fragments'])],
|
||||
'duration': float_or_none(play_info.get('timelength'), scale=1000),
|
||||
'duration': traverse_obj(play_info, ('timelength', {float_or_none(scale=1000)})),
|
||||
}
|
||||
|
||||
return {
|
||||
**metainfo,
|
||||
'formats': formats,
|
||||
'duration': float_or_none(play_info.get('timelength'), scale=1000),
|
||||
'duration': traverse_obj(play_info, ('timelength', {float_or_none(scale=1000)})),
|
||||
'chapters': self._get_chapters(aid, cid),
|
||||
'subtitles': self.extract_subtitles(video_id, cid),
|
||||
'__post_extractor': self.extract_comments(aid),
|
||||
@@ -1319,20 +1386,21 @@ class BilibiliSpaceVideoIE(BilibiliSpaceBaseIE):
|
||||
'pn': page_idx + 1,
|
||||
'ps': 30,
|
||||
'tid': 0,
|
||||
'web_location': 1550101,
|
||||
'dm_img_list': '[]',
|
||||
'dm_img_str': base64.b64encode(
|
||||
''.join(random.choices(string.printable, k=random.randint(16, 64))).encode())[:-2].decode(),
|
||||
'dm_cover_img_str': base64.b64encode(
|
||||
''.join(random.choices(string.printable, k=random.randint(32, 128))).encode())[:-2].decode(),
|
||||
'dm_img_inter': '{"ds":[],"wh":[6093,6631,31],"of":[430,760,380]}',
|
||||
'web_location': '333.1387',
|
||||
'special_type': '',
|
||||
'index': 0,
|
||||
**self._dm_params,
|
||||
}
|
||||
|
||||
try:
|
||||
response = self._download_json(
|
||||
'https://api.bilibili.com/x/space/wbi/arc/search', playlist_id,
|
||||
query=self._sign_wbi(query, playlist_id),
|
||||
note=f'Downloading space page {page_idx}', headers={'Referer': url})
|
||||
note=f'Downloading space page {page_idx}', headers={
|
||||
'Referer': url,
|
||||
'Origin': 'https://space.bilibili.com',
|
||||
'Accept-Language': 'en,zh-CN;q=0.9,zh;q=0.8',
|
||||
})
|
||||
except ExtractorError as e:
|
||||
if isinstance(e.cause, HTTPError) and e.cause.status == 412:
|
||||
raise ExtractorError(
|
||||
@@ -2030,12 +2098,9 @@ class BiliBiliDynamicIE(InfoExtractor):
|
||||
|
||||
def _real_extract(self, url):
|
||||
post_id = self._match_id(url)
|
||||
# Without the newer chrome UA, the API will return an error (-352)
|
||||
post_data = self._download_json(
|
||||
'https://api.bilibili.com/x/polymer/web-dynamic/v1/detail', post_id,
|
||||
query={'id': post_id}, headers={
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
||||
})
|
||||
query={'id': post_id})
|
||||
video_url = traverse_obj(post_data, (
|
||||
'data', 'item', (None, 'orig'), 'modules', 'module_dynamic',
|
||||
(('major', ('archive', 'pgc')), ('additional', ('reserve', 'common'))),
|
||||
|
||||
@@ -3542,7 +3542,7 @@ class InfoExtractor:
|
||||
query = urllib.parse.urlparse(url).query
|
||||
url = re.sub(r'/(?:manifest|playlist|jwplayer)\.(?:m3u8|f4m|mpd|smil)', '', url)
|
||||
mobj = re.search(
|
||||
r'(?:(?:http|rtmp|rtsp)(?P<s>s)?:)?(?P<url>//[^?]+)', url)
|
||||
r'(?:(?:http|rtmp)(?P<s>s)?:)?(?P<url>//[^?]+)', url)
|
||||
url_base = mobj.group('url')
|
||||
http_base_url = '{}{}:{}'.format('http', mobj.group('s') or '', url_base)
|
||||
formats = []
|
||||
@@ -3567,28 +3567,16 @@ class InfoExtractor:
|
||||
video_id, mpd_id='dash', fatal=False))
|
||||
if re.search(r'(?:/smil:|\.smil)', url_base):
|
||||
if 'smil' not in skip_protocols:
|
||||
rtmp_formats = self._extract_smil_formats(
|
||||
formats.extend(self._extract_smil_formats(
|
||||
manifest_url('jwplayer.smil'),
|
||||
video_id, fatal=False)
|
||||
for rtmp_format in rtmp_formats:
|
||||
rtsp_format = rtmp_format.copy()
|
||||
rtsp_format['url'] = '{}/{}'.format(rtmp_format['url'], rtmp_format['play_path'])
|
||||
del rtsp_format['play_path']
|
||||
del rtsp_format['ext']
|
||||
rtsp_format.update({
|
||||
'url': rtsp_format['url'].replace('rtmp://', 'rtsp://'),
|
||||
'format_id': rtmp_format['format_id'].replace('rtmp', 'rtsp'),
|
||||
'protocol': 'rtsp',
|
||||
})
|
||||
formats.extend([rtmp_format, rtsp_format])
|
||||
video_id, fatal=False))
|
||||
else:
|
||||
for protocol in ('rtmp', 'rtsp'):
|
||||
if protocol not in skip_protocols:
|
||||
formats.append({
|
||||
'url': f'{protocol}:{url_base}',
|
||||
'format_id': protocol,
|
||||
'protocol': protocol,
|
||||
})
|
||||
if 'rtmp' not in skip_protocols:
|
||||
formats.append({
|
||||
'url': f'rtmp:{url_base}',
|
||||
'format_id': 'rtmp',
|
||||
'protocol': 'rtmp',
|
||||
})
|
||||
return formats
|
||||
|
||||
def _find_jwplayer_data(self, webpage, video_id=None, transform_source=js_to_json):
|
||||
|
||||
@@ -29,34 +29,6 @@ class RtmpIE(InfoExtractor):
|
||||
}
|
||||
|
||||
|
||||
class MmsIE(InfoExtractor):
|
||||
IE_DESC = False # Do not list
|
||||
_VALID_URL = r'(?i)mms://.+'
|
||||
|
||||
_TEST = {
|
||||
# Direct MMS link
|
||||
'url': 'mms://kentro.kaist.ac.kr/200907/MilesReid(0709).wmv',
|
||||
'info_dict': {
|
||||
'id': 'MilesReid(0709)',
|
||||
'ext': 'wmv',
|
||||
'title': 'MilesReid(0709)',
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True, # rtsp downloads, requiring mplayer or mpv
|
||||
},
|
||||
}
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._generic_id(url)
|
||||
title = self._generic_title(url)
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'title': title,
|
||||
'url': url,
|
||||
}
|
||||
|
||||
|
||||
class ViewSourceIE(InfoExtractor):
|
||||
IE_DESC = False
|
||||
_VALID_URL = r'view-source:(?P<url>.+)'
|
||||
|
||||
@@ -11,6 +11,7 @@ from ..utils import (
|
||||
|
||||
|
||||
class DigitallySpeakingIE(InfoExtractor):
|
||||
_WORKING = False
|
||||
_VALID_URL = r'https?://(?:s?evt\.dispeak|events\.digitallyspeaking)\.com/(?:[^/]+/)+xml/(?P<id>[^.]+)\.xml'
|
||||
|
||||
_TESTS = [{
|
||||
|
||||
@@ -138,7 +138,7 @@ class FilmOnChannelIE(InfoExtractor):
|
||||
continue
|
||||
if not is_live:
|
||||
formats.extend(self._extract_wowza_formats(
|
||||
stream_url, channel_id, skip_protocols=['dash', 'rtmp', 'rtsp']))
|
||||
stream_url, channel_id, skip_protocols=['dash', 'rtmp']))
|
||||
continue
|
||||
quality = stream.get('quality')
|
||||
formats.append({
|
||||
|
||||
@@ -10,6 +10,7 @@ from ..utils import (
|
||||
|
||||
|
||||
class FolketingetIE(InfoExtractor):
|
||||
_WORKING = False
|
||||
IE_DESC = 'Folketinget (ft.dk; Danish parliament)'
|
||||
_VALID_URL = r'https?://(?:www\.)?ft\.dk/webtv/video/[^?#]*?\.(?P<id>[0-9]+)\.aspx'
|
||||
_TEST = {
|
||||
|
||||
@@ -3,9 +3,11 @@ import hashlib
|
||||
import itertools
|
||||
import json
|
||||
import re
|
||||
import urllib.parse
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..networking.exceptions import HTTPError
|
||||
from ..networking.impersonate import ImpersonateTarget
|
||||
from ..utils import (
|
||||
ExtractorError,
|
||||
bug_reports_message,
|
||||
@@ -40,17 +42,28 @@ def _id_to_pk(shortcode):
|
||||
|
||||
|
||||
class InstagramBaseIE(InfoExtractor):
|
||||
_API_BASE_URL = 'https://i.instagram.com/api/v1'
|
||||
_BASE_URL = 'https://www.instagram.com/'
|
||||
_LOGIN_URL = 'https://www.instagram.com/accounts/login'
|
||||
_APP_IDS = {
|
||||
'ios': '124024574287414',
|
||||
'web': '936619743392459', # default
|
||||
}
|
||||
_AUTH_COOKIE_NAME = 'sessionid'
|
||||
_COOKIE_DOMAINS = (
|
||||
'i.instagram.com',
|
||||
'.i.instagram.com',
|
||||
'www.instagram.com',
|
||||
'.www.instagram.com',
|
||||
'instagram.com',
|
||||
'.instagram.com',
|
||||
)
|
||||
|
||||
@functools.cached_property
|
||||
def _can_impersonate(self):
|
||||
return self._downloader._impersonate_target_available(ImpersonateTarget())
|
||||
|
||||
@property
|
||||
def _is_logged_in(self):
|
||||
return bool(self._get_cookies(self._BASE_URL).get('sessionid'))
|
||||
return bool(self._get_cookies(self._BASE_URL).get(self._AUTH_COOKIE_NAME))
|
||||
|
||||
@functools.cached_property
|
||||
def _app_id(self):
|
||||
@@ -61,6 +74,12 @@ class InstagramBaseIE(InfoExtractor):
|
||||
def _is_web_app(self):
|
||||
return self._app_id == self._APP_IDS['web']
|
||||
|
||||
@property
|
||||
def _API_BASE_URL(self):
|
||||
if not self._is_web_app:
|
||||
return 'https://i.instagram.com/api/v1'
|
||||
return 'https://www.instagram.com/api/v1'
|
||||
|
||||
@property
|
||||
def _api_headers(self):
|
||||
return {
|
||||
@@ -71,6 +90,11 @@ class InstagramBaseIE(InfoExtractor):
|
||||
'Accept': '*/*',
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _is_login_redirect(url):
|
||||
path = urllib.parse.urlparse(url).path
|
||||
return path.startswith('/accounts/login') or path == '/'
|
||||
|
||||
def _get_count(self, media, kind, *keys):
|
||||
return traverse_obj(
|
||||
media, (kind, 'count'), *((f'edge_media_{key}', 'count') for key in keys),
|
||||
@@ -397,9 +421,11 @@ class InstagramIE(InstagramBaseIE):
|
||||
|
||||
def _real_initialize(self):
|
||||
if self._is_logged_in:
|
||||
self.write_debug('Found Instagram account cookies')
|
||||
return
|
||||
if not self._lsd_token:
|
||||
webpage = self._download_webpage(self._BASE_URL, None, 'Setting up session', impersonate=True)
|
||||
webpage = self._download_webpage(
|
||||
self._BASE_URL, None, 'Setting up session', impersonate=self._can_impersonate)
|
||||
eqmc = self._search_json(
|
||||
r'<script\b[^>]*\bid="__eqmc"[^>]*>', webpage, 'eqmc JSON', None, default={})
|
||||
self._lsd_token = (
|
||||
@@ -411,15 +437,27 @@ class InstagramIE(InstagramBaseIE):
|
||||
media_id = str(_id_to_pk(video_id))
|
||||
|
||||
if self._is_logged_in:
|
||||
return self._extract_product(self._download_json(
|
||||
f'{self._API_BASE_URL}/media/{media_id}/info/', video_id,
|
||||
'Downloading video info', 'Video info extraction failed',
|
||||
impersonate=self._is_web_app, headers=self._api_headers)['items'][0])
|
||||
try:
|
||||
return self._extract_product(self._download_json(
|
||||
f'{self._API_BASE_URL}/media/{media_id}/info/', video_id,
|
||||
'Downloading video info', 'Video info extraction failed',
|
||||
impersonate=self._can_impersonate and self._is_web_app,
|
||||
headers=self._api_headers)['items'][0])
|
||||
except ExtractorError as e:
|
||||
if not (isinstance(e.cause, HTTPError) and self._is_login_redirect(e.cause.response.url)):
|
||||
raise
|
||||
|
||||
self.report_warning('The provided Instagram account cookies are no longer valid')
|
||||
# XXX: With curl-cffi, the error response may not invalidate the cookie in our jar
|
||||
for domain in self._COOKIE_DOMAINS:
|
||||
self.cookiejar.clear(domain=domain, path='/', name=self._AUTH_COOKIE_NAME)
|
||||
# Re-initialize to set lsd token for logged-out extraction
|
||||
self._real_initialize()
|
||||
|
||||
api_check = self._download_json(
|
||||
f'{self._API_BASE_URL}/web/get_ruling_for_content/', video_id,
|
||||
'Checking post accessibility', errnote=False, fatal=False,
|
||||
impersonate=True, headers=self._api_headers,
|
||||
impersonate=self._can_impersonate, headers=self._api_headers,
|
||||
query={'content_type': 'MEDIA', 'target_id': media_id}) or {}
|
||||
|
||||
csrf_token = self._get_cookies('https://www.instagram.com').get('csrftoken')
|
||||
@@ -447,7 +485,7 @@ class InstagramIE(InstagramBaseIE):
|
||||
'server_timestamps': 'true',
|
||||
'variables': json.dumps({'media_id': media_id}, separators=(',', ':')),
|
||||
'doc_id': '27130156389949648',
|
||||
}))
|
||||
})) if self._can_impersonate else None
|
||||
|
||||
media = traverse_obj(response, ('data', 'xig_polaris_media', {dict}))
|
||||
product_info = traverse_obj(media, ('if_not_gated_logged_out', {dict}))
|
||||
@@ -465,8 +503,8 @@ class InstagramIE(InstagramBaseIE):
|
||||
'This content is only available for registered users who follow this account')
|
||||
|
||||
webpage, urlh = self._download_webpage_handle(
|
||||
f'https://www.instagram.com/p/{video_id}', video_id)
|
||||
if urlh.url.startswith(self._LOGIN_URL):
|
||||
f'https://www.instagram.com/p/{video_id}', video_id, impersonate=self._can_impersonate)
|
||||
if self._is_login_redirect(urlh.url):
|
||||
self.raise_login_required(
|
||||
'The webpage request was redirected to the login page. '
|
||||
'You have exceeded the rate-limit for accessing posts anonymously')
|
||||
@@ -685,7 +723,8 @@ class InstagramStoryIE(InstagramBaseIE):
|
||||
if username == 'highlights' and not story_id: # story id is only mandatory for highlights
|
||||
raise ExtractorError('Input URL is missing a highlight ID', expected=True)
|
||||
display_id = story_id or username
|
||||
story_info = self._download_webpage(url, display_id, impersonate=self._is_web_app)
|
||||
story_info = self._download_webpage(
|
||||
url, display_id, impersonate=self._can_impersonate and self._is_web_app)
|
||||
user_info = self._search_json(r'"user":', story_info, 'user info', display_id, fatal=False)
|
||||
if not user_info:
|
||||
self.raise_login_required('This content is unreachable')
|
||||
@@ -700,8 +739,8 @@ class InstagramStoryIE(InstagramBaseIE):
|
||||
|
||||
videos = traverse_obj(self._download_json(
|
||||
f'{self._API_BASE_URL}/feed/reels_media/?reel_ids={story_info_url}',
|
||||
display_id, errnote=False, fatal=False, impersonate=self._is_web_app,
|
||||
headers=self._api_headers), 'reels')
|
||||
display_id, errnote=False, fatal=False, headers=self._api_headers,
|
||||
impersonate=self._can_impersonate and self._is_web_app), 'reels')
|
||||
if not videos:
|
||||
self.raise_login_required('You need to log in to access this content')
|
||||
user_info = traverse_obj(videos, (user_id, 'user', {dict})) or {}
|
||||
|
||||
@@ -2,6 +2,7 @@ from .common import InfoExtractor
|
||||
|
||||
|
||||
class KaraoketvIE(InfoExtractor):
|
||||
_WORKING = False
|
||||
_VALID_URL = r'https?://(?:www\.)?karaoketv\.co\.il/[^/]+/(?P<id>\d+)'
|
||||
_TEST = {
|
||||
'url': 'http://www.karaoketv.co.il/%D7%A9%D7%99%D7%A8%D7%99_%D7%A7%D7%A8%D7%99%D7%95%D7%A7%D7%99/58356/%D7%90%D7%99%D7%96%D7%95%D7%9F',
|
||||
|
||||
@@ -37,10 +37,7 @@ class MangomoloBaseIE(InfoExtractor):
|
||||
m3u8_entry_protocol = 'm3u8' if self._IS_LIVE else 'm3u8_native'
|
||||
|
||||
format_url = self._html_search_regex(
|
||||
[
|
||||
r'(?:file|src)\s*:\s*"(https?://[^"]+?/playlist\.m3u8)',
|
||||
r'<a[^>]+href="(rtsp://[^"]+)"',
|
||||
], webpage, 'format url')
|
||||
r'(?:file|src)\s*:\s*"(https?://[^"]+?/playlist\.m3u8)', webpage, 'format url')
|
||||
formats = self._extract_wowza_formats(
|
||||
format_url, page_id, m3u8_entry_protocol, ['smil'])
|
||||
|
||||
@@ -58,6 +55,18 @@ class MangomoloVideoIE(MangomoloBaseIE):
|
||||
_TYPE = 'video'
|
||||
IE_NAME = 'mangomolo:' + _TYPE
|
||||
_SLUG = r'video\?.*?\bid=(?P<id>\d+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://player.mangomolo.com/v1/video?id=29431242&user_id=168&signature=b40e58d964532fe09bf81fc08d850752&autoplay=true&fullscreen=yes&base_url=aHR0cHM6Ly9heW4ub20vdmlkZW8vMjk0MzEyNDIvJUQ4JUE3JUQ5JTg0JUQ4JUEzJUQ5JTg1JUQ5JTg2JUQ5JThBJUQ4JUE3JUQ4JUFBLSVEOCVBNyVEOSU4NCVEOCVCMyVEOCVBOCVEOCVCOS0lRDglQTclRDklODQlRDglQUQlRDklODQlRDklODIlRDglQTktOQ%3D%3D&vast=true&app_id=&zone=&filter=DENY&countries=Q0M%3D&language=ar&player_profile=',
|
||||
'info_dict': {
|
||||
'id': '29431242',
|
||||
'ext': 'flv',
|
||||
'title': '29431242',
|
||||
},
|
||||
'expected_warnings': [
|
||||
'Failed to download m3u8 information',
|
||||
'Failed to download MPD manifest',
|
||||
],
|
||||
}]
|
||||
|
||||
_IS_LIVE = False
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ from .common import InfoExtractor
|
||||
|
||||
|
||||
class OktoberfestTVIE(InfoExtractor):
|
||||
_WORKING = False
|
||||
_VALID_URL = r'https?://(?:www\.)?oktoberfest-tv\.de/[^/]+/[^/]+/video/(?P<id>[^/?#]+)'
|
||||
|
||||
_TEST = {
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
import functools
|
||||
import itertools
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
OnDemandPagedList,
|
||||
clean_html,
|
||||
filter_dict,
|
||||
float_or_none,
|
||||
int_or_none,
|
||||
parse_duration,
|
||||
parse_iso8601,
|
||||
parse_qs,
|
||||
update_url,
|
||||
url_or_none,
|
||||
)
|
||||
from ..utils.traversal import (
|
||||
require,
|
||||
traverse_obj,
|
||||
trim_str,
|
||||
)
|
||||
|
||||
|
||||
class OmnyfmIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://omny\.fm/shows/(?P<uploader_id>[\w-]+)/(?P<id>(?!playlists(?:[/?#"\']|$))[\w-]+)(?:/embed)?(?=[?#"\']|$)'
|
||||
_EMBED_REGEX = [rf'<iframe[^>]+\bsrc\s*=\s*(["\'])(?P<url>{_VALID_URL}[^"\']*)\1']
|
||||
_TESTS = [{
|
||||
'url': 'https://omny.fm/shows/sleep-hub/cannabinoids-and-sleep',
|
||||
'md5': 'e45ec0ce43da757a0be6ca117ec01bdc',
|
||||
'info_dict': {
|
||||
'id': 'cannabinoids-and-sleep',
|
||||
'ext': 'mp3',
|
||||
'title': 'Cannabinoids and Sleep',
|
||||
'categories': 'count:1',
|
||||
'chapters': [
|
||||
{'start_time': 0, 'title': 'Introduction'},
|
||||
{'start_time': 138, 'title': 'Theme: Cannabinoids and Sleep'},
|
||||
{'start_time': 1487, 'title': 'Clinical Tip'},
|
||||
{'start_time': 1635, 'title': 'Pick of the Month'},
|
||||
{'start_time': 1795, 'title': 'What\'s Coming Up?'},
|
||||
],
|
||||
'description': 'md5:c0fd2d29f3148382d344cfbd012fb00d',
|
||||
'duration': 1840.274,
|
||||
'episode': 'Episode 48',
|
||||
'episode_number': 48,
|
||||
'modified_date': r're:\d{8}',
|
||||
'modified_timestamp': int,
|
||||
'tags': 'count:6',
|
||||
'thumbnail': r're:https?://www\.omnycontent\.com/.+',
|
||||
'timestamp': 1574013600,
|
||||
'upload_date': '20191117',
|
||||
'uploader': 'Sleep Talk',
|
||||
'uploader_id': 'sleep-hub',
|
||||
},
|
||||
}, {
|
||||
'url': 'https://omny.fm/shows/the-origin-of-things/a-song-of-hope/embed',
|
||||
'md5': 'd7600ef33e3f139ff1bb8946f3651b15',
|
||||
'info_dict': {
|
||||
'id': 'a-song-of-hope',
|
||||
'ext': 'mp3',
|
||||
'title': 'A song of hope',
|
||||
'categories': 'count:3',
|
||||
'description': 'md5:f8e710765c341a48cfda8dacd428f56d',
|
||||
'duration': 478.955,
|
||||
'episode': 'Episode 17',
|
||||
'episode_number': 17,
|
||||
'modified_date': r're:\d{8}',
|
||||
'modified_timestamp': int,
|
||||
'season': 'Season 3',
|
||||
'season_number': 3,
|
||||
'tags': 'count:27',
|
||||
'thumbnail': r're:https?://www\.omnycontent\.com/.+',
|
||||
'timestamp': 1679445000,
|
||||
'upload_date': '20230322',
|
||||
'uploader': 'The Origin Of Things',
|
||||
'uploader_id': 'the-origin-of-things',
|
||||
},
|
||||
}]
|
||||
_WEBPAGE_TESTS = [{
|
||||
'url': 'https://www.asahi.com/special/podcasts/item/?itemid=311a5f48-ad71-4548-b1f2-af5e00747fbc',
|
||||
'md5': '4c788bf03323734524a7c0f98d9956ed',
|
||||
'info_dict': {
|
||||
'id': 'sdgs-271',
|
||||
'ext': 'mp3',
|
||||
'title': '「どこかのだれかの人生のにおいがする」 SDGsを音声番組で身近に #271',
|
||||
'categories': 'count:5',
|
||||
'description': 'md5:fa086ecce764d81c51648a82d0fe4850',
|
||||
'duration': 1870.524,
|
||||
'episode': 'Episode 271',
|
||||
'episode_number': 271,
|
||||
'modified_date': r're:\d{8}',
|
||||
'modified_timestamp': int,
|
||||
'season': 'Season 1',
|
||||
'season_number': 1,
|
||||
'tags': 'count:4',
|
||||
'thumbnail': r're:https?://www\.omnycontent\.com/.+',
|
||||
'timestamp': 1670266800,
|
||||
'upload_date': '20221205',
|
||||
'uploader': '朝日新聞ポッドキャスト',
|
||||
'uploader_id': 'asahi',
|
||||
'webpage_url': 'https://omny.fm/shows/asahi/sdgs-271',
|
||||
},
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
uploader_id, audio_id = self._match_valid_url(url).group('uploader_id', 'id')
|
||||
webpage = self._download_webpage(url, audio_id)
|
||||
nextjs_data = self._search_nextjs_data(webpage, audio_id)
|
||||
clip = traverse_obj(nextjs_data, ('props', 'pageProps', 'clip', {dict}))
|
||||
|
||||
return {
|
||||
'id': audio_id,
|
||||
'section_start': traverse_obj(url, ({parse_qs}, 't', -1, {parse_duration})),
|
||||
'uploader_id': uploader_id,
|
||||
'vcodec': 'none',
|
||||
**traverse_obj(clip, {
|
||||
'title': ('Title', {clean_html}, filter),
|
||||
'chapters': ('Chapters', lambda _, v: parse_duration(v['Position']) is not None, {
|
||||
'title': ('Name', {clean_html}, filter),
|
||||
'start_time': ('Position', {parse_duration}),
|
||||
}),
|
||||
'description': ('Description', {clean_html}, filter),
|
||||
'duration': ('DurationSeconds', {float_or_none}),
|
||||
'episode_number': ('Episode', {int_or_none}),
|
||||
'filesize': ('PublishedAudioSizeInBytes', {int_or_none}),
|
||||
'modified_timestamp': ('ModifiedAtUtc', {parse_iso8601}),
|
||||
'season_number': ('Season', {int_or_none}),
|
||||
'tags': ('Tags', ..., {clean_html}, filter, all, filter),
|
||||
'thumbnail': ('ImageUrl', {update_url(query=None)}),
|
||||
'timestamp': ('PublishedUtc', {parse_iso8601}),
|
||||
'url': ('AudioUrl', {url_or_none}, {require('audio URL')}),
|
||||
'webpage_url': ('PublishedUrl', {url_or_none}),
|
||||
}),
|
||||
**traverse_obj(clip, ('Program', {
|
||||
'categories': ('Categories', ..., {clean_html}, filter, all, filter),
|
||||
'uploader': ('Name', {clean_html}, filter),
|
||||
})),
|
||||
}
|
||||
|
||||
|
||||
class OmnyfmPlaylistBaseIE(InfoExtractor):
|
||||
_API_BASE = 'https://api.omny.fm'
|
||||
_BASE_URL = 'https://omny.fm/shows'
|
||||
_PAGE_SIZE = 100
|
||||
|
||||
def _yield_clips(self, clips, uploader_id):
|
||||
for audio_id in traverse_obj(clips, (
|
||||
'Clips', ..., 'Slug', {str},
|
||||
)):
|
||||
yield self.url_result(
|
||||
f'{self._BASE_URL}/{uploader_id}/{audio_id}', OmnyfmIE)
|
||||
|
||||
|
||||
class OmnyfmPlaylistIE(OmnyfmPlaylistBaseIE):
|
||||
_VALID_URL = r'https?://omny\.fm/shows/(?P<uploader_id>[\w-]+)/playlists(?:/(?P<id>[\w-]+))?(?:/embed)?/?(?=[?#"\']|$)'
|
||||
_EMBED_REGEX = [fr'<iframe[^>]+\bsrc=(["\'])(?P<url>{_VALID_URL}[^"\']*)\1']
|
||||
_TESTS = [{
|
||||
'url': 'https://omny.fm/shows/sleep-hub/playlists/sleep-talk',
|
||||
'info_dict': {
|
||||
'id': 'sleep-talk',
|
||||
'title': 'Sleep Talk - Talking all things sleep',
|
||||
'description': 'md5:c1d7e5bf32100a432307d2d32c4ab74a',
|
||||
'thumbnail': r're:https?://www\.omnycontent\.com/.+',
|
||||
},
|
||||
'playlist_mincount': 79,
|
||||
}, {
|
||||
'url': 'https://omny.fm/shows/bayfm-program03/playlists',
|
||||
'info_dict': {
|
||||
'id': 'bayfm-program03',
|
||||
},
|
||||
'playlist_count': 4,
|
||||
}]
|
||||
_WEBPAGE_TESTS = [{
|
||||
'url': 'https://www.asahi.com/articles/ASP763WDKP4JDIFI002.html',
|
||||
'info_dict': {
|
||||
'id': 'podcast',
|
||||
'title': 'ニュースの現場から',
|
||||
'description': 'md5:ed1f78462ebed09258ca31b1da5ff640',
|
||||
'thumbnail': r're:https?://www\.omnycontent\.com/.+',
|
||||
'webpage_url': 'https://omny.fm/shows/asahi/playlists/podcast',
|
||||
},
|
||||
'playlist_mincount': 2517,
|
||||
}]
|
||||
|
||||
def _entries(self, uploader_id, playlist_id):
|
||||
clip_id = None
|
||||
|
||||
for page in itertools.count(1):
|
||||
clips = self._download_json(
|
||||
f'{self._API_BASE}/programs/{uploader_id}/playlists/{playlist_id}/clips',
|
||||
playlist_id, f'Downloading page {page}', query=filter_dict({
|
||||
'clipId': clip_id,
|
||||
'direction': 'AfterExclusive',
|
||||
'pageSize': self._PAGE_SIZE,
|
||||
}))
|
||||
|
||||
yield from self._yield_clips(clips, uploader_id)
|
||||
|
||||
if not clips.get('NextClipsAvailable'):
|
||||
break
|
||||
|
||||
clip_id = traverse_obj(clips, ('Clips', -1, 'Id', {str}))
|
||||
if not clip_id:
|
||||
break
|
||||
|
||||
def _real_extract(self, url):
|
||||
uploader_id, playlist_id = self._match_valid_url(url).group('uploader_id', 'id')
|
||||
webpage = self._download_webpage(url, playlist_id or uploader_id)
|
||||
nextjs_data = self._search_nextjs_data(webpage, playlist_id or uploader_id)
|
||||
page_props = traverse_obj(nextjs_data, ('props', 'pageProps', {dict}))
|
||||
|
||||
if not playlist_id:
|
||||
entries = [self.url_result(
|
||||
f'{self._BASE_URL}/{uploader_id}/playlists/{playlist_id}', OmnyfmPlaylistIE,
|
||||
) for playlist_id in traverse_obj(page_props, (
|
||||
'playlistsWithClips', ..., 'playlist', 'Slug', {str},
|
||||
))]
|
||||
|
||||
return self.playlist_result(entries, uploader_id)
|
||||
|
||||
return self.playlist_result(
|
||||
self._entries(uploader_id, playlist_id), playlist_id,
|
||||
**traverse_obj(page_props, ('playlist', {
|
||||
'title': ('Title', {clean_html}, filter),
|
||||
'description': ('Description', {clean_html}, filter),
|
||||
'thumbnail': ('ArtworkUrl', {update_url(query=None)}),
|
||||
'webpage_url': ('EmbedUrl', {url_or_none}, {trim_str(end='/embed')}),
|
||||
})))
|
||||
|
||||
|
||||
class OmnyfmShowIE(OmnyfmPlaylistBaseIE):
|
||||
_VALID_URL = r'https?://omny\.fm/shows/(?P<id>[\w-]+)/?(?:[?#]|$)'
|
||||
_TESTS = [{
|
||||
'url': 'https://omny.fm/shows/the-origin-of-things',
|
||||
'info_dict': {
|
||||
'id': 'the-origin-of-things',
|
||||
'title': 'The Origin Of Things',
|
||||
'description': 'md5:52b7fba08201d050639c78ea88cc782e',
|
||||
'thumbnail': r're:https?://www\.omnycontent\.com/.+',
|
||||
},
|
||||
'playlist_mincount': 75,
|
||||
}]
|
||||
|
||||
def _fetch_page(self, uploader_id, organization_id, program_id, page):
|
||||
clips = self._download_json(
|
||||
f'{self._API_BASE}/orgs/{organization_id}/programs/{program_id}/clips',
|
||||
uploader_id, f'Downloading page {page + 1}', query={
|
||||
'cursor': page,
|
||||
'pageSize': self._PAGE_SIZE,
|
||||
})
|
||||
|
||||
yield from self._yield_clips(clips, uploader_id)
|
||||
|
||||
def _real_extract(self, url):
|
||||
uploader_id = self._match_id(url)
|
||||
webpage = self._download_webpage(url, uploader_id)
|
||||
nextjs_data = self._search_nextjs_data(webpage, uploader_id)
|
||||
program = traverse_obj(nextjs_data, ('props', 'pageProps', 'program', {dict}))
|
||||
|
||||
organization_id = traverse_obj(program, (
|
||||
'OrganizationId', {str}, {require('organization ID')}))
|
||||
program_id = traverse_obj(program, ('Id', {str}, {require('program ID')}))
|
||||
entries = OnDemandPagedList(
|
||||
functools.partial(self._fetch_page, uploader_id, organization_id, program_id), self._PAGE_SIZE)
|
||||
|
||||
return self.playlist_result(
|
||||
entries, uploader_id,
|
||||
**traverse_obj(program, {
|
||||
'title': ('Name', {clean_html}, filter),
|
||||
'description': ('Description', {clean_html}, filter),
|
||||
'thumbnail': ('ArtworkUrl', {update_url(query=None)}),
|
||||
}))
|
||||
+674
-99
@@ -1,153 +1,728 @@
|
||||
import collections
|
||||
import datetime as dt
|
||||
import functools
|
||||
import itertools
|
||||
import json
|
||||
import math
|
||||
import time
|
||||
import urllib.parse
|
||||
import xml.etree.ElementTree
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
ExtractorError,
|
||||
get_first,
|
||||
InAdvancePagedList,
|
||||
clean_html,
|
||||
extract_attributes,
|
||||
filter_dict,
|
||||
int_or_none,
|
||||
join_nonempty,
|
||||
parse_iso8601,
|
||||
parse_qs,
|
||||
str_or_none,
|
||||
update_url_query,
|
||||
url_or_none,
|
||||
urljoin,
|
||||
)
|
||||
from ..utils.traversal import (
|
||||
find_element,
|
||||
find_elements,
|
||||
require,
|
||||
traverse_obj,
|
||||
try_get,
|
||||
unified_strdate,
|
||||
unified_timestamp,
|
||||
)
|
||||
|
||||
|
||||
class OpenRecBaseIE(InfoExtractor):
|
||||
_M3U8_HEADERS = {'Referer': 'https://www.openrec.tv/'}
|
||||
_API_BASE = 'https://apiv5.mellow-fan.com/api/v5'
|
||||
_BASE_URL = 'https://www.mellow-fan.com'
|
||||
_HEADERS = {'Referer': f'{_BASE_URL}/'}
|
||||
_NETRC_MACHINE = 'mellowfan'
|
||||
_PUBLIC_API_BASE = 'https://public.mellow-fan.com/external/api/v5'
|
||||
|
||||
def _perform_login(self, username, password):
|
||||
if self._get_cookies(self._BASE_URL).get('access-token'):
|
||||
return
|
||||
|
||||
login = self._download_json(
|
||||
f'{self._BASE_URL}/apiv5/email/login',
|
||||
None, 'Logging in', headers={
|
||||
'Content-Type': 'application/json',
|
||||
}, data=json.dumps({
|
||||
'email': username,
|
||||
'password': password,
|
||||
}).encode())
|
||||
if traverse_obj(login, ('status', {int_or_none})) != 0:
|
||||
err_msg = traverse_obj(login, ('message', {clean_html}, filter))
|
||||
raise ExtractorError(err_msg or 'Failed to log in', expected=True)
|
||||
|
||||
def _real_initialize(self):
|
||||
cookies = self._get_cookies(self._BASE_URL)
|
||||
self._api_headers = traverse_obj(cookies, {
|
||||
'access-token': ('access_token', 'value', {str}, filter),
|
||||
'random': ('random', 'value', {str}, filter),
|
||||
'token': ('token', 'value', {str}, filter),
|
||||
'uuid': ('uuid', 'value', {str}, filter),
|
||||
})
|
||||
|
||||
def _extract_pagestore(self, webpage, video_id):
|
||||
return self._parse_json(
|
||||
self._search_regex(r'(?m)window\.pageStore\s*=\s*(\{.+?\});$', webpage, 'window.pageStore'), video_id)
|
||||
start = r'window\.pageStore\s*='
|
||||
|
||||
def _expand_media(self, video_id, media):
|
||||
for name, m3u8_url in (media or {}).items():
|
||||
if not m3u8_url:
|
||||
continue
|
||||
yield from self._extract_m3u8_formats(
|
||||
m3u8_url, video_id, ext='mp4', m3u8_id=name, headers=self._M3U8_HEADERS)
|
||||
if store := self._search_regex(
|
||||
rf'{start}\s*JSON\.parse\s*\(\s*decodeURIComponent'
|
||||
r'\s*\(\s*(?P<q>["\'])(?P<json>.*?)(?P=q)\s*\)\s*\)',
|
||||
webpage, 'encoded window pagestore', group='json', default=None,
|
||||
):
|
||||
return self._parse_json(store, video_id, transform_source=urllib.parse.unquote)
|
||||
return self._search_json(start, webpage, 'window pagestore', video_id)
|
||||
|
||||
def _extract_movie(self, webpage, video_id, name, is_live):
|
||||
window_stores = self._extract_pagestore(webpage, video_id)
|
||||
movie_stores = [
|
||||
# extract all three important data (most of data are duplicated each other, but slightly different!)
|
||||
traverse_obj(window_stores, ('v8', 'state', 'movie'), expected_type=dict),
|
||||
traverse_obj(window_stores, ('v8', 'movie'), expected_type=dict),
|
||||
traverse_obj(window_stores, 'movieStore', expected_type=dict),
|
||||
]
|
||||
if not any(movie_stores):
|
||||
raise ExtractorError(f'Failed to extract {name} info')
|
||||
def _call_api(self, path, item_id):
|
||||
return self._download_json(
|
||||
f'{self._API_BASE}/{path}', item_id,
|
||||
headers=self._api_headers, expected_status=401)
|
||||
|
||||
formats = list(self._expand_media(video_id, get_first(movie_stores, 'media')))
|
||||
if not formats:
|
||||
# archived livestreams or subscriber-only videos
|
||||
cookies = self._get_cookies('https://www.openrec.tv/')
|
||||
detail = self._download_json(
|
||||
f'https://apiv5.openrec.tv/api/v5/movies/{video_id}/detail', video_id,
|
||||
headers={
|
||||
'Origin': 'https://www.openrec.tv',
|
||||
'Referer': 'https://www.openrec.tv/',
|
||||
'access-token': try_get(cookies, lambda x: x.get('access_token').value),
|
||||
'uuid': try_get(cookies, lambda x: x.get('uuid').value),
|
||||
})
|
||||
new_media = traverse_obj(detail, ('data', 'items', ..., 'media'), get_all=False)
|
||||
formats = list(self._expand_media(video_id, new_media))
|
||||
is_live = False
|
||||
def _parse_openrec_metadata(self, page_store, video_id):
|
||||
info = traverse_obj(page_store, ('v8', 'movie', {dict}))
|
||||
|
||||
return {
|
||||
target_members = traverse_obj(info, (
|
||||
'targetMembers', ..., 'type', {str}, filter, any))
|
||||
needs_subscription = target_members == 'subscription'
|
||||
needs_auth = target_members == 'ppv'
|
||||
|
||||
me = self._call_api('users/me', video_id)
|
||||
needs_premium = traverse_obj(info, (
|
||||
'publicType', {str}, filter)) == 'premium'
|
||||
is_premium = traverse_obj(me, (
|
||||
'data', 'items', ..., 'is_premium', {bool}, any)) or False
|
||||
|
||||
detail = self._call_api(f'movies/{video_id}/detail', video_id)
|
||||
is_member = traverse_obj(detail, (
|
||||
'data', 'items', ..., 'membership', 'is_active', {bool}, any)) or False
|
||||
has_ppv = traverse_obj(detail, (
|
||||
'data', 'items', ..., 'ppv_ticket_products', ..., {dict}, any)) or False
|
||||
|
||||
need = None
|
||||
if needs_premium and not is_premium:
|
||||
need = 'premium membership'
|
||||
elif needs_subscription and not is_member:
|
||||
need = 'channel subscription'
|
||||
elif needs_auth and not has_ppv:
|
||||
need = 'PPV purchase'
|
||||
|
||||
if need:
|
||||
self.raise_login_required(
|
||||
f'This content requires a {need}', metadata_available=True)
|
||||
|
||||
return info, detail, {
|
||||
'id': video_id,
|
||||
'title': get_first(movie_stores, 'title'),
|
||||
'description': get_first(movie_stores, 'introduction'),
|
||||
'thumbnail': get_first(movie_stores, 'thumbnailUrl'),
|
||||
'formats': formats,
|
||||
'uploader': get_first(movie_stores, ('channel', 'user', 'name')),
|
||||
'uploader_id': get_first(movie_stores, ('channel', 'user', 'id')),
|
||||
'timestamp': int_or_none(get_first(movie_stores, ['publishedAt', 'time']), scale=1000) or unified_timestamp(get_first(movie_stores, 'publishedAt')),
|
||||
'is_live': is_live,
|
||||
'http_headers': self._M3U8_HEADERS,
|
||||
'availability': self._availability(
|
||||
needs_premium=needs_premium,
|
||||
needs_subscription=needs_subscription,
|
||||
needs_auth=needs_auth,
|
||||
) or 'public',
|
||||
'http_headers': self._HEADERS,
|
||||
'tags': traverse_obj(page_store, (
|
||||
'movieStore', 'tags', ..., {clean_html}, filter, all, filter)),
|
||||
**traverse_obj(info, {
|
||||
'title': ('title', {clean_html}, filter),
|
||||
'cast': ('casts', ..., 'name', {clean_html}, filter, all, filter),
|
||||
'categories': ('game', 'title', {clean_html}, filter, all, filter),
|
||||
'description': ('introduction', {clean_html}, filter),
|
||||
'duration': ('playTime', 'value', {int_or_none(scale=1000)}),
|
||||
'thumbnail': (('lThumbnailUrl', 'thumbnailUrl'), {url_or_none}, any),
|
||||
'timestamp': ('startedAt', 'time', {int_or_none(scale=1000)}),
|
||||
'view_count': ('totalViews', {int_or_none}),
|
||||
}),
|
||||
**traverse_obj(info, ('channel', 'user', {
|
||||
'channel_follower_count': ('followers', {int_or_none}),
|
||||
'channel_id': ('id', {str_or_none}),
|
||||
'channel': ('name', {clean_html}, filter),
|
||||
'channel_is_verified': ('isOfficial', {bool}),
|
||||
})),
|
||||
}
|
||||
|
||||
|
||||
class OpenRecIE(OpenRecBaseIE):
|
||||
IE_NAME = 'openrec'
|
||||
_VALID_URL = r'https?://(?:www\.)?openrec\.tv/live/(?P<id>[^/?#]+)'
|
||||
IE_NAME = 'mellowfan'
|
||||
IE_DESC = 'mellow-fan'
|
||||
|
||||
_VALID_URL = r'https?://(?:www\.)?(?:mellow-fan\.com|openrec\.tv)/(?:m/)?live/(?P<id>[^/?#]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.openrec.tv/live/2p8v31qe4zy',
|
||||
'only_matching': True,
|
||||
'url': 'https://www.openrec.tv/live/e2zwj0mp6ro',
|
||||
'info_dict': {
|
||||
'id': 'e2zwj0mp6ro',
|
||||
'ext': 'mp4',
|
||||
'title': '収束',
|
||||
'availability': 'public',
|
||||
'categories': ['雑談'],
|
||||
'channel': 'おおえのたかゆき',
|
||||
'channel_follower_count': int,
|
||||
'channel_id': 'oekaki',
|
||||
'channel_is_verified': True,
|
||||
'comment_count': int,
|
||||
'description': 'md5:62260f3060b40187282f3213bcd97abd',
|
||||
'duration': 14257,
|
||||
'live_status': 'was_live',
|
||||
'tags': ['雑談'],
|
||||
'thumbnail': r're:https?://.+',
|
||||
'timestamp': 1685271819,
|
||||
'upload_date': '20230528',
|
||||
'view_count': int,
|
||||
},
|
||||
'skip': '404 Not Found',
|
||||
}, {
|
||||
'url': 'https://www.openrec.tv/live/wez93eqvjzl',
|
||||
'only_matching': True,
|
||||
# SP
|
||||
'url': 'https://www.mellow-fan.com/live/2p8vv29438y',
|
||||
'info_dict': {
|
||||
'id': '2p8vv29438y',
|
||||
'ext': 'mp4',
|
||||
'title': 'それいけ加藤純一探検隊! 〜南の孤島で希少生物を探せスペシャル〜 (OPENRECプレミアム会員限定)',
|
||||
'availability': 'premium_only',
|
||||
'categories': ['雑談'],
|
||||
'channel': '加藤 純一',
|
||||
'channel_follower_count': int,
|
||||
'channel_id': 'junichi_kato_channel',
|
||||
'channel_is_verified': True,
|
||||
'comment_count': int,
|
||||
'description': 'md5:693bc0c838ff6080f9ca00d41dc5e840',
|
||||
'duration': 24336,
|
||||
'live_status': 'was_live',
|
||||
'release_date': '20251008',
|
||||
'release_timestamp': 1759892400,
|
||||
'thumbnail': r're:https?://.+',
|
||||
'timestamp': 1759891802,
|
||||
'upload_date': '20251008',
|
||||
'view_count': int,
|
||||
},
|
||||
}, {
|
||||
# Members only
|
||||
'url': 'https://www.mellow-fan.com/live/kdr7nldqgzj',
|
||||
'info_dict': {
|
||||
'id': 'kdr7nldqgzj',
|
||||
'ext': 'mp4',
|
||||
'title': '【ゲーム実況生配信】小森結梨のひきこもりゲーム部屋#20[ゲスト:梅澤めぐ]',
|
||||
'availability': 'subscriber_only',
|
||||
'categories': ['Human Fall Flat'],
|
||||
'channel': 'セカンドショットGAME部',
|
||||
'channel_follower_count': int,
|
||||
'channel_id': 'secondshot_games',
|
||||
'channel_is_verified': True,
|
||||
'chapters': [
|
||||
{'start_time': 0, 'title': 'バイオハザード RE:2', 'end_time': 443},
|
||||
{'start_time': 443, 'title': 'ぷにゃん', 'end_time': 3215},
|
||||
{'start_time': 3215, 'title': 'Human Fall Flat', 'end_time': 7163},
|
||||
],
|
||||
'comment_count': int,
|
||||
'description': 'md5:b77fac6fb0ad86048dc6377ec2c22646',
|
||||
'duration': 7163,
|
||||
'live_status': 'was_live',
|
||||
'release_date': '20260220',
|
||||
'release_timestamp': 1771581300,
|
||||
'tags': ['声優'],
|
||||
'thumbnail': r're:https?://.+',
|
||||
'timestamp': 1771581322,
|
||||
'upload_date': '20260220',
|
||||
'view_count': int,
|
||||
},
|
||||
'skip': 'Subscribers only',
|
||||
}, {
|
||||
# PPV
|
||||
'url': 'https://www.mellow-fan.com/live/e5rk93xn1zv',
|
||||
'info_dict': {
|
||||
'id': 'e5rk93xn1zv',
|
||||
'ext': 'mp4',
|
||||
'title': '三川華月生誕パーティー2026 【ゲスト:幸村恵理/北原沙弥香】',
|
||||
'availability': 'needs_auth',
|
||||
'categories': ['雑談'],
|
||||
'channel': '三川華月の開店!はるちゃん食堂',
|
||||
'channel_follower_count': int,
|
||||
'channel_id': 'haruna_harusyoku',
|
||||
'channel_is_verified': True,
|
||||
'description': 'md5:912d0c2d60d343e28300fc18bd491d6c',
|
||||
'duration': 6451,
|
||||
'live_status': 'was_live',
|
||||
'release_date': '20260222',
|
||||
'release_timestamp': 1771758000,
|
||||
'thumbnail': r're:https?://.+',
|
||||
'timestamp': 1771758062,
|
||||
'upload_date': '20260222',
|
||||
'view_count': int,
|
||||
},
|
||||
'skip': 'Paid video',
|
||||
}]
|
||||
|
||||
@staticmethod
|
||||
def _json2xml(subs, started_at):
|
||||
def filter_valid(items):
|
||||
yield from traverse_obj(items, (
|
||||
lambda _, v: clean_html(v['message']) and not v['stamp']))
|
||||
|
||||
total = collections.Counter(
|
||||
traverse_obj(subs, (..., 'posted_at', {parse_iso8601})))
|
||||
order = collections.defaultdict(int)
|
||||
|
||||
root = xml.etree.ElementTree.Element('packet')
|
||||
for i, s in enumerate(filter_valid(subs), 1):
|
||||
posted_at = traverse_obj(s, ('posted_at', {parse_iso8601}))
|
||||
offset = order[posted_at] / total[posted_at] + 1
|
||||
order[posted_at] += 1
|
||||
vpos = int_or_none((posted_at - started_at + offset) * 100)
|
||||
|
||||
xml.etree.ElementTree.SubElement(
|
||||
root, 'chat', filter_dict({
|
||||
**traverse_obj(s, ('user', {
|
||||
'premium': ('is_premium', {bool}, {lambda x: '1' if x else '0'}),
|
||||
'user_id': ('id', {str_or_none}),
|
||||
'name': ('nickname', {str}, filter),
|
||||
})),
|
||||
'no': str(i),
|
||||
'vpos': str_or_none(vpos),
|
||||
'date': str_or_none(posted_at),
|
||||
}),
|
||||
).text = traverse_obj(s, ('message', {clean_html}, filter))
|
||||
xml.etree.ElementTree.indent(root, space=' ')
|
||||
|
||||
return xml.etree.ElementTree.tostring(
|
||||
root, encoding='utf-8', xml_declaration=True).decode()
|
||||
|
||||
def _get_subtitles(self, duration, started_at, video_id):
|
||||
ended_at = started_at + duration
|
||||
timestamp = started_at
|
||||
|
||||
subs = []
|
||||
for page in itertools.count(1):
|
||||
created_at = dt.datetime.fromtimestamp(
|
||||
timestamp, dt.timezone(dt.timedelta(hours=9))).strftime('%Y-%m-%dT%H:%M:%S%z')
|
||||
|
||||
chats = self._download_json(
|
||||
f'{self._PUBLIC_API_BASE}/movies/{video_id}/chats',
|
||||
video_id, f'Downloading chats page {page}', query={
|
||||
'from_created_at': created_at,
|
||||
'is_including_system_message': 'true',
|
||||
})
|
||||
if not chats:
|
||||
break
|
||||
|
||||
subs.extend(chats)
|
||||
last_posted_at = traverse_obj(chats, (-1, 'posted_at', {parse_iso8601}))
|
||||
if ended_at < last_posted_at:
|
||||
break
|
||||
|
||||
timestamp = last_posted_at + 1
|
||||
time.sleep(0.1)
|
||||
|
||||
return {
|
||||
'chats': [{
|
||||
'data': json.dumps(subs, indent=2, ensure_ascii=False),
|
||||
'ext': 'json',
|
||||
}, {
|
||||
'data': self._json2xml(subs, started_at),
|
||||
'ext': 'xml',
|
||||
}],
|
||||
}
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
webpage = self._download_webpage(f'https://www.openrec.tv/live/{video_id}', video_id)
|
||||
webpage = self._download_webpage(url, video_id, expected_status=404)
|
||||
page_store = self._extract_pagestore(webpage, video_id)
|
||||
if traverse_obj(page_store, ('movieStore', 'notFound', {bool})):
|
||||
raise ExtractorError('This video in no longer available', expected=True)
|
||||
|
||||
return self._extract_movie(webpage, video_id, 'live', True)
|
||||
info, detail, metadata = self._parse_openrec_metadata(page_store, video_id)
|
||||
live_status = {
|
||||
'ARCHIVE': 'was_live',
|
||||
'COMING_UP': 'is_upcoming',
|
||||
'LIVE_STREAMING': 'is_live',
|
||||
'UPLOADED': 'not_live',
|
||||
}.get(info.get('onAirStatus'))
|
||||
|
||||
release_timestamp = traverse_obj(page_store, ('movieStore', 'willStartAt', {parse_iso8601}))
|
||||
if live_status == 'is_upcoming':
|
||||
if release_timestamp is not None:
|
||||
start_time = dt.datetime.fromtimestamp(
|
||||
release_timestamp, dt.timezone.utc,
|
||||
).astimezone().strftime('%Y-%m-%d %H:%M:%S %Z')
|
||||
msg = f'This livestream is scheduled to start at {start_time}'
|
||||
else:
|
||||
msg = 'This livestream has not yet started'
|
||||
|
||||
self.raise_no_formats(msg, expected=True)
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'live_status': live_status,
|
||||
'release_timestamp': release_timestamp,
|
||||
}
|
||||
|
||||
duration = metadata['duration']
|
||||
started_at = metadata['timestamp']
|
||||
|
||||
chapters = []
|
||||
for chapter in traverse_obj(info, (
|
||||
'chapters', lambda _, v: int_or_none(v['chapterAt']['time']),
|
||||
)):
|
||||
chapter_at = traverse_obj(chapter, ('chapterAt', 'time', {int_or_none(scale=1000)}))
|
||||
chapters.append({
|
||||
'start_time': chapter_at - started_at,
|
||||
'title': traverse_obj(chapter, ('title', {clean_html}, filter)),
|
||||
})
|
||||
|
||||
formats = []
|
||||
is_dvr = live_status == 'is_live' and self.get_param('live_from_start')
|
||||
media_keys = ('url_dvr', 'url_dvr_audio') if is_dvr else ('url', 'url_audio')
|
||||
for m3u8_url in traverse_obj(detail, (
|
||||
'data', 'items', ...,
|
||||
('media', 'subs_trial_media'), media_keys, {url_or_none},
|
||||
)):
|
||||
fmts = self._extract_m3u8_formats(
|
||||
m3u8_url, video_id, 'mp4', headers=self._HEADERS)
|
||||
for fmt in fmts:
|
||||
if is_dvr:
|
||||
fmt.setdefault('downloader_options', {}).update({'ffmpeg_args': ['-live_start_index', '0']})
|
||||
fmt['is_from_start'] = True
|
||||
formats.extend(fmts)
|
||||
|
||||
return {
|
||||
'chapters': chapters or None,
|
||||
'comment_count': traverse_obj(page_store, (
|
||||
'commentStore', 'commentCount', {int_or_none})),
|
||||
'formats': formats,
|
||||
'live_status': live_status,
|
||||
'release_timestamp': release_timestamp,
|
||||
'subtitles': self.extract_subtitles(duration, started_at, video_id),
|
||||
**metadata,
|
||||
}
|
||||
|
||||
|
||||
class OpenRecCaptureIE(OpenRecBaseIE):
|
||||
IE_NAME = 'openrec:capture'
|
||||
_VALID_URL = r'https?://(?:www\.)?openrec\.tv/capture/(?P<id>[^/?#]+)'
|
||||
IE_NAME = 'mellowfan:capture'
|
||||
|
||||
_VALID_URL = r'https?://(?:www\.)?(?:mellow-fan\.com|openrec\.tv)/(?:m/)?capture/(?P<id>[^/?#]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.openrec.tv/capture/l9nk2x4gn14',
|
||||
'only_matching': True,
|
||||
}, {
|
||||
'url': 'https://www.openrec.tv/capture/mldjr82p7qk',
|
||||
'url': 'https://www.mellow-fan.com/capture/l2q00vxl8q8',
|
||||
'info_dict': {
|
||||
'id': 'mldjr82p7qk',
|
||||
'title': 'たいじの恥ずかしい英語力',
|
||||
'uploader': 'たいちゃんねる',
|
||||
'uploader_id': 'Yaritaiji',
|
||||
'upload_date': '20210803',
|
||||
'id': 'l2q00vxl8q8',
|
||||
'ext': 'mp4',
|
||||
'title': '????',
|
||||
'channel': '布団ちゃん',
|
||||
'channel_id': 'indegnasen',
|
||||
'duration': 89,
|
||||
'thumbnail': r're:https?://.+',
|
||||
'timestamp': 1637589871,
|
||||
'upload_date': '20211122',
|
||||
},
|
||||
}, {
|
||||
'url': 'https://www.mellow-fan.com/capture/9pdz9334vng',
|
||||
'info_dict': {
|
||||
'id': '9pdz9334vng',
|
||||
'ext': 'mp4',
|
||||
'title': 'オプレの現実',
|
||||
'channel': 'ゆゆうた&みゃこの泥沼バラエティ',
|
||||
'channel_id': 'doronuma-variety',
|
||||
'duration': 64,
|
||||
'thumbnail': r're:https?://.+',
|
||||
'timestamp': 1677585253,
|
||||
'upload_date': '20230228',
|
||||
},
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
webpage = self._download_webpage(f'https://www.openrec.tv/capture/{video_id}', video_id)
|
||||
webpage = self._download_webpage(url, video_id)
|
||||
page_store = self._extract_pagestore(webpage, video_id)
|
||||
|
||||
window_stores = self._extract_pagestore(webpage, video_id)
|
||||
movie_store = window_stores.get('movie')
|
||||
|
||||
capture_data = window_stores.get('capture')
|
||||
if not capture_data:
|
||||
raise ExtractorError('Cannot extract title')
|
||||
|
||||
formats = self._extract_m3u8_formats(
|
||||
capture_data.get('source'), video_id, ext='mp4', headers=self._M3U8_HEADERS)
|
||||
capture = page_store['capture']
|
||||
m3u8_url = traverse_obj(capture, ('source', {url_or_none}))
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'title': capture_data.get('title'),
|
||||
'thumbnail': capture_data.get('thumbnailUrl'),
|
||||
'formats': formats,
|
||||
'timestamp': unified_timestamp(traverse_obj(movie_store, 'createdAt', expected_type=str)),
|
||||
'uploader': traverse_obj(movie_store, ('channel', 'name'), expected_type=str),
|
||||
'uploader_id': traverse_obj(movie_store, ('channel', 'id'), expected_type=str),
|
||||
'upload_date': unified_strdate(capture_data.get('createdAt')),
|
||||
'http_headers': self._M3U8_HEADERS,
|
||||
'formats': self._extract_m3u8_formats(
|
||||
m3u8_url, video_id, 'mp4', headers=self._HEADERS),
|
||||
'http_headers': self._HEADERS,
|
||||
**traverse_obj(page_store, ('movie', 'channel', {
|
||||
'channel': ('name', {clean_html}, filter),
|
||||
'channel_id': ('id', {str}),
|
||||
})),
|
||||
**traverse_obj(capture, {
|
||||
'title': ('title', {clean_html}, filter),
|
||||
'duration': ({lambda x: int_or_none(x['endTime']) - int_or_none(x['startTime'])}),
|
||||
'thumbnail': ('thumbnailUrl', {url_or_none}),
|
||||
'timestamp': ('publishedAt', {parse_iso8601}),
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
class OpenRecMovieIE(OpenRecBaseIE):
|
||||
IE_NAME = 'openrec:movie'
|
||||
_VALID_URL = r'https?://(?:www\.)?openrec\.tv/movie/(?P<id>[^/?#]+)'
|
||||
IE_NAME = 'mellowfan:movie'
|
||||
|
||||
_VALID_URL = r'https?://(?:www\.)?(?:mellow-fan\.com|openrec\.tv)/(?:m/)?movie/(?P<id>[^/?#]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.openrec.tv/movie/nqz5xl5km8v',
|
||||
'url': 'https://www.mellow-fan.com/movie/e5rk9k4o6zv',
|
||||
'info_dict': {
|
||||
'id': 'nqz5xl5km8v',
|
||||
'title': '限定コミュニティ(Discord)参加方法ご説明動画',
|
||||
'description': 'md5:ebd563e5f5b060cda2f02bf26b14d87f',
|
||||
'thumbnail': r're:https://.+',
|
||||
'uploader': 'タイキとカズヒロ',
|
||||
'uploader_id': 'taiki_to_kazuhiro',
|
||||
'timestamp': 1638856800,
|
||||
'id': 'e5rk9k4o6zv',
|
||||
'ext': 'mp4',
|
||||
'title': 'みゃこRaMuの企画会議#3 ~2人のやってみたいこと~',
|
||||
'availability': 'public',
|
||||
'categories': ['雑談'],
|
||||
'channel': 'みゃことRaMuの何して遊ぶ?',
|
||||
'channel_follower_count': int,
|
||||
'channel_id': 'myakoramu',
|
||||
'channel_is_verified': True,
|
||||
'description': 'md5:90924fd73356ebd574bec8d761d4fa62',
|
||||
'duration': 771,
|
||||
'tags': ['雑談'],
|
||||
'thumbnail': r're:https?://.+',
|
||||
'view_count': int,
|
||||
},
|
||||
}, {
|
||||
'url': 'https://www.openrec.tv/movie/2p8vvex548y?playlist_id=98brq96vvsgn2nd',
|
||||
'only_matching': True,
|
||||
# Members only
|
||||
'url': 'https://www.mellow-fan.com/movie/n9ze6q3eo84',
|
||||
'info_dict': {
|
||||
'id': 'n9ze6q3eo84',
|
||||
'ext': 'mp4',
|
||||
'title': '大西亜玖璃・高尾奏音のあぐのんる~むらぼ♪第123回傑作選vol.3【高画質・完全版】',
|
||||
'availability': 'subscriber_only',
|
||||
'categories': ['雑談'],
|
||||
'channel': 'セカンドショットGAME部',
|
||||
'channel_follower_count': int,
|
||||
'channel_id': 'secondshot_games',
|
||||
'channel_is_verified': True,
|
||||
'description': 'md5:b7ab5ddd71ba5edc1141cb427af7a9c6',
|
||||
'duration': 1800,
|
||||
'tags': ['声優'],
|
||||
'thumbnail': r're:https?://.+',
|
||||
'view_count': int,
|
||||
},
|
||||
'skip': 'Subscribers only',
|
||||
}, {
|
||||
# PPV
|
||||
'url': 'https://www.mellow-fan.com/movie/em8xvd4ljr2',
|
||||
'info_dict': {
|
||||
'id': 'em8xvd4ljr2',
|
||||
'ext': 'mp4',
|
||||
'title': '【PPV購入特典映像】KAWAII LAB. SESSION in OKINAWA',
|
||||
'availability': 'needs_auth',
|
||||
'categories': ['ミュージック'],
|
||||
'channel': 'KAWAII LAB.チャンネル',
|
||||
'channel_follower_count': int,
|
||||
'channel_id': 'KAWAIILAB',
|
||||
'channel_is_verified': True,
|
||||
'description': 'md5:e9f67d8648d3cbe35df1dd689afc29a8',
|
||||
'duration': 2002,
|
||||
'tags': ['アイドル'],
|
||||
'thumbnail': r're:https?://.+',
|
||||
'view_count': int,
|
||||
},
|
||||
'skip': 'Paid video',
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
webpage = self._download_webpage(f'https://www.openrec.tv/movie/{video_id}', video_id)
|
||||
webpage = self._download_webpage(url, video_id)
|
||||
page_store = self._extract_pagestore(webpage, video_id)
|
||||
_, detail, metadata = self._parse_openrec_metadata(page_store, video_id)
|
||||
|
||||
return self._extract_movie(webpage, video_id, 'movie', False)
|
||||
formats = []
|
||||
for m3u8_url in traverse_obj(detail, (
|
||||
'data', 'items', ..., 'media',
|
||||
('url', 'url_audio'), {url_or_none},
|
||||
)):
|
||||
formats.extend(self._extract_m3u8_formats(
|
||||
m3u8_url, video_id, 'mp4', headers=self._HEADERS))
|
||||
|
||||
return {
|
||||
'formats': formats,
|
||||
**metadata,
|
||||
}
|
||||
|
||||
|
||||
class OpenRecPlaylistIE(OpenRecBaseIE):
|
||||
IE_NAME = 'mellowfan:playlist'
|
||||
|
||||
_VALID_URL = r'https?://(?:www\.)?(?:mellow-fan\.com|openrec\.tv)/(?:m/)?user/[^/?#]+/playlist/(?P<id>[^/?#]+)'
|
||||
_TESTS = [{
|
||||
# live
|
||||
'url': 'https://www.mellow-fan.com/user/DbD_BPF/playlist/j59svruhtua2z8t',
|
||||
'info_dict': {
|
||||
'id': 'j59svruhtua2z8t',
|
||||
'title': 'BPFのおすすめ',
|
||||
},
|
||||
'playlist_mincount': 10,
|
||||
}, {
|
||||
# capture
|
||||
'url': 'https://www.mellow-fan.com/user/sagara_mayu/playlist/xngNMzv71yLjGdW',
|
||||
'info_dict': {
|
||||
'id': 'xngNMzv71yLjGdW',
|
||||
'title': '相良茉優のFAN!FUN!FACTORY!キャプチャ',
|
||||
},
|
||||
'playlist_mincount': 4,
|
||||
}, {
|
||||
# movie
|
||||
'url': 'https://www.mellow-fan.com/user/oreranohonoka/playlist/sficoshvi9dgkqh',
|
||||
'info_dict': {
|
||||
'id': 'sficoshvi9dgkqh',
|
||||
'title': 'SP(スペシャル)映像:(短編映像)おにいたむプランだけが視聴できます',
|
||||
},
|
||||
'playlist_mincount': 111,
|
||||
}]
|
||||
|
||||
def _entries(self, items):
|
||||
for movie in traverse_obj(items, (
|
||||
'playlist_movies', ..., 'movie', 'id', {str_or_none},
|
||||
)):
|
||||
is_live = traverse_obj(movie, ('movie', 'is_live', {bool}))
|
||||
path, ie = ('live', OpenRecIE) if is_live else ('movie', OpenRecMovieIE)
|
||||
movie_id = movie['movie']['id']
|
||||
|
||||
yield self.url_result(f'{self._BASE_URL}/{path}/{movie_id}', ie)
|
||||
|
||||
for capture in traverse_obj(items, (
|
||||
'playlist_captures', ...,
|
||||
'capture_relation', 'capture', 'id', {str_or_none},
|
||||
)):
|
||||
capture_id = capture['capture_relation']['capture']['id']
|
||||
|
||||
yield self.url_result(
|
||||
f'{self._BASE_URL}/capture/{capture_id}', OpenRecCaptureIE)
|
||||
|
||||
def _real_extract(self, url):
|
||||
playlist_id = self._match_id(url)
|
||||
me = self._call_api(f'users/me/playlists/{playlist_id}', playlist_id)
|
||||
|
||||
if items := traverse_obj(me, (
|
||||
'data', 'items', ..., {dict}, any,
|
||||
)):
|
||||
return self.playlist_result(
|
||||
self._entries(items), playlist_id,
|
||||
traverse_obj(items, ('title', {clean_html}, filter)))
|
||||
|
||||
webpage = self._download_webpage(url, playlist_id)
|
||||
playlist_title = traverse_obj(webpage, (
|
||||
{find_element(cls='sc-1ak77bz-10')}, {clean_html}, filter))
|
||||
|
||||
return self.playlist_from_matches(traverse_obj(webpage, (
|
||||
{find_elements(cls='sc-vc0xhn-0')}, ...,
|
||||
{find_element(cls='sc-1ddd11y-0', html=True)},
|
||||
{extract_attributes}, 'href', {str},
|
||||
)), playlist_id, playlist_title, getter=urljoin(f'{self._BASE_URL}/'))
|
||||
|
||||
|
||||
class OpenRecChannelIE(OpenRecBaseIE):
|
||||
IE_NAME = 'mellowfan:channel'
|
||||
|
||||
_PAGE_SIZE = 40
|
||||
_VALID_URL = r'https?://(?:www\.)?(?:mellow-fan\.com|openrec\.tv)/(?:m/)?user/(?P<id>[^/?#]+)$'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.mellow-fan.com/user/OPENRECPARK',
|
||||
'info_dict': {
|
||||
'id': 'OPENRECPARK',
|
||||
'title': 'OPENREC PARK',
|
||||
},
|
||||
'playlist_mincount': 40,
|
||||
}]
|
||||
|
||||
def _fetch_page(self, channel_id, page):
|
||||
page += 1
|
||||
search_movies = self._download_json(
|
||||
f'{self._PUBLIC_API_BASE}/search-movies', channel_id,
|
||||
f'Downloading page {page}', query={
|
||||
'channel_ids': channel_id,
|
||||
'include_live': 'true',
|
||||
'include_upload': 'true',
|
||||
'onair_status': '2',
|
||||
'include_deleted': 'true',
|
||||
'sort': 'published_at',
|
||||
'page': str(page),
|
||||
})
|
||||
|
||||
for movie in traverse_obj(search_movies, (
|
||||
lambda _, v: str_or_none(v['movie_type']) and str_or_none(v['id']),
|
||||
)):
|
||||
path, ie = ('live', OpenRecIE) if movie['movie_type'] == '1' else ('movie', OpenRecMovieIE)
|
||||
|
||||
yield self.url_result(f'{self._BASE_URL}/{path}/{movie["id"]}', ie)
|
||||
|
||||
def _real_extract(self, url):
|
||||
channel_id = self._match_id(url)
|
||||
webpage = self._download_webpage(url, channel_id)
|
||||
page_store = self._extract_pagestore(webpage, channel_id)
|
||||
|
||||
channel = traverse_obj(page_store, ('state', '_channel', {dict}))
|
||||
movie_count = traverse_obj(channel, ('movieCount', {int_or_none}))
|
||||
|
||||
return self.playlist_result(InAdvancePagedList(
|
||||
functools.partial(self._fetch_page, channel_id),
|
||||
math.ceil(movie_count / self._PAGE_SIZE), self._PAGE_SIZE,
|
||||
), channel_id, traverse_obj(channel, ('user', 'name', {clean_html}, filter)))
|
||||
|
||||
|
||||
class OpenRecChannelSearchIE(OpenRecBaseIE):
|
||||
IE_NAME = 'mellowfan:channel:search'
|
||||
|
||||
_VALID_URL = r'https?://(?:www\.)?(?:mellow-fan\.com|openrec\.tv)/(?:m/)?user/(?P<id>[^/?#]+)/search(?:/(?P<type>capture|movie))?(?:[/?#]|$)'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.mellow-fan.com/user/indegnasen/search?search_query=%E3%82%B9%E3%82%A4%E3%82%AB',
|
||||
'info_dict': {
|
||||
'id': 'indegnasen',
|
||||
'title': 'indegnasen:スイカ',
|
||||
},
|
||||
'playlist_count': 2,
|
||||
}, {
|
||||
'url': 'https://www.mellow-fan.com/user/ofurekodesu/search/movie?search_query=%E3%82%A2%E3%83%AA%E3%82%AA%E5%85%AB%E5%B0%BE',
|
||||
'info_dict': {
|
||||
'id': 'ofurekodesu',
|
||||
'title': 'ofurekodesu:アリオ八尾:movie',
|
||||
},
|
||||
'playlist_mincount': 31,
|
||||
}, {
|
||||
'url': 'https://www.mellow-fan.com/user/DbD_BPF/search/capture?search_query=%E3%81%82%E3%81%A3%E3%81%95%E3%82%8A%E3%81%97%E3%82%87%E3%81%93',
|
||||
'info_dict': {
|
||||
'id': 'DbD_BPF',
|
||||
'title': 'DbD_BPF:あっさりしょこ:capture',
|
||||
},
|
||||
'playlist_mincount': 10,
|
||||
}]
|
||||
|
||||
def _entries(self, channel_id, search_type, search_query):
|
||||
api_url = f'{self._PUBLIC_API_BASE}/search-{search_type}s'
|
||||
type_map = {
|
||||
'capture': OpenRecCaptureIE,
|
||||
'live': OpenRecIE,
|
||||
'movie': OpenRecMovieIE,
|
||||
}
|
||||
|
||||
for page in itertools.count(1):
|
||||
search_items = self._download_json(
|
||||
api_url, channel_id, f'Downloading page {page}', query={
|
||||
'channel_ids': channel_id,
|
||||
'page': page,
|
||||
'search_query': search_query,
|
||||
})
|
||||
if not search_items:
|
||||
break
|
||||
|
||||
for item in search_items:
|
||||
item_type = 'live' if search_type == 'movie' and traverse_obj(item, ('is_live', {bool})) else search_type
|
||||
item_id = traverse_obj(item, ((None, 'capture'), 'id', {str_or_none}, any))
|
||||
|
||||
yield self.url_result(
|
||||
f'{self._BASE_URL}/{item_type}/{item_id}', type_map[item_type])
|
||||
|
||||
def _real_extract(self, url):
|
||||
channel_id, search_type = self._match_valid_url(url).group('id', 'type')
|
||||
search_query = traverse_obj(url, (
|
||||
{parse_qs}, 'search_query', -1, {str}, filter,
|
||||
{require('search query', expected=True)}))
|
||||
|
||||
if not search_type:
|
||||
entries = []
|
||||
for search_type in ('capture', 'movie'):
|
||||
search_url = update_url_query(
|
||||
f'{self._BASE_URL}/user/{channel_id}/search/{search_type}', {'search_query': search_query})
|
||||
entries.append(self.url_result(search_url, OpenRecChannelSearchIE))
|
||||
|
||||
return self.playlist_result(
|
||||
entries, channel_id, join_nonempty(channel_id, search_query, delim=':'))
|
||||
|
||||
return self.playlist_result(
|
||||
self._entries(channel_id, search_type, search_query),
|
||||
channel_id, join_nonempty(channel_id, search_query, search_type, delim=':'))
|
||||
|
||||
@@ -10,6 +10,7 @@ from ..utils import (
|
||||
|
||||
|
||||
class PlaytvakIE(InfoExtractor):
|
||||
_WORKING = False
|
||||
IE_DESC = 'Playtvak.cz, iDNES.cz and Lidovky.cz'
|
||||
_VALID_URL = r'https?://(?:.+?\.)?(?:playtvak|idnes|lidovky|metro)\.cz/.*\?(?:c|idvideo)=(?P<id>[^&]+)'
|
||||
_TESTS = [{
|
||||
|
||||
@@ -5,6 +5,7 @@ from ..utils import int_or_none
|
||||
|
||||
|
||||
class RTL2IE(InfoExtractor):
|
||||
_WORKING = False
|
||||
IE_NAME = 'rtl2'
|
||||
_VALID_URL = r'https?://(?:www\.)?rtl2\.de/sendung/[^/]+/(?:video/(?P<vico_id>\d+)[^/]+/(?P<vivi_id>\d+)-|folge/)(?P<id>[^/?#]+)'
|
||||
_TESTS = [{
|
||||
|
||||
@@ -1,79 +1,134 @@
|
||||
import datetime as dt
|
||||
import json
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
ExtractorError,
|
||||
UserNotLive,
|
||||
clean_html,
|
||||
extract_attributes,
|
||||
int_or_none,
|
||||
urljoin,
|
||||
str_or_none,
|
||||
url_or_none,
|
||||
)
|
||||
from ..utils.traversal import (
|
||||
find_element,
|
||||
require,
|
||||
traverse_obj,
|
||||
)
|
||||
|
||||
|
||||
class ShowRoomLiveIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://(?:www\.)?showroom-live\.com/(?!onlive|timetable|event|campaign|news|ranking|room)(?P<id>[^/?#&]+)'
|
||||
_TEST = {
|
||||
'url': 'https://www.showroom-live.com/48_Nana_Okada',
|
||||
IE_NAME = 'showroom:live'
|
||||
IE_DESC = 'SHOWROOM'
|
||||
|
||||
_VALID_URL = r'https?://(?:www\.)?showroom-live\.com/r/(?P<id>[\w-]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.showroom-live.com/r/48_Yui_Oguri',
|
||||
'only_matching': True,
|
||||
}
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
broadcaster_id = self._match_id(url)
|
||||
webpage = self._download_webpage(
|
||||
url, broadcaster_id, headers={'Accept-Language': 'ja'})
|
||||
nuxt_data = self._search_nuxt_json(webpage, broadcaster_id)['data']
|
||||
|
||||
webpage = self._download_webpage(url, broadcaster_id)
|
||||
cookies = self._get_cookies(url)
|
||||
sr_id = traverse_obj(cookies, ('sr_id', 'value', {str}, filter))
|
||||
if not sr_id:
|
||||
self.raise_login_required()
|
||||
|
||||
room_id = self._search_regex(
|
||||
(r'SrGlobal\.roomId\s*=\s*(\d+)',
|
||||
r'(?:profile|room)\?room_id\=(\d+)'), webpage, 'room_id')
|
||||
room_profile = traverse_obj(nuxt_data, (
|
||||
f'roomProfile-{broadcaster_id}-{sr_id}', {dict}))
|
||||
start_timestamp = traverse_obj(room_profile, ('current_live_started_at', {int_or_none}))
|
||||
is_live = traverse_obj(room_profile, ('is_onlive', {bool}))
|
||||
|
||||
room = self._download_json(
|
||||
urljoin(url, f'/api/room/profile?room_id={room_id}'),
|
||||
broadcaster_id)
|
||||
if not is_live:
|
||||
if start_timestamp:
|
||||
start_time = dt.datetime.fromtimestamp(
|
||||
start_timestamp, dt.timezone.utc,
|
||||
).astimezone().strftime('%Y-%m-%d %H:%M:%S %Z')
|
||||
|
||||
is_live = room.get('is_onlive')
|
||||
if is_live is not True:
|
||||
raise ExtractorError(f'{broadcaster_id} is offline', expected=True)
|
||||
self.raise_no_formats(
|
||||
f'Next livestream is scheduled to start at {start_time}', expected=True)
|
||||
|
||||
uploader = room.get('performer_name') or broadcaster_id
|
||||
title = room.get('room_name') or room.get('main_name') or uploader
|
||||
return {
|
||||
'id': broadcaster_id,
|
||||
'live_status': 'is_upcoming',
|
||||
'release_timestamp': start_timestamp,
|
||||
}
|
||||
raise UserNotLive(video_id=broadcaster_id)
|
||||
|
||||
room_id = traverse_obj(room_profile, ('room_id', {str_or_none}))
|
||||
room_name = traverse_obj(room_profile, (
|
||||
('room_name', 'main_name'), {clean_html}, filter, any))
|
||||
|
||||
streaming_url_list = self._download_json(
|
||||
urljoin(url, f'/api/live/streaming_url?room_id={room_id}'),
|
||||
broadcaster_id)['streaming_url_list']
|
||||
|
||||
formats = []
|
||||
for stream in streaming_url_list:
|
||||
stream_url = stream.get('url')
|
||||
if not stream_url:
|
||||
continue
|
||||
stream_type = stream.get('type')
|
||||
if stream_type == 'hls':
|
||||
m3u8_formats = self._extract_m3u8_formats(
|
||||
stream_url, broadcaster_id, ext='mp4', m3u8_id='hls',
|
||||
live=True)
|
||||
for f in m3u8_formats:
|
||||
f['quality'] = int_or_none(stream.get('quality', 100))
|
||||
formats.extend(m3u8_formats)
|
||||
elif stream_type == 'rtmp':
|
||||
stream_name = stream.get('stream_name')
|
||||
if not stream_name:
|
||||
continue
|
||||
formats.append({
|
||||
'url': stream_url,
|
||||
'play_path': stream_name,
|
||||
'page_url': url,
|
||||
'player_url': 'https://www.showroom-live.com/assets/swf/v3/ShowRoomLive.swf',
|
||||
'rtmp_live': True,
|
||||
'ext': 'flv',
|
||||
'format_id': 'rtmp',
|
||||
'format_note': stream.get('label'),
|
||||
'quality': int_or_none(stream.get('quality', 100)),
|
||||
})
|
||||
'https://www.showroom-live.com/api/live/streaming_url',
|
||||
broadcaster_id, query={'room_id': room_id})
|
||||
m3u8_url = traverse_obj(streaming_url_list, (
|
||||
'streaming_url_list', lambda _, v: v['type'] == 'hls_all',
|
||||
'url', {url_or_none}, any, {require('m3u8 URL')}))
|
||||
|
||||
return {
|
||||
'id': str(room.get('live_id') or broadcaster_id),
|
||||
'title': title,
|
||||
'description': room.get('description'),
|
||||
'timestamp': int_or_none(room.get('current_live_started_at')),
|
||||
'uploader': uploader,
|
||||
'uploader_id': broadcaster_id,
|
||||
'view_count': int_or_none(room.get('view_num')),
|
||||
'formats': formats,
|
||||
'is_live': True,
|
||||
'title': room_name,
|
||||
'channel': room_name,
|
||||
'channel_id': broadcaster_id,
|
||||
'formats': self._extract_m3u8_formats(m3u8_url, broadcaster_id, 'mp4'),
|
||||
'is_live': is_live,
|
||||
'release_timestamp': start_timestamp,
|
||||
**traverse_obj(room_profile, {
|
||||
'id': ('live_id', {str_or_none}),
|
||||
'channel_follower_count': ('follower_num', {int_or_none}),
|
||||
'channel_is_verified': ('is_official', {bool}),
|
||||
'description': ('description', {clean_html}, filter),
|
||||
'genres': ('genre_name', {clean_html}, filter, all, filter),
|
||||
'tags': ('live_tags', ..., 'name', {clean_html}, filter, all, filter),
|
||||
'thumbnail': ('image_square', {url_or_none}),
|
||||
'view_count': ('view_num', {int_or_none}),
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
class ShowRoomVodIE(InfoExtractor):
|
||||
IE_NAME = 'showroom:vod'
|
||||
|
||||
_VALID_URL = r'https?://(?:www\.)?showroom-live\.com/episode/watch\?(?:[^#]+&)?id=(?P<id>\w+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.showroom-live.com/episode/watch?id=214',
|
||||
'info_dict': {
|
||||
'id': '214',
|
||||
'ext': 'mp4',
|
||||
'title': 'aaa',
|
||||
},
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
episode_id = self._match_id(url)
|
||||
webpage = self._download_webpage(url, episode_id)
|
||||
episode_data = traverse_obj(webpage, (
|
||||
{find_element(id='episode-data', html=True)},
|
||||
{extract_attributes}, 'data-episode', {json.loads}, {dict}))
|
||||
|
||||
streaming_url_list = self._download_json(
|
||||
'https://www.showroom-live.com/api/episode/streaming_url',
|
||||
episode_id, query={'episode_id': episode_id})
|
||||
m3u8_url = traverse_obj(streaming_url_list, (
|
||||
'streaming_url_list', 'hls_all',
|
||||
'hls_all', {url_or_none}, {require('m3u8 URL')}))
|
||||
|
||||
return {
|
||||
'id': episode_id,
|
||||
'formats': self._extract_m3u8_formats(m3u8_url, episode_id, 'mp4'),
|
||||
'thumbnail': traverse_obj(streaming_url_list, ('thumbnail_url', {url_or_none})),
|
||||
**traverse_obj(episode_data, {
|
||||
'title': ('title', {clean_html}, filter),
|
||||
'description': ('description', {clean_html}, filter),
|
||||
'duration': ('video_time', {int_or_none}),
|
||||
'timestamp': ('display_started_at', {int_or_none}),
|
||||
}),
|
||||
**traverse_obj(episode_data, ('series', {
|
||||
'series': ('name', {clean_html}, filter),
|
||||
'series_id': ('id', {str_or_none}),
|
||||
})),
|
||||
}
|
||||
|
||||
+63
-42
@@ -5,12 +5,13 @@ from .common import InfoExtractor
|
||||
from ..networking.exceptions import HTTPError
|
||||
from ..utils import (
|
||||
ExtractorError,
|
||||
clean_html,
|
||||
filter_dict,
|
||||
float_or_none,
|
||||
int_or_none,
|
||||
join_nonempty,
|
||||
mimetype2ext,
|
||||
parse_iso8601,
|
||||
unsmuggle_url,
|
||||
update_url_query,
|
||||
url_or_none,
|
||||
)
|
||||
@@ -22,22 +23,28 @@ class StreaksBaseIE(InfoExtractor):
|
||||
_GEO_BYPASS = False
|
||||
_GEO_COUNTRIES = ['JP']
|
||||
|
||||
def _extract_from_streaks_api(self, project_id, media_id, headers=None, query=None, ssai=False, live_from_start=False):
|
||||
def _streaks_playback_api_url(self, project_id, media_id):
|
||||
return self._API_URL_TEMPLATE.format('playback', project_id, media_id, '')
|
||||
|
||||
def _download_streaks_playback_json(self, project_id, media_id, headers=None):
|
||||
return self._download_json(
|
||||
self._streaks_playback_api_url(project_id, media_id),
|
||||
media_id, 'Downloading STREAKS playback API JSON', headers={
|
||||
'Accept': 'application/json',
|
||||
'Origin': 'https://players.streaks.jp',
|
||||
**self.geo_verification_headers(),
|
||||
**(headers or {}),
|
||||
})
|
||||
|
||||
def _extract_from_streaks_api(self, project_id, media_id, headers=None, query=None, live_from_start=False):
|
||||
try:
|
||||
response = self._download_json(
|
||||
self._API_URL_TEMPLATE.format('playback', project_id, media_id, ''),
|
||||
media_id, 'Downloading STREAKS playback API JSON', headers={
|
||||
'Accept': 'application/json',
|
||||
'Origin': 'https://players.streaks.jp',
|
||||
**self.geo_verification_headers(),
|
||||
**(headers or {}),
|
||||
})
|
||||
response = self._download_streaks_playback_json(project_id, media_id, headers=headers)
|
||||
except ExtractorError as e:
|
||||
if isinstance(e.cause, HTTPError) and e.cause.status in (403, 404):
|
||||
error = self._parse_json(e.cause.response.read().decode(), media_id, fatal=False)
|
||||
message = traverse_obj(error, ('message', {str}))
|
||||
code = traverse_obj(error, ('code', {str}))
|
||||
error_id = traverse_obj(error, ('id', {int}))
|
||||
message = traverse_obj(error, ('message', {clean_html}, filter))
|
||||
code = traverse_obj(error, ('code', {clean_html}, filter))
|
||||
error_id = traverse_obj(error, ('id', {int_or_none}))
|
||||
if code == 'REQUEST_FAILED':
|
||||
if error_id == 124:
|
||||
self.raise_geo_restricted(countries=self._GEO_COUNTRIES)
|
||||
@@ -59,8 +66,12 @@ class StreaksBaseIE(InfoExtractor):
|
||||
|
||||
formats, subtitles = [], {}
|
||||
drm_formats = False
|
||||
sources = response['sources']
|
||||
ssai = traverse_obj(sources, (..., 'ssai', {dict}, any))
|
||||
|
||||
for source in traverse_obj(response, ('sources', lambda _, v: v['src'])):
|
||||
for source in traverse_obj(sources, (
|
||||
lambda _, v: url_or_none(v['src']),
|
||||
)):
|
||||
if source.get('key_systems'):
|
||||
drm_formats = True
|
||||
continue
|
||||
@@ -108,12 +119,12 @@ class StreaksBaseIE(InfoExtractor):
|
||||
'subtitles': subtitles,
|
||||
'uploader_id': project_id,
|
||||
**traverse_obj(response, {
|
||||
'title': ('name', {str}),
|
||||
'description': ('description', {str}, filter),
|
||||
'title': ('name', {clean_html}, filter),
|
||||
'description': ('description', {clean_html}, filter),
|
||||
'duration': ('duration', {float_or_none}),
|
||||
'modified_timestamp': ('updated_at', {parse_iso8601}),
|
||||
'tags': ('tags', ..., {str}),
|
||||
'thumbnails': (('poster', 'thumbnail'), 'src', {'url': {url_or_none}}),
|
||||
'tags': ('tags', ..., {clean_html}, filter, all, filter),
|
||||
'thumbnail': (('thumbnail', 'poster'), 'src', {url_or_none}, any),
|
||||
'timestamp': ('created_at', {parse_iso8601}),
|
||||
}),
|
||||
}
|
||||
@@ -121,26 +132,27 @@ class StreaksBaseIE(InfoExtractor):
|
||||
|
||||
class StreaksIE(StreaksBaseIE):
|
||||
_VALID_URL = [
|
||||
r'https?://players\.streaks\.jp/(?P<project_id>[\w-]+)/[\da-f]+/index\.html\?(?:[^#]+&)?m=(?P<id>(?:ref:)?[\w-]+)',
|
||||
r'https?://players\.streaks\.jp/(?P<project_id>[\w-]+)/(?P<api_key>[\da-f]+)/index\.html\?(?:[^#]+&)?m=(?P<id>(?:ref:)?[\w-]+)',
|
||||
r'https?://playback\.api\.streaks\.jp/v1/projects/(?P<project_id>[\w-]+)/medias/(?P<id>(?:ref:)?[\w-]+)',
|
||||
]
|
||||
_EMBED_REGEX = [rf'<iframe\s+[^>]*\bsrc\s*=\s*["\'](?P<url>{_VALID_URL[0]})']
|
||||
_TESTS = [{
|
||||
'url': 'https://players.streaks.jp/tipness/08155cd19dc14c12bebefb69b92eafcc/index.html?m=dbdf2df35b4d483ebaeeaeb38c594647',
|
||||
# https://online.tipness.co.jp/contents/9a064492-a62b-5d03-4668-6b40d6325b1e
|
||||
'url': 'https://players.streaks.jp/tipness/08155cd19dc14c12bebefb69b92eafcc/index.html?m=ba2c253508914d9ea061a5f26bc58b20',
|
||||
'info_dict': {
|
||||
'id': 'dbdf2df35b4d483ebaeeaeb38c594647',
|
||||
'id': 'ba2c253508914d9ea061a5f26bc58b20',
|
||||
'ext': 'mp4',
|
||||
'title': '3shunenCM_edit.mp4',
|
||||
'display_id': 'dbdf2df35b4d483ebaeeaeb38c594647',
|
||||
'duration': 47.533,
|
||||
'title': 'tarun_suimin.mp4',
|
||||
'duration': 265.344,
|
||||
'live_status': 'not_live',
|
||||
'modified_date': '20230726',
|
||||
'modified_timestamp': 1690356180,
|
||||
'timestamp': 1690355996,
|
||||
'upload_date': '20230726',
|
||||
'modified_date': '20230908',
|
||||
'modified_timestamp': 1694146842,
|
||||
'timestamp': 1694145352,
|
||||
'upload_date': '20230908',
|
||||
'uploader_id': 'tipness',
|
||||
},
|
||||
}, {
|
||||
# https://www.ktv.jp/mycoffeetime/
|
||||
'url': 'https://players.streaks.jp/ktv-web/0298e8964c164ab384c07ef6e08c444b/index.html?m=ref:mycoffeetime_250317',
|
||||
'info_dict': {
|
||||
'id': 'dccdc079e3fd41f88b0c8435e2d453ab',
|
||||
@@ -157,22 +169,24 @@ class StreaksIE(StreaksBaseIE):
|
||||
'uploader_id': 'ktv-web',
|
||||
},
|
||||
}, {
|
||||
'url': 'https://playback.api.streaks.jp/v1/projects/ktv-web/medias/b5411938e1e5435dac71edf829dd4813',
|
||||
# https://www.ktv.jp/news/articles/?id=28105
|
||||
'url': 'https://playback.api.streaks.jp/v1/projects/ktv-news/medias/714171c4c53c409bb41e1572997ebfcf',
|
||||
'info_dict': {
|
||||
'id': 'b5411938e1e5435dac71edf829dd4813',
|
||||
'id': '714171c4c53c409bb41e1572997ebfcf',
|
||||
'ext': 'mp4',
|
||||
'title': 'KANTELE_SYUSEi_0630',
|
||||
'display_id': 'b5411938e1e5435dac71edf829dd4813',
|
||||
'title': '28105.mp4',
|
||||
'duration': 49.984,
|
||||
'live_status': 'not_live',
|
||||
'modified_date': '20250122',
|
||||
'modified_timestamp': 1737522999,
|
||||
'modified_date': '20260702',
|
||||
'modified_timestamp': 1783028559,
|
||||
'thumbnail': r're:https?://.+\.jpg',
|
||||
'timestamp': 1735205137,
|
||||
'upload_date': '20241226',
|
||||
'uploader_id': 'ktv-web',
|
||||
'timestamp': 1783028407,
|
||||
'upload_date': '20260702',
|
||||
'uploader_id': 'ktv-news',
|
||||
},
|
||||
'params': {'extractor_args': {'streaks': {'api_key': ['0ff2ccfb6381401582d6ee60e3cb66a1']}}},
|
||||
}, {
|
||||
# TVer Olympics: website already down, but api remains accessible
|
||||
# https://tver.jp/olympic/paris2024/live/FBLMTEAM11------------SFNL000100--/
|
||||
'url': 'https://playback.api.streaks.jp/v1/projects/tver-olympic/medias/ref:sp_240806_1748_dvr',
|
||||
'info_dict': {
|
||||
'id': 'c10f7345adb648cf804d7578ab93b2e3',
|
||||
@@ -187,8 +201,10 @@ class StreaksIE(StreaksBaseIE):
|
||||
'upload_date': '20240804',
|
||||
'uploader_id': 'tver-olympic',
|
||||
},
|
||||
'params': {'extractor_args': {'streaks': {'api_key': ['e09168c4383d4b18949067022558f071']}}},
|
||||
'skip': 'Invalid URL',
|
||||
}, {
|
||||
# TBS FREE: 24-hour stream
|
||||
# https://cu.tbs.co.jp/simul/simul-02
|
||||
'url': 'https://playback.api.streaks.jp/v1/projects/tbs/medias/ref:simul-02',
|
||||
'info_dict': {
|
||||
'id': 'c4e83a7b48f4409a96adacec674b4e22',
|
||||
@@ -202,12 +218,14 @@ class StreaksIE(StreaksBaseIE):
|
||||
'upload_date': '20240117',
|
||||
'uploader_id': 'tbs',
|
||||
},
|
||||
'skip': 'Invalid URL',
|
||||
}, {
|
||||
# DRM protected
|
||||
'url': 'https://players.streaks.jp/sp-jbc/a12d7ee0f40c49d6a0a2bff520639677/index.html?m=5f89c62f37ee4a68be8e6e3b1396c7d8',
|
||||
'only_matching': True,
|
||||
}]
|
||||
_WEBPAGE_TESTS = [{
|
||||
# https://players.streaks.jp/play/719af2a1d2d544e89bcad3456eeae5d9/index.html?m=2d975178293140dc8074a7fc536a7604
|
||||
'url': 'https://event.play.jp/playnext2023/',
|
||||
'info_dict': {
|
||||
'id': '2d975178293140dc8074a7fc536a7604',
|
||||
@@ -222,6 +240,7 @@ class StreaksIE(StreaksBaseIE):
|
||||
'modified_date': '20250213',
|
||||
'live_status': 'not_live',
|
||||
},
|
||||
'params': {'nocheckcertificate': True},
|
||||
}, {
|
||||
'url': 'https://wowshop.jp/Page/special/cooking_goods/?bid=wowshop&srsltid=AfmBOor_phUNoPEE_UCPiGGSCMrJE5T2US397smvsbrSdLqUxwON0el4',
|
||||
'playlist_mincount': 2,
|
||||
@@ -232,13 +251,15 @@ class StreaksIE(StreaksBaseIE):
|
||||
'age_limit': 0,
|
||||
'thumbnail': 'https://wowshop.jp/Page/special/cooking_goods/images/ogp.jpg',
|
||||
},
|
||||
'skip': 'Invalid URL',
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
url, smuggled_data = unsmuggle_url(url, {})
|
||||
project_id, media_id = self._match_valid_url(url).group('project_id', 'id')
|
||||
mobj = self._match_valid_url(url).groupdict()
|
||||
project_id, media_id = mobj['project_id'], mobj['id']
|
||||
api_key = mobj.get('api_key') or self._configuration_arg('api_key', [None])[0]
|
||||
|
||||
return self._extract_from_streaks_api(
|
||||
project_id, media_id, headers=filter_dict({
|
||||
'X-Streaks-Api-Key': smuggled_data.get('api_key'),
|
||||
'X-Streaks-Api-Key': api_key,
|
||||
}))
|
||||
|
||||
@@ -331,7 +331,7 @@ class KnownLiabilityIE(UnsupportedInfoExtractor):
|
||||
"""
|
||||
|
||||
URLS = (
|
||||
r'motherless\.com',
|
||||
r'motherless\.\w+',
|
||||
r'suno\.com',
|
||||
r'udio\.com',
|
||||
)
|
||||
|
||||
+17
-26
@@ -49,7 +49,7 @@ class VimeoBaseInfoExtractor(InfoExtractor):
|
||||
'Cannot download embed-only video without embedding URL. Please call yt-dlp '
|
||||
'with the URL of the page that embeds this video.')
|
||||
|
||||
_DEFAULT_CLIENT = 'macos'
|
||||
_DEFAULT_CLIENT = 'macos_basic'
|
||||
_DEFAULT_AUTHED_CLIENT = 'web'
|
||||
_CLIENT_HEADERS = {
|
||||
'Accept': 'application/vnd.vimeo.*+json; version=3.4.10',
|
||||
@@ -59,7 +59,6 @@ class VimeoBaseInfoExtractor(InfoExtractor):
|
||||
'android': {
|
||||
'CACHE_KEY': 'oauth-token-android',
|
||||
'CACHE_ONLY': True,
|
||||
'VIEWER_JWT': False,
|
||||
'REQUIRES_AUTH': False,
|
||||
'AUTH': 'NzRmYTg5YjgxMWExY2JiNzUwZDg1MjhkMTYzZjQ4YWYyOGEyZGJlMTp4OGx2NFd3QnNvY1lkamI2UVZsdjdDYlNwSDUrdm50YzdNNThvWDcwN1JrenJGZC9tR1lReUNlRjRSVklZeWhYZVpRS0tBcU9YYzRoTGY2Z1dlVkJFYkdJc0dMRHpoZWFZbU0reDRqZ1dkZ1diZmdIdGUrNUM5RVBySlM0VG1qcw==',
|
||||
'USER_AGENT': 'com.vimeo.android.videoapp (OnePlus, ONEPLUS A6003, OnePlus, Android 14/34 Version 11.8.1) Kotlin VimeoNetworking/3.12.0',
|
||||
@@ -71,26 +70,8 @@ class VimeoBaseInfoExtractor(InfoExtractor):
|
||||
'resource_key', 'badge', 'upload', 'transcode', 'is_playable', 'has_audio',
|
||||
),
|
||||
},
|
||||
'ios': {
|
||||
'CACHE_KEY': 'oauth-token-ios',
|
||||
'CACHE_ONLY': True,
|
||||
'VIEWER_JWT': False,
|
||||
'REQUIRES_AUTH': False,
|
||||
'AUTH': 'MTMxNzViY2Y0NDE0YTQ5YzhjZTc0YmU0NjVjNDQxYzNkYWVjOWRlOTpHKzRvMmgzVUh4UkxjdU5FRW80cDNDbDhDWGR5dVJLNUJZZ055dHBHTTB4V1VzaG41bEx1a2hiN0NWYWNUcldSSW53dzRUdFRYZlJEZmFoTTArOTBUZkJHS3R4V2llYU04Qnl1bERSWWxUdXRidjNqR2J4SHFpVmtFSUcyRktuQw==',
|
||||
'USER_AGENT': 'Vimeo/11.10.0 (com.vimeo; build:250424.164813.0; iOS 18.4.1) Alamofire/5.9.0 VimeoNetworking/5.0.0',
|
||||
'VIDEOS_FIELDS': (
|
||||
'uri', 'name', 'description', 'type', 'link', 'player_embed_url', 'duration',
|
||||
'width', 'language', 'height', 'embed', 'created_time', 'modified_time', 'release_time',
|
||||
'content_rating', 'content_rating_class', 'rating_mod_locked', 'license', 'config_url',
|
||||
'embed_player_config_url', 'privacy', 'pictures', 'tags', 'stats', 'categories', 'uploader',
|
||||
'metadata', 'user', 'files', 'download', 'app', 'play', 'status', 'resource_key', 'badge',
|
||||
'upload', 'transcode', 'is_playable', 'has_audio',
|
||||
),
|
||||
},
|
||||
'macos': {
|
||||
'CACHE_KEY': 'oauth-token-macos',
|
||||
'macos_basic': {
|
||||
'CACHE_ONLY': False,
|
||||
'VIEWER_JWT': False,
|
||||
'REQUIRES_AUTH': False,
|
||||
'AUTH': 'NDc1N2JlN2Y5ZjZmMjU3NzE3NTRkZTg1NmY2YzU2MTI0OTFlNjJiYjpwVUNDWUlBZmZqSHhQcndBYWxGMzgyYys2NkN5d1JrREJZZXdPcEdsU05tdjFlVVo2aE1lYk9GcWE3ZW9KVldlYnFlOWh5Vno5UWtpUGJ5empYZFBpYkFwV0FFTnB5VWV4ZEh3aHZnRUNEL0VySnBzTmFraDdNbS9nMXhWanhIcw==',
|
||||
'USER_AGENT': 'Vimeo/1.6.3 (com.vimeo.mac; build:251121.142637.0; macOS 13.7.8) Alamofire/5.9.0 VimeoNetworking/5.0.0',
|
||||
@@ -104,7 +85,6 @@ class VimeoBaseInfoExtractor(InfoExtractor):
|
||||
},
|
||||
'web': {
|
||||
'CACHE_ONLY': False,
|
||||
'VIEWER_JWT': True,
|
||||
'REQUIRES_AUTH': True,
|
||||
'USER_AGENT': None,
|
||||
'VIDEOS_FIELDS': (
|
||||
@@ -182,7 +162,8 @@ class VimeoBaseInfoExtractor(InfoExtractor):
|
||||
if self._LOGIN_REQUIRED:
|
||||
self.raise_login_required()
|
||||
|
||||
if self._DEFAULT_CLIENT != 'web':
|
||||
# Don't auto-load token from cache if the user has specified a client
|
||||
if self._configuration_arg('client', [None], ie_key=VimeoIE)[0]:
|
||||
return
|
||||
|
||||
for client_name, client_config in self._CLIENT_CONFIGS.items():
|
||||
@@ -367,11 +348,14 @@ class VimeoBaseInfoExtractor(InfoExtractor):
|
||||
}
|
||||
|
||||
def _fetch_oauth_token(self, client):
|
||||
client_config = self._CLIENT_CONFIGS[client]
|
||||
|
||||
if client_config['VIEWER_JWT']:
|
||||
base_client, _, variant = client.partition('_')
|
||||
if base_client == 'web':
|
||||
return f'jwt {self._fetch_viewer_info()["jwt"]}'
|
||||
|
||||
client_config = self._CLIENT_CONFIGS[client]
|
||||
if variant == 'basic':
|
||||
return f'Basic {client_config["AUTH"]}'
|
||||
|
||||
cache_key = client_config['CACHE_KEY']
|
||||
|
||||
if not self._oauth_tokens.get(cache_key):
|
||||
@@ -1213,6 +1197,9 @@ class VimeoIE(VimeoBaseInfoExtractor):
|
||||
'If your IP address is located in Europe you could try using a VPN/proxy,',
|
||||
f'or else u{self._login_hint()[1:]}',
|
||||
delim=' '), method=None)
|
||||
# XXX: Temporary while macos_basic is the default client
|
||||
elif e.cause.status == 401 and self._get_requested_client() == 'macos_basic':
|
||||
self.raise_login_required('The Vimeo extractor only works when logged-in')
|
||||
else:
|
||||
raise
|
||||
|
||||
@@ -1221,6 +1208,10 @@ class VimeoIE(VimeoBaseInfoExtractor):
|
||||
else:
|
||||
info = self._parse_api_response(video, video_id, unlisted_hash)
|
||||
|
||||
# XXX: Temporary while macos_basic is the default client
|
||||
if not info.get('formats') and self._get_requested_client() == 'macos_basic':
|
||||
self.raise_login_required('The Vimeo extractor only works when logged-in')
|
||||
|
||||
source_format = self._extract_original_format(
|
||||
f'https://vimeo.com/{video_id}', video_id, unlisted_hash)
|
||||
if source_format:
|
||||
|
||||
@@ -8,6 +8,7 @@ from ..utils import (
|
||||
|
||||
|
||||
class WallaIE(InfoExtractor):
|
||||
_WORKING = False
|
||||
_VALID_URL = r'https?://vod\.walla\.co\.il/[^/]+/(?P<id>\d+)/(?P<display_id>.+)'
|
||||
_TEST = {
|
||||
'url': 'http://vod.walla.co.il/movie/2642630/one-direction-all-for-one',
|
||||
|
||||
@@ -9,6 +9,7 @@ from ..utils import (
|
||||
|
||||
|
||||
class WhoWatchIE(InfoExtractor):
|
||||
_WORKING = False
|
||||
IE_NAME = 'whowatch'
|
||||
_VALID_URL = r'https?://whowatch\.tv/viewer/(?P<id>\d+)'
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ INNERTUBE_CLIENTS = {
|
||||
'INNERTUBE_CONTEXT': {
|
||||
'client': {
|
||||
'clientName': 'WEB',
|
||||
'clientVersion': '2.20260114.08.00',
|
||||
'clientVersion': '2.20260708.00.00',
|
||||
},
|
||||
},
|
||||
'INNERTUBE_CONTEXT_CLIENT_NAME': 1,
|
||||
@@ -107,11 +107,12 @@ INNERTUBE_CLIENTS = {
|
||||
**WEB_PO_TOKEN_POLICIES,
|
||||
},
|
||||
# Safari UA returns pre-merged video+audio 144p/240p/360p/720p/1080p HLS formats
|
||||
# Since 2026.07, HLS formats are only returned with some logged-in or "trusted" sessions
|
||||
'web_safari': {
|
||||
'INNERTUBE_CONTEXT': {
|
||||
'client': {
|
||||
'clientName': 'WEB',
|
||||
'clientVersion': '2.20260114.08.00',
|
||||
'clientVersion': '2.20260708.00.00',
|
||||
'userAgent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Safari/605.1.15,gzip(gfe)',
|
||||
},
|
||||
},
|
||||
@@ -123,7 +124,7 @@ INNERTUBE_CLIENTS = {
|
||||
'INNERTUBE_CONTEXT': {
|
||||
'client': {
|
||||
'clientName': 'WEB_EMBEDDED_PLAYER',
|
||||
'clientVersion': '1.20260115.01.00',
|
||||
'clientVersion': '2.20260708.00.00',
|
||||
},
|
||||
},
|
||||
'INNERTUBE_CONTEXT_CLIENT_NAME': 56,
|
||||
@@ -134,7 +135,7 @@ INNERTUBE_CLIENTS = {
|
||||
'INNERTUBE_CONTEXT': {
|
||||
'client': {
|
||||
'clientName': 'WEB_REMIX',
|
||||
'clientVersion': '1.20260114.03.00',
|
||||
'clientVersion': '1.20260707.12.00',
|
||||
},
|
||||
},
|
||||
'INNERTUBE_CONTEXT_CLIENT_NAME': 67,
|
||||
@@ -164,7 +165,7 @@ INNERTUBE_CLIENTS = {
|
||||
'INNERTUBE_CONTEXT': {
|
||||
'client': {
|
||||
'clientName': 'WEB_CREATOR',
|
||||
'clientVersion': '1.20260114.05.00',
|
||||
'clientVersion': '1.20260708.06.00',
|
||||
},
|
||||
},
|
||||
'INNERTUBE_CONTEXT_CLIENT_NAME': 62,
|
||||
@@ -193,9 +194,9 @@ INNERTUBE_CLIENTS = {
|
||||
'INNERTUBE_CONTEXT': {
|
||||
'client': {
|
||||
'clientName': 'ANDROID',
|
||||
'clientVersion': '21.02.35',
|
||||
'clientVersion': '21.26.364',
|
||||
'androidSdkVersion': 30,
|
||||
'userAgent': 'com.google.android.youtube/21.02.35 (Linux; U; Android 11) gzip',
|
||||
'userAgent': 'com.google.android.youtube/21.26.364 (Linux; U; Android 11) gzip',
|
||||
'osName': 'Android',
|
||||
'osVersion': '11',
|
||||
},
|
||||
@@ -223,6 +224,7 @@ INNERTUBE_CLIENTS = {
|
||||
},
|
||||
# "Made for kids" videos aren't available with this client
|
||||
# Using a clientVersion>1.65 may return SABR streams only
|
||||
# Since 2026.07, intermittent/selective POT enforcement has been observed for non-HLS formats
|
||||
'android_vr': {
|
||||
'INNERTUBE_CONTEXT': {
|
||||
'client': {
|
||||
@@ -238,6 +240,24 @@ INNERTUBE_CLIENTS = {
|
||||
},
|
||||
'INNERTUBE_CONTEXT_CLIENT_NAME': 28,
|
||||
'REQUIRE_JS_PLAYER': False,
|
||||
'GVS_PO_TOKEN_POLICY': {
|
||||
StreamingProtocol.HTTPS: GvsPoTokenPolicy(
|
||||
required=True,
|
||||
recommended=True,
|
||||
not_required_with_player_token=True,
|
||||
),
|
||||
StreamingProtocol.DASH: GvsPoTokenPolicy(
|
||||
required=True,
|
||||
recommended=True,
|
||||
not_required_with_player_token=True,
|
||||
),
|
||||
StreamingProtocol.HLS: GvsPoTokenPolicy(
|
||||
required=False,
|
||||
recommended=True,
|
||||
not_required_with_player_token=True,
|
||||
),
|
||||
},
|
||||
'PLAYER_PO_TOKEN_POLICY': PlayerPoTokenPolicy(required=False, recommended=True),
|
||||
},
|
||||
# iOS clients have HLS live streams. Setting device model to get 60fps formats.
|
||||
# See: https://github.com/TeamNewPipe/NewPipeExtractor/issues/680#issuecomment-1002724558
|
||||
@@ -245,10 +265,10 @@ INNERTUBE_CLIENTS = {
|
||||
'INNERTUBE_CONTEXT': {
|
||||
'client': {
|
||||
'clientName': 'IOS',
|
||||
'clientVersion': '21.02.3',
|
||||
'clientVersion': '21.26.4',
|
||||
'deviceMake': 'Apple',
|
||||
'deviceModel': 'iPhone16,2',
|
||||
'userAgent': 'com.google.ios.youtube/21.02.3 (iPhone16,2; U; CPU iOS 18_3_2 like Mac OS X;)',
|
||||
'userAgent': 'com.google.ios.youtube/21.26.4 (iPhone16,2; U; CPU iOS 18_3_2 like Mac OS X;)',
|
||||
'osName': 'iPhone',
|
||||
'osVersion': '18.3.2.22D82',
|
||||
},
|
||||
@@ -270,13 +290,29 @@ INNERTUBE_CLIENTS = {
|
||||
'PLAYER_PO_TOKEN_POLICY': PlayerPoTokenPolicy(required=False, recommended=True),
|
||||
'REQUIRE_JS_PLAYER': False,
|
||||
},
|
||||
# "Made for kids" videos aren't available with this client
|
||||
'visionos': {
|
||||
'INNERTUBE_CONTEXT': {
|
||||
'client': {
|
||||
'clientName': 'VISIONOS',
|
||||
'clientVersion': '1.02',
|
||||
'deviceMake': 'Apple',
|
||||
'deviceModel': 'RealityDevice17,1',
|
||||
'userAgent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 15_7_3) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 Safari/605.1.15',
|
||||
'osName': 'visionOS',
|
||||
'osVersion': '26.5.23O471',
|
||||
},
|
||||
},
|
||||
'INNERTUBE_CONTEXT_CLIENT_NAME': 101,
|
||||
'REQUIRE_JS_PLAYER': False,
|
||||
},
|
||||
# mweb has 'ultralow' formats
|
||||
# See: https://github.com/yt-dlp/yt-dlp/pull/557
|
||||
'mweb': {
|
||||
'INNERTUBE_CONTEXT': {
|
||||
'client': {
|
||||
'clientName': 'MWEB',
|
||||
'clientVersion': '2.20260115.01.00',
|
||||
'clientVersion': '2.20260708.05.00',
|
||||
# mweb previously did not require PO Token with this UA
|
||||
'userAgent': 'Mozilla/5.0 (iPad; CPU OS 16_7_10 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1,gzip(gfe)',
|
||||
},
|
||||
@@ -307,7 +343,7 @@ INNERTUBE_CLIENTS = {
|
||||
'INNERTUBE_CONTEXT': {
|
||||
'client': {
|
||||
'clientName': 'TVHTML5',
|
||||
'clientVersion': '7.20260114.12.00',
|
||||
'clientVersion': '7.20260707.07.00',
|
||||
# See: https://github.com/youtube/cobalt/blob/main/cobalt/browser/user_agent/user_agent_platform_info.cc#L506
|
||||
'userAgent': 'Mozilla/5.0 (ChromiumStylePlatform) Cobalt/25.lts.30.1034943-gold (unlike Gecko), Unknown_TV_Unknown_0/Unknown (Unknown, Unknown)',
|
||||
},
|
||||
@@ -319,12 +355,11 @@ INNERTUBE_CLIENTS = {
|
||||
'INNERTUBE_CONTEXT': {
|
||||
'client': {
|
||||
'clientName': 'TVHTML5',
|
||||
'clientVersion': '5.20260114',
|
||||
'clientVersion': '5.20260707',
|
||||
'userAgent': 'Mozilla/5.0 (ChromiumStylePlatform) Cobalt/Version',
|
||||
},
|
||||
},
|
||||
'INNERTUBE_CONTEXT_CLIENT_NAME': 7,
|
||||
'REQUIRE_AUTH': True,
|
||||
'SUPPORTS_COOKIES': True,
|
||||
},
|
||||
'tv_simply': {
|
||||
|
||||
+132
-133
@@ -28,7 +28,7 @@ from .jsc._director import initialize_jsc_director
|
||||
from .jsc.provider import JsChallengeRequest, JsChallengeType, NChallengeInput, SigChallengeInput
|
||||
from .pot._director import initialize_pot_director
|
||||
from .pot.provider import PoTokenContext, PoTokenRequest
|
||||
from ...networking.exceptions import HTTPError
|
||||
from ...networking import HEADRequest
|
||||
from ...utils import (
|
||||
NO_DEFAULT,
|
||||
ExtractorError,
|
||||
@@ -140,13 +140,13 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
_RETURN_TYPE = 'video' # XXX: How to handle multifeed?
|
||||
|
||||
_SUBTITLE_FORMATS = ('json3', 'srv1', 'srv2', 'srv3', 'ttml', 'srt', 'vtt')
|
||||
_DEFAULT_CLIENTS = ('android_vr', 'web_safari')
|
||||
_DEFAULT_JSLESS_CLIENTS = ('android_vr',)
|
||||
_DEFAULT_AUTHED_CLIENTS = ('tv_downgraded', 'web_safari')
|
||||
_DEFAULT_CLIENTS = ('visionos', 'android_vr', 'web')
|
||||
_DEFAULT_JSLESS_CLIENTS = ('visionos', 'android_vr')
|
||||
_DEFAULT_AUTHED_CLIENTS = ('tv_downgraded', 'web')
|
||||
# Premium does not require POT (except for subtitles)
|
||||
_DEFAULT_PREMIUM_CLIENTS = ('tv_downgraded', 'web_creator')
|
||||
_DEFAULT_PREMIUM_CLIENTS = ('tv_downgraded', 'web_creator', 'web')
|
||||
_WEBPAGE_CLIENTS = ('web', 'web_safari')
|
||||
_DEFAULT_WEBPAGE_CLIENT = 'web_safari'
|
||||
_DEFAULT_WEBPAGE_CLIENT = 'web'
|
||||
|
||||
_GEO_BYPASS = False
|
||||
|
||||
@@ -1600,13 +1600,13 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'info_dict': {
|
||||
'id': 'brhfDfLdDZ8',
|
||||
'ext': 'mp4',
|
||||
'title': 'This is the WORST Movie Science We\'ve Ever Seen',
|
||||
'title': 'Scientists React to Terrible Movie Science | Moonfall (2021)',
|
||||
'description': 'md5:8afd0a3cd69ec63438fc573580436f92',
|
||||
'media_type': 'video',
|
||||
'uploader': 'Open Sauce',
|
||||
'uploader_id': '@opensaucelive',
|
||||
'uploader_url': 'https://www.youtube.com/@opensaucelive',
|
||||
'channel': 'Open Sauce',
|
||||
'uploader': 'Sauce +',
|
||||
'uploader_id': '@sauceplusofficial',
|
||||
'uploader_url': 'https://www.youtube.com/@sauceplusofficial',
|
||||
'channel': 'Sauce +',
|
||||
'channel_id': 'UC2EiGVmCeD79l_vZ204DUSw',
|
||||
'channel_url': 'https://www.youtube.com/channel/UC2EiGVmCeD79l_vZ204DUSw',
|
||||
'comment_count': int,
|
||||
@@ -1614,15 +1614,17 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'like_count': int,
|
||||
'age_limit': 0,
|
||||
'duration': 1664,
|
||||
'thumbnail': 'https://i.ytimg.com/vi/brhfDfLdDZ8/hqdefault.jpg',
|
||||
'thumbnail': 'https://i.ytimg.com/vi/brhfDfLdDZ8/sddefault.jpg',
|
||||
'categories': ['Entertainment'],
|
||||
'tags': ['Moonfall', 'Bad Science', 'Open Sauce', 'Sauce+', 'The Backyard Scientist', 'William Osman', 'Allen Pan'],
|
||||
'creators': ['Open Sauce', 'William Osman 2'],
|
||||
'creators': ['Sauce +', 'William Osman 2'],
|
||||
'timestamp': 1759452918,
|
||||
'upload_date': '20251003',
|
||||
'playable_in_embed': True,
|
||||
'availability': 'public',
|
||||
'live_status': 'not_live',
|
||||
'channel_follower_count': int,
|
||||
'heatmap': 'count:100',
|
||||
},
|
||||
'params': {'skip_download': True},
|
||||
}, {
|
||||
@@ -1656,6 +1658,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'playable_in_embed': True,
|
||||
'availability': 'public',
|
||||
'live_status': 'not_live',
|
||||
'channel_follower_count': int,
|
||||
},
|
||||
'params': {'skip_download': True},
|
||||
}, {
|
||||
@@ -1940,6 +1943,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
lock = threading.Lock()
|
||||
start_time = time.time()
|
||||
formats = [f for f in formats if f.get('is_from_start')]
|
||||
adaptive_last_seq_cache = {}
|
||||
|
||||
def refetch_manifest(itag, client_name, delay):
|
||||
nonlocal formats, start_time, is_live
|
||||
@@ -1956,9 +1960,9 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
is_live = live_status == 'is_live'
|
||||
start_time = time.time()
|
||||
|
||||
def mpd_feed(itag, client_name, delay):
|
||||
def url_feed(itag, client_name, delay):
|
||||
"""
|
||||
@returns (manifest_url, manifest_stream_number, is_live) or None
|
||||
@returns (base_url, is_live) or None
|
||||
"""
|
||||
for retry in self.RetryManager(fatal=False):
|
||||
with lock:
|
||||
@@ -1969,33 +1973,39 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
if not is_live:
|
||||
retry.error = f'{video_id}: Video is no longer live'
|
||||
else:
|
||||
retry.error = f'Cannot find refreshed manifest for format {itag}{bug_reports_message()}'
|
||||
retry.error = f'Cannot find refreshed url for format {itag}{bug_reports_message()}'
|
||||
continue
|
||||
|
||||
# Formats from ended premieres will be missing a manifest_url
|
||||
# See https://github.com/yt-dlp/yt-dlp/issues/8543
|
||||
if not f.get('manifest_url'):
|
||||
if not f.get('url'):
|
||||
break
|
||||
|
||||
return f['manifest_url'], f['manifest_stream_number'], is_live
|
||||
return f['url'], is_live
|
||||
return None
|
||||
|
||||
for f in formats:
|
||||
f['is_live'] = is_live
|
||||
gen = functools.partial(self._live_dash_fragments, video_id, f['_itag'], f['_client'],
|
||||
live_start_time, mpd_feed, not is_live and f.copy())
|
||||
gen = functools.partial(
|
||||
self._live_adaptive_fragments,
|
||||
video_id,
|
||||
f['_itag'],
|
||||
f['_client'],
|
||||
live_start_time,
|
||||
url_feed if is_live else None,
|
||||
f.get('url') if not is_live else None,
|
||||
f.get('target_duration'),
|
||||
adaptive_last_seq_cache,
|
||||
)
|
||||
if is_live:
|
||||
f['fragments'] = gen
|
||||
f['protocol'] = 'http_dash_segments_generator'
|
||||
else:
|
||||
f['fragments'] = LazyList(gen({}))
|
||||
f['protocol'] = 'http_dash_segments'
|
||||
del f['is_from_start']
|
||||
|
||||
def _live_dash_fragments(self, video_id, itag, client_name, live_start_time, mpd_feed, manifestless_orig_fmt, ctx):
|
||||
def _live_adaptive_fragments(self, video_id, itag, client_name, live_start_time, url_feed, base_url, fragment_duration, last_seq_cache, ctx):
|
||||
FETCH_SPAN, MAX_DURATION = 5, 432000
|
||||
|
||||
mpd_url, stream_number, is_live = None, None, True
|
||||
|
||||
begin_index = 0
|
||||
download_start_time = ctx.get('start') or time.time()
|
||||
|
||||
@@ -2006,98 +2016,77 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'YouTube does not have data before that. If you think this is wrong,'), only_once=True)
|
||||
lack_early_segments = True
|
||||
|
||||
known_idx, no_fragment_score, last_segment_url = begin_index, 0, None
|
||||
fragments, fragment_base_url = None, None
|
||||
|
||||
def _extract_sequence_from_mpd(refresh_sequence, immediate):
|
||||
nonlocal mpd_url, stream_number, is_live, no_fragment_score, fragments, fragment_base_url
|
||||
# Obtain from MPD's maximum seq value
|
||||
old_mpd_url = mpd_url
|
||||
last_error = ctx.pop('last_error', None)
|
||||
expire_fast = immediate or (last_error and isinstance(last_error, HTTPError) and last_error.status == 403)
|
||||
mpd_url, stream_number, is_live = (mpd_feed(itag, client_name, 5 if expire_fast else 18000)
|
||||
or (mpd_url, stream_number, False))
|
||||
if not refresh_sequence:
|
||||
if expire_fast and not is_live:
|
||||
return False, last_seq
|
||||
elif old_mpd_url == mpd_url:
|
||||
return True, last_seq
|
||||
if manifestless_orig_fmt:
|
||||
fmt_info = manifestless_orig_fmt
|
||||
else:
|
||||
try:
|
||||
fmts, _ = self._extract_mpd_formats_and_subtitles(
|
||||
mpd_url, None, note=False, errnote=False, fatal=False)
|
||||
except ExtractorError:
|
||||
fmts = None
|
||||
if not fmts:
|
||||
no_fragment_score += 2
|
||||
return False, last_seq
|
||||
fmt_info = next(x for x in fmts if x['manifest_stream_number'] == stream_number)
|
||||
fragments = fmt_info['fragments']
|
||||
fragment_base_url = fmt_info['fragment_base_url']
|
||||
assert fragment_base_url
|
||||
|
||||
_last_seq = int(re.search(r'(?:/|^)sq/(\d+)', fragments[-1]['path']).group(1))
|
||||
return True, _last_seq
|
||||
known_idx, no_fragment_score = begin_index, 0
|
||||
|
||||
self.write_debug(f'[{video_id}] Generating fragments for format {itag}')
|
||||
while is_live:
|
||||
should_iterate = True
|
||||
while should_iterate:
|
||||
fetch_time = time.time()
|
||||
if no_fragment_score > 30:
|
||||
return
|
||||
if last_segment_url:
|
||||
# Obtain from "X-Head-Seqnum" header value from each segment
|
||||
|
||||
if url_feed and (feed_results := url_feed(itag, client_name, 5 if no_fragment_score > 15 else 18000)):
|
||||
base_url, should_iterate = feed_results
|
||||
else:
|
||||
should_iterate = False
|
||||
if not base_url:
|
||||
no_fragment_score += 2
|
||||
continue
|
||||
|
||||
# Obtain from "X-Head-Seqnum" header value. The bare base URL
|
||||
# may return an empty response body, but the headers are still usable.
|
||||
cache_key = should_iterate, int(time.time() // FETCH_SPAN)
|
||||
if cache_key in last_seq_cache:
|
||||
last_seq = last_seq_cache[cache_key]
|
||||
else:
|
||||
try:
|
||||
urlh = self._request_webpage(
|
||||
last_segment_url, None, note=False, errnote=False, fatal=False)
|
||||
except ExtractorError:
|
||||
HEADRequest(base_url), None,
|
||||
note=False, errnote='Fragment request failed')
|
||||
except ExtractorError as e:
|
||||
self.write_debug(e.msg)
|
||||
urlh = None
|
||||
last_seq = try_get(urlh, lambda x: int_or_none(x.headers['X-Head-Seqnum']))
|
||||
if last_seq is None:
|
||||
no_fragment_score += 2
|
||||
last_segment_url = None
|
||||
continue
|
||||
else:
|
||||
should_continue, last_seq = _extract_sequence_from_mpd(True, no_fragment_score > 15)
|
||||
if urlh:
|
||||
urlh.close()
|
||||
if last_seq is not None:
|
||||
last_seq_cache.clear()
|
||||
last_seq_cache[cache_key] = last_seq
|
||||
if last_seq is None:
|
||||
no_fragment_score += 2
|
||||
if not should_continue:
|
||||
continue
|
||||
continue
|
||||
|
||||
if known_idx > last_seq:
|
||||
last_segment_url = None
|
||||
no_fragment_score += 5
|
||||
if should_iterate:
|
||||
time.sleep(max(0, FETCH_SPAN + fetch_time - time.time()))
|
||||
continue
|
||||
|
||||
last_seq += 1
|
||||
|
||||
if not url_feed:
|
||||
last_seq -= 2
|
||||
|
||||
if begin_index < 0 and known_idx < 0:
|
||||
# skip from the start when it's negative value
|
||||
known_idx = last_seq + begin_index
|
||||
if lack_early_segments:
|
||||
known_idx = max(known_idx, last_seq - int(MAX_DURATION // fragments[-1]['duration']))
|
||||
try:
|
||||
for idx in range(known_idx, last_seq):
|
||||
# do not update sequence here or you'll get skipped some part of it
|
||||
should_continue, _ = _extract_sequence_from_mpd(False, False)
|
||||
if not should_continue:
|
||||
known_idx = idx - 1
|
||||
raise ExtractorError('breaking out of outer loop')
|
||||
last_segment_url = urljoin(fragment_base_url, f'sq/{idx}')
|
||||
yield {
|
||||
'url': last_segment_url,
|
||||
'fragment_count': last_seq,
|
||||
}
|
||||
if known_idx == last_seq:
|
||||
no_fragment_score += 5
|
||||
else:
|
||||
no_fragment_score = 0
|
||||
known_idx = last_seq
|
||||
except ExtractorError:
|
||||
continue
|
||||
known_idx = max(known_idx, last_seq - int(MAX_DURATION // fragment_duration))
|
||||
|
||||
if manifestless_orig_fmt:
|
||||
# Stop at the first iteration if running for post-live manifestless;
|
||||
# fragment count no longer increase since it starts
|
||||
for idx in range(known_idx, last_seq):
|
||||
yield {
|
||||
'url': update_url_query(base_url, {'sq': str(idx)}),
|
||||
'fragment_count': last_seq,
|
||||
}
|
||||
|
||||
if known_idx == last_seq:
|
||||
no_fragment_score += 5
|
||||
else:
|
||||
no_fragment_score = 0
|
||||
known_idx = last_seq
|
||||
|
||||
if not url_feed:
|
||||
# Post-live: stop after first iteration
|
||||
break
|
||||
|
||||
time.sleep(max(0, FETCH_SPAN + fetch_time - time.time()))
|
||||
@@ -2918,6 +2907,10 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
def _is_unplayable(player_response):
|
||||
return traverse_obj(player_response, ('playabilityStatus', 'status')) == 'UNPLAYABLE'
|
||||
|
||||
@staticmethod
|
||||
def _is_error_response(player_response):
|
||||
return traverse_obj(player_response, ('playabilityStatus', 'status')) == 'ERROR'
|
||||
|
||||
def _extract_player_response(self, client, video_id, webpage_ytcfg, player_ytcfg, player_url, initial_pr, visitor_data, data_sync_id, po_token):
|
||||
headers = self.generate_api_headers(
|
||||
ytcfg=player_ytcfg,
|
||||
@@ -3150,13 +3143,14 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
prs.append(pr)
|
||||
|
||||
if (
|
||||
# Is this a "made for kids" video that can't be downloaded with android_vr?
|
||||
client == 'android_vr' and self._is_unplayable(pr)
|
||||
# Is this a "made for kids" video that can't be downloaded with android_vr/visionos?
|
||||
client in {'android_vr', 'visionos'}
|
||||
and (self._is_unplayable(pr) or self._is_error_response(pr))
|
||||
and webpage and 'made for kids' in webpage
|
||||
# ...and is a JS runtime is available?
|
||||
and any(p.is_available() for p in self._jsc_director.providers.values())
|
||||
):
|
||||
append_client('web_embedded')
|
||||
append_client('tv_downgraded')
|
||||
|
||||
# web_embedded can work around age-gate and age-verification for some embeddable videos
|
||||
if self._is_agegated(pr) and variant != 'web_embedded':
|
||||
@@ -3194,10 +3188,9 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
raise ExtractorError('Failed to extract any player response')
|
||||
return prs, player_url
|
||||
|
||||
def _needs_live_processing(self, live_status, duration):
|
||||
if ((live_status == 'is_live' and self.get_param('live_from_start'))
|
||||
or (live_status == 'post_live' and (duration or 0) > 2 * 3600)):
|
||||
return live_status
|
||||
def _needs_live_processing(self, live_status):
|
||||
return live_status == 'post_live' or (
|
||||
live_status == 'is_live' and self.get_param('live_from_start'))
|
||||
|
||||
def _report_pot_format_skipped(self, video_id, client_name, proto):
|
||||
msg = (
|
||||
@@ -3497,8 +3490,14 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
https_fmts = []
|
||||
|
||||
for fmt_stream in streaming_formats:
|
||||
# Live adaptive https formats are not supported: skip unless extractor-arg given
|
||||
if fmt_stream.get('targetDurationSec') and skip_bad_formats:
|
||||
if (
|
||||
# It's a live adaptive format
|
||||
fmt_stream.get('targetDurationSec')
|
||||
# The user didn't pass the formats=incomplete extractor-arg
|
||||
and skip_bad_formats
|
||||
# This is not a --live-from-start or post-live stream
|
||||
and not self._needs_live_processing(live_status)
|
||||
):
|
||||
continue
|
||||
|
||||
# FORMAT_STREAM_TYPE_OTF(otf=1) requires downloading the init fragment
|
||||
@@ -3593,6 +3592,12 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
if live_status not in ('is_live', 'post_live'):
|
||||
fmt['available_at'] = available_at
|
||||
|
||||
if fmt_stream.get('targetDurationSec') and self._needs_live_processing(live_status):
|
||||
fmt['is_from_start'] = True
|
||||
fmt['target_duration'] = fmt_stream['targetDurationSec']
|
||||
fmt['_itag'] = stream_id[0]
|
||||
fmt['_client'] = client_name
|
||||
|
||||
https_fmts.append(fmt)
|
||||
|
||||
for fmt in https_fmts:
|
||||
@@ -3609,14 +3614,16 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
|
||||
yield from process_https_formats()
|
||||
|
||||
needs_live_processing = self._needs_live_processing(live_status, duration)
|
||||
needs_live_processing = self._needs_live_processing(live_status)
|
||||
|
||||
skip_manifests = set(self._configuration_arg('skip'))
|
||||
if (needs_live_processing == 'is_live' # These will be filtered out by YoutubeDL anyway
|
||||
or (needs_live_processing and skip_bad_formats)):
|
||||
if needs_live_processing and skip_bad_formats:
|
||||
skip_manifests.add('hls')
|
||||
|
||||
if skip_bad_formats and live_status == 'is_live' and needs_live_processing != 'is_live':
|
||||
if skip_bad_formats and (
|
||||
live_status == 'is_live'
|
||||
or (live_status == 'post_live' and (duration or 0) > 2 * 3600)
|
||||
):
|
||||
skip_manifests.add('dash')
|
||||
|
||||
def process_manifest_format(f, proto, client_name, itag, missing_pot):
|
||||
@@ -3754,10 +3761,6 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
if process_manifest_format(f, 'dash', client_name, format_id, require_po_token and not po_token):
|
||||
f['filesize'] = int_or_none(self._search_regex(
|
||||
r'/clen/(\d+)', f.get('fragment_base_url') or f['url'], 'file size', default=None))
|
||||
if needs_live_processing:
|
||||
f['is_from_start'] = True
|
||||
f['_itag'] = format_id
|
||||
f['_client'] = client_name
|
||||
yield f
|
||||
yield subtitles
|
||||
|
||||
@@ -4129,7 +4132,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
if not duration and live_end_time and live_start_time:
|
||||
duration = live_end_time - live_start_time
|
||||
|
||||
needs_live_processing = self._needs_live_processing(live_status, duration)
|
||||
needs_live_processing = self._needs_live_processing(live_status)
|
||||
|
||||
def adjust_incomplete_format(fmt, note_suffix='(Last 2 hours)', pref_adjustment=-10):
|
||||
fmt['preference'] = (fmt.get('preference') or -1) + pref_adjustment
|
||||
@@ -4138,26 +4141,19 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
# Adjust preference and format note for incomplete live/post-live formats
|
||||
if live_status in ('is_live', 'post_live'):
|
||||
for fmt in formats:
|
||||
protocol = fmt.get('protocol')
|
||||
# Currently, protocol isn't set for adaptive https formats, but this could change
|
||||
is_adaptive = protocol in (None, 'http', 'https')
|
||||
if live_status == 'post_live' and is_adaptive:
|
||||
# Post-live adaptive formats cause HttpFD to raise "Did not get any data blocks"
|
||||
# These formats are *only* useful to external applications, so we can hide them
|
||||
# Set their preference <= -1000 so that FormatSorter flags them as 'hidden'
|
||||
adjust_incomplete_format(fmt, note_suffix='(ended)', pref_adjustment=-5000)
|
||||
# Is it live with --live-from-start? Or is it post-live and its duration is >2hrs?
|
||||
elif needs_live_processing:
|
||||
# Is it live with --live-from-start or post-live?
|
||||
if needs_live_processing:
|
||||
if not fmt.get('is_from_start'):
|
||||
# Post-live m3u8 formats for >2hr streams
|
||||
# Post-live DASH and m3u8 manifests only have the last ~2 hours
|
||||
adjust_incomplete_format(fmt)
|
||||
elif live_status == 'is_live':
|
||||
if protocol == 'http_dash_segments':
|
||||
# Live DASH formats without --live-from-start
|
||||
adjust_incomplete_format(fmt)
|
||||
elif is_adaptive:
|
||||
# Incomplete live adaptive https formats
|
||||
protocol = fmt.get('protocol')
|
||||
# Currently, protocol isn't set for incomplete (non-generated) live adaptive formats
|
||||
if protocol in (None, 'http', 'https'):
|
||||
adjust_incomplete_format(fmt, note_suffix='(incomplete)', pref_adjustment=-20)
|
||||
# Live DASH formats (no longer properly supported)
|
||||
elif protocol == 'http_dash_segments':
|
||||
adjust_incomplete_format(fmt)
|
||||
|
||||
if needs_live_processing:
|
||||
self._prepare_live_from_start_formats(
|
||||
@@ -4475,13 +4471,16 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
vsir = get_first(contents, 'videoSecondaryInfoRenderer')
|
||||
if vsir:
|
||||
vor = traverse_obj(vsir, ('owner', 'videoOwnerRenderer'))
|
||||
collaborators = traverse_obj(vor, (
|
||||
collab_view_models = traverse_obj(vor, (
|
||||
'attributedTitle', 'commandRuns', ..., 'onTap', 'innertubeCommand', 'showDialogCommand',
|
||||
'panelLoadingStrategy', 'inlineContent', 'dialogViewModel', 'customContent', 'listViewModel',
|
||||
'listItems', ..., 'listItemViewModel', 'title', 'content', {str}))
|
||||
'listItems', ..., 'listItemViewModel', {dict}))
|
||||
collaborators = traverse_obj(collab_view_models, (..., 'title', 'content', {str}))
|
||||
info.update({
|
||||
'channel': self._get_text(vor, 'title') or (collaborators[0] if collaborators else None),
|
||||
'channel_follower_count': self._get_count(vor, 'subscriberCountText'),
|
||||
'channel_follower_count': (
|
||||
self._get_count(vor, 'subscriberCountText')
|
||||
or traverse_obj(collab_view_models, (0, 'rendererContext', 'accessibilityContext', 'label', {parse_count}))),
|
||||
'creators': collaborators if collaborators else None,
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
import datetime as dt
|
||||
import itertools
|
||||
import math
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
ExtractorError,
|
||||
clean_html,
|
||||
extract_attributes,
|
||||
float_or_none,
|
||||
int_or_none,
|
||||
merge_dicts,
|
||||
parse_iso8601,
|
||||
parse_m3u8_attributes,
|
||||
parse_resolution,
|
||||
urlencode_postdata,
|
||||
urljoin,
|
||||
)
|
||||
from ..utils.traversal import find_element, traverse_obj
|
||||
|
||||
|
||||
class ZanIE(InfoExtractor):
|
||||
IE_NAME = 'zan'
|
||||
IE_DESC = 'Z-aN'
|
||||
|
||||
_BASE_URL = 'https://www.zan-live.com'
|
||||
_GEO_BYPASS = False
|
||||
_GEO_COUNTRIES = ['JP']
|
||||
_VALID_URL = r'https?://(www\.)?zan-live\.com/[^/?#]+/live/play/\d+/(?P<id>\d+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.zan-live.com/en/live/play/1797/663',
|
||||
'info_dict': {
|
||||
'id': '663',
|
||||
'ext': 'mp4',
|
||||
'title': 'The sample video page',
|
||||
'alt_title': 'こちらはサンプル動画の再生テストページとなります。',
|
||||
'description': 'md5:12ba331396215fe345b9362c56f3da86',
|
||||
'release_date': '20220228',
|
||||
'release_timestamp': 1646060400,
|
||||
'thumbnail': r're:https?://storage\.zan-live\.com/image/.+\.(?:jpe?g|png)',
|
||||
},
|
||||
}, {
|
||||
'url': 'https://www.zan-live.com/ja/live/play/6910/4268',
|
||||
'info_dict': {
|
||||
'id': '4268',
|
||||
'ext': 'mp4',
|
||||
'title': '縁結びのゆかり様1stイベント 結び、結ばれ、桃源郷',
|
||||
'alt_title': '縁結びのゆかり様1stイベント 結び、結ばれ、桃源郷 昼公演[アーカイブ]',
|
||||
'description': 'md5:f53156a49ce5b45265d07966679ab494',
|
||||
'release_date': '20260614',
|
||||
'release_timestamp': 1781419500,
|
||||
'thumbnail': r're:https?://storage\.zan-live\.com/image/.+\.(?:jpe?g|png)',
|
||||
},
|
||||
}]
|
||||
|
||||
@staticmethod
|
||||
def _fixup_m3u8_formats(formats, m3u8_doc, m3u8_url):
|
||||
for stream_inf, media_url in itertools.pairwise(
|
||||
map(str.strip, m3u8_doc.splitlines()),
|
||||
):
|
||||
if not stream_inf.startswith('#EXT-X-STREAM-INF:') or not media_url:
|
||||
continue
|
||||
|
||||
res = traverse_obj(stream_inf, (
|
||||
{parse_m3u8_attributes}, 'DISPLAY-NAME', {parse_resolution}))
|
||||
if not res:
|
||||
continue
|
||||
|
||||
format_url = urljoin(m3u8_url, media_url)
|
||||
for fmt in traverse_obj(formats, (
|
||||
lambda _, v: v.get('url') == format_url,
|
||||
)):
|
||||
fmt.update(merge_dicts(fmt, res))
|
||||
|
||||
@staticmethod
|
||||
def _get_multiangle_layout(ma_type, ma_number):
|
||||
# Z-aN packs all angles into one video frame
|
||||
# Square divisions use an n x n grid
|
||||
# 6 divisions use a 3 x 3 grid where angle 1 covers a 2 x 2 block
|
||||
# https://static.zan-live.com/static/js/live/shaka_multiangle.js
|
||||
|
||||
divisions = int_or_none(ma_type.partition('_')[0]) or 0
|
||||
if divisions <= 0:
|
||||
return None, None
|
||||
|
||||
unit = math.isqrt(divisions)
|
||||
if unit ** 2 == divisions:
|
||||
areas = [(i % unit, i // unit, 1, 1) for i in range(divisions)]
|
||||
elif divisions == 6:
|
||||
unit = 3
|
||||
areas = [
|
||||
(0, 0, 2, 2),
|
||||
*[(2, i, 1, 1) for i in range(2)],
|
||||
*[(i, 2, 1, 1) for i in range(3)],
|
||||
]
|
||||
else:
|
||||
return None, None
|
||||
|
||||
if not (ma_number and 1 <= ma_number <= len(areas)):
|
||||
ma_number = len(areas)
|
||||
|
||||
return unit, areas[:ma_number]
|
||||
|
||||
@staticmethod
|
||||
def _multiangle_crop(x, y, w, h, unit, margin_y):
|
||||
margin_x = margin_y * 9 / 16
|
||||
|
||||
return (
|
||||
f'crop='
|
||||
f'trunc((iw*{w}/{unit}-{margin_x * 2:g})/2)*2:'
|
||||
f'trunc((ih*{h}/{unit}-{margin_y * 2:g})/2)*2:'
|
||||
f'trunc(iw*{x}/{unit}+{margin_x:g}):'
|
||||
f'trunc(ih*{y}/{unit}+{margin_y:g})'
|
||||
)
|
||||
|
||||
def _multiangle_formats(self, formats, ma_type, ma_number, ma_margin):
|
||||
if self._configuration_arg('split_angles', ['false'])[0] == 'false':
|
||||
self.to_screen(
|
||||
'Multi-angle formats are available. Use --extractor-args '
|
||||
'"zan:split_angles=true" to extract separate angle formats')
|
||||
return formats
|
||||
|
||||
unit, areas = self._get_multiangle_layout(ma_type, ma_number)
|
||||
if areas is None:
|
||||
self.report_warning(f'Unsupported multiangle type: {ma_type}')
|
||||
return formats
|
||||
|
||||
angle_formats = []
|
||||
for fmt in formats:
|
||||
if fmt.get('vcodec') == 'none':
|
||||
continue
|
||||
|
||||
height = traverse_obj(fmt, ('height', {int_or_none}))
|
||||
for i, (x, y, w, h) in enumerate(areas, 1):
|
||||
angle_formats.append({
|
||||
**fmt,
|
||||
'downloader_options': {
|
||||
'ffmpeg_args_out': [
|
||||
'-vf', self._multiangle_crop(x, y, w, h, unit, ma_margin),
|
||||
'-c:v', 'libx264',
|
||||
'-c:a', 'copy',
|
||||
],
|
||||
},
|
||||
'format_id': f'{fmt["format_id"]}-angle{i}',
|
||||
'height': int_or_none(
|
||||
(height * h / unit - ma_margin * 2) // 2 * 2) if height else None,
|
||||
'protocol': 'm3u8',
|
||||
'source_preference': -i,
|
||||
})
|
||||
|
||||
return angle_formats
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
webpage = self._download_webpage(url, video_id)
|
||||
if error_msg := traverse_obj(webpage, (
|
||||
{find_element(cls='p-common_message__headline--error')}, {clean_html}, filter,
|
||||
)):
|
||||
self.raise_geo_restricted(error_msg, countries=self._GEO_COUNTRIES)
|
||||
|
||||
csrf_token = self._html_search_meta('csrf-token', webpage, default=None)
|
||||
pct = self._html_search_meta('vod-pct', webpage, default=None)
|
||||
token = self._html_search_meta('live-player-token', webpage, default=None)
|
||||
if not all((csrf_token, pct, token)):
|
||||
self.raise_login_required()
|
||||
|
||||
status = self._download_json(
|
||||
f'{self._BASE_URL}/api/live/{video_id}/getLiveStatus', video_id, headers={
|
||||
'X-Csrf-Token': csrf_token,
|
||||
}, data=urlencode_postdata({
|
||||
'pct': pct,
|
||||
'token': token,
|
||||
}))
|
||||
if not traverse_obj(status, ('isSuccess', {bool})):
|
||||
raise ExtractorError('Unexpected error')
|
||||
|
||||
result = traverse_obj(status, ('result', {dict}))
|
||||
for key, required, error_message in (
|
||||
('isFinished', False, 'This video is no longer available'),
|
||||
('canPlay', True, 'Ticket has expired'),
|
||||
):
|
||||
if traverse_obj(result, (key, {bool})) is not required:
|
||||
raise ExtractorError(error_message, expected=True)
|
||||
|
||||
is_live = not traverse_obj(result, ('isVod', {bool}))
|
||||
release_timestamp = parse_iso8601(self._html_search_meta('open-live-date', webpage))
|
||||
srv_time = traverse_obj(status, ('srvTime', {int_or_none}), default=0)
|
||||
|
||||
if is_live and release_timestamp and srv_time < release_timestamp:
|
||||
start_time = dt.datetime.fromtimestamp(
|
||||
release_timestamp, dt.timezone.utc,
|
||||
).astimezone().strftime('%Y-%m-%d %H:%M:%S %Z')
|
||||
self.raise_no_formats(
|
||||
f'This livestream is scheduled to start at {start_time}', expected=True)
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'live_status': 'is_upcoming',
|
||||
'release_timestamp': release_timestamp,
|
||||
}
|
||||
|
||||
m3u8_url = self._html_search_meta('live-url', webpage, fatal=True)
|
||||
m3u8_doc, urlh = self._download_webpage_handle(
|
||||
m3u8_url, video_id, note='Downloading m3u8 information')
|
||||
m3u8_url = urlh.url
|
||||
|
||||
formats, _ = self._parse_m3u8_formats_and_subtitles(m3u8_doc, m3u8_url, 'mp4')
|
||||
self._fixup_m3u8_formats(formats, m3u8_doc, m3u8_url)
|
||||
|
||||
if ma_type := self._html_search_meta('multiangle-type', webpage, default=None):
|
||||
ma_number = int_or_none(self._html_search_meta(
|
||||
'multiangle-number', webpage, default=None))
|
||||
ma_margin = float_or_none(self._html_search_meta(
|
||||
'multiangle-margin', webpage, default=None), default=0)
|
||||
formats = self._multiangle_formats(formats, ma_type, ma_number, ma_margin)
|
||||
|
||||
detail_url = traverse_obj(webpage, (
|
||||
{find_element(cls='linkTxt')},
|
||||
{find_element(cls='d-flex align-items-center', html=True)},
|
||||
{extract_attributes}, 'href', {urljoin(f'{self._BASE_URL}/')}))
|
||||
detail = self._download_webpage(detail_url, video_id, fatal=False) or ''
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'alt_title': traverse_obj(webpage, (
|
||||
{self._og_search_title}, {clean_html}, filter)),
|
||||
'formats': formats,
|
||||
'is_live': is_live,
|
||||
'release_timestamp': release_timestamp,
|
||||
'thumbnail': self._og_search_thumbnail(detail),
|
||||
**traverse_obj(detail, {
|
||||
'title': ({self._og_search_title}, {clean_html}, filter),
|
||||
'description': ((
|
||||
{find_element(cls='p-eventinfo__detail')},
|
||||
{find_element(cls='groupDetail')},
|
||||
), {clean_html}, filter, any),
|
||||
}),
|
||||
}
|
||||
+3
-3
@@ -511,7 +511,7 @@ def create_parser():
|
||||
general.add_option(
|
||||
'--live-from-start',
|
||||
action='store_true', dest='live_from_start',
|
||||
help='Download livestreams from the start. Currently experimental and only supported for YouTube, Twitch, and TVer')
|
||||
help='Download livestreams from the start. Currently experimental and only supported for YouTube, Twitch, TVer, and mellow-fan')
|
||||
general.add_option(
|
||||
'--no-live-from-start',
|
||||
action='store_false', dest='live_from_start',
|
||||
@@ -1139,12 +1139,12 @@ def create_parser():
|
||||
dest='external_downloader', metavar='[PROTO:]NAME', default={}, type='str',
|
||||
action='callback', callback=_dict_from_options_callback,
|
||||
callback_kwargs={
|
||||
'allowed_keys': 'http|ftp|m3u8|dash|rtsp|rtmp|mms',
|
||||
'allowed_keys': 'http|ftp|m3u8|dash|rtmp',
|
||||
'default_key': 'default',
|
||||
'process': str.strip,
|
||||
}, help=(
|
||||
'Name or path of the external downloader to use (optionally) prefixed by '
|
||||
'the protocols (http, ftp, m3u8, dash, rstp, rtmp, mms) to use it for. '
|
||||
'the protocols (http, ftp, m3u8, dash, rtmp) to use it for. '
|
||||
f'Currently supports native, {", ".join(sorted(list_external_downloaders()))}. '
|
||||
'You can use this option multiple times to set different downloaders for different protocols. '
|
||||
'E.g. --downloader aria2c --downloader "dash,m3u8:native" will use '
|
||||
|
||||
+1
-2
@@ -181,8 +181,7 @@ def _get_system_deprecation():
|
||||
# Do not inappropriately warn for unofficial/third-party binaries
|
||||
if not ORIGIN.startswith('yt-dlp/'):
|
||||
return None
|
||||
platform_name = platform.platform()
|
||||
if any(platform_name.startswith(f'Windows-{name}') for name in ('8', '2012Server')):
|
||||
if platform.platform().startswith(('Windows-8', 'Windows-2012Server')):
|
||||
return (
|
||||
'Support for Windows 8.x and Windows Server 2012 has been deprecated. '
|
||||
'See https://github.com/yt-dlp/yt-dlp/issues/16917 for details.\n'
|
||||
|
||||
+19
-7
@@ -2076,7 +2076,7 @@ def url_or_none(url):
|
||||
if not url or not isinstance(url, str):
|
||||
return None
|
||||
url = url.strip()
|
||||
return url if re.match(r'(?:(?:https?|rt(?:m(?:pt?[es]?|fp)|sp[su]?)|mms|ftps?|wss?):)?//', url) else None
|
||||
return url if re.match(r'(?:(?:https?|rtm(?:pt?[es]?|fp)|ftps?|wss?):)?//', url) else None
|
||||
|
||||
|
||||
def strftime_or_none(timestamp, date_format='%Y%m%d', default=None):
|
||||
@@ -3195,10 +3195,6 @@ def determine_protocol(info_dict):
|
||||
url = sanitize_url(info_dict['url'])
|
||||
if url.startswith('rtmp'):
|
||||
return 'rtmp'
|
||||
elif url.startswith('mms'):
|
||||
return 'mms'
|
||||
elif url.startswith('rtsp'):
|
||||
return 'rtsp'
|
||||
|
||||
ext = determine_ext(url)
|
||||
if ext == 'm3u8':
|
||||
@@ -4635,8 +4631,21 @@ LINK_TEMPLATES = {
|
||||
'webloc': DOT_WEBLOC_LINK_TEMPLATE,
|
||||
}
|
||||
|
||||
# Ref: https://specifications.freedesktop.org/desktop-entry/latest/value-types.html
|
||||
_DESKTOP_ENTRY_TRANS = str.maketrans({
|
||||
' ': R'\s',
|
||||
'\n': R'\n',
|
||||
'\t': R'\t',
|
||||
'\r': R'\r',
|
||||
'\\': R'\\',
|
||||
})
|
||||
|
||||
def iri_to_uri(iri):
|
||||
|
||||
def _desktop_entry_localestring(s):
|
||||
return s.translate(_DESKTOP_ENTRY_TRANS)
|
||||
|
||||
|
||||
def iri_to_uri(iri, *, allowed_schemes=('http', 'https')):
|
||||
"""
|
||||
Converts an IRI (Internationalized Resource Identifier, allowing Unicode characters) to a URI (Uniform Resource Identifier, ASCII-only).
|
||||
|
||||
@@ -4645,6 +4654,9 @@ def iri_to_uri(iri):
|
||||
|
||||
iri_parts = urllib.parse.urlparse(iri)
|
||||
|
||||
if iri_parts.scheme not in allowed_schemes:
|
||||
raise ValueError(f'"{iri_parts.scheme}" is not in allowed_schemes: {", ".join(allowed_schemes)}')
|
||||
|
||||
if '[' in iri_parts.netloc:
|
||||
raise ValueError('IPv6 URIs are not, yet, supported.')
|
||||
# Querying `.netloc`, when there's only one bracket, also raises a ValueError.
|
||||
@@ -5373,7 +5385,7 @@ class FormatSorter:
|
||||
'hdr': {'type': 'ordered', 'regex': True, 'field': 'dynamic_range',
|
||||
'order': ['dv', '(hdr)?12', r'(hdr)?10\+', '(hdr)?10', 'hlg', '', 'sdr', None]},
|
||||
'proto': {'type': 'ordered', 'regex': True, 'field': 'protocol',
|
||||
'order': ['(ht|f)tps', '(ht|f)tp$', 'm3u8.*', '.*dash', 'websocket_frag', 'rtmpe?', '', 'mms|rtsp', 'ws|websocket', 'f4']},
|
||||
'order': ['(ht|f)tps', '(ht|f)tp$', 'm3u8.*', '.*dash', 'websocket_frag', 'rtmpe?', '', 'ws|websocket', 'f4']},
|
||||
'vext': {'type': 'ordered', 'field': 'video_ext',
|
||||
'order': ('mp4', 'mov', 'webm', 'flv', '', 'none'),
|
||||
'order_free': ('webm', 'mp4', 'mov', 'flv', '', 'none')},
|
||||
|
||||
@@ -15,10 +15,10 @@ from .traversal import traverse_obj
|
||||
|
||||
|
||||
def random_user_agent():
|
||||
USER_AGENT_TMPL = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{} Safari/537.36'
|
||||
USER_AGENT_TMPL = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{}.0.0.0 Safari/537.36'
|
||||
# Target versions released within the last ~6 months
|
||||
CHROME_MAJOR_VERSION_RANGE = (143, 149)
|
||||
return USER_AGENT_TMPL.format(f'{random.randint(*CHROME_MAJOR_VERSION_RANGE)}.0.0.0')
|
||||
CHROME_MAJOR_VERSION_RANGE = (145, 151)
|
||||
return USER_AGENT_TMPL.format(random.randint(*CHROME_MAJOR_VERSION_RANGE))
|
||||
|
||||
|
||||
class HTTPHeaderDict(dict):
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
# Autogenerated by devscripts/update-version.py
|
||||
|
||||
__version__ = '2026.06.09'
|
||||
__version__ = '2026.07.04'
|
||||
|
||||
RELEASE_GIT_HEAD = '821bef0f00178916d60dbc86bc0bcb8cc3bae8d5'
|
||||
RELEASE_GIT_HEAD = '997fa140840a08df3938b40da470c78049fef1f6'
|
||||
|
||||
VARIANT = None
|
||||
|
||||
@@ -12,4 +12,4 @@ CHANNEL = 'stable'
|
||||
|
||||
ORIGIN = 'yt-dlp/yt-dlp'
|
||||
|
||||
_pkg_version = '2026.06.09'
|
||||
_pkg_version = '2026.07.04'
|
||||
|
||||
Reference in New Issue
Block a user