mirror of
https://github.com/yt-dlp/yt-dlp
synced 2025-12-16 06:05:41 +07:00
Compare commits
9 Commits
264044286d
...
a75399d89f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a75399d89f | ||
|
|
c9356f308d | ||
|
|
de7b3c0705 | ||
|
|
2c9091e355 | ||
|
|
dfc0a84c19 | ||
|
|
fe5ae54a7b | ||
|
|
78748b506f | ||
|
|
c7bda2192a | ||
|
|
4e6a693057 |
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -10,4 +10,4 @@ liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
|
||||
custom: ['https://github.com/yt-dlp/yt-dlp/blob/master/Collaborators.md#collaborators']
|
||||
custom: ['https://github.com/yt-dlp/yt-dlp/blob/master/Maintainers.md#maintainers']
|
||||
|
||||
22
.github/workflows/build.yml
vendored
22
.github/workflows/build.yml
vendored
@@ -194,7 +194,7 @@ jobs:
|
||||
UPDATE_TO: yt-dlp/yt-dlp@2025.09.05
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0 # Needed for changelog
|
||||
|
||||
@@ -255,7 +255,7 @@ jobs:
|
||||
SKIP_ONEFILE_BUILD: ${{ (!matrix.onefile && '1') || '' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Cache requirements
|
||||
if: matrix.cache_requirements
|
||||
@@ -318,7 +318,7 @@ jobs:
|
||||
UPDATE_TO: yt-dlp/yt-dlp@2025.09.05
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
# NB: Building universal2 does not work with python from actions/setup-python
|
||||
|
||||
- name: Cache requirements
|
||||
@@ -448,7 +448,7 @@ jobs:
|
||||
PYI_WHEEL: pyinstaller-${{ matrix.pyi_version }}-py3-none-${{ matrix.platform_tag }}.whl
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python_version }}
|
||||
@@ -536,7 +536,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
path: artifact
|
||||
pattern: build-bin-*
|
||||
@@ -558,35 +558,39 @@ jobs:
|
||||
cat >> _update_spec << EOF
|
||||
# This file is used for regulating self-update
|
||||
lock 2022.08.18.36 .+ Python 3\.6
|
||||
lock 2023.11.16 (?!win_x86_exe).+ Python 3\.7
|
||||
lock 2023.11.16 zip Python 3\.7
|
||||
lock 2023.11.16 win_x86_exe .+ Windows-(?:Vista|2008Server)
|
||||
lock 2024.10.22 py2exe .+
|
||||
lock 2024.10.22 zip Python 3\.8
|
||||
lock 2024.10.22 win(?:_x86)?_exe Python 3\.[78].+ Windows-(?:7-|2008ServerR2)
|
||||
lock 2025.08.11 darwin_legacy_exe .+
|
||||
lock 2025.08.27 linux_armv7l_exe .+
|
||||
lock 2025.10.14 zip Python 3\.9
|
||||
lockV2 yt-dlp/yt-dlp 2022.08.18.36 .+ Python 3\.6
|
||||
lockV2 yt-dlp/yt-dlp 2023.11.16 (?!win_x86_exe).+ Python 3\.7
|
||||
lockV2 yt-dlp/yt-dlp 2023.11.16 zip Python 3\.7
|
||||
lockV2 yt-dlp/yt-dlp 2023.11.16 win_x86_exe .+ Windows-(?:Vista|2008Server)
|
||||
lockV2 yt-dlp/yt-dlp 2024.10.22 py2exe .+
|
||||
lockV2 yt-dlp/yt-dlp 2024.10.22 zip Python 3\.8
|
||||
lockV2 yt-dlp/yt-dlp 2024.10.22 win(?:_x86)?_exe Python 3\.[78].+ Windows-(?:7-|2008ServerR2)
|
||||
lockV2 yt-dlp/yt-dlp 2025.08.11 darwin_legacy_exe .+
|
||||
lockV2 yt-dlp/yt-dlp 2025.08.27 linux_armv7l_exe .+
|
||||
lockV2 yt-dlp/yt-dlp-nightly-builds 2023.11.15.232826 (?!win_x86_exe).+ Python 3\.7
|
||||
lockV2 yt-dlp/yt-dlp 2025.10.14 zip Python 3\.9
|
||||
lockV2 yt-dlp/yt-dlp-nightly-builds 2023.11.15.232826 zip Python 3\.7
|
||||
lockV2 yt-dlp/yt-dlp-nightly-builds 2023.11.15.232826 win_x86_exe .+ Windows-(?:Vista|2008Server)
|
||||
lockV2 yt-dlp/yt-dlp-nightly-builds 2024.10.22.051025 py2exe .+
|
||||
lockV2 yt-dlp/yt-dlp-nightly-builds 2024.10.22.051025 zip Python 3\.8
|
||||
lockV2 yt-dlp/yt-dlp-nightly-builds 2024.10.22.051025 win(?:_x86)?_exe Python 3\.[78].+ Windows-(?:7-|2008ServerR2)
|
||||
lockV2 yt-dlp/yt-dlp-nightly-builds 2025.08.12.233030 darwin_legacy_exe .+
|
||||
lockV2 yt-dlp/yt-dlp-nightly-builds 2025.08.30.232839 linux_armv7l_exe .+
|
||||
lockV2 yt-dlp/yt-dlp-master-builds 2023.11.15.232812 (?!win_x86_exe).+ Python 3\.7
|
||||
lockV2 yt-dlp/yt-dlp-nightly-builds 2025.10.14.232845 zip Python 3\.9
|
||||
lockV2 yt-dlp/yt-dlp-master-builds 2023.11.15.232812 zip Python 3\.7
|
||||
lockV2 yt-dlp/yt-dlp-master-builds 2023.11.15.232812 win_x86_exe .+ Windows-(?:Vista|2008Server)
|
||||
lockV2 yt-dlp/yt-dlp-master-builds 2024.10.22.045052 py2exe .+
|
||||
lockV2 yt-dlp/yt-dlp-master-builds 2024.10.22.060347 zip Python 3\.8
|
||||
lockV2 yt-dlp/yt-dlp-master-builds 2024.10.22.060347 win(?:_x86)?_exe Python 3\.[78].+ Windows-(?:7-|2008ServerR2)
|
||||
lockV2 yt-dlp/yt-dlp-master-builds 2025.08.12.232447 darwin_legacy_exe .+
|
||||
lockV2 yt-dlp/yt-dlp-master-builds 2025.09.05.212910 linux_armv7l_exe .+
|
||||
lockV2 yt-dlp/yt-dlp-master-builds 2025.10.14.232330 zip Python 3\.9
|
||||
EOF
|
||||
|
||||
- name: Sign checksum files
|
||||
|
||||
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
|
||||
10
.github/workflows/core.yml
vendored
10
.github/workflows/core.yml
vendored
@@ -36,12 +36,10 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
# CPython 3.9 is in quick-test
|
||||
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14-dev', pypy-3.11]
|
||||
# CPython 3.10 is in quick-test
|
||||
python-version: ['3.11', '3.12', '3.13', '3.14', pypy-3.11]
|
||||
include:
|
||||
# atleast one of each CPython/PyPy tests must be in windows
|
||||
- os: windows-latest
|
||||
python-version: '3.9'
|
||||
- os: windows-latest
|
||||
python-version: '3.10'
|
||||
- os: windows-latest
|
||||
@@ -51,11 +49,11 @@ jobs:
|
||||
- os: windows-latest
|
||||
python-version: '3.13'
|
||||
- os: windows-latest
|
||||
python-version: '3.14-dev'
|
||||
python-version: '3.14'
|
||||
- os: windows-latest
|
||||
python-version: pypy-3.11
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
|
||||
10
.github/workflows/download.yml
vendored
10
.github/workflows/download.yml
vendored
@@ -9,11 +9,11 @@ jobs:
|
||||
if: "contains(github.event.head_commit.message, 'ci run dl')"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.9
|
||||
python-version: '3.10'
|
||||
- name: Install test requirements
|
||||
run: python3 ./devscripts/install_deps.py --include dev
|
||||
- name: Run tests
|
||||
@@ -28,15 +28,15 @@ jobs:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14-dev', pypy-3.11]
|
||||
python-version: ['3.11', '3.12', '3.13', '3.14', pypy-3.11]
|
||||
include:
|
||||
# atleast one of each CPython/PyPy tests must be in windows
|
||||
- os: windows-latest
|
||||
python-version: '3.9'
|
||||
python-version: '3.10'
|
||||
- os: windows-latest
|
||||
python-version: pypy-3.11
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
|
||||
10
.github/workflows/quick-test.yml
vendored
10
.github/workflows/quick-test.yml
vendored
@@ -9,11 +9,11 @@ jobs:
|
||||
if: "!contains(github.event.head_commit.message, 'ci skip all')"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python 3.9
|
||||
- uses: actions/checkout@v5
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.9'
|
||||
python-version: '3.10'
|
||||
- name: Install test requirements
|
||||
run: python3 ./devscripts/install_deps.py -o --include test
|
||||
- name: Run tests
|
||||
@@ -26,10 +26,10 @@ jobs:
|
||||
if: "!contains(github.event.head_commit.message, 'ci skip all')"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.9'
|
||||
python-version: '3.10'
|
||||
- name: Install dev dependencies
|
||||
run: python3 ./devscripts/install_deps.py -o --include static-analysis
|
||||
- name: Make lazy extractors
|
||||
|
||||
2
.github/workflows/release-master.yml
vendored
2
.github/workflows/release-master.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
id-token: write # mandatory for trusted publishing
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
path: dist
|
||||
name: build-pypi
|
||||
|
||||
4
.github/workflows/release-nightly.yml
vendored
4
.github/workflows/release-nightly.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
outputs:
|
||||
commit: ${{ steps.check_for_new_commits.outputs.commit }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Check for new commits
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
id-token: write # mandatory for trusted publishing
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
path: dist
|
||||
name: build-pypi
|
||||
|
||||
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
@@ -75,7 +75,7 @@ jobs:
|
||||
head_sha: ${{ steps.get_target.outputs.head_sha }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -170,7 +170,7 @@ jobs:
|
||||
id-token: write # mandatory for trusted publishing
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-python@v6
|
||||
@@ -233,10 +233,10 @@ jobs:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
HEAD_SHA: ${{ needs.prepare.outputs.head_sha }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v5
|
||||
with:
|
||||
path: artifact
|
||||
pattern: build-*
|
||||
@@ -259,7 +259,7 @@ jobs:
|
||||
"[]" \
|
||||
"(https://discord.gg/H5MNcFW63r \"Discord\") " \
|
||||
"[]" \
|
||||
"(https://github.com/${BASE_REPO}/blob/master/Collaborators.md#collaborators \"Donate\") " \
|
||||
"(https://github.com/${BASE_REPO}/blob/master/Maintainers.md#maintainers \"Donate\") " \
|
||||
"[]" \
|
||||
"(https://github.com/${REPOSITORY}${DOCS_PATH}#readme \"Documentation\") " > ./RELEASE_NOTES
|
||||
if [[ "${TARGET_REPO}" == "${BASE_REPO}" ]]; then
|
||||
|
||||
4
.github/workflows/signature-tests.yml
vendored
4
.github/workflows/signature-tests.yml
vendored
@@ -25,9 +25,9 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14-dev', pypy-3.11]
|
||||
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14', pypy-3.11]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
|
||||
2
.github/workflows/test-workflows.yml
vendored
2
.github/workflows/test-workflows.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
name: Check workflows
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.10" # Keep this in sync with release.yml's prepare job
|
||||
|
||||
@@ -284,7 +284,7 @@ ## Adding support for a new site
|
||||
|
||||
You can use `hatch fmt` to automatically fix problems. Rules that the linter/formatter enforces should not be disabled with `# noqa` unless a maintainer requests it. The only exception allowed is for old/printf-style string formatting in GraphQL query templates (use `# noqa: UP031`).
|
||||
|
||||
1. Make sure your code works under all [Python](https://www.python.org/) versions supported by yt-dlp, namely CPython >=3.9 and PyPy >=3.11. Backward compatibility is not required for even older versions of Python.
|
||||
1. Make sure your code works under all [Python](https://www.python.org/) versions supported by yt-dlp, namely CPython >=3.10 and PyPy >=3.11. Backward compatibility is not required for even older versions of Python.
|
||||
1. When the tests pass, [add](https://git-scm.com/docs/git-add) the new files, [commit](https://git-scm.com/docs/git-commit) them and [push](https://git-scm.com/docs/git-push) the result, like this:
|
||||
|
||||
```shell
|
||||
|
||||
12
CONTRIBUTORS
12
CONTRIBUTORS
@@ -1,10 +1,10 @@
|
||||
pukkandan (owner)
|
||||
shirt-dev (collaborator)
|
||||
coletdjnz/colethedj (collaborator)
|
||||
Ashish0804 (collaborator)
|
||||
bashonly (collaborator)
|
||||
Grub4K (collaborator)
|
||||
seproDev (collaborator)
|
||||
shirt-dev (maintainer)
|
||||
coletdjnz (maintainer)
|
||||
Ashish0804 (maintainer)
|
||||
bashonly (maintainer)
|
||||
Grub4K (maintainer)
|
||||
seproDev (maintainer)
|
||||
h-h-h-h
|
||||
pauldubois98
|
||||
nixxo
|
||||
|
||||
24
Changelog.md
24
Changelog.md
@@ -4,6 +4,30 @@ # Changelog
|
||||
# To create a release, dispatch the https://github.com/yt-dlp/yt-dlp/actions/workflows/release.yml workflow on master
|
||||
-->
|
||||
|
||||
### 2025.10.22
|
||||
|
||||
#### Important changes
|
||||
- **A stopgap release with a *TEMPORARY partial* fix for YouTube support**
|
||||
Some formats may still be unavailable, especially if cookies are passed to yt-dlp. The ***NEXT*** release, expected very soon, **will require an external JS runtime (e.g. Deno)** in order for YouTube downloads to work properly. [Read more](https://github.com/yt-dlp/yt-dlp/issues/14404)
|
||||
- **The minimum *required* Python version has been raised to 3.10**
|
||||
Python 3.9 has reached its end-of-life as of October 2025, and yt-dlp has now removed support for it. [Read more](https://github.com/yt-dlp/yt-dlp/issues/13858)
|
||||
|
||||
#### Core changes
|
||||
- [Remove Python 3.9 support](https://github.com/yt-dlp/yt-dlp/commit/4e6a693057cfaf1ce1f07b019ed3bfce2bf936f6) ([#13861](https://github.com/yt-dlp/yt-dlp/issues/13861)) by [bashonly](https://github.com/bashonly)
|
||||
|
||||
#### Extractor changes
|
||||
- **appleconnect**: [Rework extractor](https://github.com/yt-dlp/yt-dlp/commit/78748b506f0dca8236ac0045ed7f72f7cf334b62) ([#13229](https://github.com/yt-dlp/yt-dlp/issues/13229)) by [doe1080](https://github.com/doe1080)
|
||||
- **idagio**: [Support URLs with country codes](https://github.com/yt-dlp/yt-dlp/commit/c9356f308dd3c5f9f494cb40ed14c5df017b4fe0) ([#14655](https://github.com/yt-dlp/yt-dlp/issues/14655)) by [robin-mu](https://github.com/robin-mu)
|
||||
- **tvnoe**: [Rework Extractor](https://github.com/yt-dlp/yt-dlp/commit/fe5ae54a7b08ebe679f03afdeafbe1cee5784d5b) ([#13369](https://github.com/yt-dlp/yt-dlp/issues/13369)) by [doe1080](https://github.com/doe1080)
|
||||
- **youtube**: [Use temporary player client workaround](https://github.com/yt-dlp/yt-dlp/commit/2c9091e355a7ba5d1edb69796ecdca48199b77fb) ([#14693](https://github.com/yt-dlp/yt-dlp/issues/14693)) by [gamer191](https://github.com/gamer191)
|
||||
|
||||
#### Misc. changes
|
||||
- **cleanup**
|
||||
- Miscellaneous
|
||||
- [c7bda21](https://github.com/yt-dlp/yt-dlp/commit/c7bda2192aa24afce40fdbbbe056d269aa3b2872) by [bashonly](https://github.com/bashonly), [seproDev](https://github.com/seproDev)
|
||||
- [de7b3c0](https://github.com/yt-dlp/yt-dlp/commit/de7b3c0705022cb777c5b4b7f0c69c59ad6ff538) by [bashonly](https://github.com/bashonly)
|
||||
- **docs**: [Update list of maintainers](https://github.com/yt-dlp/yt-dlp/commit/dfc0a84c192a7357dd1768cc345d590253a14fe5) ([#14148](https://github.com/yt-dlp/yt-dlp/issues/14148)) by [bashonly](https://github.com/bashonly), [coletdjnz](https://github.com/coletdjnz), [seproDev](https://github.com/seproDev)
|
||||
|
||||
### 2025.10.14
|
||||
|
||||
#### Core changes
|
||||
|
||||
@@ -1,59 +1,34 @@
|
||||
# Collaborators
|
||||
# Maintainers
|
||||
|
||||
This is a list of the collaborators of the project and their major contributions. See the [Changelog](Changelog.md) for more details.
|
||||
This file lists the maintainers of yt-dlp and their major contributions. See the [Changelog](Changelog.md) for more details.
|
||||
|
||||
You can also find lists of all [contributors of yt-dlp](CONTRIBUTORS) and [authors of youtube-dl](https://github.com/ytdl-org/youtube-dl/blob/master/AUTHORS)
|
||||
|
||||
## Core Maintainers
|
||||
|
||||
## [pukkandan](https://github.com/pukkandan)
|
||||
Core Maintainers are responsible for reviewing and merging contributions, publishing releases, and steering the overall direction of the project.
|
||||
|
||||
[](https://ko-fi.com/pukkandan)
|
||||
[](https://github.com/sponsors/pukkandan)
|
||||
**You can contact the core maintainers via `maintainers@yt-dlp.org`.**
|
||||
|
||||
* Owner of the fork
|
||||
|
||||
|
||||
|
||||
## [shirt](https://github.com/shirt-dev)
|
||||
|
||||
[](https://ko-fi.com/shirt)
|
||||
|
||||
* Multithreading (`-N`) and aria2c support for fragment downloads
|
||||
* Support for media initialization and discontinuity in HLS
|
||||
* The self-updater (`-U`)
|
||||
|
||||
|
||||
|
||||
## [coletdjnz](https://github.com/coletdjnz)
|
||||
### [coletdjnz](https://github.com/coletdjnz)
|
||||
|
||||
[](https://github.com/sponsors/coletdjnz)
|
||||
|
||||
* Improved plugin architecture
|
||||
* Rewrote the networking infrastructure, implemented support for `requests`
|
||||
* YouTube improvements including: age-gate bypass, private playlists, multiple-clients (to avoid throttling) and a lot of under-the-hood improvements
|
||||
* Added support for new websites YoutubeWebArchive, MainStreaming, PRX, nzherald, Mediaklikk, StarTV etc
|
||||
* Improved/fixed support for Patreon, panopto, gfycat, itv, pbs, SouthParkDE etc
|
||||
* Overhauled the networking stack and implemented support for `requests` and `curl_cffi` (`--impersonate`) HTTP clients
|
||||
* Reworked the plugin architecture to support installing plugins across all yt-dlp distributions (exe, pip, etc.)
|
||||
* Maintains support for YouTube
|
||||
* Added and fixed support for various other sites
|
||||
|
||||
### [bashonly](https://github.com/bashonly)
|
||||
|
||||
* Rewrote and maintains the build/release workflows and the self-updater: executables, automated/nightly/master releases, `--update-to`
|
||||
* Overhauled external downloader cookie handling
|
||||
* Added `--cookies-from-browser` support for Firefox containers
|
||||
* Overhauled and maintains support for sites like Youtube, Vimeo, Twitter, TikTok, etc
|
||||
* Added support for sites like Dacast, Kick, Loom, SproutVideo, Triller, Weverse, etc
|
||||
|
||||
|
||||
|
||||
## [Ashish0804](https://github.com/Ashish0804) <sub><sup>[Inactive]</sup></sub>
|
||||
|
||||
[](https://ko-fi.com/ashish0804)
|
||||
|
||||
* Added support for new websites BiliIntl, DiscoveryPlusIndia, OlympicsReplay, PlanetMarathi, ShemarooMe, Utreon, Zee5 etc
|
||||
* Added playlist/series downloads for Hotstar, ParamountPlus, Rumble, SonyLIV, Trovo, TubiTv, Voot etc
|
||||
* Improved/fixed support for HiDive, HotStar, Hungama, LBRY, LinkedInLearning, Mxplayer, SonyLiv, TV2, Vimeo, VLive etc
|
||||
|
||||
|
||||
## [bashonly](https://github.com/bashonly)
|
||||
|
||||
* `--update-to`, self-updater rewrite, automated/nightly/master releases
|
||||
* `--cookies-from-browser` support for Firefox containers, external downloader cookie handling overhaul
|
||||
* Added support for new websites like Dacast, Kick, NBCStations, Triller, VideoKen, Weverse, WrestleUniverse etc
|
||||
* Improved/fixed support for Anvato, Brightcove, Reddit, SlidesLive, TikTok, Twitter, Vimeo etc
|
||||
|
||||
|
||||
## [Grub4K](https://github.com/Grub4K)
|
||||
### [Grub4K](https://github.com/Grub4K)
|
||||
|
||||
[](https://github.com/sponsors/Grub4K) [](https://ko-fi.com/Grub4K)
|
||||
|
||||
@@ -63,8 +38,48 @@ ## [Grub4K](https://github.com/Grub4K)
|
||||
* Improved/fixed/added Bundestag, crunchyroll, pr0gramm, Twitter, WrestleUniverse etc
|
||||
|
||||
|
||||
## [sepro](https://github.com/seproDev)
|
||||
### [sepro](https://github.com/seproDev)
|
||||
|
||||
* UX improvements: Warn when ffmpeg is missing, warn when double-clicking exe
|
||||
* Code cleanup: Remove dead extractors, mark extractors as broken, enable/apply ruff rules
|
||||
* Improved/fixed/added ArdMediathek, DRTV, Floatplane, MagentaMusik, Naver, Nebula, OnDemandKorea, Vbox7 etc
|
||||
|
||||
|
||||
## Inactive Core Maintainers
|
||||
|
||||
### [pukkandan](https://github.com/pukkandan)
|
||||
|
||||
[](https://ko-fi.com/pukkandan)
|
||||
[](https://github.com/sponsors/pukkandan)
|
||||
|
||||
* Founder of the fork
|
||||
* Lead Maintainer from 2021-2024
|
||||
|
||||
|
||||
### [shirt](https://github.com/shirt-dev)
|
||||
|
||||
[](https://ko-fi.com/shirt)
|
||||
|
||||
* Multithreading (`-N`) and aria2c support for fragment downloads
|
||||
* Support for media initialization and discontinuity in HLS
|
||||
* The self-updater (`-U`)
|
||||
|
||||
|
||||
### [Ashish0804](https://github.com/Ashish0804)
|
||||
|
||||
[](https://ko-fi.com/ashish0804)
|
||||
|
||||
* Added support for new websites BiliIntl, DiscoveryPlusIndia, OlympicsReplay, PlanetMarathi, ShemarooMe, Utreon, Zee5 etc
|
||||
* Added playlist/series downloads for Hotstar, ParamountPlus, Rumble, SonyLIV, Trovo, TubiTv, Voot etc
|
||||
* Improved/fixed support for HiDive, HotStar, Hungama, LBRY, LinkedInLearning, Mxplayer, SonyLiv, TV2, Vimeo, VLive etc
|
||||
|
||||
## Triage Maintainers
|
||||
|
||||
Triage Maintainers are frequent contributors who can manage issues and pull requests.
|
||||
|
||||
- [gamer191](https://github.com/gamer191)
|
||||
- [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)
|
||||
2
Makefile
2
Makefile
@@ -157,7 +157,7 @@ yt-dlp.tar.gz: all
|
||||
--exclude '.git' \
|
||||
-- \
|
||||
README.md supportedsites.md Changelog.md LICENSE \
|
||||
CONTRIBUTING.md Collaborators.md CONTRIBUTORS AUTHORS \
|
||||
CONTRIBUTING.md Maintainers.md CONTRIBUTORS AUTHORS \
|
||||
Makefile yt-dlp.1 README.txt completions .gitignore \
|
||||
yt-dlp yt_dlp pyproject.toml devscripts test
|
||||
|
||||
|
||||
12
README.md
12
README.md
@@ -5,7 +5,7 @@
|
||||
|
||||
[](#installation "Installation")
|
||||
[](https://pypi.org/project/yt-dlp "PyPI")
|
||||
[](Collaborators.md#collaborators "Donate")
|
||||
[](Maintainers.md#maintainers "Donate")
|
||||
[](https://discord.gg/H5MNcFW63r "Discord")
|
||||
[](supportedsites.md "Supported Sites")
|
||||
[](LICENSE "License")
|
||||
@@ -194,7 +194,7 @@ # To install nightly with pip:
|
||||
You can suppress this warning by adding `--no-update` to your command or configuration file.
|
||||
|
||||
## DEPENDENCIES
|
||||
Python versions 3.9+ (CPython) and 3.11+ (PyPy) are supported. Other versions and implementations may or may not work correctly.
|
||||
Python versions 3.10+ (CPython) and 3.11+ (PyPy) are supported. Other versions and implementations may or may not work correctly.
|
||||
|
||||
<!-- Python 3.5+ uses VC++14 and it is already embedded in the binary created
|
||||
<!x-- https://www.microsoft.com/en-us/download/details.aspx?id=26999 --x>
|
||||
@@ -273,7 +273,7 @@ ### Standalone PyInstaller Builds
|
||||
**Important**: Running `pyinstaller` directly **instead of** using `python -m bundle.pyinstaller` is **not** officially supported. This may or may not work correctly.
|
||||
|
||||
### Platform-independent Binary (UNIX)
|
||||
You will need the build tools `python` (3.9+), `zip`, `make` (GNU), `pandoc`\* and `pytest`\*.
|
||||
You will need the build tools `python` (3.10+), `zip`, `make` (GNU), `pandoc`\* and `pytest`\*.
|
||||
|
||||
After installing these, simply run `make`.
|
||||
|
||||
@@ -1814,12 +1814,12 @@ # EXTRACTOR ARGUMENTS
|
||||
#### 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_simply` and `tv_embedded`. By default, `tv,web_safari,web` is used, and `tv,web_creator,web` is used with 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 works if the video is embeddable. The `tv_embedded` and `web_creator` clients are added for age-restricted videos if account age-verification is required. Some clients, such as `web` 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,-ios`
|
||||
* `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_sdkless`, `android_vr`, `tv`, `tv_simply` and `tv_embedded`. By default, `android_sdkless,tv,web_safari,web` is used. `android_sdkless` is omitted if cookies are passed. If premium cookies are passed, `tv,web_creator,web_safari,web` is used instead. 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 works if the video is embeddable. The `tv_embedded` and `web_creator` clients are added for age-restricted videos if account age-verification is required. Some clients, such as `web` 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,-ios`
|
||||
* `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
|
||||
* `player_params`: YouTube player parameters to use for player requests. Will overwrite any default ones set by yt-dlp.
|
||||
* `player_js_variant`: The player javascript variant to use for n/sig deciphering. The known variants are: `main`, `tcc`, `tce`, `es5`, `es6`, `tv`, `tv_es6`, `phone`, `tablet`. The default is `main`, and the others are for debugging purposes. You can use `actual` to go with what is prescribed by the site
|
||||
* `player_js_version`: The player javascript version to use for n/sig deciphering, in the format of `signature_timestamp@hash`. Currently, the default is to force `20348@0004de42`. You can use `actual` to go with what is prescribed by the site
|
||||
* `player_js_version`: The player javascript version to use for n/sig deciphering, in the format of `signature_timestamp@hash` (e.g. `20348@0004de42`). The default is to use what is prescribed by the site, and can be selected with `actual`
|
||||
* `comment_sort`: `top` or `new` (default) - choose comment sorting mode (on YouTube's side)
|
||||
* `max_comments`: Limit the amount of comments to gather. Comma-separated list of integers representing `max-comments,max-parents,max-replies,max-replies-per-thread`. Default is `all,all,all,all`
|
||||
* E.g. `all,all,1000,10` will get a maximum of 1000 replies total, with up to 10 replies per thread. `1000,all,100` will get a maximum of 1000 comments, with a maximum of 100 replies total
|
||||
@@ -2255,7 +2255,7 @@ ### Differences in default behavior
|
||||
|
||||
Some of yt-dlp's default options are different from that of youtube-dl and youtube-dlc:
|
||||
|
||||
* yt-dlp supports only [Python 3.9+](## "Windows 8"), and will remove support for more versions as they [become EOL](https://devguide.python.org/versions/#python-release-cycle); while [youtube-dl still supports Python 2.6+ and 3.2+](https://github.com/ytdl-org/youtube-dl/issues/30568#issue-1118238743)
|
||||
* yt-dlp supports only [Python 3.10+](## "Windows 8"), and will remove support for more versions as they [become EOL](https://devguide.python.org/versions/#python-release-cycle); while [youtube-dl still supports Python 2.6+ and 3.2+](https://github.com/ytdl-org/youtube-dl/issues/30568#issue-1118238743)
|
||||
* The options `--auto-number` (`-A`), `--title` (`-t`) and `--literal` (`-l`), no longer work. See [removed options](#Removed) for details
|
||||
* `avconv` is not supported as an alternative to `ffmpeg`
|
||||
* yt-dlp stores config files in slightly different locations to youtube-dl. See [CONFIGURATION](#configuration) for a list of correct locations
|
||||
|
||||
@@ -298,5 +298,15 @@
|
||||
"action": "add",
|
||||
"when": "08d78996831bd8e1e3c2592d740c3def00bbf548",
|
||||
"short": "[priority] **Several options have been deprecated**\nIn order to simplify the codebase and reduce maintenance burden, various options have been deprecated. Please remove them from your commands/configurations. [Read more](https://github.com/yt-dlp/yt-dlp/issues/14198)"
|
||||
},
|
||||
{
|
||||
"action": "add",
|
||||
"when": "4e6a693057cfaf1ce1f07b019ed3bfce2bf936f6",
|
||||
"short": "[priority] **The minimum *required* Python version has been raised to 3.10**\nPython 3.9 has reached its end-of-life as of October 2025, and yt-dlp has now removed support for it. [Read more](https://github.com/yt-dlp/yt-dlp/issues/13858)"
|
||||
},
|
||||
{
|
||||
"action": "add",
|
||||
"when": "2c9091e355a7ba5d1edb69796ecdca48199b77fb",
|
||||
"short": "[priority] **A stopgap release with a *TEMPORARY partial* fix for YouTube support**\nSome formats may still be unavailable, especially if cookies are passed to yt-dlp. The ***NEXT*** release, expected very soon, **will require an external JS runtime (e.g. Deno)** in order for YouTube downloads to work properly. [Read more](https://github.com/yt-dlp/yt-dlp/issues/14404)"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -373,7 +373,7 @@ def groups(self):
|
||||
issues = [issue.strip()[1:] for issue in issues.split(',')] if issues else []
|
||||
|
||||
if prefix:
|
||||
groups, details, sub_details = zip(*map(self.details_from_prefix, prefix.split(',')))
|
||||
groups, details, sub_details = zip(*map(self.details_from_prefix, prefix.split(',')), strict=True)
|
||||
group = next(iter(filter(None, groups)), None)
|
||||
details = ', '.join(unique(details))
|
||||
sub_details = list(itertools.chain.from_iterable(sub_details))
|
||||
|
||||
@@ -4,8 +4,11 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "yt-dlp"
|
||||
maintainers = [
|
||||
authors = [
|
||||
{name = "pukkandan", email = "pukkandan.ytdlp@gmail.com"},
|
||||
]
|
||||
maintainers = [
|
||||
{email = "maintainers@yt-dlp.org"},
|
||||
{name = "Grub4K", email = "contact@grub4k.xyz"},
|
||||
{name = "bashonly", email = "bashonly@protonmail.com"},
|
||||
{name = "coletdjnz", email = "coletdjnz@protonmail.com"},
|
||||
@@ -13,7 +16,7 @@ maintainers = [
|
||||
]
|
||||
description = "A feature-rich command-line audio/video downloader"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.9"
|
||||
requires-python = ">=3.10"
|
||||
keywords = [
|
||||
"cli",
|
||||
"downloader",
|
||||
@@ -30,7 +33,6 @@ classifiers = [
|
||||
"Environment :: Console",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
@@ -76,7 +78,7 @@ dev = [
|
||||
]
|
||||
static-analysis = [
|
||||
"autopep8~=2.0",
|
||||
"ruff~=0.13.0",
|
||||
"ruff~=0.14.0",
|
||||
]
|
||||
test = [
|
||||
"pytest~=8.1",
|
||||
@@ -90,7 +92,7 @@ pyinstaller = [
|
||||
Documentation = "https://github.com/yt-dlp/yt-dlp#readme"
|
||||
Repository = "https://github.com/yt-dlp/yt-dlp"
|
||||
Tracker = "https://github.com/yt-dlp/yt-dlp/issues"
|
||||
Funding = "https://github.com/yt-dlp/yt-dlp/blob/master/Collaborators.md#collaborators"
|
||||
Funding = "https://github.com/yt-dlp/yt-dlp/blob/master/Maintainers.md#maintainers"
|
||||
|
||||
[project.scripts]
|
||||
yt-dlp = "yt_dlp:main"
|
||||
@@ -168,7 +170,6 @@ run-cov = "echo Code coverage not implemented && exit 1"
|
||||
|
||||
[[tool.hatch.envs.hatch-test.matrix]]
|
||||
python = [
|
||||
"3.9",
|
||||
"3.10",
|
||||
"3.11",
|
||||
"3.12",
|
||||
|
||||
@@ -85,7 +85,7 @@ # Supported sites
|
||||
- **aol.com**: Yahoo screen and movies (**Currently broken**)
|
||||
- **APA**
|
||||
- **Aparat**
|
||||
- **AppleConnect**
|
||||
- **apple:music:connect**: Apple Music Connect
|
||||
- **AppleDaily**: 臺灣蘋果日報
|
||||
- **ApplePodcasts**
|
||||
- **appletrailers**
|
||||
@@ -1541,7 +1541,7 @@ # Supported sites
|
||||
- **tvigle**: Интернет-телевидение Tvigle.ru
|
||||
- **TVIPlayer**
|
||||
- **TVN24**: (**Currently broken**)
|
||||
- **TVNoe**: (**Currently broken**)
|
||||
- **tvnoe**: Televize Noe
|
||||
- **tvopengr:embed**: tvopen.gr embedded videos
|
||||
- **tvopengr:watch**: tvopen.gr (and ethnos.gr) videos
|
||||
- **tvp**: Telewizja Polska
|
||||
|
||||
@@ -176,7 +176,7 @@ def _iter_differences(got, expected, field):
|
||||
yield field, f'expected length of {len(expected)}, got {len(got)}'
|
||||
return
|
||||
|
||||
for index, (got_val, expected_val) in enumerate(zip(got, expected)):
|
||||
for index, (got_val, expected_val) in enumerate(zip(got, expected, strict=True)):
|
||||
field_name = str(index) if field is None else f'{field}.{index}'
|
||||
yield from _iter_differences(got_val, expected_val, field_name)
|
||||
return
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
import contextlib
|
||||
import copy
|
||||
import itertools
|
||||
import json
|
||||
|
||||
from test.helper import FakeYDL, assertRegexpMatches, try_rm
|
||||
@@ -414,7 +415,7 @@ def format_info(f_id):
|
||||
downloaded_ids = [info['format_id'] for info in ydl.downloaded_info_dicts]
|
||||
self.assertEqual(downloaded_ids, ['248+141'])
|
||||
|
||||
for f1, f2 in zip(formats_order, formats_order[1:]):
|
||||
for f1, f2 in itertools.pairwise(formats_order):
|
||||
info_dict = _make_result([f1, f2], extractor='youtube')
|
||||
ydl = YDL({'format': 'best/bestvideo'})
|
||||
ydl.sort_formats(info_dict)
|
||||
@@ -749,7 +750,7 @@ def test(tmpl, expected, *, info=None, **params):
|
||||
|
||||
if not isinstance(expected, (list, tuple)):
|
||||
expected = (expected, expected)
|
||||
for (name, got), expect in zip((('outtmpl', out), ('filename', fname)), expected):
|
||||
for (name, got), expect in zip((('outtmpl', out), ('filename', fname)), expected, strict=True):
|
||||
if callable(expect):
|
||||
self.assertTrue(expect(got), f'Wrong {name} from {tmpl}')
|
||||
elif expect is not None:
|
||||
@@ -1147,7 +1148,7 @@ def test_selection(params, expected_ids, evaluate_all=False):
|
||||
entries = func(evaluated)
|
||||
results = [(v['playlist_autonumber'] - 1, (int(v['id']), v['playlist_index']))
|
||||
for v in get_downloaded_info_dicts(params, entries)]
|
||||
self.assertEqual(results, list(enumerate(zip(expected_ids, expected_ids))), f'Entries of {name} for {params}')
|
||||
self.assertEqual(results, list(enumerate(zip(expected_ids, expected_ids, strict=True))), f'Entries of {name} for {params}')
|
||||
self.assertEqual(sorted(evaluated), expected_eval, f'Evaluation of {name} for {params}')
|
||||
|
||||
test_selection({}, INDICES)
|
||||
|
||||
@@ -115,7 +115,7 @@ def _chapters(self, ends, titles):
|
||||
self.assertEqual(len(ends), len(titles))
|
||||
start = 0
|
||||
chapters = []
|
||||
for e, t in zip(ends, titles):
|
||||
for e, t in zip(ends, titles, strict=True):
|
||||
chapters.append(self._chapter(start, e, t))
|
||||
start = e
|
||||
return chapters
|
||||
|
||||
@@ -417,7 +417,7 @@ def test_traversal_unbranching(self):
|
||||
|
||||
def test_traversal_morsel(self):
|
||||
morsel = http.cookies.Morsel()
|
||||
values = dict(zip(morsel, 'abcdefghijklmnop'))
|
||||
values = dict(zip(morsel, 'abcdefghijklmnop', strict=False))
|
||||
morsel.set('item_key', 'item_value', 'coded_value')
|
||||
morsel.update(values)
|
||||
values['key'] = 'item_key'
|
||||
|
||||
@@ -1863,7 +1863,7 @@ def test_get_elements_text_and_html_by_attribute(self):
|
||||
|
||||
self.assertEqual(
|
||||
list(get_elements_text_and_html_by_attribute('class', 'foo bar', html)),
|
||||
list(zip(['nice', 'also nice'], self.GET_ELEMENTS_BY_CLASS_RES)))
|
||||
list(zip(['nice', 'also nice'], self.GET_ELEMENTS_BY_CLASS_RES, strict=True)))
|
||||
self.assertEqual(list(get_elements_text_and_html_by_attribute('class', 'foo', html)), [])
|
||||
self.assertEqual(list(get_elements_text_and_html_by_attribute('class', 'no-such-foo', html)), [])
|
||||
|
||||
|
||||
@@ -2007,7 +2007,7 @@ def __process_playlist(self, ie_result, download):
|
||||
else:
|
||||
entries = resolved_entries = list(entries)
|
||||
n_entries = len(resolved_entries)
|
||||
ie_result['requested_entries'], ie_result['entries'] = tuple(zip(*resolved_entries)) or ([], [])
|
||||
ie_result['requested_entries'], ie_result['entries'] = tuple(zip(*resolved_entries, strict=True)) or ([], [])
|
||||
if not ie_result.get('playlist_count'):
|
||||
# Better to do this after potentially exhausting entries
|
||||
ie_result['playlist_count'] = all_entries.get_full_count()
|
||||
@@ -2785,7 +2785,7 @@ def sanitize_numeric_fields(info):
|
||||
|
||||
dummy_chapter = {'end_time': 0, 'start_time': info_dict.get('duration')}
|
||||
for idx, (prev, current, next_) in enumerate(zip(
|
||||
(dummy_chapter, *chapters), chapters, (*chapters[1:], dummy_chapter)), 1):
|
||||
(dummy_chapter, *chapters), chapters, (*chapters[1:], dummy_chapter), strict=False), 1):
|
||||
if current.get('start_time') is None:
|
||||
current['start_time'] = prev.get('end_time')
|
||||
if not current.get('end_time'):
|
||||
@@ -3370,7 +3370,7 @@ def _write_link_file(link_type):
|
||||
def existing_video_file(*filepaths):
|
||||
ext = info_dict.get('ext')
|
||||
converted = lambda file: replace_extension(file, self.params.get('final_ext') or ext, ext)
|
||||
file = self.existing_file(itertools.chain(*zip(map(converted, filepaths), filepaths)),
|
||||
file = self.existing_file(itertools.chain(*zip(map(converted, filepaths), filepaths, strict=True)),
|
||||
default_overwrite=False)
|
||||
if file:
|
||||
info_dict['ext'] = os.path.splitext(file)[1][1:]
|
||||
@@ -3956,7 +3956,7 @@ def render_thumbnails_table(self, info_dict):
|
||||
|
||||
def render_subtitles_table(self, video_id, subtitles):
|
||||
def _row(lang, formats):
|
||||
exts, names = zip(*((f['ext'], f.get('name') or 'unknown') for f in reversed(formats)))
|
||||
exts, names = zip(*((f['ext'], f.get('name') or 'unknown') for f in reversed(formats)), strict=True)
|
||||
if len(set(names)) == 1:
|
||||
names = [] if names[0] == 'unknown' else names[:1]
|
||||
return [lang, ', '.join(names), ', '.join(exts)]
|
||||
@@ -4112,8 +4112,7 @@ def cookiejar(self):
|
||||
self.params.get('cookiefile'), self.params.get('cookiesfrombrowser'), self)
|
||||
except CookieLoadError as error:
|
||||
cause = error.__context__
|
||||
# compat: <=py3.9: `traceback.format_exception` has a different signature
|
||||
self.report_error(str(cause), tb=''.join(traceback.format_exception(None, cause, cause.__traceback__)))
|
||||
self.report_error(str(cause), tb=''.join(traceback.format_exception(cause)))
|
||||
raise
|
||||
|
||||
@property
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import sys
|
||||
|
||||
if sys.version_info < (3, 9):
|
||||
if sys.version_info < (3, 10):
|
||||
raise ImportError(
|
||||
f'You are using an unsupported version of Python. Only Python versions 3.9 and above are supported by yt-dlp') # noqa: F541
|
||||
f'You are using an unsupported version of Python. Only Python versions 3.10 and above are supported by yt-dlp') # noqa: F541
|
||||
|
||||
__license__ = 'The Unlicense'
|
||||
|
||||
@@ -974,13 +974,8 @@ def _real_main(argv=None):
|
||||
|
||||
try:
|
||||
updater = Updater(ydl, opts.update_self)
|
||||
if opts.update_self and updater.update() and actual_use:
|
||||
if updater.cmd:
|
||||
return updater.restart()
|
||||
# This code is reachable only for zip variant in py < 3.10
|
||||
# It makes sense to exit here, but the old behavior is to continue
|
||||
ydl.report_warning('Restart yt-dlp to use the updated version')
|
||||
# return 100, 'ERROR: The program must exit for the update to complete'
|
||||
if opts.update_self and updater.update() and actual_use and updater.cmd:
|
||||
return updater.restart()
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
ydl._download_retcode = 100
|
||||
|
||||
@@ -447,7 +447,7 @@ def key_schedule_core(data, rcon_iteration):
|
||||
|
||||
|
||||
def xor(data1, data2):
|
||||
return [x ^ y for x, y in zip(data1, data2)]
|
||||
return [x ^ y for x, y in zip(data1, data2, strict=False)]
|
||||
|
||||
|
||||
def iter_mix_columns(data, matrix):
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
# flake8: noqa: F405
|
||||
from types import * # noqa: F403
|
||||
|
||||
from .compat_utils import passthrough_module
|
||||
|
||||
passthrough_module(__name__, 'types')
|
||||
del passthrough_module
|
||||
|
||||
try:
|
||||
# NB: pypy has builtin NoneType, so checking NameError won't work
|
||||
from types import NoneType # >= 3.10
|
||||
except ImportError:
|
||||
NoneType = type(None)
|
||||
@@ -22,15 +22,11 @@
|
||||
|
||||
def getproxies_registry_patched():
|
||||
proxies = getproxies_registry()
|
||||
if (
|
||||
sys.version_info >= (3, 10, 5) # https://docs.python.org/3.10/whatsnew/changelog.html#python-3-10-5-final
|
||||
or (3, 9, 13) <= sys.version_info < (3, 10) # https://docs.python.org/3.9/whatsnew/changelog.html#python-3-9-13-final
|
||||
):
|
||||
return proxies
|
||||
|
||||
for scheme in ('https', 'ftp'):
|
||||
if scheme in proxies and proxies[scheme].startswith(f'{scheme}://'):
|
||||
proxies[scheme] = 'http' + proxies[scheme][len(scheme):]
|
||||
if sys.version_info < (3, 10, 5): # https://docs.python.org/3.10/whatsnew/changelog.html#python-3-10-5-final
|
||||
for scheme in ('https', 'ftp'):
|
||||
if scheme in proxies and proxies[scheme].startswith(f'{scheme}://'):
|
||||
proxies[scheme] = 'http' + proxies[scheme][len(scheme):]
|
||||
|
||||
return proxies
|
||||
|
||||
|
||||
@@ -1,47 +1,125 @@
|
||||
import time
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import ExtractorError, str_to_int
|
||||
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):
|
||||
_VALID_URL = r'https?://itunes\.apple\.com/\w{0,2}/?post/(?:id)?sa\.(?P<id>[\w-]+)'
|
||||
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://itunes.apple.com/us/post/idsa.4ab17a39-2720-11e5-96c5-a5b38f6c42d3',
|
||||
'md5': 'c1d41f72c8bcaf222e089434619316e4',
|
||||
'url': 'https://music.apple.com/us/post/1018290019',
|
||||
'info_dict': {
|
||||
'id': '4ab17a39-2720-11e5-96c5-a5b38f6c42d3',
|
||||
'id': '1018290019',
|
||||
'ext': 'm4v',
|
||||
'title': 'Energy',
|
||||
'uploader': 'Drake',
|
||||
'thumbnail': r're:^https?://.*\.jpg$',
|
||||
'duration': 177.911,
|
||||
'thumbnail': r're:https?://.+\.png',
|
||||
'upload_date': '20150710',
|
||||
'timestamp': 1436545535,
|
||||
'uploader': 'Drake',
|
||||
},
|
||||
}, {
|
||||
'url': 'https://itunes.apple.com/us/post/sa.0fe0229f-2457-11e5-9f40-1bb645f2d5d9',
|
||||
'only_matching': True,
|
||||
'url': 'https://music.apple.com/us/post/1016746627',
|
||||
'info_dict': {
|
||||
'id': '1016746627',
|
||||
'ext': 'm4v',
|
||||
'title': 'Body Shop (Madonna) - Chellous Lima (Acoustic Cover)',
|
||||
'duration': 210.278,
|
||||
'thumbnail': r're:https?://.+\.png',
|
||||
'upload_date': '20150706',
|
||||
'uploader': 'Chellous Lima',
|
||||
},
|
||||
}]
|
||||
|
||||
_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
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
webpage = self._download_webpage(url, video_id)
|
||||
|
||||
try:
|
||||
video_json = self._html_search_regex(
|
||||
r'class="auc-video-data">(\{.*?\})', webpage, 'json')
|
||||
except ExtractorError:
|
||||
raise ExtractorError('This post doesn\'t contain a video', expected=True)
|
||||
videos = self._download_json(
|
||||
'https://amp-api.music.apple.com/v1/catalog/us/uploaded-videos',
|
||||
video_id, headers={
|
||||
'Authorization': f'Bearer {self._get_token(webpage, video_id)}',
|
||||
'Origin': self._BASE_URL,
|
||||
}, query={'ids': video_id, 'l': 'en-US'})
|
||||
attributes = traverse_obj(videos, (
|
||||
'data', ..., 'attributes', any, {require('video information')}))
|
||||
|
||||
video_data = self._parse_json(video_json, video_id)
|
||||
timestamp = str_to_int(self._html_search_regex(r'data-timestamp="(\d+)"', webpage, 'timestamp'))
|
||||
like_count = str_to_int(self._html_search_regex(r'(\d+) Loves', webpage, 'like count', default=None))
|
||||
formats = []
|
||||
quality = qualities(list(self._QUALITIES.keys()))
|
||||
for format_id, src_url in traverse_obj(attributes, (
|
||||
'assetTokens', {dict.items}, lambda _, v: url_or_none(v[1]),
|
||||
)):
|
||||
formats.append({
|
||||
'ext': 'm4v',
|
||||
'format_id': format_id,
|
||||
'height': self._QUALITIES.get(format_id),
|
||||
'quality': quality(format_id),
|
||||
'url': src_url,
|
||||
**parse_resolution(update_url(src_url, query=None), lenient=True),
|
||||
})
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'url': video_data['sslSrc'],
|
||||
'title': video_data['title'],
|
||||
'description': video_data['description'],
|
||||
'uploader': video_data['artistName'],
|
||||
'thumbnail': video_data['artworkUrl'],
|
||||
'timestamp': timestamp,
|
||||
'like_count': like_count,
|
||||
'formats': formats,
|
||||
'thumbnail': self._html_search_meta(
|
||||
['og:image', 'og:image:secure_url', 'twitter:image'], webpage),
|
||||
**traverse_obj(attributes, {
|
||||
'title': ('name', {str}),
|
||||
'duration': ('durationInMilliseconds', {float_or_none(scale=1000)}),
|
||||
'upload_date': ('uploadDate', {unified_strdate}),
|
||||
'uploader': (('artistName', 'uploadingArtistName'), {str}, any),
|
||||
'webpage_url': ('postUrl', {url_or_none}),
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -740,7 +740,7 @@ def _call_cdx_api(self, item_id, url, filters: list | None = None, collapse: lis
|
||||
note or 'Downloading CDX API JSON', query=query, fatal=fatal)
|
||||
if isinstance(res, list) and len(res) >= 2:
|
||||
# format response to make it easier to use
|
||||
return [dict(zip(res[0], v)) for v in res[1:]]
|
||||
return [dict(zip(res[0], v)) for v in res[1:]] # noqa: B905
|
||||
elif not isinstance(res, list) or len(res) != 0:
|
||||
self.report_warning('Error while parsing CDX API response' + bug_reports_message())
|
||||
|
||||
|
||||
@@ -1663,7 +1663,7 @@ def extract_chapter_information(e):
|
||||
'end_time': part.get('endOffset'),
|
||||
} for part in variadic(e.get('hasPart') or []) if part.get('@type') == 'Clip']
|
||||
for idx, (last_c, current_c, next_c) in enumerate(zip(
|
||||
[{'end_time': 0}, *chapters], chapters, chapters[1:])):
|
||||
[{'end_time': 0}, *chapters], chapters, chapters[1:], strict=False)):
|
||||
current_c['end_time'] = current_c['end_time'] or next_c['start_time']
|
||||
current_c['start_time'] = current_c['start_time'] or last_c['end_time']
|
||||
if None in current_c.values():
|
||||
@@ -1848,7 +1848,7 @@ def _search_nuxt_data(self, webpage, video_id, context_name='__NUXT__', *, fatal
|
||||
return {}
|
||||
|
||||
args = dict(zip(arg_keys.split(','), map(json.dumps, self._parse_json(
|
||||
f'[{arg_vals}]', video_id, transform_source=js_to_json, fatal=fatal) or ())))
|
||||
f'[{arg_vals}]', video_id, transform_source=js_to_json, fatal=fatal) or ()), strict=True))
|
||||
|
||||
ret = self._parse_json(js, video_id, transform_source=functools.partial(js_to_json, vars=args), fatal=fatal)
|
||||
return traverse_obj(ret, traverse) or {}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import json
|
||||
import socket
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
@@ -56,7 +55,7 @@ def canonical_url(h):
|
||||
try:
|
||||
self.to_screen(f'{video_id}: Checking {format_id} video format URL')
|
||||
self._downloader._opener.open(video_url, timeout=5).close()
|
||||
except socket.timeout:
|
||||
except TimeoutError:
|
||||
self.to_screen(
|
||||
f'{video_id}: {format_id} URL is invalid, skipping')
|
||||
continue
|
||||
|
||||
@@ -56,7 +56,7 @@ def _real_extract(self, url):
|
||||
fmt, subs = self._extract_m3u8_formats_and_subtitles(src['url'], video_id, 'ts')
|
||||
for f in fmt:
|
||||
f.update(dict(zip(('height', 'width'),
|
||||
self._BITRATE_MAP.get(f.get('tbr'), ()))))
|
||||
self._BITRATE_MAP.get(f.get('tbr'), ()), strict=False)))
|
||||
formats.extend(fmt)
|
||||
subtitles = self._merge_subtitles(subtitles, subs)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
|
||||
class IdagioTrackIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://(?:www\.)?app\.idagio\.com/recordings/\d+\?(?:[^#]+&)?trackId=(?P<id>\d+)'
|
||||
_VALID_URL = r'https?://(?:www\.)?app\.idagio\.com(?:/[a-z]{2})?/recordings/\d+\?(?:[^#]+&)?trackId=(?P<id>\d+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://app.idagio.com/recordings/30576934?trackId=30576943',
|
||||
'md5': '15148bd71804b2450a2508931a116b56',
|
||||
@@ -29,12 +29,14 @@ class IdagioTrackIE(InfoExtractor):
|
||||
'title': 'I. Adagio sostenuto',
|
||||
'duration': 316,
|
||||
'composers': ['Ludwig van Beethoven'],
|
||||
'artists': [],
|
||||
'genres': ['Keyboard', 'Sonata (Keyboard)'],
|
||||
'track': 'I. Adagio sostenuto',
|
||||
'timestamp': 1518076337,
|
||||
'upload_date': '20180208',
|
||||
},
|
||||
}, {
|
||||
'url': 'https://app.idagio.com/de/recordings/20514467?trackId=20514478&utm_source=pcl',
|
||||
'only_matching': True,
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
@@ -97,7 +99,7 @@ def _real_extract(self, url):
|
||||
|
||||
|
||||
class IdagioRecordingIE(IdagioPlaylistBaseIE):
|
||||
_VALID_URL = r'https?://(?:www\.)?app\.idagio\.com/recordings/(?P<id>\d+)(?![^#]*[&?]trackId=\d+)'
|
||||
_VALID_URL = r'https?://(?:www\.)?app\.idagio\.com(?:/[a-z]{2})?/recordings/(?P<id>\d+)(?![^#]*[&?]trackId=\d+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://app.idagio.com/recordings/30576934',
|
||||
'info_dict': {
|
||||
@@ -112,6 +114,19 @@ class IdagioRecordingIE(IdagioPlaylistBaseIE):
|
||||
'upload_date': '20190405',
|
||||
},
|
||||
'playlist_count': 15,
|
||||
}, {
|
||||
'url': 'https://app.idagio.com/de/recordings/20514467',
|
||||
'info_dict': {
|
||||
'id': '20514467',
|
||||
'title': 'Sonata for Piano No. 14 in C sharp minor op. 27/2',
|
||||
'composers': ['Ludwig van Beethoven'],
|
||||
'genres': ['Keyboard', 'Sonata (Keyboard)'],
|
||||
'timestamp': 1518076337,
|
||||
'upload_date': '20180208',
|
||||
'modified_timestamp': 1518076337,
|
||||
'modified_date': '20180208',
|
||||
},
|
||||
'playlist_count': 3,
|
||||
}]
|
||||
_API_URL_TMPL = 'https://api.idagio.com/v2.0/metadata/recordings/{}'
|
||||
|
||||
@@ -129,7 +144,7 @@ def _parse_playlist_metadata(self, playlist_info):
|
||||
|
||||
|
||||
class IdagioAlbumIE(IdagioPlaylistBaseIE):
|
||||
_VALID_URL = r'https?://(?:www\.)?app\.idagio\.com/albums/(?P<id>[\w-]+)'
|
||||
_VALID_URL = r'https?://(?:www\.)?app\.idagio\.com(?:/[a-z]{2})?/albums/(?P<id>[\w-]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://app.idagio.com/albums/elgar-enigma-variations-in-the-south-serenade-for-strings',
|
||||
'info_dict': {
|
||||
@@ -137,7 +152,7 @@ class IdagioAlbumIE(IdagioPlaylistBaseIE):
|
||||
'display_id': 'elgar-enigma-variations-in-the-south-serenade-for-strings',
|
||||
'title': 'Elgar: Enigma Variations, In the South, Serenade for Strings',
|
||||
'description': '',
|
||||
'thumbnail': 'https://idagio-images.global.ssl.fastly.net/albums/880040420521/main.jpg',
|
||||
'thumbnail': r're:https://.+/albums/880040420521/main\.jpg',
|
||||
'artists': ['Vasily Petrenko', 'Royal Liverpool Philharmonic Orchestra', 'Edward Elgar'],
|
||||
'timestamp': 1553817600,
|
||||
'upload_date': '20190329',
|
||||
@@ -146,19 +161,19 @@ class IdagioAlbumIE(IdagioPlaylistBaseIE):
|
||||
},
|
||||
'playlist_count': 19,
|
||||
}, {
|
||||
'url': 'https://app.idagio.com/albums/brahms-ein-deutsches-requiem-3B403DF6-62D7-4A42-807B-47173F3E0192',
|
||||
'url': 'https://app.idagio.com/de/albums/brahms-ein-deutsches-requiem-3B403DF6-62D7-4A42-807B-47173F3E0192',
|
||||
'info_dict': {
|
||||
'id': '2862ad4e-4a61-45ad-9ce4-7fcf0c2626fe',
|
||||
'display_id': 'brahms-ein-deutsches-requiem-3B403DF6-62D7-4A42-807B-47173F3E0192',
|
||||
'title': 'Brahms: Ein deutsches Requiem',
|
||||
'description': '',
|
||||
'thumbnail': 'https://idagio-images.global.ssl.fastly.net/albums/3149020954522/main.jpg',
|
||||
'tags': ['recent-release'],
|
||||
'description': 'GRAMOPHONE CLASSICAL MUSIC AWARDS 2025 Recording of the Year & Choral',
|
||||
'thumbnail': r're:https://.+/albums/3149020954522/main\.jpg',
|
||||
'artists': ['Sabine Devieilhe', 'Stéphane Degout', 'Raphaël Pichon', 'Pygmalion', 'Johannes Brahms'],
|
||||
'timestamp': 1760054400,
|
||||
'upload_date': '20251010',
|
||||
'modified_timestamp': 1760101611,
|
||||
'modified_date': '20251010',
|
||||
'modified_timestamp': 1760624868,
|
||||
'modified_date': '20251016',
|
||||
'tags': ['recommended', 'recent-release'],
|
||||
},
|
||||
'playlist_count': 7,
|
||||
}]
|
||||
@@ -179,7 +194,7 @@ def _parse_playlist_metadata(self, playlist_info):
|
||||
|
||||
|
||||
class IdagioPlaylistIE(IdagioPlaylistBaseIE):
|
||||
_VALID_URL = r'https?://(?:www\.)?app\.idagio\.com/playlists/(?!personal/)(?P<id>[\w-]+)'
|
||||
_VALID_URL = r'https?://(?:www\.)?app\.idagio\.com(?:/[a-z]{2})?/playlists/(?!personal/)(?P<id>[\w-]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://app.idagio.com/playlists/beethoven-the-most-beautiful-piano-music',
|
||||
'info_dict': {
|
||||
@@ -191,6 +206,17 @@ class IdagioPlaylistIE(IdagioPlaylistBaseIE):
|
||||
'creators': ['IDAGIO'],
|
||||
},
|
||||
'playlist_mincount': 16, # one entry is geo-restricted
|
||||
}, {
|
||||
'url': 'https://app.idagio.com/de/playlists/piano-music-for-an-autumn-day',
|
||||
'info_dict': {
|
||||
'id': 'd70e9c7f-7080-4308-ae0f-f890dddeda82',
|
||||
'display_id': 'piano-music-for-an-autumn-day',
|
||||
'title': 'Piano Music for an Autumn Day',
|
||||
'description': 'Get ready to snuggle up and enjoy all the musical colours of this cosy, autumnal playlist.',
|
||||
'thumbnail': r're:https://.+/playlists/d70e9c7f-7080-4308-ae0f-f890dddeda82/main\.jpg',
|
||||
'creators': ['IDAGIO'],
|
||||
},
|
||||
'playlist_count': 35,
|
||||
}]
|
||||
_API_URL_TMPL = 'https://api.idagio.com/v2.0/playlists/{}'
|
||||
_PLAYLIST_ID_KEY = 'display_id'
|
||||
@@ -206,7 +232,7 @@ def _parse_playlist_metadata(self, playlist_info):
|
||||
|
||||
|
||||
class IdagioPersonalPlaylistIE(IdagioPlaylistBaseIE):
|
||||
_VALID_URL = r'https?://(?:www\.)?app\.idagio\.com/playlists/personal/(?P<id>[\da-f-]+)'
|
||||
_VALID_URL = r'https?://(?:www\.)?app\.idagio\.com(?:/[a-z]{2})?/playlists/personal/(?P<id>[\da-f-]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://app.idagio.com/playlists/personal/99dad72e-7b3a-45a4-b216-867c08046ed8',
|
||||
'info_dict': {
|
||||
@@ -220,6 +246,9 @@ class IdagioPersonalPlaylistIE(IdagioPlaylistBaseIE):
|
||||
'modified_date': '20250819',
|
||||
},
|
||||
'playlist_count': 100,
|
||||
}, {
|
||||
'url': 'https://app.idagio.com/de/playlists/personal/99dad72e-7b3a-45a4-b216-867c08046ed8',
|
||||
'only_matching': True,
|
||||
}]
|
||||
_API_URL_TMPL = 'https://api.idagio.com/v1.0/personal-playlists/{}'
|
||||
|
||||
|
||||
@@ -437,7 +437,7 @@ def _real_extract(self, url):
|
||||
params = urllib.parse.parse_qs(query)
|
||||
if path:
|
||||
splitted_path = path.split('/')
|
||||
params.update(dict(zip(splitted_path[::2], [[v] for v in splitted_path[1::2]])))
|
||||
params.update(dict(zip(splitted_path[::2], [[v] for v in splitted_path[1::2]]))) # noqa: B905
|
||||
if 'wid' in params:
|
||||
partner_id = remove_start(params['wid'][0], '_')
|
||||
elif 'p' in params:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import itertools
|
||||
import re
|
||||
import urllib.parse
|
||||
|
||||
@@ -216,7 +217,7 @@ def _real_extract(self, url):
|
||||
def _fix_subtitles(self, subs):
|
||||
srt = ''
|
||||
seq_counter = 0
|
||||
for seq_current, seq_next in zip(subs, subs[1:]):
|
||||
for seq_current, seq_next in itertools.pairwise(subs):
|
||||
m_current = re.match(self._TIMECODE_REGEX, seq_current['Timecode'])
|
||||
if m_current is None:
|
||||
continue
|
||||
|
||||
@@ -92,7 +92,7 @@ def _real_extract(self, url):
|
||||
contains_pattern=r'\[(?s:.+)\]', transform_source=js_to_json)
|
||||
|
||||
formats = []
|
||||
for video_hash, (suffix, quality, format_note) in zip(video_hashes, [
|
||||
for video_hash, (suffix, quality, format_note) in zip(video_hashes, [ # noqa: B905
|
||||
('', 1, 'normálna kvalita'),
|
||||
('_lq', 0, 'nízka kvalita'),
|
||||
('_hd', 2, 'HD-720p'),
|
||||
|
||||
@@ -503,7 +503,7 @@ def _real_extract(self, url):
|
||||
'start_time': s,
|
||||
'end_time': e,
|
||||
'title': t,
|
||||
} for s, e, t in zip(start_time, end_time, chapter_titles)]
|
||||
} for s, e, t in zip(start_time, end_time, chapter_titles, strict=True)]
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
|
||||
@@ -181,7 +181,7 @@ class PBSIE(InfoExtractor):
|
||||
)
|
||||
|
||||
IE_NAME = 'pbs'
|
||||
IE_DESC = 'Public Broadcasting Service (PBS) and member stations: {}'.format(', '.join(list(zip(*_STATIONS))[1]))
|
||||
IE_DESC = 'Public Broadcasting Service (PBS) and member stations: {}'.format(', '.join(list(zip(*_STATIONS, strict=True))[1]))
|
||||
|
||||
_VALID_URL = r'''(?x)https?://
|
||||
(?:
|
||||
@@ -193,7 +193,7 @@ class PBSIE(InfoExtractor):
|
||||
(?:[^/?#]+/){{1,5}}(?P<presumptive_id>[^/?#]+?)(?:\.html)?/?(?:$|[?#])
|
||||
)
|
||||
)
|
||||
'''.format('|'.join(next(zip(*_STATIONS))))
|
||||
'''.format('|'.join(next(zip(*_STATIONS, strict=True))))
|
||||
|
||||
_GEO_COUNTRIES = ['US']
|
||||
|
||||
|
||||
@@ -405,7 +405,7 @@ def _entries(self, url, page, category_id):
|
||||
tab_content = self._download_json(
|
||||
'https://www.polskieradio.pl/CMS/TemplateBoxesManagement/TemplateBoxTabContent.aspx/GetTabContent',
|
||||
category_id, f'Downloading page {page_num}', headers={'content-type': 'application/json'},
|
||||
data=json.dumps(dict(zip((
|
||||
data=json.dumps(dict(zip(( # noqa: B905
|
||||
'boxInstanceId', 'tabId', 'categoryType', 'sectionId', 'categoryId', 'pagerMode',
|
||||
'subjectIds', 'tagIndexId', 'queryString', 'name', 'openArticlesInParentTemplate',
|
||||
'idSectionFromUrl', 'maxDocumentAge', 'showCategoryForArticle', 'pageNumber',
|
||||
|
||||
@@ -155,7 +155,7 @@ def _real_extract(self, url):
|
||||
# Sorted by "confidence", higher confidence = earlier in list
|
||||
confidences = traverse_obj(metadata, ('tags', ..., 'confidence', ({int}, {float})))
|
||||
if confidences:
|
||||
tags = [tag for _, tag in sorted(zip(confidences, tags), reverse=True)]
|
||||
tags = [tag for _, tag in sorted(zip(confidences, tags), reverse=True)] # noqa: B905
|
||||
|
||||
formats = traverse_obj(video_info, ('variants', ..., {
|
||||
'format_id': ('name', {str}),
|
||||
|
||||
@@ -81,7 +81,7 @@ def _get_next_app_info(self):
|
||||
}
|
||||
self._APP_INFO_POOL = [
|
||||
{**defaults, **dict(
|
||||
(k, v) for k, v in zip(self._APP_INFO_DEFAULTS, app_info.split('/')) if v
|
||||
(k, v) for k, v in zip(self._APP_INFO_DEFAULTS, app_info.split('/'), strict=False) if v
|
||||
)} for app_info in self._KNOWN_APP_INFO
|
||||
]
|
||||
|
||||
|
||||
@@ -1,46 +1,82 @@
|
||||
import re
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
clean_html,
|
||||
get_element_by_class,
|
||||
extract_attributes,
|
||||
js_to_json,
|
||||
mimetype2ext,
|
||||
unified_strdate,
|
||||
url_or_none,
|
||||
urljoin,
|
||||
)
|
||||
from ..utils.traversal import find_element, traverse_obj
|
||||
|
||||
|
||||
class TVNoeIE(InfoExtractor):
|
||||
_WORKING = False
|
||||
_VALID_URL = r'https?://(?:www\.)?tvnoe\.cz/video/(?P<id>[0-9]+)'
|
||||
_TEST = {
|
||||
'url': 'http://www.tvnoe.cz/video/10362',
|
||||
'md5': 'aee983f279aab96ec45ab6e2abb3c2ca',
|
||||
IE_NAME = 'tvnoe'
|
||||
IE_DESC = 'Televize Noe'
|
||||
|
||||
_VALID_URL = r'https?://(?:www\.)?tvnoe\.cz/porad/(?P<id>[\w-]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.tvnoe.cz/porad/43216-outdoor-films-s-mudr-tomasem-kempnym-pomahat-potrebnym-nejen-u-nas',
|
||||
'info_dict': {
|
||||
'id': '10362',
|
||||
'id': '43216-outdoor-films-s-mudr-tomasem-kempnym-pomahat-potrebnym-nejen-u-nas',
|
||||
'ext': 'mp4',
|
||||
'series': 'Noční univerzita',
|
||||
'title': 'prof. Tomáš Halík, Th.D. - Návrat náboženství a střet civilizací',
|
||||
'description': 'md5:f337bae384e1a531a52c55ebc50fff41',
|
||||
'title': 'Pomáhat potřebným nejen u nás',
|
||||
'description': 'md5:78b538ee32f7e881ec23b9c278a0ff3a',
|
||||
'release_date': '20250531',
|
||||
'series': 'Outdoor Films s MUDr. Tomášem Kempným',
|
||||
'thumbnail': r're:https?://www\.tvnoe\.cz/.+\.jpg',
|
||||
},
|
||||
}
|
||||
}, {
|
||||
'url': 'https://www.tvnoe.cz/porad/43205-zamysleni-tomase-halika-7-nedele-velikonocni',
|
||||
'info_dict': {
|
||||
'id': '43205-zamysleni-tomase-halika-7-nedele-velikonocni',
|
||||
'ext': 'mp4',
|
||||
'title': '7. neděle velikonoční',
|
||||
'description': 'md5:6bb9908efc59abe60e1c8c7c0e9bb6cd',
|
||||
'release_date': '20250531',
|
||||
'series': 'Zamyšlení Tomáše Halíka',
|
||||
'thumbnail': r're:https?://www\.tvnoe\.cz/.+\.jpg',
|
||||
},
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
video_id = self._match_id(url)
|
||||
webpage = self._download_webpage(url, video_id)
|
||||
player = self._search_json(
|
||||
r'var\s+INIT_PLAYER\s*=', webpage, 'init player',
|
||||
video_id, transform_source=js_to_json)
|
||||
|
||||
iframe_url = self._search_regex(
|
||||
r'<iframe[^>]+src="([^"]+)"', webpage, 'iframe URL')
|
||||
formats = []
|
||||
for source in traverse_obj(player, ('tracks', ..., lambda _, v: url_or_none(v['src']))):
|
||||
src_url = source['src']
|
||||
ext = mimetype2ext(source.get('type'))
|
||||
if ext == 'm3u8':
|
||||
fmts = self._extract_m3u8_formats(
|
||||
src_url, video_id, 'mp4', m3u8_id='hls', fatal=False)
|
||||
elif ext == 'mpd':
|
||||
fmts = self._extract_mpd_formats(
|
||||
src_url, video_id, mpd_id='dash', fatal=False)
|
||||
else:
|
||||
self.report_warning(f'Unsupported stream type: {ext}')
|
||||
continue
|
||||
formats.extend(fmts)
|
||||
|
||||
ifs_page = self._download_webpage(iframe_url, video_id)
|
||||
jwplayer_data = self._find_jwplayer_data(
|
||||
ifs_page, video_id, transform_source=js_to_json)
|
||||
info_dict = self._parse_jwplayer_data(
|
||||
jwplayer_data, video_id, require_title=False, base_url=iframe_url)
|
||||
|
||||
info_dict.update({
|
||||
return {
|
||||
'id': video_id,
|
||||
'title': clean_html(get_element_by_class(
|
||||
'field-name-field-podnazev', webpage)),
|
||||
'description': clean_html(get_element_by_class(
|
||||
'field-name-body', webpage)),
|
||||
'series': clean_html(get_element_by_class('title', webpage)),
|
||||
})
|
||||
|
||||
return info_dict
|
||||
'description': clean_html(self._search_regex(
|
||||
r'<p\s+class="">(.+?)</p>', webpage, 'description', default=None)),
|
||||
'formats': formats,
|
||||
**traverse_obj(webpage, {
|
||||
'title': ({find_element(tag='h2')}, {clean_html}),
|
||||
'release_date': (
|
||||
{clean_html}, {re.compile(r'Premiéra:\s*(\d{1,2}\.\d{1,2}\.\d{4})').findall},
|
||||
..., {str}, {unified_strdate}, any),
|
||||
'series': ({find_element(tag='h1')}, {clean_html}),
|
||||
'thumbnail': (
|
||||
{find_element(id='player-live', html=True)}, {extract_attributes},
|
||||
'poster', {urljoin('https://www.tvnoe.cz/')}),
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -220,6 +220,20 @@ class SubsPoTokenPolicy(BasePoTokenPolicy):
|
||||
},
|
||||
'PLAYER_PO_TOKEN_POLICY': PlayerPoTokenPolicy(required=False, recommended=True),
|
||||
},
|
||||
# Doesn't require a PoToken for some reason
|
||||
'android_sdkless': {
|
||||
'INNERTUBE_CONTEXT': {
|
||||
'client': {
|
||||
'clientName': 'ANDROID',
|
||||
'clientVersion': '20.10.38',
|
||||
'userAgent': 'com.google.android.youtube/20.10.38 (Linux; U; Android 11) gzip',
|
||||
'osName': 'Android',
|
||||
'osVersion': '11',
|
||||
},
|
||||
},
|
||||
'INNERTUBE_CONTEXT_CLIENT_NAME': 3,
|
||||
'REQUIRE_JS_PLAYER': False,
|
||||
},
|
||||
# YouTube Kids videos aren't returned on this client for some reason
|
||||
'android_vr': {
|
||||
'INNERTUBE_CONTEXT': {
|
||||
|
||||
@@ -257,10 +257,10 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'401': {'ext': 'mp4', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'av01.0.12M.08'},
|
||||
}
|
||||
_SUBTITLE_FORMATS = ('json3', 'srv1', 'srv2', 'srv3', 'ttml', 'srt', 'vtt')
|
||||
_DEFAULT_CLIENTS = ('tv', 'web_safari', 'web')
|
||||
_DEFAULT_CLIENTS = ('android_sdkless', 'tv', 'web_safari', 'web')
|
||||
_DEFAULT_AUTHED_CLIENTS = ('tv', 'web_safari', 'web')
|
||||
# Premium does not require POT (except for subtitles)
|
||||
_DEFAULT_PREMIUM_CLIENTS = ('tv', 'web_creator', 'web')
|
||||
_DEFAULT_PREMIUM_CLIENTS = ('tv', 'web_creator', 'web_safari', 'web')
|
||||
|
||||
_GEO_BYPASS = False
|
||||
|
||||
@@ -1815,7 +1815,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'params': {'skip_download': True},
|
||||
}]
|
||||
|
||||
_DEFAULT_PLAYER_JS_VERSION = '20348@0004de42' # TODO: revert to 'actual' when n/sig is fixed
|
||||
_DEFAULT_PLAYER_JS_VERSION = 'actual'
|
||||
_DEFAULT_PLAYER_JS_VARIANT = 'main'
|
||||
_PLAYER_JS_VARIANT_MAP = {
|
||||
'main': 'player_ias.vflset/en_US/base.js',
|
||||
|
||||
@@ -186,7 +186,7 @@ def js_number_to_string(val: float, radix: int = 10):
|
||||
_COMP_OPERATORS = {'===', '!==', '==', '!=', '<=', '>=', '<', '>'}
|
||||
|
||||
_NAME_RE = r'[a-zA-Z_$][\w$]*'
|
||||
_MATCHING_PARENS = dict(zip(*zip('()', '{}', '[]')))
|
||||
_MATCHING_PARENS = dict(zip(*zip('()', '{}', '[]', strict=True), strict=True))
|
||||
_QUOTES = '\'"/'
|
||||
_NESTED_BRACKETS = r'[^[\]]+(?:\[[^[\]]+(?:\[[^\]]+\])?\])?'
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import http.client
|
||||
import logging
|
||||
import re
|
||||
import socket
|
||||
import warnings
|
||||
|
||||
from ..dependencies import brotli, requests, urllib3
|
||||
@@ -125,7 +124,7 @@ def read(self, amt: int | None = None):
|
||||
# Work around issue with `.read(amt)` then `.read()`
|
||||
# See: https://github.com/urllib3/urllib3/issues/3636
|
||||
if amt is None:
|
||||
# Python 3.9 preallocates the whole read buffer, read in chunks
|
||||
# compat: py3.9: Python 3.9 preallocates the whole read buffer, read in chunks
|
||||
read_chunk = functools.partial(self.fp.read, 1 << 20, decode_content=True)
|
||||
return b''.join(iter(read_chunk, b''))
|
||||
# Interact with urllib3 response directly.
|
||||
@@ -378,7 +377,7 @@ def _new_conn(self):
|
||||
source_address=self.source_address,
|
||||
_create_socket_func=functools.partial(
|
||||
create_socks_proxy_socket, (self.host, self.port), self._proxy_args))
|
||||
except (socket.timeout, TimeoutError) as e:
|
||||
except TimeoutError as e:
|
||||
raise urllib3.exceptions.ConnectTimeoutError(
|
||||
self, f'Connection to {self.host} timed out. (connect timeout={self.timeout})') from e
|
||||
except SocksProxyError as e:
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
from collections.abc import Iterable, Mapping
|
||||
from email.message import Message
|
||||
from http import HTTPStatus
|
||||
from types import NoneType
|
||||
|
||||
from ._helper import make_ssl_context, wrap_request_errors
|
||||
from .exceptions import (
|
||||
@@ -20,7 +21,6 @@
|
||||
TransportError,
|
||||
UnsupportedRequest,
|
||||
)
|
||||
from ..compat.types import NoneType
|
||||
from ..cookies import YoutubeDLCookieJar
|
||||
from ..utils import (
|
||||
bug_reports_message,
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
import re
|
||||
from abc import ABC
|
||||
from dataclasses import dataclass
|
||||
from types import NoneType
|
||||
from typing import Any
|
||||
|
||||
from .common import RequestHandler, register_preference, Request
|
||||
from .exceptions import UnsupportedRequest
|
||||
from ..compat.types import NoneType
|
||||
from ..utils import classproperty, join_nonempty
|
||||
from ..utils.networking import std_headers, HTTPHeaderDict
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
import pkgutil
|
||||
import sys
|
||||
import traceback
|
||||
import zipimport
|
||||
from pathlib import Path
|
||||
from zipfile import ZipFile
|
||||
|
||||
@@ -202,16 +201,10 @@ def load_plugins(plugin_spec: PluginSpec):
|
||||
if any(x.startswith('_') for x in module_name.split('.')):
|
||||
continue
|
||||
try:
|
||||
if sys.version_info < (3, 10) and isinstance(finder, zipimport.zipimporter):
|
||||
# zipimporter.load_module() is deprecated in 3.10 and removed in 3.12
|
||||
# The exec_module branch below is the replacement for >= 3.10
|
||||
# See: https://docs.python.org/3/library/zipimport.html#zipimport.zipimporter.exec_module
|
||||
module = finder.load_module(module_name)
|
||||
else:
|
||||
spec = finder.find_spec(module_name)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[module_name] = module
|
||||
spec.loader.exec_module(module)
|
||||
spec = finder.find_spec(module_name)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[module_name] = module
|
||||
spec.loader.exec_module(module)
|
||||
except Exception:
|
||||
write_string(
|
||||
f'Error while importing module {module_name!r}\n{traceback.format_exc(limit=-1)}',
|
||||
|
||||
@@ -418,7 +418,7 @@ def _concat_spec(cls, in_files, concat_opts=None):
|
||||
if concat_opts is None:
|
||||
concat_opts = [{}] * len(in_files)
|
||||
yield 'ffconcat version 1.0\n'
|
||||
for file, opts in zip(in_files, concat_opts):
|
||||
for file, opts in zip(in_files, concat_opts, strict=True):
|
||||
yield f'file {cls._quote_for_ffmpeg(cls._ffmpeg_filename_argument(file))}\n'
|
||||
# Iterate explicitly to yield the following directives in order, ignoring the rest.
|
||||
for directive in 'inpoint', 'outpoint', 'duration':
|
||||
@@ -639,7 +639,7 @@ def run(self, info):
|
||||
# postprocessor a second time
|
||||
'-map', '-0:s',
|
||||
]
|
||||
for i, (lang, name) in enumerate(zip(sub_langs, sub_names)):
|
||||
for i, (lang, name) in enumerate(zip(sub_langs, sub_names, strict=True)):
|
||||
opts.extend(['-map', f'{i + 1}:0'])
|
||||
lang_code = ISO639Utils.short2long(lang) or lang
|
||||
opts.extend([f'-metadata:s:s:{i}', f'language={lang_code}'])
|
||||
|
||||
@@ -154,7 +154,7 @@ def _get_binary_name():
|
||||
|
||||
|
||||
def _get_system_deprecation():
|
||||
MIN_SUPPORTED, MIN_RECOMMENDED = (3, 9), (3, 10)
|
||||
MIN_SUPPORTED, MIN_RECOMMENDED = (3, 10), (3, 10)
|
||||
|
||||
if sys.version_info > MIN_RECOMMENDED:
|
||||
return None
|
||||
@@ -559,11 +559,9 @@ def filename(self):
|
||||
@functools.cached_property
|
||||
def cmd(self):
|
||||
"""The command-line to run the executable, if known"""
|
||||
argv = None
|
||||
# There is no sys.orig_argv in py < 3.10. Also, it can be [] when frozen
|
||||
if getattr(sys, 'orig_argv', None):
|
||||
argv = sys.orig_argv
|
||||
elif getattr(sys, 'frozen', False):
|
||||
argv = sys.orig_argv
|
||||
# sys.orig_argv can be [] when frozen
|
||||
if not argv and getattr(sys, 'frozen', False):
|
||||
argv = sys.argv
|
||||
# linux_static exe's argv[0] will be /tmp/staticx-NNNN/yt-dlp_linux if we don't fixup here
|
||||
if argv and os.getenv('STATICX_PROG_PATH'):
|
||||
@@ -572,7 +570,7 @@ def cmd(self):
|
||||
|
||||
def restart(self):
|
||||
"""Restart the executable"""
|
||||
assert self.cmd, 'Must be frozen or Py >= 3.10'
|
||||
assert self.cmd, 'Unable to determine argv'
|
||||
self.ydl.write_debug(f'Restarting: {shell_quote(self.cmd)}')
|
||||
_, _, returncode = Popen.run(self.cmd)
|
||||
return returncode
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
"""No longer used and new code should not use. Exists only for API compat."""
|
||||
import asyncio
|
||||
import atexit
|
||||
import platform
|
||||
import struct
|
||||
import sys
|
||||
@@ -34,77 +32,6 @@
|
||||
has_websockets = bool(websockets)
|
||||
|
||||
|
||||
class WebSocketsWrapper:
|
||||
"""Wraps websockets module to use in non-async scopes"""
|
||||
pool = None
|
||||
|
||||
def __init__(self, url, headers=None, connect=True, **ws_kwargs):
|
||||
self.loop = asyncio.new_event_loop()
|
||||
# XXX: "loop" is deprecated
|
||||
self.conn = websockets.connect(
|
||||
url, extra_headers=headers, ping_interval=None,
|
||||
close_timeout=float('inf'), loop=self.loop, ping_timeout=float('inf'), **ws_kwargs)
|
||||
if connect:
|
||||
self.__enter__()
|
||||
atexit.register(self.__exit__, None, None, None)
|
||||
|
||||
def __enter__(self):
|
||||
if not self.pool:
|
||||
self.pool = self.run_with_loop(self.conn.__aenter__(), self.loop)
|
||||
return self
|
||||
|
||||
def send(self, *args):
|
||||
self.run_with_loop(self.pool.send(*args), self.loop)
|
||||
|
||||
def recv(self, *args):
|
||||
return self.run_with_loop(self.pool.recv(*args), self.loop)
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
try:
|
||||
return self.run_with_loop(self.conn.__aexit__(type, value, traceback), self.loop)
|
||||
finally:
|
||||
self.loop.close()
|
||||
self._cancel_all_tasks(self.loop)
|
||||
|
||||
# taken from https://github.com/python/cpython/blob/3.9/Lib/asyncio/runners.py with modifications
|
||||
# for contributors: If there's any new library using asyncio needs to be run in non-async, move these function out of this class
|
||||
@staticmethod
|
||||
def run_with_loop(main, loop):
|
||||
if not asyncio.iscoroutine(main):
|
||||
raise ValueError(f'a coroutine was expected, got {main!r}')
|
||||
|
||||
try:
|
||||
return loop.run_until_complete(main)
|
||||
finally:
|
||||
loop.run_until_complete(loop.shutdown_asyncgens())
|
||||
if hasattr(loop, 'shutdown_default_executor'):
|
||||
loop.run_until_complete(loop.shutdown_default_executor())
|
||||
|
||||
@staticmethod
|
||||
def _cancel_all_tasks(loop):
|
||||
to_cancel = asyncio.all_tasks(loop)
|
||||
|
||||
if not to_cancel:
|
||||
return
|
||||
|
||||
for task in to_cancel:
|
||||
task.cancel()
|
||||
|
||||
# XXX: "loop" is removed in Python 3.10+
|
||||
loop.run_until_complete(
|
||||
asyncio.gather(*to_cancel, loop=loop, return_exceptions=True))
|
||||
|
||||
for task in to_cancel:
|
||||
if task.cancelled():
|
||||
continue
|
||||
if task.exception() is not None:
|
||||
loop.call_exception_handler({
|
||||
'message': 'unhandled exception during asyncio.run() shutdown',
|
||||
'exception': task.exception(),
|
||||
'task': task,
|
||||
})
|
||||
|
||||
|
||||
def load_plugins(name, suffix, namespace):
|
||||
from ..plugins import load_plugins
|
||||
ret = load_plugins(name, suffix)
|
||||
|
||||
@@ -95,7 +95,7 @@ def IDENTITY(x):
|
||||
# needed for sanitizing filenames in restricted mode
|
||||
ACCENT_CHARS = dict(zip('ÂÃÄÀÁÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖŐØŒÙÚÛÜŰÝÞßàáâãäåæçèéêëìíîïðñòóôõöőøœùúûüűýþÿ',
|
||||
itertools.chain('AAAAAA', ['AE'], 'CEEEEIIIIDNOOOOOOO', ['OE'], 'UUUUUY', ['TH', 'ss'],
|
||||
'aaaaaa', ['ae'], 'ceeeeiiiionooooooo', ['oe'], 'uuuuuy', ['th'], 'y')))
|
||||
'aaaaaa', ['ae'], 'ceeeeiiiionooooooo', ['oe'], 'uuuuuy', ['th'], 'y'), strict=True))
|
||||
|
||||
DATE_FORMATS = (
|
||||
'%d %B %Y',
|
||||
@@ -2415,7 +2415,7 @@ def __init__(self, ydl, info_dict):
|
||||
if self.is_incomplete:
|
||||
assert self.is_exhausted
|
||||
self._entries = [self.MissingEntry] * max(requested_entries or [0])
|
||||
for i, entry in zip(requested_entries, entries):
|
||||
for i, entry in zip(requested_entries, entries): # noqa: B905
|
||||
self._entries[i - 1] = entry
|
||||
elif isinstance(entries, (list, PagedList, LazyList)):
|
||||
self._entries = entries
|
||||
@@ -3184,7 +3184,7 @@ def width(string):
|
||||
return len(remove_terminal_sequences(string).replace('\t', ''))
|
||||
|
||||
def get_max_lens(table):
|
||||
return [max(width(str(v)) for v in col) for col in zip(*table)]
|
||||
return [max(width(str(v)) for v in col) for col in zip(*table, strict=True)]
|
||||
|
||||
def filter_using_list(row, filter_array):
|
||||
return [col for take, col in itertools.zip_longest(filter_array, row, fillvalue=True) if take]
|
||||
@@ -3540,7 +3540,7 @@ def parse_node(node):
|
||||
continue
|
||||
default_style.update(style)
|
||||
|
||||
for para, index in zip(paras, itertools.count(1)):
|
||||
for para, index in zip(paras, itertools.count(1), strict=False):
|
||||
begin_time = parse_dfxp_time_expr(para.attrib.get('begin'))
|
||||
end_time = parse_dfxp_time_expr(para.attrib.get('end'))
|
||||
dur = parse_dfxp_time_expr(para.attrib.get('dur'))
|
||||
@@ -4854,7 +4854,7 @@ def scale_thumbnails_to_max_format_width(formats, thumbnails, url_width_re):
|
||||
return [
|
||||
merge_dicts(
|
||||
{'url': re.sub(url_width_re, str(max_dimensions[0]), thumbnail['url'])},
|
||||
dict(zip(_keys, max_dimensions)), thumbnail)
|
||||
dict(zip(_keys, max_dimensions, strict=True)), thumbnail)
|
||||
for thumbnail in thumbnails
|
||||
]
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ def parse_iter(parsed: typing.Any, /, *, revivers: dict[str, collections.abc.Cal
|
||||
|
||||
elif value[0] == 'Map':
|
||||
result = []
|
||||
for key, new_source in zip(*(iter(value[1:]),) * 2):
|
||||
for key, new_source in zip(*(iter(value[1:]),) * 2, strict=True):
|
||||
pair = [None, None]
|
||||
stack.append((pair, 0, key))
|
||||
stack.append((pair, 1, new_source))
|
||||
@@ -129,7 +129,7 @@ def parse_iter(parsed: typing.Any, /, *, revivers: dict[str, collections.abc.Cal
|
||||
|
||||
elif value[0] == 'null':
|
||||
result = {}
|
||||
for key, new_source in zip(*(iter(value[1:]),) * 2):
|
||||
for key, new_source in zip(*(iter(value[1:]),) * 2, strict=True):
|
||||
stack.append((result, key, new_source))
|
||||
|
||||
elif value[0] in _ARRAY_TYPE_LOOKUP:
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Autogenerated by devscripts/update-version.py
|
||||
|
||||
__version__ = '2025.10.14'
|
||||
__version__ = '2025.10.22'
|
||||
|
||||
RELEASE_GIT_HEAD = 'a98e7f9f58a9492d2cb216baa59c890ed8ce02f3'
|
||||
RELEASE_GIT_HEAD = 'c9356f308dd3c5f9f494cb40ed14c5df017b4fe0'
|
||||
|
||||
VARIANT = None
|
||||
|
||||
@@ -12,4 +12,4 @@
|
||||
|
||||
ORIGIN = 'yt-dlp/yt-dlp'
|
||||
|
||||
_pkg_version = '2025.10.14'
|
||||
_pkg_version = '2025.10.22'
|
||||
|
||||
@@ -103,7 +103,7 @@ def _parse_ts(ts):
|
||||
into an MPEG PES timestamp: a tick counter at 90 kHz resolution.
|
||||
"""
|
||||
return 90 * sum(
|
||||
int(part or 0) * mult for part, mult in zip(ts.groups(), (3600_000, 60_000, 1000, 1)))
|
||||
int(part or 0) * mult for part, mult in zip(ts.groups(), (3600_000, 60_000, 1000, 1), strict=True))
|
||||
|
||||
|
||||
def _format_ts(ts):
|
||||
|
||||
Reference in New Issue
Block a user