[cleanup] Misc (#16452)

* Include `pin*` extras in lockfile
* Fix and clean up `devscripts/update_requirements.py`
* Improve release channel documentation
* Remove false statement from `--prefer-insecure` documentation
* Assorted code cleanup
* Set `GH_TELEMETRY=false` in CI/CD whenever `gh` is used
* Add comments about required checks in CI workflows
* Run `test-workflows.yml` for every PR so its checks can be required
* Verify actionlint attestation in CI
* Remove zizmor version to reduce workflow maintenance burden
  (zizmor-action handles pinning on its end)

Authored by: bashonly
This commit is contained in:
bashonly 2026-05-03 17:19:08 -05:00 committed by GitHub
parent 27973bae5e
commit 35684c1171
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 158 additions and 108 deletions

View File

@ -1,6 +1,7 @@
name: Challenge Tests name: Challenge Tests
on: on:
push: push:
branches: ['master']
paths: paths:
- .github/workflows/challenge-tests.yml - .github/workflows/challenge-tests.yml
- test/test_jsc/*.py - test/test_jsc/*.py
@ -9,6 +10,7 @@ on:
- yt_dlp/extractor/youtube/pot/**.py - yt_dlp/extractor/youtube/pot/**.py
- yt_dlp/utils/_jsruntime.py - yt_dlp/utils/_jsruntime.py
pull_request: pull_request:
branches: ['**']
paths: paths:
- .github/workflows/challenge-tests.yml - .github/workflows/challenge-tests.yml
- test/test_jsc/*.py - test/test_jsc/*.py
@ -25,7 +27,7 @@ concurrency:
jobs: jobs:
tests: tests:
name: Challenge Tests name: Challenge tests
if: ${{ !contains(github.event.head_commit.message, ':ci skip') }} if: ${{ !contains(github.event.head_commit.message, ':ci skip') }}
permissions: permissions:
contents: read contents: read

View File

@ -1,6 +1,7 @@
name: Core Tests name: Core Tests
on: on:
push: push:
branches: ['master']
paths: paths:
- pyproject.toml - pyproject.toml
- .github/** - .github/**
@ -13,6 +14,7 @@ on:
- yt_dlp/extractor/common.py - yt_dlp/extractor/common.py
- yt_dlp/extractor/extractors.py - yt_dlp/extractor/extractors.py
pull_request: pull_request:
branches: ['**']
paths: paths:
- pyproject.toml - pyproject.toml
- .github/** - .github/**
@ -33,7 +35,7 @@ concurrency:
jobs: jobs:
tests: tests:
name: Core Tests name: Core tests
if: ${{ !contains(github.event.head_commit.message, ':ci skip') }} if: ${{ !contains(github.event.head_commit.message, ':ci skip') }}
permissions: permissions:
contents: read contents: read

View File

@ -5,9 +5,12 @@ on:
permissions: {} permissions: {}
env:
GH_TELEMETRY: "false"
jobs: jobs:
lockdown: lockdown:
name: Issue Lockdown name: Issue lockdown
if: vars.ISSUE_LOCKDOWN if: vars.ISSUE_LOCKDOWN
permissions: permissions:
issues: write # Needed to lock issues issues: write # Needed to lock issues
@ -19,4 +22,4 @@ jobs:
ISSUE_NUMBER: ${{ github.event.issue.number }} ISSUE_NUMBER: ${{ github.event.issue.number }}
REPOSITORY: ${{ github.repository }} REPOSITORY: ${{ github.repository }}
run: | run: |
gh issue lock "${ISSUE_NUMBER}" -R "${REPOSITORY}" gh issue lock -R "${REPOSITORY}" "${ISSUE_NUMBER}"

View File

@ -1,5 +1,10 @@
name: Quick Test name: Quick Test
on: [push, pull_request] on:
push:
branches: ['master']
# This workflow contains required checks and needs to run for EVERY pull_request
pull_request:
branches: ['**']
permissions: {} permissions: {}
@ -9,7 +14,8 @@ concurrency:
jobs: jobs:
tests: tests:
name: Core Test # Required check; do not change name
name: Core test
if: ${{ !contains(github.event.head_commit.message, ':ci skip all') }} if: ${{ !contains(github.event.head_commit.message, ':ci skip all') }}
permissions: permissions:
contents: read contents: read
@ -31,7 +37,9 @@ jobs:
run: | run: |
python3 -m yt_dlp -v || true python3 -m yt_dlp -v || true
python3 ./devscripts/run_tests.py --pytest-args '--reruns 2 --reruns-delay 3.0' core python3 ./devscripts/run_tests.py --pytest-args '--reruns 2 --reruns-delay 3.0' core
check: check:
# Required check; do not change name
name: Code check name: Code check
if: ${{ !contains(github.event.head_commit.message, ':ci skip all') }} if: ${{ !contains(github.event.head_commit.message, ':ci skip all') }}
permissions: permissions:
@ -43,7 +51,7 @@ jobs:
persist-credentials: false persist-credentials: false
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with: with:
python-version: '3.10' python-version: '3.14'
- name: Install dev dependencies - name: Install dev dependencies
run: python ./devscripts/install_deps.py --omit-default --include-group static-analysis run: python ./devscripts/install_deps.py --omit-default --include-group static-analysis
- name: Make lazy extractors - name: Make lazy extractors

View File

@ -19,7 +19,7 @@ permissions: {}
jobs: jobs:
release: release:
name: Publish Github release name: Publish GitHub release
if: vars.BUILD_MASTER if: vars.BUILD_MASTER
permissions: permissions:
contents: write # May be needed to publish release contents: write # May be needed to publish release

View File

@ -70,7 +70,7 @@ jobs:
run: echo "${HEAD}" | tee .nightly_commit_hash run: echo "${HEAD}" | tee .nightly_commit_hash
release: release:
name: Publish Github release name: Publish GitHub release
needs: [check_nightly] needs: [check_nightly]
if: needs.check_nightly.outputs.commit if: needs.check_nightly.outputs.commit
permissions: permissions:

View File

@ -65,6 +65,9 @@ on:
permissions: {} permissions: {}
env:
GH_TELEMETRY: "false"
jobs: jobs:
prepare: prepare:
name: Prepare name: Prepare
@ -226,7 +229,7 @@ jobs:
verbose: true verbose: true
publish: publish:
name: Publish Github release name: Publish GitHub release
needs: [prepare, build] needs: [prepare, build]
permissions: permissions:
contents: write # Needed by gh to publish release to Github contents: write # Needed by gh to publish release to Github

View File

@ -1,23 +1,10 @@
name: Test and lint workflows name: Test and lint workflows
on: on:
push: push:
branches: [master] branches: ['master']
paths: # This workflow contains required checks and needs to run for EVERY pull_request
- .github/*.yml
- .github/workflows/*
- bundle/docker/linux/*.sh
- devscripts/setup_variables.py
- devscripts/setup_variables_tests.py
- devscripts/utils.py
pull_request: pull_request:
branches: [master] branches: ['**']
paths:
- .github/*.yml
- .github/workflows/*
- bundle/docker/linux/*.sh
- devscripts/setup_variables.py
- devscripts/setup_variables_tests.py
- devscripts/utils.py
permissions: {} permissions: {}
@ -28,10 +15,12 @@ concurrency:
env: env:
ACTIONLINT_VERSION: "1.7.11" ACTIONLINT_VERSION: "1.7.11"
ACTIONLINT_SHA256SUM: 900919a84f2229bac68ca9cd4103ea297abc35e9689ebb842c6e34a3d1b01b0a ACTIONLINT_SHA256SUM: 900919a84f2229bac68ca9cd4103ea297abc35e9689ebb842c6e34a3d1b01b0a
ACTIONLINT_REPO: https://github.com/rhysd/actionlint ACTIONLINT_REPO: rhysd/actionlint
GH_TELEMETRY: "false"
jobs: jobs:
check: check:
# Required check; do not change name
name: Check workflows name: Check workflows
permissions: permissions:
contents: read contents: read
@ -45,19 +34,26 @@ jobs:
python-version: "3.13" # Keep this in sync with release.yml's prepare job python-version: "3.13" # Keep this in sync with release.yml's prepare job
- name: Install requirements - name: Install requirements
env: env:
GH_TOKEN: ${{ github.token }}
ACTIONLINT_TARBALL: ${{ format('actionlint_{0}_linux_amd64.tar.gz', env.ACTIONLINT_VERSION) }} ACTIONLINT_TARBALL: ${{ format('actionlint_{0}_linux_amd64.tar.gz', env.ACTIONLINT_VERSION) }}
shell: bash shell: bash
run: | run: |
python -m devscripts.install_deps --omit-default --include-group test python -m devscripts.install_deps --omit-default --include-group test
sudo apt -y install shellcheck sudo apt -y install shellcheck
python -m pip install -U pyflakes python -m pip install -U pyflakes
curl -LO "${ACTIONLINT_REPO}/releases/download/v${ACTIONLINT_VERSION}/${ACTIONLINT_TARBALL}" gh release download \
--repo "${ACTIONLINT_REPO}" \
--pattern "${ACTIONLINT_TARBALL}" \
"v${ACTIONLINT_VERSION}"
gh attestation verify \
--repo "${ACTIONLINT_REPO}" \
"${ACTIONLINT_TARBALL}"
printf '%s %s' "${ACTIONLINT_SHA256SUM}" "${ACTIONLINT_TARBALL}" | sha256sum -c - printf '%s %s' "${ACTIONLINT_SHA256SUM}" "${ACTIONLINT_TARBALL}" | sha256sum -c -
tar xvzf "${ACTIONLINT_TARBALL}" actionlint tar xvzf "${ACTIONLINT_TARBALL}" actionlint
chmod +x actionlint sudo install -D --mode=755 actionlint /usr/bin/
- name: Run actionlint - name: Run actionlint
run: | run: |
./actionlint -color actionlint -color
- name: Check Docker shell scripts - name: Check Docker shell scripts
run: | run: |
shellcheck bundle/docker/linux/*.sh shellcheck bundle/docker/linux/*.sh
@ -66,6 +62,7 @@ jobs:
pytest -Werror --tb=short --color=yes devscripts/setup_variables_tests.py pytest -Werror --tb=short --color=yes devscripts/setup_variables_tests.py
zizmor: zizmor:
# Required check; do not change name
name: Run zizmor name: Run zizmor
permissions: permissions:
contents: read contents: read
@ -80,4 +77,3 @@ jobs:
with: with:
advanced-security: false advanced-security: false
persona: pedantic persona: pedantic
version: v1.23.1

View File

@ -3,7 +3,7 @@ all-extra: lazy-extractors yt-dlp-extra doc pypi-files
clean: clean-test clean-dist clean: clean-test clean-dist
clean-all: clean clean-cache clean-all: clean clean-cache
completions: completion-bash completion-fish completion-zsh completions: completion-bash completion-fish completion-zsh
doc: README.md CONTRIBUTING.md CONTRIBUTORS issuetemplates supportedsites doc: README.md CONTRIBUTORS issuetemplates supportedsites
ot: offlinetest ot: offlinetest
tar: yt-dlp.tar.gz tar: yt-dlp.tar.gz
@ -30,7 +30,7 @@ clean-test:
test/testdata/sigs/player-*.js test/testdata/thumbnails/empty.webp "test/testdata/thumbnails/foo %d bar/foo_%d."* test/testdata/sigs/player-*.js test/testdata/thumbnails/empty.webp "test/testdata/thumbnails/foo %d bar/foo_%d."*
clean-dist: clean-dist:
rm -rf yt-dlp.1.temp.md yt-dlp.1 README.txt MANIFEST build/ dist/ .coverage cover/ yt-dlp.tar.gz completions/ \ rm -rf yt-dlp.1.temp.md yt-dlp.1 README.txt MANIFEST build/ dist/ .coverage cover/ yt-dlp.tar.gz completions/ \
yt_dlp/extractor/lazy_extractors.py *.spec CONTRIBUTING.md.tmp yt-dlp yt-dlp.exe yt_dlp.egg-info/ AUTHORS \ yt_dlp/extractor/lazy_extractors.py *.spec yt-dlp yt-dlp.exe yt_dlp.egg-info/ AUTHORS \
yt-dlp.zip .ejs-* yt_dlp_ejs/ yt-dlp.zip .ejs-* yt_dlp_ejs/
clean-cache: clean-cache:
find . \( \ find . \( \
@ -132,9 +132,6 @@ yt-dlp: yt-dlp.zip
README.md: $(PY_CODE_FILES) devscripts/make_readme.py README.md: $(PY_CODE_FILES) devscripts/make_readme.py
COLUMNS=80 $(PYTHON) yt_dlp/__main__.py --ignore-config --help | $(PYTHON) devscripts/make_readme.py COLUMNS=80 $(PYTHON) yt_dlp/__main__.py --ignore-config --help | $(PYTHON) devscripts/make_readme.py
CONTRIBUTING.md: README.md devscripts/make_contributing.py
$(PYTHON) devscripts/make_contributing.py README.md CONTRIBUTING.md
issuetemplates: devscripts/make_issue_template.py .github/ISSUE_TEMPLATE_tmpl/1_broken_site.yml .github/ISSUE_TEMPLATE_tmpl/2_site_support_request.yml .github/ISSUE_TEMPLATE_tmpl/3_site_feature_request.yml .github/ISSUE_TEMPLATE_tmpl/4_bug_report.yml .github/ISSUE_TEMPLATE_tmpl/5_feature_request.yml yt_dlp/version.py issuetemplates: devscripts/make_issue_template.py .github/ISSUE_TEMPLATE_tmpl/1_broken_site.yml .github/ISSUE_TEMPLATE_tmpl/2_site_support_request.yml .github/ISSUE_TEMPLATE_tmpl/3_site_feature_request.yml .github/ISSUE_TEMPLATE_tmpl/4_bug_report.yml .github/ISSUE_TEMPLATE_tmpl/5_feature_request.yml yt_dlp/version.py
$(PYTHON) devscripts/make_issue_template.py .github/ISSUE_TEMPLATE_tmpl/1_broken_site.yml .github/ISSUE_TEMPLATE/1_broken_site.yml $(PYTHON) devscripts/make_issue_template.py .github/ISSUE_TEMPLATE_tmpl/1_broken_site.yml .github/ISSUE_TEMPLATE/1_broken_site.yml
$(PYTHON) devscripts/make_issue_template.py .github/ISSUE_TEMPLATE_tmpl/2_site_support_request.yml .github/ISSUE_TEMPLATE/2_site_support_request.yml $(PYTHON) devscripts/make_issue_template.py .github/ISSUE_TEMPLATE_tmpl/2_site_support_request.yml .github/ISSUE_TEMPLATE/2_site_support_request.yml

View File

@ -167,9 +167,9 @@ For other third-party package managers, see [the wiki](https://github.com/yt-dlp
There are currently three release channels for binaries: `stable`, `nightly` and `master`. There are currently three release channels for binaries: `stable`, `nightly` and `master`.
* `stable` is the default channel, and many of its changes have been tested by users of the `nightly` and `master` channels. * `stable` is the default channel, which offers releases published on a (mostly) monthly schedule. While it is named `stable` due to many of its changes having been tested by users of the `nightly` or `master` release channels, the latest `stable` release is often "stale" and prone to external breakage (i.e. sites changing things on their end and breaking yt-dlp).
* The `nightly` channel has releases scheduled to build every day around midnight UTC, for a snapshot of the project's new patches and changes. This is the **recommended channel for regular users** of yt-dlp. The `nightly` releases are available from [yt-dlp/yt-dlp-nightly-builds](https://github.com/yt-dlp/yt-dlp-nightly-builds/releases) or as development releases of the `yt-dlp` PyPI package (which can be installed with pip's `--pre` flag). * The `nightly` channel offers releases that publish shortly before midnight UTC on any day that sees changes to the codebase. This channel serves as a snapshot of the project's development, and it is the **recommended channel for regular users** of yt-dlp. The `nightly` releases are available from [yt-dlp/yt-dlp-nightly-builds](https://github.com/yt-dlp/yt-dlp-nightly-builds/releases) or as development releases of the `yt-dlp` PyPI package (which can be installed with pip's `--pre` flag).
* The `master` channel features releases that are built after each push to the master branch, and these will have the very latest fixes and additions, but may also be more prone to regressions. They are available from [yt-dlp/yt-dlp-master-builds](https://github.com/yt-dlp/yt-dlp-master-builds/releases). * The `master` channel offers "canary" releases that publish after each push to the master branch. This channel will always provide the very latest fixes and features, but may be prone to bugs or regressions. The `master` releases are available from [yt-dlp/yt-dlp-master-builds](https://github.com/yt-dlp/yt-dlp-master-builds/releases).
When using `--update`/`-U`, a release binary will only update to its current channel. When using `--update`/`-U`, a release binary will only update to its current channel.
`--update-to CHANNEL` can be used to switch to a different channel when a newer version is available. `--update-to [CHANNEL@]TAG` can also be used to upgrade or downgrade to specific tags from a channel. `--update-to CHANNEL` can be used to switch to a different channel when a newer version is available. `--update-to [CHANNEL@]TAG` can also be used to upgrade or downgrade to specific tags from a channel.

View File

@ -1,32 +0,0 @@
#!/usr/bin/env python3
import optparse
import re
def main():
return # This is unused in yt-dlp
parser = optparse.OptionParser(usage='%prog INFILE OUTFILE')
_, args = parser.parse_args()
if len(args) != 2:
parser.error('Expected an input and an output filename')
infile, outfile = args
with open(infile, encoding='utf-8') as inf:
readme = inf.read()
bug_text = re.search(
r'(?s)#\s*BUGS\s*[^\n]*\s*(.*?)#\s*COPYRIGHT', readme).group(1)
dev_text = re.search(
r'(?s)(#\s*DEVELOPER INSTRUCTIONS.*?)#\s*EMBEDDING yt-dlp', readme).group(1)
out = bug_text + dev_text
with open(outfile, 'w', encoding='utf-8') as outf:
outf.write(out)
if __name__ == '__main__':
main()

View File

@ -47,12 +47,6 @@ PINNED_EXTRAS = {
'pin-deno': 'deno', 'pin-deno': 'deno',
} }
WELLKNOWN_PACKAGES = {
'deno': {'owner': 'denoland', 'repo': 'deno'},
'protobug': {'owner': 'yt-dlp', 'repo': 'protobug'},
'yt-dlp-ejs': {'owner': 'yt-dlp', 'repo': 'ejs'},
}
EJS_ASSETS = { EJS_ASSETS = {
'yt.solver.lib.js': False, 'yt.solver.lib.js': False,
'yt.solver.lib.min.js': False, 'yt.solver.lib.min.js': False,
@ -120,10 +114,15 @@ PYINSTALLER_BUILDS_TARGETS = {
'win-arm64-pyinstaller': 'win_arm64', 'win-arm64-pyinstaller': 'win_arm64',
} }
PYINSTALLER_BUILDS_TMPL = '''\ WELLKNOWN_PACKAGES = {
{}pyinstaller @ {} \\ 'deno': {'owner': 'denoland', 'repo': 'deno'},
--hash={} 'protobug': {'owner': 'yt-dlp', 'repo': 'protobug'},
''' 'yt-dlp-ejs': {'owner': 'yt-dlp', 'repo': 'ejs'},
**{
f'pyinstaller[{asset_tag}]': {'owner': 'pyinstaller', 'repo': 'pyinstaller'}
for asset_tag in PYINSTALLER_BUILDS_TARGETS.values()
},
}
def call_pypi_api(project: str) -> dict[str, dict[str, typing.Any]]: def call_pypi_api(project: str) -> dict[str, dict[str, typing.Any]]:
@ -447,7 +446,7 @@ def parse_version_from_dist(filename: str, name: str, *, require: bool = False)
normalized_name = re.sub(r'[-_.]+', '-', name).lower().replace('-', '_') normalized_name = re.sub(r'[-_.]+', '-', name).lower().replace('-', '_')
# Ref: https://packaging.python.org/en/latest/specifications/version-specifiers/#version-specifiers # Ref: https://packaging.python.org/en/latest/specifications/version-specifiers/#version-specifiers
if mobj := re.match(rf'{normalized_name}-(?P<version>[^-]+)-', filename): if mobj := re.fullmatch(rf'{normalized_name}-(?P<version>[^-]+)(?:-.+\.whl|\.tar\.gz)', filename):
return mobj.group('version') return mobj.group('version')
if require: if require:
@ -579,27 +578,33 @@ def update_requirements(
all_updates = package_diff_dict(old_packages, new_packages) all_updates = package_diff_dict(old_packages, new_packages)
# Update Windows PyInstaller requirements; need to compare before & after .txt's for reporting # Update Windows PyInstaller requirements; need to compare before & after .txt's for reporting
if not upgrade_only or upgrade_only.lower() == 'pyinstaller': if not upgrade_only:
info = fetch_latest_github_release('yt-dlp', 'Pyinstaller-Builds') info = fetch_latest_github_release('yt-dlp', 'Pyinstaller-Builds')
for target_suffix, asset_tag in PYINSTALLER_BUILDS_TARGETS.items(): for target_suffix, asset_tag in PYINSTALLER_BUILDS_TARGETS.items():
asset_info = next(asset for asset in info['assets'] if asset_tag in asset['name']) asset_info = next(asset for asset in info['assets'] if asset_tag in asset['name'])
pyinstaller_version = parse_version_from_dist(
asset_info['name'], 'pyinstaller', require=True)
pyinstaller_builds_deps = run_pip_compile(
'--no-emit-package=pyinstaller',
upgrade_arg,
input_line=f'pyinstaller=={pyinstaller_version}',
env=env)
requirements_path = REQUIREMENTS_PATH / REQS_OUTPUT_TMPL.format(target_suffix) requirements_path = REQUIREMENTS_PATH / REQS_OUTPUT_TMPL.format(target_suffix)
if requirements_path.is_file(): if requirements_path.is_file():
old_requirements_txt = requirements_path.read_text() old_requirements_txt = requirements_path.read_text()
else: else:
old_requirements_txt = '' old_requirements_txt = ''
new_requirements_txt = PYINSTALLER_BUILDS_TMPL.format( run_pip_compile(
pyinstaller_builds_deps, asset_info['browser_download_url'], asset_info['digest']) upgrade_arg,
requirements_path.write_text(new_requirements_txt) input_line=f'pyinstaller @ {asset_info["browser_download_url"]}',
all_updates.update(evaluate_requirements_txt(old_requirements_txt, new_requirements_txt)) output_file=requirements_path,
env=env)
new_requirements_txt = requirements_path.read_text()
if asset_info['digest'] not in new_requirements_txt:
raise ValueError(
f'expected pyinstaller wheel hash {asset_info["digest"]} '
f'not found in {requirements_path}')
diff_dict = evaluate_requirements_txt(old_requirements_txt, new_requirements_txt)
if pyinstaller_diff := diff_dict.get('pyinstaller'):
# NB: this depends on 'pyinstaller[asset_tag]' keys in WELLKNOWN_PACKAGES
all_updates.update({f'pyinstaller[{asset_tag}]': pyinstaller_diff})
# Export bundle requirements; any updates to these are already recorded w/ uv.lock package diff # Export bundle requirements; any updates to these are already recorded w/ uv.lock package diff
for target_suffix, target in BUNDLE_TARGETS.items(): for target_suffix, target in BUNDLE_TARGETS.items():
@ -640,6 +645,10 @@ def update_requirements(
# Write the finalized pyproject.toml # Write the finalized pyproject.toml
modify_and_write_pyproject(pyproject_text, table_name=EXTRAS_TABLE, table=extras) modify_and_write_pyproject(pyproject_text, table_name=EXTRAS_TABLE, table=extras)
# Generate/upgrade final lockfile that includes pinned extras
print(f'Running: uv lock {upgrade_arg}', file=sys.stderr)
run_process('uv', 'lock', upgrade_arg, env=env)
return all_updates return all_updates
@ -682,7 +691,9 @@ def generate_report(
if offset is not None: if offset is not None:
md_old = '.'.join(old_parts[:offset]) + '.***' + '.'.join(old_parts[offset:]) + '***' md_old = '.'.join(old_parts[:offset]) + '.***' + '.'.join(old_parts[offset:]) + '***'
md_old = md_old.lstrip('.')
md_new = '.'.join(new_parts[:offset]) + '.***' + '.'.join(new_parts[offset:]) + '***' md_new = '.'.join(new_parts[:offset]) + '.***' + '.'.join(new_parts[offset:]) + '***'
md_new = md_new.lstrip('.')
compare = '' compare = ''
if github_info: if github_info:
@ -692,14 +703,15 @@ def generate_report(
new_tag = tags_info.get(new) and tags_info[new]['name'] new_tag = tags_info.get(new) and tags_info[new]['name']
github_url = 'https://github.com/{owner}/{repo}'.format(**github_info) github_url = 'https://github.com/{owner}/{repo}'.format(**github_info)
if new_tag: if new_tag:
md_new = f'[{md_new}]({github_url}/releases/tag/{new_tag})' md_new = f'[{md_new}](<{github_url}/releases/tag/{new_tag}>)'
if old_tag: if old_tag:
md_old = f'[{md_old}]({github_url}/releases/tag/{old_tag})' md_old = f'[{md_old}](<{github_url}/releases/tag/{old_tag}>)'
if new_tag and old_tag: if new_tag and old_tag:
compare = f'[`{old_tag}...{new_tag}`]({github_url}/compare/{old_tag}...{new_tag})' compare = f'[`{old_tag}...{new_tag}`](<{github_url}/compare/{old_tag}...{new_tag}>)'
yield ' | '.join(( yield ' | '.join((
f'[**`{package}`**](https://pypi.org/project/{package})', # Strip the [win*] tag from package in the URL (e.g. pyinstaller[win32])
f'[**`{package}`**](<https://pypi.org/project/{package.split("[")[0]}/>)',
md_old, md_old,
md_new, md_new,
compare, compare,

View File

@ -180,9 +180,6 @@ exclude = [
path = "yt_dlp/version.py" path = "yt_dlp/version.py"
pattern = "_pkg_version = '(?P<version>[^']+)'" pattern = "_pkg_version = '(?P<version>[^']+)'"
[tool.hatch.metadata]
allow-direct-references = true
[tool.hatch.envs.default] [tool.hatch.envs.default]
features = [ features = [
"curl-cffi", "curl-cffi",

66
uv.lock generated
View File

@ -1166,6 +1166,42 @@ default = [
deno = [ deno = [
{ name = "deno" }, { name = "deno" },
] ]
pin = [
{ name = "brotli", marker = "implementation_name == 'cpython' and sys_platform != 'ios'" },
{ name = "brotlicffi", marker = "implementation_name != 'cpython'" },
{ name = "certifi" },
{ name = "cffi", marker = "implementation_name != 'cpython'" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "mutagen" },
{ name = "pycparser", marker = "implementation_name != 'PyPy' and implementation_name != 'cpython'" },
{ name = "pycryptodomex" },
{ name = "requests" },
{ name = "urllib3" },
{ name = "websockets" },
{ name = "yt-dlp-ejs" },
]
pin-curl-cffi = [
{ name = "certifi", marker = "implementation_name == 'cpython'" },
{ name = "cffi", marker = "implementation_name == 'cpython'" },
{ name = "curl-cffi", marker = "implementation_name == 'cpython'" },
{ name = "markdown-it-py", marker = "implementation_name == 'cpython'" },
{ name = "mdurl", marker = "implementation_name == 'cpython'" },
{ name = "pycparser", marker = "implementation_name == 'cpython'" },
{ name = "pygments", marker = "implementation_name == 'cpython'" },
{ name = "rich", marker = "implementation_name == 'cpython'" },
]
pin-deno = [
{ name = "deno" },
]
pin-secretstorage = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
{ name = "cryptography" },
{ name = "jeepney" },
{ name = "pycparser", marker = "implementation_name != 'PyPy' and platform_python_implementation != 'PyPy'" },
{ name = "secretstorage" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
secretstorage = [ secretstorage = [
{ name = "secretstorage" }, { name = "secretstorage" },
] ]
@ -1200,19 +1236,47 @@ test = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "brotli", marker = "implementation_name == 'cpython' and sys_platform != 'ios' and extra == 'default'" }, { name = "brotli", marker = "implementation_name == 'cpython' and sys_platform != 'ios' and extra == 'default'" },
{ name = "brotli", marker = "implementation_name == 'cpython' and sys_platform != 'ios' and extra == 'pin'", specifier = "==1.2.0" },
{ name = "brotlicffi", marker = "implementation_name != 'cpython' and extra == 'default'" }, { name = "brotlicffi", marker = "implementation_name != 'cpython' and extra == 'default'" },
{ name = "brotlicffi", marker = "implementation_name != 'cpython' and extra == 'pin'", specifier = "==1.2.0.1" },
{ name = "certifi", marker = "implementation_name == 'cpython' and extra == 'pin-curl-cffi'", specifier = "==2026.2.25" },
{ name = "certifi", marker = "extra == 'default'" }, { name = "certifi", marker = "extra == 'default'" },
{ name = "certifi", marker = "extra == 'pin'", specifier = "==2026.2.25" },
{ name = "cffi", marker = "platform_python_implementation != 'PyPy' and extra == 'pin-secretstorage'", specifier = "==2.0.0" },
{ name = "cffi", marker = "implementation_name == 'cpython' and extra == 'pin-curl-cffi'", specifier = "==2.0.0" },
{ name = "cffi", marker = "implementation_name != 'cpython' and extra == 'pin'", specifier = "==2.0.0" },
{ name = "charset-normalizer", marker = "extra == 'pin'", specifier = "==3.4.6" },
{ name = "cryptography", marker = "extra == 'pin-secretstorage'", specifier = "==46.0.6" },
{ name = "curl-cffi", marker = "implementation_name == 'cpython' and extra == 'curl-cffi'", specifier = ">=0.5.10,!=0.6.*,!=0.7.*,!=0.8.*,!=0.9.*,<0.16" }, { name = "curl-cffi", marker = "implementation_name == 'cpython' and extra == 'curl-cffi'", specifier = ">=0.5.10,!=0.6.*,!=0.7.*,!=0.8.*,!=0.9.*,<0.16" },
{ name = "curl-cffi", marker = "implementation_name == 'cpython' and extra == 'pin-curl-cffi'", specifier = "==0.15.0" },
{ name = "deno", marker = "extra == 'deno'", specifier = ">=2.6.6" }, { name = "deno", marker = "extra == 'deno'", specifier = ">=2.6.6" },
{ name = "deno", marker = "extra == 'pin-deno'", specifier = "==2.7.8" },
{ name = "idna", marker = "extra == 'pin'", specifier = "==3.11" },
{ name = "jeepney", marker = "extra == 'pin-secretstorage'", specifier = "==0.9.0" },
{ name = "markdown-it-py", marker = "implementation_name == 'cpython' and extra == 'pin-curl-cffi'", specifier = "==4.0.0" },
{ name = "mdurl", marker = "implementation_name == 'cpython' and extra == 'pin-curl-cffi'", specifier = "==0.1.2" },
{ name = "mutagen", marker = "extra == 'default'" }, { name = "mutagen", marker = "extra == 'default'" },
{ name = "mutagen", marker = "extra == 'pin'", specifier = "==1.47.0" },
{ name = "pycparser", marker = "implementation_name != 'PyPy' and platform_python_implementation != 'PyPy' and extra == 'pin-secretstorage'", specifier = "==3.0" },
{ name = "pycparser", marker = "implementation_name != 'PyPy' and implementation_name != 'cpython' and extra == 'pin'", specifier = "==3.0" },
{ name = "pycparser", marker = "implementation_name == 'cpython' and extra == 'pin-curl-cffi'", specifier = "==3.0" },
{ name = "pycryptodomex", marker = "extra == 'default'" }, { name = "pycryptodomex", marker = "extra == 'default'" },
{ name = "pycryptodomex", marker = "extra == 'pin'", specifier = "==3.23.0" },
{ name = "pygments", marker = "implementation_name == 'cpython' and extra == 'pin-curl-cffi'", specifier = "==2.19.2" },
{ name = "requests", marker = "extra == 'default'", specifier = ">=2.32.2,<3" }, { name = "requests", marker = "extra == 'default'", specifier = ">=2.32.2,<3" },
{ name = "requests", marker = "extra == 'pin'", specifier = "==2.33.0" },
{ name = "rich", marker = "implementation_name == 'cpython' and extra == 'pin-curl-cffi'", specifier = "==14.3.3" },
{ name = "secretstorage", marker = "extra == 'pin-secretstorage'", specifier = "==3.5.0" },
{ name = "secretstorage", marker = "extra == 'secretstorage'" }, { name = "secretstorage", marker = "extra == 'secretstorage'" },
{ name = "typing-extensions", marker = "python_full_version < '3.11' and extra == 'pin-secretstorage'", specifier = "==4.15.0" },
{ name = "urllib3", marker = "extra == 'default'", specifier = ">=2.0.2,<3" }, { name = "urllib3", marker = "extra == 'default'", specifier = ">=2.0.2,<3" },
{ name = "urllib3", marker = "extra == 'pin'", specifier = "==2.6.3" },
{ name = "websockets", marker = "extra == 'default'", specifier = ">=13.0" }, { name = "websockets", marker = "extra == 'default'", specifier = ">=13.0" },
{ name = "websockets", marker = "extra == 'pin'", specifier = "==16.0" },
{ name = "yt-dlp-ejs", marker = "extra == 'default'", specifier = "==0.8.0" }, { name = "yt-dlp-ejs", marker = "extra == 'default'", specifier = "==0.8.0" },
{ name = "yt-dlp-ejs", marker = "extra == 'pin'", specifier = "==0.8.0" },
] ]
provides-extras = ["curl-cffi", "default", "deno", "secretstorage"] provides-extras = ["curl-cffi", "default", "deno", "pin", "pin-curl-cffi", "pin-deno", "pin-secretstorage", "secretstorage"]
[package.metadata.requires-dev] [package.metadata.requires-dev]
build = [ build = [

View File

@ -3,9 +3,7 @@ import collections
import contextlib import contextlib
import functools import functools
import getpass import getpass
import http.client
import http.cookiejar import http.cookiejar
import http.cookies
import inspect import inspect
import itertools import itertools
import json import json

View File

@ -1322,7 +1322,7 @@ class PeerTubeIE(InfoExtractor):
) )
(?P<id>{_UUID_RE}) (?P<id>{_UUID_RE})
''' '''
_EMBED_REGEX = [r'''(?x)<iframe[^>]+\bsrc=["\'](?P<url>(?:https?:)?//{_INSTANCES_RE}/videos/embed/{cls._UUID_RE})'''] _EMBED_REGEX = [rf'''(?x)<iframe[^>]+\bsrc=["\'](?P<url>(?:https?:)?//{_INSTANCES_RE}/videos/embed/{_UUID_RE})''']
_TESTS = [{ _TESTS = [{
'url': 'https://framatube.org/videos/watch/9c9de5e8-0a1e-484a-b099-e80766180a6d', 'url': 'https://framatube.org/videos/watch/9c9de5e8-0a1e-484a-b099-e80766180a6d',
'md5': '8563064d245a4be5705bddb22bb00a28', 'md5': '8563064d245a4be5705bddb22bb00a28',

View File

@ -1180,7 +1180,7 @@ def create_parser():
workarounds.add_option( workarounds.add_option(
'--prefer-insecure', '--prefer-unsecure', '--prefer-insecure', '--prefer-unsecure',
action='store_true', dest='prefer_insecure', action='store_true', dest='prefer_insecure',
help='Use an unencrypted connection to retrieve information about the video (Currently supported only for YouTube)') help='Use an unencrypted connection to retrieve information about the video')
workarounds.add_option( workarounds.add_option(
'--user-agent', '--user-agent',
metavar='UA', dest='user_agent', metavar='UA', dest='user_agent',

View File

@ -41,12 +41,12 @@ def _find_exe(basename: str) -> str:
else: else:
exts = tuple(ext for ext in pathext.split(os.pathsep) if ext) exts = tuple(ext for ext in pathext.split(os.pathsep) if ext)
visited = [] visited = set()
for path in map(os.path.realpath, paths): for path in map(os.path.realpath, paths):
normed = os.path.normcase(path) normed = os.path.normcase(path)
if normed in visited: if normed in visited:
continue continue
visited.append(normed) visited.add(normed)
for ext in exts: for ext in exts:
binary = os.path.join(path, f'{basename}{ext}') binary = os.path.join(path, f'{basename}{ext}')