Update On Wed Oct 15 20:47:54 CEST 2025

This commit is contained in:
github-action[bot]
2025-10-15 20:47:54 +02:00
parent 21107cea32
commit 7ff1f8080e
125 changed files with 1613 additions and 904 deletions

View File

@@ -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 }}
@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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,7 +233,7 @@ 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

View File

@@ -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:

View File

@@ -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

View File

@@ -284,7 +284,7 @@ After you have ensured this site is distributing its content legally, you can fo
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

View File

@@ -811,3 +811,10 @@ zakaryan2004
cdce8p
nicolaasjan
willsmillie
CasualYT31
cecilia-sanare
dhwz
robin-mu
shssoichiro
thanhtaivtt
uoag

View File

@@ -4,6 +4,32 @@
# To create a release, dispatch the https://github.com/yt-dlp/yt-dlp/actions/workflows/release.yml workflow on master
-->
### 2025.10.14
#### Core changes
- [Fix `prefer-vp9-sort` compat option](https://github.com/yt-dlp/yt-dlp/commit/a6673a8e82276ea529c1773ed09e5bc4a22e822a) ([#14603](https://github.com/yt-dlp/yt-dlp/issues/14603)) by [seproDev](https://github.com/seproDev)
#### Extractor changes
- **10play**
- [Handle geo-restriction errors](https://github.com/yt-dlp/yt-dlp/commit/ad55bfcfb700fbfc1364c04e3425761d6f95c0a7) ([#14618](https://github.com/yt-dlp/yt-dlp/issues/14618)) by [bashonly](https://github.com/bashonly)
- [Rework extractor](https://github.com/yt-dlp/yt-dlp/commit/eafedc21817bb0de20e9aaccd7151a1d4c4e1ebd) ([#14417](https://github.com/yt-dlp/yt-dlp/issues/14417)) by [seproDev](https://github.com/seproDev), [Sipherdrakon](https://github.com/Sipherdrakon)
- **abc.net.au**: [Support listen URLs](https://github.com/yt-dlp/yt-dlp/commit/0ea5d5882def84415f946907cfc00ab431c18fed) ([#14389](https://github.com/yt-dlp/yt-dlp/issues/14389)) by [uoag](https://github.com/uoag)
- **cbc.ca**: listen: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/df160ab18db523f6629f2e7e20123d7a3551df28) ([#14391](https://github.com/yt-dlp/yt-dlp/issues/14391)) by [uoag](https://github.com/uoag)
- **dropout**: [Update extractor for new domain](https://github.com/yt-dlp/yt-dlp/commit/8eb8695139dece6351aac10463df63b87b45b000) ([#14531](https://github.com/yt-dlp/yt-dlp/issues/14531)) by [cecilia-sanare](https://github.com/cecilia-sanare)
- **idagio**: [Add extractors](https://github.com/yt-dlp/yt-dlp/commit/a98e7f9f58a9492d2cb216baa59c890ed8ce02f3) ([#14586](https://github.com/yt-dlp/yt-dlp/issues/14586)) by [robin-mu](https://github.com/robin-mu)
- **musescore**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/87be1bb96ac47abaaa4cfc6d7dd651e511b74551) ([#14598](https://github.com/yt-dlp/yt-dlp/issues/14598)) by [seproDev](https://github.com/seproDev)
- **prankcastpost**: [Rework extractor](https://github.com/yt-dlp/yt-dlp/commit/5d7678195a7d0c045a9fe0418383171a71a7ea43) ([#14445](https://github.com/yt-dlp/yt-dlp/issues/14445)) by [columndeeply](https://github.com/columndeeply)
- **slideslive**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/c2e124881f9aa02097589e853b3d3505e78372c4) ([#14619](https://github.com/yt-dlp/yt-dlp/issues/14619)) by [bashonly](https://github.com/bashonly)
- **soundcloud**: [Support new API URLs](https://github.com/yt-dlp/yt-dlp/commit/6d41aaf21c61a87e74564646abd0a8ee887e888d) ([#14449](https://github.com/yt-dlp/yt-dlp/issues/14449)) by [seproDev](https://github.com/seproDev)
- **tiktok**
- [Support browser impersonation](https://github.com/yt-dlp/yt-dlp/commit/5513036104ed9710f624c537fb3644b07a0680db) ([#14473](https://github.com/yt-dlp/yt-dlp/issues/14473)) by [bashonly](https://github.com/bashonly), [thanhtaivtt](https://github.com/thanhtaivtt)
- user: [Fix private account extraction](https://github.com/yt-dlp/yt-dlp/commit/cdc533b114c35ceb8a2e9dd3eb9c172a8737ae5e) ([#14585](https://github.com/yt-dlp/yt-dlp/issues/14585)) by [CasualYT31](https://github.com/CasualYT31)
- **vidyard**: [Extract chapters](https://github.com/yt-dlp/yt-dlp/commit/5f94f054907c12e68129cd9ac2508ed8aba1b223) ([#14478](https://github.com/yt-dlp/yt-dlp/issues/14478)) by [exterrestris](https://github.com/exterrestris)
- **xhamster**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/739125d40f8ede3beb7be68fc4df55bec0d226fd) ([#14446](https://github.com/yt-dlp/yt-dlp/issues/14446)) by [dhwz](https://github.com/dhwz), [dirkf](https://github.com/dirkf), [shssoichiro](https://github.com/shssoichiro)
- **youtube**
- [Detect experiment binding GVS PO Token to video id](https://github.com/yt-dlp/yt-dlp/commit/bd5ed90419eea18adfb2f0d8efa9d22b2029119f) ([#14471](https://github.com/yt-dlp/yt-dlp/issues/14471)) by [coletdjnz](https://github.com/coletdjnz)
- tab: [Fix approximate timestamp extraction for feeds](https://github.com/yt-dlp/yt-dlp/commit/ccc25d6710a4aa373b7e15c558e07f8a2ffae5f3) ([#14539](https://github.com/yt-dlp/yt-dlp/issues/14539)) by [coletdjnz](https://github.com/coletdjnz)
### 2025.09.26
#### Extractor changes

View File

@@ -194,7 +194,7 @@ When running a yt-dlp version that is older than 90 days, you will see a warning
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 @@ On some systems, you may need to use `py` or `python` instead of `python3`.
**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`.
@@ -2255,7 +2255,7 @@ Features marked with a **\*** have been back-ported to youtube-dl
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

View File

@@ -298,5 +298,10 @@
"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)"
}
]

View File

@@ -373,7 +373,7 @@ class CommitRange:
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))

View File

@@ -13,7 +13,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 +30,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 +75,7 @@ dev = [
]
static-analysis = [
"autopep8~=2.0",
"ruff~=0.13.0",
"ruff~=0.14.0",
]
test = [
"pytest~=8.1",
@@ -168,7 +167,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",

View File

@@ -242,6 +242,7 @@ The only reliable way to check if a site is supported is to try it.
- **Canalsurmas**
- **CaracolTvPlay**: [*caracoltv-play*](## "netrc machine")
- **cbc.ca**
- **cbc.ca:listen**
- **cbc.ca:player**
- **cbc.ca:player:playlist**
- **CBS**: (**Currently broken**)
@@ -579,6 +580,11 @@ The only reliable way to check if a site is supported is to try it.
- **Hypem**
- **Hytale**
- **Icareus**
- **IdagioAlbum**
- **IdagioPersonalPlaylist**
- **IdagioPlaylist**
- **IdagioRecording**
- **IdagioTrack**
- **IdolPlus**
- **iflix:episode**
- **IflixSeries**

View File

@@ -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

View File

@@ -13,6 +13,7 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import contextlib
import copy
import itertools
import json
from test.helper import FakeYDL, assertRegexpMatches, try_rm
@@ -414,7 +415,7 @@ class TestFormatSelection(unittest.TestCase):
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 @@ class TestYoutubeDL(unittest.TestCase):
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 @@ class TestYoutubeDL(unittest.TestCase):
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)

View File

@@ -115,7 +115,7 @@ class TestModifyChaptersPP(unittest.TestCase):
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

View File

@@ -417,7 +417,7 @@ class TestTraversal:
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'

View File

@@ -1863,7 +1863,7 @@ Line 1
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)), [])

View File

@@ -2007,7 +2007,7 @@ class YoutubeDL:
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 @@ class YoutubeDL:
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 @@ class YoutubeDL:
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 @@ class YoutubeDL:
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 @@ class YoutubeDL:
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

View File

@@ -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

View File

@@ -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):

View File

@@ -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)

View File

@@ -22,15 +22,11 @@ if os.name == 'nt':
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

View File

@@ -824,6 +824,13 @@ from .ichinanalive import (
IchinanaLiveIE,
IchinanaLiveVODIE,
)
from .idagio import (
IdagioAlbumIE,
IdagioPersonalPlaylistIE,
IdagioPlaylistIE,
IdagioRecordingIE,
IdagioTrackIE,
)
from .idolplus import IdolPlusIE
from .ign import (
IGNIE,

View File

@@ -21,7 +21,7 @@ from ..utils import (
class ABCIE(InfoExtractor):
IE_NAME = 'abc.net.au'
_VALID_URL = r'https?://(?:www\.)?abc\.net\.au/(?:news|btn)/(?:[^/]+/){1,4}(?P<id>\d{5,})'
_VALID_URL = r'https?://(?:www\.)?abc\.net\.au/(?:news|btn|listen)/(?:[^/?#]+/){1,4}(?P<id>\d{5,})'
_TESTS = [{
'url': 'http://www.abc.net.au/news/2014-11-05/australia-to-staff-ebola-treatment-centre-in-sierra-leone/5868334',
@@ -53,8 +53,9 @@ class ABCIE(InfoExtractor):
'info_dict': {
'id': '6880080',
'ext': 'mp3',
'title': 'NAB lifts interest rates, following Westpac and CBA',
'title': 'NAB lifts interest rates, following Westpac and CBA - ABC listen',
'description': 'md5:f13d8edc81e462fce4a0437c7dc04728',
'thumbnail': r're:https://live-production\.wcms\.abc-cdn\.net\.au/2193d7437c84b25eafd6360c82b5fa21',
},
}, {
'url': 'http://www.abc.net.au/news/2015-10-19/6866214',
@@ -64,8 +65,9 @@ class ABCIE(InfoExtractor):
'info_dict': {
'id': '10527914',
'ext': 'mp4',
'title': 'WWI Centenary',
'description': 'md5:c2379ec0ca84072e86b446e536954546',
'title': 'WWI Centenary - Behind The News',
'description': 'md5:fa4405939ff750fade46ff0cd4c66a52',
'thumbnail': r're:https://live-production\.wcms\.abc-cdn\.net\.au/bcc3433c97bf992dff32ec5a768713c9',
},
}, {
'url': 'https://www.abc.net.au/news/programs/the-world/2020-06-10/black-lives-matter-protests-spawn-support-for/12342074',
@@ -73,7 +75,8 @@ class ABCIE(InfoExtractor):
'id': '12342074',
'ext': 'mp4',
'title': 'Black Lives Matter protests spawn support for Papuans in Indonesia',
'description': 'md5:2961a17dc53abc558589ccd0fb8edd6f',
'description': 'md5:625257209f2d14ce23cb4e3785da9beb',
'thumbnail': r're:https://live-production\.wcms\.abc-cdn\.net\.au/7ee6f190de6d7dbb04203e514bfae9ec',
},
}, {
'url': 'https://www.abc.net.au/btn/newsbreak/btn-newsbreak-20200814/12560476',
@@ -93,7 +96,16 @@ class ABCIE(InfoExtractor):
'title': 'Wagner Group retreating from Russia, leader Prigozhin to move to Belarus',
'ext': 'mp4',
'description': 'Wagner troops leave Rostov-on-Don and\xa0Yevgeny Prigozhin will move to Belarus under a deal brokered by Belarusian President Alexander Lukashenko to end the mutiny.',
'thumbnail': 'https://live-production.wcms.abc-cdn.net.au/0c170f5b57f0105c432f366c0e8e267b?impolicy=wcms_crop_resize&cropH=2813&cropW=5000&xPos=0&yPos=249&width=862&height=485',
'thumbnail': r're:https://live-production\.wcm\.abc-cdn\.net\.au/0c170f5b57f0105c432f366c0e8e267b',
},
}, {
'url': 'https://www.abc.net.au/listen/programs/the-followers-madness-of-two/presents-followers-madness-of-two/105697646',
'info_dict': {
'id': '105697646',
'title': 'INTRODUCING — The Followers: Madness of Two - ABC listen',
'ext': 'mp3',
'description': 'md5:2310cd0d440a4e01656abea15db8d1f3',
'thumbnail': r're:https://live-production\.wcms\.abc-cdn\.net\.au/90d7078214e5d66553ffb7fcf0da0cda',
},
}]

View File

@@ -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}),
}),
}

View File

@@ -740,7 +740,7 @@ class YoutubeWebArchiveIE(InfoExtractor):
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())

View File

@@ -1663,7 +1663,7 @@ class InfoExtractor:
'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 @@ class InfoExtractor:
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 {}

View File

@@ -1,5 +1,4 @@
import json
import socket
from .common import InfoExtractor
from ..utils import (
@@ -56,7 +55,7 @@ class DTubeIE(InfoExtractor):
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

View File

@@ -56,7 +56,7 @@ class FujiTVFODPlus7IE(InfoExtractor):
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)

View File

@@ -0,0 +1,233 @@
from .common import InfoExtractor
from ..utils import int_or_none, unified_timestamp, url_or_none
from ..utils.traversal import traverse_obj
class IdagioTrackIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?app\.idagio\.com/recordings/\d+\?(?:[^#]+&)?trackId=(?P<id>\d+)'
_TESTS = [{
'url': 'https://app.idagio.com/recordings/30576934?trackId=30576943',
'md5': '15148bd71804b2450a2508931a116b56',
'info_dict': {
'id': '30576943',
'ext': 'mp3',
'title': 'Theme. Andante',
'duration': 82,
'composers': ['Edward Elgar'],
'artists': ['Vasily Petrenko', 'Royal Liverpool Philharmonic Orchestra'],
'genres': ['Orchestral', 'Other Orchestral Music'],
'track': 'Theme. Andante',
'timestamp': 1554474370,
'upload_date': '20190405',
},
}, {
'url': 'https://app.idagio.com/recordings/20514467?trackId=20514478&utm_source=pcl',
'md5': '3acef2ea0feadf889123b70e5a1e7fa7',
'info_dict': {
'id': '20514478',
'ext': 'mp3',
'title': 'I. Adagio sostenuto',
'duration': 316,
'composers': ['Ludwig van Beethoven'],
'artists': [],
'genres': ['Keyboard', 'Sonata (Keyboard)'],
'track': 'I. Adagio sostenuto',
'timestamp': 1518076337,
'upload_date': '20180208',
},
}]
def _real_extract(self, url):
track_id = self._match_id(url)
track_info = self._download_json(
f'https://api.idagio.com/v2.0/metadata/tracks/{track_id}',
track_id, fatal=False, expected_status=406)
if traverse_obj(track_info, 'error_code') == 'idagio.error.blocked.location':
self.raise_geo_restricted()
content_info = self._download_json(
f'https://api.idagio.com/v1.8/content/track/{track_id}', track_id,
query={
'quality': '0',
'format': '2',
'client_type': 'web-4',
})
return {
'ext': 'mp3',
'vcodec': 'none',
'id': track_id,
'url': traverse_obj(content_info, ('url', {url_or_none})),
**traverse_obj(track_info, ('result', {
'title': ('piece', 'title', {str}),
'timestamp': ('recording', 'created_at', {int_or_none(scale=1000)}),
'location': ('recording', 'location', {str}),
'duration': ('duration', {int_or_none}),
'track': ('piece', 'title', {str}),
'artists': ('recording', ('conductor', ('ensembles', ...), ('soloists', ...)), 'name', {str}, filter),
'composers': ('piece', 'workpart', 'work', 'composer', 'name', {str}, filter, all, filter),
'genres': ('piece', 'workpart', 'work', ('genre', 'subgenre'), 'title', {str}, filter),
})),
}
class IdagioPlaylistBaseIE(InfoExtractor):
"""Subclasses must set _API_URL_TMPL and define _parse_playlist_metadata"""
_PLAYLIST_ID_KEY = 'id' # vs. 'display_id'
def _entries(self, playlist_info):
for track_data in traverse_obj(playlist_info, ('tracks', lambda _, v: v['id'] and v['recording']['id'])):
track_id = track_data['id']
recording_id = track_data['recording']['id']
yield self.url_result(
f'https://app.idagio.com/recordings/{recording_id}?trackId={track_id}',
ie=IdagioTrackIE, video_id=track_id)
def _real_extract(self, url):
playlist_id = self._match_id(url)
playlist_info = self._download_json(
self._API_URL_TMPL.format(playlist_id), playlist_id)['result']
return {
'_type': 'playlist',
self._PLAYLIST_ID_KEY: playlist_id,
'entries': self._entries(playlist_info),
**self._parse_playlist_metadata(playlist_info),
}
class IdagioRecordingIE(IdagioPlaylistBaseIE):
_VALID_URL = r'https?://(?:www\.)?app\.idagio\.com/recordings/(?P<id>\d+)(?![^#]*[&?]trackId=\d+)'
_TESTS = [{
'url': 'https://app.idagio.com/recordings/30576934',
'info_dict': {
'id': '30576934',
'title': 'Variations on an Original Theme op. 36',
'composers': ['Edward Elgar'],
'artists': ['Vasily Petrenko', 'Royal Liverpool Philharmonic Orchestra'],
'genres': ['Orchestral', 'Other Orchestral Music'],
'timestamp': 1554474370,
'modified_timestamp': 1554474370,
'modified_date': '20190405',
'upload_date': '20190405',
},
'playlist_count': 15,
}]
_API_URL_TMPL = 'https://api.idagio.com/v2.0/metadata/recordings/{}'
def _parse_playlist_metadata(self, playlist_info):
return traverse_obj(playlist_info, {
'title': ('work', 'title', {str}),
'timestamp': ('created_at', {int_or_none(scale=1000)}),
'modified_timestamp': ('created_at', {int_or_none(scale=1000)}),
'location': ('location', {str}),
'artists': (('conductor', ('ensembles', ...), ('soloists', ...)), 'name', {str}),
'composers': ('work', 'composer', 'name', {str}, all),
'genres': ('work', ('genre', 'subgenre'), 'title', {str}),
'tags': ('tags', ..., {str}),
})
class IdagioAlbumIE(IdagioPlaylistBaseIE):
_VALID_URL = r'https?://(?:www\.)?app\.idagio\.com/albums/(?P<id>[\w-]+)'
_TESTS = [{
'url': 'https://app.idagio.com/albums/elgar-enigma-variations-in-the-south-serenade-for-strings',
'info_dict': {
'id': 'a9f139b8-f70d-4b8a-a9a4-5fe8d35eaf9c',
'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',
'artists': ['Vasily Petrenko', 'Royal Liverpool Philharmonic Orchestra', 'Edward Elgar'],
'timestamp': 1553817600,
'upload_date': '20190329',
'modified_timestamp': 1562566559.0,
'modified_date': '20190708',
},
'playlist_count': 19,
}, {
'url': 'https://app.idagio.com/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'],
'artists': ['Sabine Devieilhe', 'Stéphane Degout', 'Raphaël Pichon', 'Pygmalion', 'Johannes Brahms'],
'timestamp': 1760054400,
'upload_date': '20251010',
'modified_timestamp': 1760101611,
'modified_date': '20251010',
},
'playlist_count': 7,
}]
_API_URL_TMPL = 'https://api.idagio.com/v2.0/metadata/albums/{}'
_PLAYLIST_ID_KEY = 'display_id'
def _parse_playlist_metadata(self, playlist_info):
return traverse_obj(playlist_info, {
'id': ('id', {str}),
'title': ('title', {str}),
'timestamp': ('publishDate', {unified_timestamp}),
'modified_timestamp': ('lastModified', {unified_timestamp}),
'thumbnail': ('imageUrl', {url_or_none}),
'description': ('description', {str}),
'artists': ('participants', ..., 'name', {str}),
'tags': ('tags', ..., {str}),
})
class IdagioPlaylistIE(IdagioPlaylistBaseIE):
_VALID_URL = r'https?://(?:www\.)?app\.idagio\.com/playlists/(?!personal/)(?P<id>[\w-]+)'
_TESTS = [{
'url': 'https://app.idagio.com/playlists/beethoven-the-most-beautiful-piano-music',
'info_dict': {
'id': '31652bec-8c5b-460e-a3f0-cf1f69817f53',
'display_id': 'beethoven-the-most-beautiful-piano-music',
'title': 'Beethoven: the most beautiful piano music',
'description': 'md5:d41bb04b8896bb69377f5c2cd9345ad1',
'thumbnail': r're:https://.+/playlists/31652bec-8c5b-460e-a3f0-cf1f69817f53/main\.jpg',
'creators': ['IDAGIO'],
},
'playlist_mincount': 16, # one entry is geo-restricted
}]
_API_URL_TMPL = 'https://api.idagio.com/v2.0/playlists/{}'
_PLAYLIST_ID_KEY = 'display_id'
def _parse_playlist_metadata(self, playlist_info):
return traverse_obj(playlist_info, {
'id': ('id', {str}),
'title': ('title', {str}),
'thumbnail': ('imageUrl', {url_or_none}),
'description': ('description', {str}),
'creators': ('curator', 'name', {str}, all),
})
class IdagioPersonalPlaylistIE(IdagioPlaylistBaseIE):
_VALID_URL = r'https?://(?:www\.)?app\.idagio\.com/playlists/personal/(?P<id>[\da-f-]+)'
_TESTS = [{
'url': 'https://app.idagio.com/playlists/personal/99dad72e-7b3a-45a4-b216-867c08046ed8',
'info_dict': {
'id': '99dad72e-7b3a-45a4-b216-867c08046ed8',
'title': 'Test',
'creators': ['1a6f16a6-4514-4d0c-b481-3a9877835626'],
'thumbnail': r're:https://.+/artists/86371/main\.jpg',
'timestamp': 1602859138,
'modified_timestamp': 1755616667,
'upload_date': '20201016',
'modified_date': '20250819',
},
'playlist_count': 100,
}]
_API_URL_TMPL = 'https://api.idagio.com/v1.0/personal-playlists/{}'
def _parse_playlist_metadata(self, playlist_info):
return traverse_obj(playlist_info, {
'title': ('title', {str}),
'thumbnail': ('image_url', {url_or_none}),
'creators': ('user_id', {str}, all),
'timestamp': ('created_at', {int_or_none(scale=1000)}),
'modified_timestamp': ('updated_at', {int_or_none(scale=1000)}),
})

View File

@@ -437,7 +437,7 @@ class KalturaIE(InfoExtractor):
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:

View File

@@ -1,3 +1,4 @@
import itertools
import re
import urllib.parse
@@ -216,7 +217,7 @@ class LyndaIE(LyndaBaseIE):
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

View File

@@ -92,7 +92,7 @@ class MojevideoIE(InfoExtractor):
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'),

View File

@@ -503,7 +503,7 @@ class NhkForSchoolBangumiIE(InfoExtractor):
'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,

View File

@@ -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']

View File

@@ -405,7 +405,7 @@ class PolskieRadioCategoryIE(InfoExtractor):
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',

View File

@@ -155,7 +155,7 @@ class Pr0grammIE(InfoExtractor):
# 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}),

View File

@@ -248,35 +248,17 @@ class SlidesLiveIE(InfoExtractor):
'skip_download': 'm3u8',
},
}, {
# /v3/ slides, .jpg and .png, service_name = youtube
# /v3/ slides, .jpg and .png, formerly service_name = youtube, now native
'url': 'https://slideslive.com/embed/38932460/',
'info_dict': {
'id': 'RTPdrgkyTiE',
'display_id': '38932460',
'id': '38932460',
'ext': 'mp4',
'title': 'Active Learning for Hierarchical Multi-Label Classification',
'description': 'Watch full version of this video at https://slideslive.com/38932460.',
'channel': 'SlidesLive Videos - A',
'channel_id': 'UC62SdArr41t_-_fX40QCLRw',
'channel_url': 'https://www.youtube.com/channel/UC62SdArr41t_-_fX40QCLRw',
'uploader': 'SlidesLive Videos - A',
'uploader_id': '@slideslivevideos-a6075',
'uploader_url': 'https://www.youtube.com/@slideslivevideos-a6075',
'upload_date': '20200903',
'timestamp': 1697805922,
'duration': 942,
'age_limit': 0,
'live_status': 'not_live',
'playable_in_embed': True,
'availability': 'unlisted',
'categories': ['People & Blogs'],
'tags': [],
'channel_follower_count': int,
'like_count': int,
'view_count': int,
'thumbnail': r're:^https?://.*\.(?:jpg|png|webp)',
'thumbnails': 'count:21',
'duration': 941,
'thumbnail': r're:https?://.+/.+\.(?:jpg|png)',
'chapters': 'count:20',
'timestamp': 1708338974,
'upload_date': '20240219',
},
'params': {
'skip_download': 'm3u8',
@@ -425,7 +407,7 @@ class SlidesLiveIE(InfoExtractor):
player_token = self._search_regex(r'data-player-token="([^"]+)"', webpage, 'player token')
player_data = self._download_webpage(
f'https://ben.slideslive.com/player/{video_id}', video_id,
f'https://slideslive.com/player/{video_id}', video_id,
note='Downloading player info', query={'player_token': player_token})
player_info = self._extract_custom_m3u8_info(player_data)
@@ -525,7 +507,7 @@ class SlidesLiveIE(InfoExtractor):
yield info
service_data = self._download_json(
f'https://ben.slideslive.com/player/{video_id}/slides_video_service_data',
f'https://slideslive.com/player/{video_id}/slides_video_service_data',
video_id, fatal=False, query={
'player_token': player_token,
'videos': ','.join(video_slides),

View File

@@ -98,7 +98,7 @@ class TenPlayIE(InfoExtractor):
'only_matching': True,
}]
_GEO_BYPASS = False
_GEO_COUNTRIES = ['AU']
_AUS_AGES = {
'G': 0,
'PG': 15,
@@ -208,8 +208,15 @@ class TenPlayIE(InfoExtractor):
def _real_extract(self, url):
content_id = self._match_id(url)
data = self._download_json(
f'https://10.com.au/api/v1/videos/{content_id}', content_id)
try:
data = self._download_json(f'https://10.com.au/api/v1/videos/{content_id}', content_id)
except ExtractorError as e:
if (
isinstance(e.cause, HTTPError) and e.cause.status == 403
and 'Error 54113' in e.cause.response.read().decode()
):
self.raise_geo_restricted(countries=self._GEO_COUNTRIES)
raise
video_data, urlh = self._call_playback_api(content_id)
content_source_id = video_data['dai']['contentSourceId']

View File

@@ -81,7 +81,7 @@ class TikTokBaseIE(InfoExtractor):
}
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
]
@@ -1074,9 +1074,12 @@ class TikTokUserIE(TikTokBaseIE):
fatal=False, impersonate=True) or ''
detail = traverse_obj(
self._get_universal_data(webpage, user_name), ('webapp.user-detail', {dict})) or {}
if detail.get('statusCode') == 10222:
video_count = traverse_obj(detail, ('userInfo', ('stats', 'statsV2'), 'videoCount', {int}, any))
if not video_count and detail.get('statusCode') == 10222:
self.raise_login_required(
'This user\'s account is private. Log into an account that has access')
elif video_count == 0:
raise ExtractorError('This account does not have any videos posted', expected=True)
sec_uid = traverse_obj(detail, ('userInfo', 'user', 'secUid', {str}))
if sec_uid:
fail_early = not traverse_obj(detail, ('userInfo', 'itemList', ...))

View File

@@ -2,6 +2,7 @@ import base64
import codecs
import itertools
import re
import string
from .common import InfoExtractor
from ..utils import (
@@ -22,6 +23,47 @@ from ..utils import (
)
def to_signed_32(n):
return n % ((-1 if n < 0 else 1) * 2**32)
class _ByteGenerator:
def __init__(self, algo_id, seed):
try:
self._algorithm = getattr(self, f'_algo{algo_id}')
except AttributeError:
raise ExtractorError(f'Unknown algorithm ID: {algo_id}')
self._s = to_signed_32(seed)
def _algo1(self, s):
# LCG (a=1664525, c=1013904223, m=2^32)
# Ref: https://en.wikipedia.org/wiki/Linear_congruential_generator
s = self._s = to_signed_32(s * 1664525 + 1013904223)
return s
def _algo2(self, s):
# xorshift32
# Ref: https://en.wikipedia.org/wiki/Xorshift
s = to_signed_32(s ^ (s << 13))
s = to_signed_32(s ^ ((s & 0xFFFFFFFF) >> 17))
s = self._s = to_signed_32(s ^ (s << 5))
return s
def _algo3(self, s):
# Weyl Sequence (k≈2^32*φ, m=2^32) + MurmurHash3 (fmix32)
# Ref: https://en.wikipedia.org/wiki/Weyl_sequence
# https://commons.apache.org/proper/commons-codec/jacoco/org.apache.commons.codec.digest/MurmurHash3.java.html
s = self._s = to_signed_32(s + 0x9e3779b9)
s = to_signed_32(s ^ ((s & 0xFFFFFFFF) >> 16))
s = to_signed_32(s * to_signed_32(0x85ebca77))
s = to_signed_32(s ^ ((s & 0xFFFFFFFF) >> 13))
s = to_signed_32(s * to_signed_32(0xc2b2ae3d))
return to_signed_32(s ^ ((s & 0xFFFFFFFF) >> 16))
def __next__(self):
return self._algorithm(self._s) & 0xFF
class XHamsterIE(InfoExtractor):
_DOMAINS = r'(?:xhamster\.(?:com|one|desi)|xhms\.pro|xhamster\d+\.(?:com|desi)|xhday\.com|xhvid\.com)'
_VALID_URL = rf'''(?x)
@@ -146,6 +188,12 @@ class XHamsterIE(InfoExtractor):
_XOR_KEY = b'xh7999'
def _decipher_format_url(self, format_url, format_id):
if all(char in string.hexdigits for char in format_url):
byte_data = bytes.fromhex(format_url)
seed = int.from_bytes(byte_data[1:5], byteorder='little', signed=True)
byte_gen = _ByteGenerator(byte_data[0], seed)
return bytearray(byte ^ next(byte_gen) for byte in byte_data[5:]).decode('latin-1')
cipher_type, _, ciphertext = try_call(
lambda: base64.b64decode(format_url).decode().partition('_')) or [None] * 3
@@ -164,6 +212,16 @@ class XHamsterIE(InfoExtractor):
self.report_warning(f'Skipping format "{format_id}": unsupported cipher type "{cipher_type}"')
return None
def _fixup_formats(self, formats):
for f in formats:
if f.get('vcodec'):
continue
for vcodec in ('av1', 'h264'):
if any(f'.{vcodec}.' in f_url for f_url in (f['url'], f.get('manifest_url', ''))):
f['vcodec'] = vcodec
break
return formats
def _real_extract(self, url):
mobj = self._match_valid_url(url)
video_id = mobj.group('id') or mobj.group('id_2')
@@ -312,7 +370,8 @@ class XHamsterIE(InfoExtractor):
'comment_count': int_or_none(video.get('comments')),
'age_limit': age_limit if age_limit is not None else 18,
'categories': categories,
'formats': formats,
'formats': self._fixup_formats(formats),
'_format_sort_fields': ('res', 'proto', 'tbr'),
}
# Old layout fallback

View File

@@ -186,7 +186,7 @@ _OPERATORS = { # None => Defined in JSInterpreter._operator
_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'[^[\]]+(?:\[[^[\]]+(?:\[[^\]]+\])?\])?'

View File

@@ -4,7 +4,6 @@ import functools
import http.client
import logging
import re
import socket
import warnings
from ..dependencies import brotli, requests, urllib3
@@ -125,7 +124,7 @@ class RequestsResponseAdapter(Response):
# 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 @@ class SocksHTTPConnection(urllib3.connection.HTTPConnection):
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:

View File

@@ -12,6 +12,7 @@ import urllib.response
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 @@ from .exceptions import (
TransportError,
UnsupportedRequest,
)
from ..compat.types import NoneType
from ..cookies import YoutubeDLCookieJar
from ..utils import (
bug_reports_message,

View File

@@ -3,11 +3,11 @@ from __future__ import annotations
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

View File

@@ -11,7 +11,6 @@ import os
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)}',

View File

@@ -418,7 +418,7 @@ class FFmpegPostProcessor(PostProcessor):
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 @@ class FFmpegEmbedSubtitlePP(FFmpegPostProcessor):
# 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}'])

View File

@@ -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 @@ class Updater:
@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 @@ class Updater:
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

View File

@@ -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_certifi = bool(certifi)
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)

View File

@@ -95,7 +95,7 @@ TIMEZONE_NAMES = {
# 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 @@ class PlaylistEntries:
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 render_table(header_row, data, delim=False, extra_gap=0, hide_empty=False):
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 dfxp2srt(dfxp_data):
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
]

View File

@@ -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:

View File

@@ -1,8 +1,8 @@
# Autogenerated by devscripts/update-version.py
__version__ = '2025.09.26'
__version__ = '2025.10.14'
RELEASE_GIT_HEAD = '12b57d2858845c0c7fb33bf9aa8ed7be6905535d'
RELEASE_GIT_HEAD = 'a98e7f9f58a9492d2cb216baa59c890ed8ce02f3'
VARIANT = None
@@ -12,4 +12,4 @@ CHANNEL = 'stable'
ORIGIN = 'yt-dlp/yt-dlp'
_pkg_version = '2025.09.26'
_pkg_version = '2025.10.14'

View File

@@ -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):