Compare commits

..

No commits in common. "master" and "2026.07.04" have entirely different histories.

20 changed files with 907 additions and 1446 deletions

View File

@ -43,61 +43,45 @@ jobs:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
persist-credentials: false persist-credentials: false
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
allow-prereleases: true allow-prereleases: true
- name: Install Deno - name: Install Deno
uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4 uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4
with: with:
deno-version: '2.3.0' # minimum supported version deno-version: '2.3.0' # minimum supported version
- name: Install Bun - name: Install Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with: with:
bun-version: '1.2.11' # minimum supported version bun-version: '1.2.11' # minimum supported version
no-cache: true no-cache: true
- name: Install Node - name: Install Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with: with:
node-version: '22.0' # minimum supported version node-version: '22.0' # minimum supported version
- name: Install QuickJS (Linux) - name: Install QuickJS (Linux)
if: matrix.os == 'ubuntu-latest' if: matrix.os == 'ubuntu-latest'
shell: bash shell: bash
run: | run: |
wget "https://bellard.org/quickjs/binary_releases/quickjs-linux-x86_64-${QJS_VERSION}.zip" -O quickjs.zip wget "https://bellard.org/quickjs/binary_releases/quickjs-linux-x86_64-${QJS_VERSION}.zip" -O quickjs.zip
unzip quickjs.zip qjs unzip quickjs.zip qjs
sudo install qjs /usr/local/bin/qjs sudo install qjs /usr/local/bin/qjs
- name: Install QuickJS (Windows) - name: Install QuickJS (Windows)
if: matrix.os == 'windows-latest' if: matrix.os == 'windows-latest'
shell: pwsh shell: pwsh
run: | run: |
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
$PSNativeCommandUseErrorActionPreference = $true $PSNativeCommandUseErrorActionPreference = $true
Invoke-WebRequest "https://bellard.org/quickjs/binary_releases/quickjs-win-x86_64-${Env:QJS_VERSION}.zip" -OutFile quickjs.zip Invoke-WebRequest "https://bellard.org/quickjs/binary_releases/quickjs-win-x86_64-${Env:QJS_VERSION}.zip" -OutFile quickjs.zip
unzip quickjs.zip unzip quickjs.zip
- name: Install test requirements
- name: Install test requirements (cpython) shell: bash
if: ${{ !startsWith(matrix.python-version, 'pypy') }}
run: | run: |
python -m pip install -U --require-hashes -r "bundle/requirements/pip.txt" python -m pip install -U --require-hashes -r "bundle/requirements/pip.txt"
python -m pip install -U --require-hashes -r "bundle/requirements/test.txt" python -m pip install -U --require-hashes -r "bundle/requirements/test.txt"
python -m pip install -U --require-hashes -r "bundle/requirements/default.txt" python -m pip install -U --require-hashes -r "bundle/requirements/default.txt"
- name: Install test requirements (PyPy)
if: ${{ startsWith(matrix.python-version, 'pypy') }}
run: |
# Upgrading/downgrading pip on Windows with actions/setup-python's PyPy can cause breakage
# See https://github.com/actions/setup-python/issues/1348
python -m pip install -U --require-hashes -r "bundle/requirements/test.txt"
python -m pip install -U --require-hashes -r "bundle/requirements/default.txt"
- name: Run tests - name: Run tests
timeout-minutes: 15 timeout-minutes: 15
shell: bash shell: bash

View File

@ -85,8 +85,7 @@ jobs:
- name: Install test requirements (PyPy) - name: Install test requirements (PyPy)
if: ${{ startsWith(matrix.python-version, 'pypy') }} if: ${{ startsWith(matrix.python-version, 'pypy') }}
run: | run: |
# Upgrading/downgrading pip on Windows with actions/setup-python's PyPy can cause breakage python -m pip install -U --require-hashes -r "bundle/requirements/pip.txt"
# See https://github.com/actions/setup-python/issues/1348
python -m pip install -U --require-hashes -r "bundle/requirements/test.txt" python -m pip install -U --require-hashes -r "bundle/requirements/test.txt"
python -m pip install -U --require-hashes -r "bundle/requirements/default.txt" python -m pip install -U --require-hashes -r "bundle/requirements/default.txt"

View File

@ -280,7 +280,7 @@ jobs:
fi fi
printf '\n\n%s\n\n%s%s%s\n\n---\n' \ printf '\n\n%s\n\n%s%s%s\n\n---\n' \
"#### A description of the various files is in the [README](https://github.com/${REPOSITORY}#release-files)" \ "#### A description of the various files is in the [README](https://github.com/${REPOSITORY}#release-files)" \
"The zipimport Unix executable and release tarball contain code licensed under ISC and MIT. " \ "The zipimport Unix executable contains code licensed under ISC and MIT. " \
"The PyInstaller-bundled executables are subject to these and other licenses, all of which are compiled in " \ "The PyInstaller-bundled executables are subject to these and other licenses, all of which are compiled in " \
"[THIRD_PARTY_LICENSES.txt](https://github.com/${BASE_REPO}/blob/${HEAD_SHA}/THIRD_PARTY_LICENSES.txt)" >> ./RELEASE_NOTES "[THIRD_PARTY_LICENSES.txt](https://github.com/${BASE_REPO}/blob/${HEAD_SHA}/THIRD_PARTY_LICENSES.txt)" >> ./RELEASE_NOTES
python ./devscripts/make_changelog.py -vv --collapsible >> ./RELEASE_NOTES python ./devscripts/make_changelog.py -vv --collapsible >> ./RELEASE_NOTES

View File

@ -142,11 +142,11 @@ While yt-dlp is licensed under the [Unlicense](LICENSE), many of the release fil
Most notably, the PyInstaller-bundled executables include GPLv3+ licensed code, and as such the combined work is licensed under [GPLv3+](https://www.gnu.org/licenses/gpl-3.0.html). Most notably, the PyInstaller-bundled executables include GPLv3+ licensed code, and as such the combined work is licensed under [GPLv3+](https://www.gnu.org/licenses/gpl-3.0.html).
The zipimport Unix executable (`yt-dlp`) and release tarball (`yt-dlp.tar.gz`) contain [ISC](https://github.com/meriyah/meriyah/blob/main/LICENSE.md) licensed code from [`meriyah`](https://github.com/meriyah/meriyah) and [MIT](https://github.com/davidbonnet/astring/blob/main/LICENSE) licensed code from [`astring`](https://github.com/davidbonnet/astring). The zipimport Unix executable (`yt-dlp`) contains [ISC](https://github.com/meriyah/meriyah/blob/main/LICENSE.md) licensed code from [`meriyah`](https://github.com/meriyah/meriyah) and [MIT](https://github.com/davidbonnet/astring/blob/main/LICENSE) licensed code from [`astring`](https://github.com/davidbonnet/astring).
See [THIRD_PARTY_LICENSES.txt](THIRD_PARTY_LICENSES.txt) for more details. See [THIRD_PARTY_LICENSES.txt](THIRD_PARTY_LICENSES.txt) for more details.
The git repository, the PyPI source distribution and the PyPI built distribution (wheel) only contain code licensed under the [Unlicense](LICENSE). The git repository, the source tarball (`yt-dlp.tar.gz`), the PyPI source distribution and the PyPI built distribution (wheel) only contain code licensed under the [Unlicense](LICENSE).
<!-- MANPAGE: END EXCLUDED SECTION --> <!-- MANPAGE: END EXCLUDED SECTION -->
@ -1859,7 +1859,7 @@ The following extractors use this feature:
#### youtube #### youtube
* `lang`: Prefer translated metadata (`title`, `description` etc) of this language code (case-sensitive). By default, the video primary language metadata is preferred, with a fallback to `en` translated. See [youtube/_base.py](https://github.com/yt-dlp/yt-dlp/blob/415b4c9f955b1a0391204bd24a7132590e7b3bdb/yt_dlp/extractor/youtube/_base.py#L402-L409) for the list of supported content language codes * `lang`: Prefer translated metadata (`title`, `description` etc) of this language code (case-sensitive). By default, the video primary language metadata is preferred, with a fallback to `en` translated. See [youtube/_base.py](https://github.com/yt-dlp/yt-dlp/blob/415b4c9f955b1a0391204bd24a7132590e7b3bdb/yt_dlp/extractor/youtube/_base.py#L402-L409) for the list of supported content language codes
* `skip`: One or more of `hls`, `dash` or `translated_subs` to skip extraction of the m3u8 manifests, dash manifests and [auto-translated subtitles](https://github.com/yt-dlp/yt-dlp/issues/4090#issuecomment-1158102032) respectively * `skip`: One or more of `hls`, `dash` or `translated_subs` to skip extraction of the m3u8 manifests, dash manifests and [auto-translated subtitles](https://github.com/yt-dlp/yt-dlp/issues/4090#issuecomment-1158102032) respectively
* `player_client`: Clients to extract video data from. The currently available clients are `web`, `web_safari`, `web_embedded`, `web_music`, `web_creator`, `mweb`, `ios`, `visionos`, `android`, `android_vr`, `tv`, `tv_downgraded`, and `tv_simply`. By default, `visionos,android_vr,web` is used. If no JavaScript runtime/engine is available, then `web` is omitted. If logged-in cookies are passed to yt-dlp, then `tv_downgraded,web` is used for free accounts and `tv_downgraded,web_creator,web` is used for premium accounts. The `web_music` client is added for `music.youtube.com` URLs when logged-in cookies are used. The `web_embedded` client is added for age-restricted videos but only successfully works around the age-restriction sometimes (e.g. if the video is embeddable). The `tv_downgraded` client may be added as a fallback if `android_vr` or `visionos` is unable to access a video. The `web_creator` client is added for age-restricted videos if account age-verification is required. Some clients, such as `web_creator` and `web_music`, require a `po_token` for their formats to be downloadable. Some clients, such as `web_creator`, will only work with authentication. Not all clients support authentication via cookies. You can use `default` for the default clients, or you can use `all` for all clients (not recommended). You can prefix a client with `-` to exclude it, e.g. `youtube:player_client=default,-web` * `player_client`: Clients to extract video data from. The currently available clients are `web`, `web_safari`, `web_embedded`, `web_music`, `web_creator`, `mweb`, `ios`, `android`, `android_vr`, `tv`, `tv_downgraded`, and `tv_simply`. By default, `android_vr,web_safari` is used. If no JavaScript runtime/engine is available, then only `android_vr` is used. If logged-in cookies are passed to yt-dlp, then `tv_downgraded,web_safari` is used for free accounts and `tv_downgraded,web_creator` is used for premium accounts. The `web_music` client is added for `music.youtube.com` URLs when logged-in cookies are used. The `web_embedded` client is added for age-restricted videos but only successfully works around the age-restriction sometimes (e.g. if the video is embeddable), and may be added as a fallback if `android_vr` is unable to access a video. The `web_creator` client is added for age-restricted videos if account age-verification is required. Some clients, such as `web_creator` and `web_music`, require a `po_token` for their formats to be downloadable. Some clients, such as `web_creator`, will only work with authentication. Not all clients support authentication via cookies. You can use `default` for the default clients, or you can use `all` for all clients (not recommended). You can prefix a client with `-` to exclude it, e.g. `youtube:player_client=default,-web_safari`
* `player_skip`: Skip some network requests that are generally needed for robust extraction. One or more of `configs` (skip client configs), `webpage` (skip initial webpage), `js` (skip js player), `initial_data` (skip initial data/next ep request). While these options can help reduce the number of requests needed or avoid some rate-limiting, they could cause issues such as missing formats or metadata. See [#860](https://github.com/yt-dlp/yt-dlp/pull/860) and [#12826](https://github.com/yt-dlp/yt-dlp/issues/12826) for more details * `player_skip`: Skip some network requests that are generally needed for robust extraction. One or more of `configs` (skip client configs), `webpage` (skip initial webpage), `js` (skip js player), `initial_data` (skip initial data/next ep request). While these options can help reduce the number of requests needed or avoid some rate-limiting, they could cause issues such as missing formats or metadata. See [#860](https://github.com/yt-dlp/yt-dlp/pull/860) and [#12826](https://github.com/yt-dlp/yt-dlp/issues/12826) for more details
* `webpage_skip`: Skip extraction of embedded webpage data. One or both of `player_response`, `initial_data`. These options are for testing purposes and don't skip any network requests. Neither is skipped by default; however, if a `player_js_version` value other than `actual` is used, then `webpage_skip=player_response` is implied * `webpage_skip`: Skip extraction of embedded webpage data. One or both of `player_response`, `initial_data`. These options are for testing purposes and don't skip any network requests. Neither is skipped by default; however, if a `player_js_version` value other than `actual` is used, then `webpage_skip=player_response` is implied
* `webpage_client`: Client to use for the video webpage request. One of `web` or `web_safari` (default) * `webpage_client`: Client to use for the video webpage request. One of `web` or `web_safari` (default)
@ -1975,7 +1975,7 @@ The following extractors use this feature:
* `backend`: Backend API to use for extraction - one of `streaks` (default) or `brightcove` (deprecated) * `backend`: Backend API to use for extraction - one of `streaks` (default) or `brightcove` (deprecated)
#### vimeo #### vimeo
* `client`: Client to extract video data from. The currently available clients are `android`, `macos_basic`, and `web`. Only one client can be used. The `macos_basic` client is used by default, but the `web` client is used when logged-in. The `web` client only works with account cookies or login credentials. The `android` client only works with previously cached OAuth tokens * `client`: Client to extract video data from. The currently available clients are `android`, `ios`, `macos` and `web`. Only one client can be used. The `macos` client is used by default, but the `web` client is used when logged-in. The `web` client only works with account cookies or login credentials. The `android` and `ios` clients only work with previously cached OAuth tokens
* `original_format_policy`: Policy for when to try extracting original formats. One of `always`, `never`, or `auto`. The default `auto` policy tries to avoid exceeding the web client's API rate-limit by only making an extra request when Vimeo publicizes the video's downloadability * `original_format_policy`: Policy for when to try extracting original formats. One of `always`, `never`, or `auto`. The default `auto` policy tries to avoid exceeding the web client's API rate-limit by only making an extra request when Vimeo publicizes the video's downloadability
#### zan #### zan

File diff suppressed because it is too large Load Diff

View File

@ -38,16 +38,11 @@ def main():
f'--name={name}', f'--name={name}',
'--icon=devscripts/logo.ico', '--icon=devscripts/logo.ico',
'--upx-exclude=vcruntime140.dll', '--upx-exclude=vcruntime140.dll',
# setuptools and packaging are PyInstaller runtime dependencies,
# but would be collected due to cffi's imports if we don't exclude
'--exclude-module=setuptools',
'--exclude-module=packaging',
# Ref: https://github.com/yt-dlp/yt-dlp/issues/13311 # Ref: https://github.com/yt-dlp/yt-dlp/issues/13311
# https://github.com/pyinstaller/pyinstaller/issues/9149 # https://github.com/pyinstaller/pyinstaller/issues/9149
'--exclude-module=pkg_resources', '--exclude-module=pkg_resources',
'--noconfirm', '--noconfirm',
'--additional-hooks-dir=yt_dlp/__pyinstaller', '--additional-hooks-dir=yt_dlp/__pyinstaller',
'--add-data=THIRD_PARTY_LICENSES.txt:.',
*opts, *opts,
'yt_dlp/__main__.py', 'yt_dlp/__main__.py',
] ]

View File

@ -382,9 +382,5 @@
"action": "add", "action": "add",
"when": "b6590aaa1e3808155d69c9a79a797ae484163789", "when": "b6590aaa1e3808155d69c9a79a797ae484163789",
"short": "[priority] Security: [[CVE-2026-55404](https://nvd.nist.gov/vuln/detail/CVE-2026-55404)] [Downstream command injection via improper sanitization of --write-link output](https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-6v4j-43gg-vj32)\n - Shortcut file data is now properly validated and sanitized when the `--write-link` options are used" "short": "[priority] Security: [[CVE-2026-55404](https://nvd.nist.gov/vuln/detail/CVE-2026-55404)] [Downstream command injection via improper sanitization of --write-link output](https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-6v4j-43gg-vj32)\n - Shortcut file data is now properly validated and sanitized when the `--write-link` options are used"
},
{
"action": "remove",
"when": "a8be438aac1b90c3888e974056d967b8be90fa7e"
} }
] ]

View File

@ -10,7 +10,7 @@ HEADER = '''THIRD-PARTY LICENSES
This file aggregates license texts of third-party components included with the yt-dlp PyInstaller-bundled executables. This file aggregates license texts of third-party components included with the yt-dlp PyInstaller-bundled executables.
yt-dlp itself is licensed under the Unlicense (see LICENSE file). yt-dlp itself is licensed under the Unlicense (see LICENSE file).
Source code for bundled third-party components is available from the original projects. Source code for bundled third-party components is available from the original projects.
If you cannot obtain it, the maintainers will provide it as per license obligation: email maintainers@yt-dlp.org''' If you cannot obtain it, the maintainers will provide it as per license obligation; maintainer emails are listed in pyproject.toml.'''
@dataclass(frozen=True) @dataclass(frozen=True)
@ -47,13 +47,6 @@ DEPENDENCIES: list[Dependency] = [
license_url='https://raw.githubusercontent.com/libffi/libffi/refs/heads/master/LICENSE', license_url='https://raw.githubusercontent.com/libffi/libffi/refs/heads/master/LICENSE',
project_url='https://sourceware.org/libffi/', project_url='https://sourceware.org/libffi/',
), ),
Dependency(
name='OpenSSL 1.x',
license='OpenSSL',
license_url='https://raw.githubusercontent.com/openssl/openssl/refs/tags/OpenSSL_1_1_1t/LICENSE',
comment='Only included in `yt-dlp.exe` and `yt-dlp_x86.exe` Windows builds',
project_url='https://www.openssl.org/',
),
Dependency( Dependency(
name='OpenSSL 3.0+', name='OpenSSL 3.0+',
license='Apache-2.0', license='Apache-2.0',
@ -130,79 +123,43 @@ DEPENDENCIES: list[Dependency] = [
name='libintl', name='libintl',
license='LGPL-2.1-or-later', license='LGPL-2.1-or-later',
license_url='https://raw.githubusercontent.com/autotools-mirror/gettext/refs/heads/master/gettext-runtime/intl/COPYING.LIB', license_url='https://raw.githubusercontent.com/autotools-mirror/gettext/refs/heads/master/gettext-runtime/intl/COPYING.LIB',
comment='Only included in Linux builds', comment='Only included in macOS builds',
project_url='https://www.gnu.org/software/gettext/', project_url='https://www.gnu.org/software/gettext/',
), ),
Dependency( Dependency(
name='libidn2', name='libidn2',
license='LGPL-3.0-or-later', license='LGPL-3.0-or-later',
license_url='https://gitlab.com/libidn/libidn2/-/raw/master/COPYING.LESSERv3', license_url='https://gitlab.com/libidn/libidn2/-/raw/master/COPYING.LESSERv3',
comment='Only included in Linux builds', comment='Only included in macOS builds',
project_url='https://www.gnu.org/software/libidn/', project_url='https://www.gnu.org/software/libidn/',
), ),
Dependency( Dependency(
name='libidn2 (Unicode character data files)', name='libidn2 (Unicode character data files)',
license='Unicode-TOU AND Unicode-DFS-2016', license='Unicode-TOU AND Unicode-DFS-2016',
license_url='https://gitlab.com/libidn/libidn2/-/raw/master/COPYING.unicode', license_url='https://gitlab.com/libidn/libidn2/-/raw/master/COPYING.unicode',
comment='Only included in Linux builds', comment='Only included in macOS builds',
project_url='https://www.gnu.org/software/libidn/', project_url='https://www.gnu.org/software/libidn/',
), ),
Dependency( Dependency(
name='libunistring', name='libunistring',
license='LGPL-3.0-or-later', license='LGPL-3.0-or-later',
license_url='https://gitweb.git.savannah.gnu.org/gitweb/?p=libunistring.git;a=blob_plain;f=COPYING.LIB;hb=HEAD', license_url='https://gitweb.git.savannah.gnu.org/gitweb/?p=libunistring.git;a=blob_plain;f=COPYING.LIB;hb=HEAD',
comment='Only included in Linux builds', comment='Only included in macOS builds',
project_url='https://www.gnu.org/software/libunistring/', project_url='https://www.gnu.org/software/libunistring/',
), ),
# Non-Python dependencies of curl_cffi
Dependency( Dependency(
name='curl-impersonate', name='librtmp',
license='MIT', license='LGPL-2.1-or-later',
license_url='https://raw.githubusercontent.com/lexiforest/curl-impersonate/refs/heads/main/LICENSE', # No official repo URL
comment='Not included in `yt-dlp_x86.exe` Windows builds', license_url='https://gist.githubusercontent.com/seproDev/31d8c691ccddebe37b8b379307cb232d/raw/053408e98547ea8c7d9ba3a80c965f33e163b881/librtmp_COPYING.txt',
project_url='https://github.com/lexiforest/curl-impersonate', comment='Only included in macOS builds',
), project_url='https://rtmpdump.mplayerhq.hu/',
Dependency(
name='curl',
license='curl',
license_url='https://raw.githubusercontent.com/curl/curl/refs/heads/master/LICENSES/curl.txt',
comment='Not included in `yt-dlp_x86.exe` Windows builds',
project_url='https://curl.se/',
),
Dependency(
name='BoringSSL',
license='Apache-2.0',
license_url='https://raw.githubusercontent.com/google/boringssl/refs/heads/main/LICENSE',
comment='Not included in `yt-dlp_x86.exe` Windows builds',
project_url='https://boringssl.googlesource.com/boringssl',
),
Dependency(
name='nghttp2',
license='MIT',
license_url='https://raw.githubusercontent.com/nghttp2/nghttp2/refs/heads/master/COPYING',
comment='Not included in `yt-dlp_x86.exe` Windows builds',
project_url='https://nghttp2.org/',
),
Dependency(
name='ngtcp2',
license='MIT',
license_url='https://raw.githubusercontent.com/ngtcp2/ngtcp2/refs/heads/main/COPYING',
comment='Not included in `yt-dlp_x86.exe` Windows builds',
project_url='https://nghttp2.org/ngtcp2/',
),
Dependency(
name='nghttp3',
license='MIT',
license_url='https://raw.githubusercontent.com/ngtcp2/nghttp3/refs/heads/main/COPYING',
comment='Not included in `yt-dlp_x86.exe` Windows builds',
project_url='https://nghttp2.org/nghttp3/',
), ),
Dependency( Dependency(
name='zstd', name='zstd',
license='BSD-3-Clause', license='BSD-3-Clause',
license_url='https://raw.githubusercontent.com/facebook/zstd/refs/heads/dev/LICENSE', license_url='https://raw.githubusercontent.com/facebook/zstd/refs/heads/dev/LICENSE',
comment='Not included in `yt-dlp_x86.exe` Windows builds', comment='Only included in macOS builds',
project_url='https://facebook.github.io/zstd/', project_url='https://facebook.github.io/zstd/',
), ),
@ -217,74 +174,28 @@ DEPENDENCIES: list[Dependency] = [
name='curl_cffi', name='curl_cffi',
license='MIT', license='MIT',
license_url='https://raw.githubusercontent.com/lexiforest/curl_cffi/refs/heads/main/LICENSE', license_url='https://raw.githubusercontent.com/lexiforest/curl_cffi/refs/heads/main/LICENSE',
comment='Not included in `yt-dlp_x86.exe` Windows builds', comment='Not included in `yt-dlp_x86` and `yt-dlp_musllinux_aarch64` builds',
project_url='https://curl-cffi.readthedocs.io/', project_url='https://curl-cffi.readthedocs.io/',
), ),
# curl_cffi vendored code:
# - https://github.com/lexiforest/curl_cffi/blob/v0.15.0/curl_cffi/_asyncio_selector.py
Dependency(
name='Tornado',
license='Apache-2.0',
license_url='https://raw.githubusercontent.com/tornadoweb/tornado/master/LICENSE',
comment='Not included in `yt-dlp_x86.exe` Windows builds',
project_url='http://www.tornadoweb.org/',
),
# curl_cffi vendored code:
# - https://github.com/lexiforest/curl_cffi/blob/v0.15.0/curl_cffi/requests/cookies.py
# - https://github.com/lexiforest/curl_cffi/blob/v0.15.0/curl_cffi/requests/headers.py
Dependency(
name='httpx',
license='BSD-3-Clause',
license_url='https://github.com/encode/httpx/raw/master/LICENSE.md',
comment='Not included in `yt-dlp_x86.exe` Windows builds',
project_url='https://www.python-httpx.org/',
),
# Dependency of curl_cffi # Dependency of curl_cffi
Dependency( Dependency(
name='rich', name='curl-impersonate',
license='MIT', license='MIT',
license_url='https://raw.githubusercontent.com/Textualize/rich/refs/heads/main/LICENSE', license_url='https://raw.githubusercontent.com/lexiforest/curl-impersonate/refs/heads/main/LICENSE',
comment='Not included in `yt-dlp_x86.exe` Windows builds', comment='Not included in `yt-dlp_x86` and `yt-dlp_musllinux_aarch64` builds',
project_url='https://rich.readthedocs.io/', project_url='https://github.com/lexiforest/curl-impersonate',
), ),
# Dependency of rich
Dependency(
name='pygments',
license='BSD-2-Clause',
license_url='https://raw.githubusercontent.com/pygments/pygments/refs/heads/master/LICENSE',
comment='Not included in `yt-dlp_x86.exe` Windows builds',
project_url='http://pygments.org/',
),
# Dependency of rich
Dependency(
name='markdown-it-py',
license='MIT',
license_url='https://raw.githubusercontent.com/executablebooks/markdown-it-py/refs/heads/master/LICENSE',
comment='Not included in `yt-dlp_x86.exe` Windows builds',
project_url='https://markdown-it-py.readthedocs.io/',
),
# Dependency of markdown-it-py
Dependency(
name='mdurl',
license='MIT',
license_url='https://raw.githubusercontent.com/executablebooks/mdurl/refs/heads/master/LICENSE',
comment='Not included in `yt-dlp_x86.exe` Windows builds',
project_url='https://github.com/executablebooks/mdurl',
),
# Dependency of cryptography and curl_cffi
Dependency( Dependency(
name='cffi', name='cffi',
license='MIT-0', # Technically does not need to be included license='MIT-0', # Technically does not need to be included
license_url='https://raw.githubusercontent.com/python-cffi/cffi/refs/heads/main/LICENSE', license_url='https://raw.githubusercontent.com/python-cffi/cffi/refs/heads/main/LICENSE',
comment='Not included in `yt-dlp_x86.exe` Windows builds',
project_url='https://cffi.readthedocs.io/', project_url='https://cffi.readthedocs.io/',
), ),
# Dependency of cffi # Dependecy of cffi
Dependency( Dependency(
name='pycparser', name='pycparser',
license='BSD-3-Clause', license='BSD-3-Clause',
license_url='https://raw.githubusercontent.com/eliben/pycparser/refs/heads/main/LICENSE', license_url='https://raw.githubusercontent.com/eliben/pycparser/refs/heads/main/LICENSE',
comment='Not included in `yt-dlp_x86.exe` Windows builds',
project_url='https://github.com/eliben/pycparser', project_url='https://github.com/eliben/pycparser',
), ),
Dependency( Dependency(
@ -365,14 +276,12 @@ DEPENDENCIES: list[Dependency] = [
name='Meriyah', name='Meriyah',
license='ISC', license='ISC',
license_url='https://raw.githubusercontent.com/meriyah/meriyah/refs/heads/main/LICENSE.md', license_url='https://raw.githubusercontent.com/meriyah/meriyah/refs/heads/main/LICENSE.md',
comment='Also included in `yt-dlp` zipimport Unix executables and `yt-dlp.tar.gz` release tarballs',
project_url='https://github.com/meriyah/meriyah', project_url='https://github.com/meriyah/meriyah',
), ),
Dependency( Dependency(
name='Astring', name='Astring',
license='MIT', license='MIT',
license_url='https://raw.githubusercontent.com/davidbonnet/astring/refs/heads/main/LICENSE', license_url='https://raw.githubusercontent.com/davidbonnet/astring/refs/heads/main/LICENSE',
comment='Also included in `yt-dlp` zipimport Unix executables and `yt-dlp.tar.gz` release tarballs',
project_url='https://github.com/davidbonnet/astring/', project_url='https://github.com/davidbonnet/astring/',
), ),
] ]

View File

@ -300,7 +300,7 @@ class HlsFD(FragmentFD):
# We only download the first fragment during the test # We only download the first fragment during the test
if self.params.get('test', False): if self.params.get('test', False):
fragments = fragments[:1] fragments = [fragments[0] if fragments else None]
if real_downloader: if real_downloader:
info_dict['fragments'] = fragments info_dict['fragments'] = fragments

View File

@ -106,6 +106,7 @@ from .archiveorg import (
) )
from .arcpublishing import ArcPublishingIE from .arcpublishing import ArcPublishingIE
from .ard import ( from .ard import (
ARDIE,
ARDAudiothekIE, ARDAudiothekIE,
ARDAudiothekPlaylistIE, ARDAudiothekPlaylistIE,
ARDBetaMediathekIE, ARDBetaMediathekIE,
@ -1718,10 +1719,7 @@ from .shahid import (
from .sharepoint import SharePointIE from .sharepoint import SharePointIE
from .shemaroome import ShemarooMeIE from .shemaroome import ShemarooMeIE
from .shiey import ShieyIE from .shiey import ShieyIE
from .showroomlive import ( from .showroomlive import ShowRoomLiveIE
ShowRoomLiveIE,
ShowRoomVodIE,
)
from .sibnet import SibnetEmbedIE from .sibnet import SibnetEmbedIE
from .simplecast import ( from .simplecast import (
SimplecastEpisodeIE, SimplecastEpisodeIE,

View File

@ -1,16 +1,12 @@
import itertools import json
import re
from .common import InfoExtractor from .common import InfoExtractor
from ..networking.exceptions import HTTPError
from ..utils import ( from ..utils import (
ExtractorError,
clean_html, clean_html,
int_or_none, int_or_none,
try_get, try_get,
unified_strdate, unified_strdate,
unified_timestamp, unified_timestamp,
urljoin,
) )
@ -21,7 +17,7 @@ class AmericasTestKitchenIE(InfoExtractor):
'md5': 'b861c3e365ac38ad319cfd509c30577f', 'md5': 'b861c3e365ac38ad319cfd509c30577f',
'info_dict': { 'info_dict': {
'id': '5b400b9ee338f922cb06450c', 'id': '5b400b9ee338f922cb06450c',
'title': 'Weeknight Japanese Suppers', 'title': 'Japanese Suppers',
'ext': 'mp4', 'ext': 'mp4',
'display_id': 'weeknight-japanese-suppers', 'display_id': 'weeknight-japanese-suppers',
'description': 'md5:64e606bfee910627efc4b5f050de92b3', 'description': 'md5:64e606bfee910627efc4b5f050de92b3',
@ -110,44 +106,23 @@ class AmericasTestKitchenIE(InfoExtractor):
class AmericasTestKitchenSeasonIE(InfoExtractor): class AmericasTestKitchenSeasonIE(InfoExtractor):
_VALID_URL = r'''(?x) _VALID_URL = r'https?://(?:www\.)?(?P<show>americastestkitchen|(?P<cooks>cooks(?:country|illustrated)))\.com(?:(?:/(?P<show2>cooks(?:country|illustrated)))?(?:/?$|(?<!ated)(?<!ated\.com)/episodes/browse/season_(?P<season>\d+)))'
https?://(?:www\.)?(?P<domain>americastestkitchen|cookscountry|cooksillustrated)\.com
(?:/(?P<path>cookscountry|cooksillustrated))?
(?:/episodes(?:/browse)?/season[-_](?P<season>\d+))?
/?(?:[?#]|$)'''
_SHOWS = {
'americastestkitchen': ('', 'America\'s Test Kitchen'),
'cookscountry': ('/cookscountry', 'Cook\'s Country'),
'cooksillustrated': ('/cooksillustrated', 'Cook\'s Illustrated'),
}
_TESTS = [{ _TESTS = [{
# ATK Season # ATK Season
'url': 'https://www.americastestkitchen.com/episodes/season-1', 'url': 'https://www.americastestkitchen.com/episodes/browse/season_1',
'info_dict': { 'info_dict': {
'id': 'season_1', 'id': 'season_1',
'title': 'Season 1', 'title': 'Season 1',
}, },
'playlist_mincount': 13, 'playlist_count': 13,
}, {
# Latest ATK Season (new URL scheme)
'url': 'https://www.americastestkitchen.com/episodes/season-26',
'info_dict': {
'id': 'season_26',
'title': 'Season 26',
},
'playlist_count': 26,
}, { }, {
# Cooks Country Season # Cooks Country Season
'url': 'https://www.americastestkitchen.com/cookscountry/episodes/season-12', 'url': 'https://www.americastestkitchen.com/cookscountry/episodes/browse/season_12',
'info_dict': { 'info_dict': {
'id': 'season_12', 'id': 'season_12',
'title': 'Season 12', 'title': 'Season 12',
}, },
'playlist_mincount': 13, 'playlist_count': 13,
}, {
# Old-style URL (redirects to the new season page)
'url': 'https://www.americastestkitchen.com/episodes/browse/season_1',
'only_matching': True,
}, { }, {
# America's Test Kitchen Series # America's Test Kitchen Series
'url': 'https://www.americastestkitchen.com/', 'url': 'https://www.americastestkitchen.com/',
@ -155,7 +130,7 @@ class AmericasTestKitchenSeasonIE(InfoExtractor):
'id': 'americastestkitchen', 'id': 'americastestkitchen',
'title': 'America\'s Test Kitchen', 'title': 'America\'s Test Kitchen',
}, },
'playlist_mincount': 558, 'playlist_count': 558,
}, { }, {
# Cooks Country Series # Cooks Country Series
'url': 'https://www.americastestkitchen.com/cookscountry', 'url': 'https://www.americastestkitchen.com/cookscountry',
@ -163,7 +138,7 @@ class AmericasTestKitchenSeasonIE(InfoExtractor):
'id': 'cookscountry', 'id': 'cookscountry',
'title': 'Cook\'s Country', 'title': 'Cook\'s Country',
}, },
'playlist_mincount': 199, 'playlist_count': 199,
}, { }, {
'url': 'https://www.americastestkitchen.com/cookscountry/', 'url': 'https://www.americastestkitchen.com/cookscountry/',
'only_matching': True, 'only_matching': True,
@ -182,46 +157,59 @@ class AmericasTestKitchenSeasonIE(InfoExtractor):
}] }]
def _real_extract(self, url): def _real_extract(self, url):
domain, url_path, season = self._match_valid_url(url).group('domain', 'path', 'season') season_number, show1, show = self._match_valid_url(url).group('season', 'show', 'show2')
show_path, title = self._SHOWS[url_path or domain] show_path = ('/' + show) if show else ''
season = int_or_none(season) show = show or show1
season_number = int_or_none(season_number)
if season: slug, title = {
playlist_id = f'season_{season}' 'americastestkitchen': ('atk', 'America\'s Test Kitchen'),
playlist_title = f'Season {season}' 'cookscountry': ('cco', 'Cook\'s Country'),
'cooksillustrated': ('cio', 'Cook\'s Illustrated'),
}[show]
def entries(): facet_filters = [
yield from self._season_entries(show_path, season) 'search_document_klass:episode',
'search_show_slug:' + slug,
]
if season_number:
playlist_id = f'season_{season_number}'
playlist_title = f'Season {season_number}'
facet_filters.append('search_season_list:' + playlist_title)
else: else:
playlist_id = url_path or domain playlist_id = show
playlist_title = title playlist_title = title
def entries(): season_search = self._download_json(
for season_number in itertools.count(1): f'https://y1fnzxui30-dsn.algolia.net/1/indexes/everest_search_{slug}_season_desc_production',
try: playlist_id, headers={
yield from self._season_entries(show_path, season_number) 'Origin': 'https://www.americastestkitchen.com',
except ExtractorError as e: 'X-Algolia-API-Key': '8d504d0099ed27c1b73708d22871d805',
if isinstance(e.cause, HTTPError) and e.cause.status == 404: 'X-Algolia-Application-Id': 'Y1FNZXUI30',
break }, query={
raise 'facetFilters': json.dumps(facet_filters),
'attributesToRetrieve': f'description,search_{slug}_episode_number,search_document_date,search_url,title,search_atk_episode_season',
'attributesToHighlight': '',
'hitsPerPage': 1000,
})
def entries():
for episode in (season_search.get('hits') or []):
search_url = episode.get('search_url') # always formatted like '/episode/123-title-of-episode'
if not search_url:
continue
yield {
'_type': 'url',
'url': f'https://www.americastestkitchen.com{show_path or ""}{search_url}',
'id': try_get(episode, lambda e: e['objectID'].split('_')[-1]),
'title': episode.get('title'),
'description': episode.get('description'),
'timestamp': unified_timestamp(episode.get('search_document_date')),
'season_number': season_number,
'episode_number': int_or_none(episode.get(f'search_{slug}_episode_number')),
'ie_key': AmericasTestKitchenIE.ie_key(),
}
return self.playlist_result( return self.playlist_result(
entries(), playlist_id, playlist_title) entries(), playlist_id, playlist_title)
def _season_entries(self, show_path, season_number):
webpage = self._download_webpage(
f'https://www.americastestkitchen.com{show_path}/episodes/season-{season_number}',
f'season-{season_number}', f'Downloading season {season_number} webpage')
seen = set()
for episode in re.finditer(
r'<a [^>]*\bhref="(?P<path>/(?:cookscountry/|cooksillustrated/)?episode/(?P<id>\d+)-[^"]+)"[^>]*>\s*<h3[^>]*>(?P<title>[^<]+)</h3>',
webpage):
path = episode.group('path')
if path in seen:
continue
seen.add(path)
yield self.url_result(
urljoin('https://www.americastestkitchen.com', path),
AmericasTestKitchenIE, episode.group('id'),
clean_html(episode.group('title')),
season_number=season_number)

View File

@ -1,22 +1,39 @@
from .applepodcasts import AppleBaseIE import time
from .common import InfoExtractor
from ..utils import ( from ..utils import (
ExtractorError,
extract_attributes,
float_or_none, float_or_none,
jwt_decode_hs256,
jwt_encode,
parse_resolution, parse_resolution,
qualities, qualities,
unified_strdate, unified_strdate,
update_url, update_url,
url_or_none, url_or_none,
urljoin,
) )
from ..utils.traversal import ( from ..utils.traversal import (
find_element,
require, require,
traverse_obj, traverse_obj,
) )
class AppleConnectIE(AppleBaseIE): class AppleConnectIE(InfoExtractor):
IE_NAME = 'apple:music:connect' IE_NAME = 'apple:music:connect'
IE_DESC = '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+)' _VALID_URL = r'https?://music\.apple\.com/[\w-]+/post/(?P<id>\d+)'
_TESTS = [{ _TESTS = [{
'url': 'https://music.apple.com/us/post/1018290019', 'url': 'https://music.apple.com/us/post/1018290019',
@ -42,16 +59,29 @@ class AppleConnectIE(AppleBaseIE):
}, },
}] }]
_BASE_URL = 'https://music.apple.com' _jwt = None
_JWT_KEY_ID = 'WebPlayKid'
_QUALITIES = { @staticmethod
'provisionalUploadVideo': None, def _jwt_is_expired(token):
'sdVideo': 480, return jwt_decode_hs256(token)['exp'] - time.time() < 120
'sdVideoWithPlusAudio': 480,
'sd480pVideo': 480, def _get_token(self, webpage, video_id):
'720pHdVideo': 720, if self._jwt and not self._jwt_is_expired(self._jwt):
'1080pHdVideo': 1080, 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): def _real_extract(self, url):
video_id = self._match_id(url) video_id = self._match_id(url)

View File

@ -1,55 +1,15 @@
import time
from .common import InfoExtractor from .common import InfoExtractor
from ..utils import ( from ..utils import (
ExtractorError,
clean_html, clean_html,
clean_podcast_url, clean_podcast_url,
int_or_none, int_or_none,
jwt_decode_hs256,
jwt_encode,
parse_iso8601, parse_iso8601,
try_call,
update_url,
url_or_none,
urljoin,
) )
from ..utils.traversal import traverse_obj from ..utils.traversal import traverse_obj
class AppleBaseIE(InfoExtractor): class ApplePodcastsIE(InfoExtractor):
"""Subclasses must set _BASE_URL and _JWT_KEY_ID""" _VALID_URL = r'https?://podcasts\.apple\.com/(?:[^/]+/)?podcast(?:/[^/]+){1,2}.*?\bi=(?P<id>\d+)'
_jwt_cache = {}
@staticmethod
def _jwt_is_expired(token):
return jwt_decode_hs256(token)['exp'] - time.time() < 120
def _get_token(self, webpage, episode_id):
if self._jwt_cache.get(self._BASE_URL) and not self._jwt_is_expired(self._jwt_cache[self._BASE_URL]):
return self._jwt
js_path = self._search_regex(
r'<script [^>]*\bsrc="(/assets/index~[0-9a-f]+\.js)">', webpage, 'JS asset path')
js_code = self._download_webpage(
urljoin(self._BASE_URL, js_path), episode_id,
'Downloading JS asset', 'Unable to download JS asset')
header = jwt_encode({}, '', headers={'typ': 'JWT', 'alg': 'ES256', 'kid': self._JWT_KEY_ID}).split('.')[0]
self._jwt_cache[self._BASE_URL] = self._search_regex(
fr'(["\'])(?P<jwt>{header}(?:\.[\w-]+){{2}})\1', js_code, 'JSON Web Token', group='jwt')
if self._jwt_is_expired(self._jwt_cache[self._BASE_URL]):
raise ExtractorError('The fetched token is already expired')
return self._jwt_cache[self._BASE_URL]
class ApplePodcastsIE(AppleBaseIE):
IE_NAME = 'apple:podcasts'
IE_DESC = 'Apple Podcasts'
_VALID_URL = r'https?://podcasts\.apple\.com/(?P<country>[^/?#]+/)?podcast(?:/[^/?#]+){1,2}/?\?(?:[^#]+&)?i=(?P<id>\d+)'
_TESTS = [{ _TESTS = [{
'url': 'https://podcasts.apple.com/us/podcast/urbana-podcast-724-by-david-penn/id1531349107?i=1000748574256', 'url': 'https://podcasts.apple.com/us/podcast/urbana-podcast-724-by-david-penn/id1531349107?i=1000748574256',
'md5': 'f8a6f92735d0cfbd5e6a7294151e28d8', 'md5': 'f8a6f92735d0cfbd5e6a7294151e28d8',
@ -63,7 +23,7 @@ class ApplePodcastsIE(AppleBaseIE):
'timestamp': 1770400801, 'timestamp': 1770400801,
'duration': 3602, 'duration': 3602,
'series': 'Urbana Radio Show', 'series': 'Urbana Radio Show',
'thumbnail': r're:https://.+/.+\.jpg', 'thumbnail': 're:.+[.](png|jpe?g|webp)',
}, },
}, { }, {
'url': 'https://podcasts.apple.com/us/podcast/207-whitney-webb-returns/id1135137367?i=1000482637777', 'url': 'https://podcasts.apple.com/us/podcast/207-whitney-webb-returns/id1135137367?i=1000482637777',
@ -79,7 +39,7 @@ class ApplePodcastsIE(AppleBaseIE):
'timestamp': 1593932400, 'timestamp': 1593932400,
'duration': 5369, 'duration': 5369,
'series': 'The Tim Dillon Show', 'series': 'The Tim Dillon Show',
'thumbnail': r're:https://.+/.+\.jpg', 'thumbnail': 're:.+[.](png|jpe?g|webp)',
}, },
}, { }, {
'url': 'https://podcasts.apple.com/podcast/207-whitney-webb-returns/id1135137367?i=1000482637777', 'url': 'https://podcasts.apple.com/podcast/207-whitney-webb-returns/id1135137367?i=1000482637777',
@ -92,55 +52,15 @@ class ApplePodcastsIE(AppleBaseIE):
'only_matching': True, 'only_matching': True,
}] }]
_BASE_URL = 'https://podcasts.apple.com' def _real_extract(self, url):
_JWT_KEY_ID = 'C4J7GBP74H' episode_id = self._match_id(url)
webpage = self._download_webpage(url, episode_id)
def _extract_podcast_from_api(self, webpage, episode_id, country_code):
data = self._download_json(
f'https://amp-api.podcasts.apple.com/v1/catalog/{country_code or "us"}/podcast-episodes/{episode_id}',
episode_id, headers={
'Authorization': f'Bearer {self._get_token(webpage, episode_id)}',
'Origin': self._BASE_URL,
},
query={
# XXX: if video is available, try adding the params 'with=entitlements,hlsVideo'
'extend': 'fullDescription',
'include': 'podcast',
'l': 'en-US',
})['data'][0]
thumb_info = traverse_obj(data, ('attributes', 'artwork', {
'url': ('url', {url_or_none}),
'h': ('height', {int_or_none}),
'w': ('width', {int_or_none}),
}))
return {
'id': episode_id,
**traverse_obj(data, {
'title': ('attributes', 'name', {str}),
'description': ('attributes', 'fullDescription', {clean_html}),
'url': ('attributes', 'assetUrl', {clean_podcast_url}, {update_url(scheme='https')}),
'timestamp': ('attributes', 'releaseDateTime', {parse_iso8601}),
'duration': ('attributes', 'durationInMilliseconds', {int_or_none(scale=1000)}),
'episode': ('attributes', 'name', {str}),
'episode_number': ('attributes', 'episodeNumber', {int_or_none}),
'series': ('relationships', 'podcast', 'data', 0, 'attributes', 'name', {str}),
}),
'thumbnail': try_call(lambda: thumb_info.pop('url').format(f='jpg', **thumb_info)),
'vcodec': 'none',
}
def _extract_podcast_from_webpage(self, webpage, episode_id):
server_data = self._search_json( server_data = self._search_json(
r'<script [^>]*\bid=["\']serialized-server-data["\'][^>]*>', webpage, r'<script [^>]*\bid=["\']serialized-server-data["\'][^>]*>', webpage,
'server data', episode_id, default=None) 'server data', episode_id)['data'][0]['data']
model_data = traverse_obj(server_data, ( model_data = traverse_obj(server_data, (
'data', 0, 'data', 'headerButtonItems', 'headerButtonItems', lambda _, v: v['$kind'] == 'share' and v['modelType'] == 'EpisodeLockup',
lambda _, v: v['$kind'] == 'share' and v['modelType'] == 'EpisodeLockup',
'model', {dict}, any)) 'model', {dict}, any))
if not model_data:
return None
return { return {
'id': episode_id, 'id': episode_id,
@ -157,12 +77,3 @@ class ApplePodcastsIE(AppleBaseIE):
'thumbnail': self._og_search_thumbnail(webpage), 'thumbnail': self._og_search_thumbnail(webpage),
'vcodec': 'none', 'vcodec': 'none',
} }
def _real_extract(self, url):
episode_id, country_code = self._match_valid_url(url).group('id', 'country')
# Webpage may be unavailable, see https://github.com/yt-dlp/yt-dlp/issues/17266
webpage = self._download_webpage(url, episode_id, expected_status=500)
return (
self._extract_podcast_from_webpage(webpage, episode_id)
or self._extract_podcast_from_api(webpage, episode_id, country_code))

View File

@ -11,12 +11,15 @@ from ..utils import (
join_nonempty, join_nonempty,
jwt_decode_hs256, jwt_decode_hs256,
make_archive_id, make_archive_id,
parse_duration,
parse_iso8601, parse_iso8601,
remove_start, remove_start,
str_or_none, str_or_none,
unified_strdate,
update_url, update_url,
update_url_query, update_url_query,
url_or_none, url_or_none,
xpath_text,
) )
from ..utils.traversal import traverse_obj, value from ..utils.traversal import traverse_obj, value
@ -116,6 +119,118 @@ class ARDMediathekBaseIE(InfoExtractor):
return formats return formats
class ARDIE(InfoExtractor):
_VALID_URL = r'(?P<mainurl>https?://(?:www\.)?daserste\.de/(?:[^/?#&]+/)+(?P<id>[^/?#&]+))\.html'
_TESTS = [{
# available till 7.12.2023
'url': 'https://www.daserste.de/information/talk/maischberger/videos/maischberger-video-424.html',
'md5': '94812e6438488fb923c361a44469614b',
'info_dict': {
'id': 'maischberger-video-424',
'display_id': 'maischberger-video-424',
'ext': 'mp4',
'duration': 4452.0,
'title': 'maischberger am 07.12.2022',
'upload_date': '20221207',
'thumbnail': r're:^https?://.*\.jpg$',
},
}, {
'url': 'https://www.daserste.de/information/politik-weltgeschehen/morgenmagazin/videosextern/dominik-kahun-aus-der-nhl-direkt-zur-weltmeisterschaft-100.html',
'only_matching': True,
}, {
'url': 'https://www.daserste.de/information/nachrichten-wetter/tagesthemen/videosextern/tagesthemen-17736.html',
'only_matching': True,
}, {
'url': 'https://www.daserste.de/unterhaltung/serie/in-aller-freundschaft-die-jungen-aerzte/videos/diversity-tag-sanam-afrashteh100.html',
'only_matching': True,
}, {
'url': 'http://www.daserste.de/information/reportage-dokumentation/dokus/videos/die-story-im-ersten-mission-unter-falscher-flagge-100.html',
'only_matching': True,
}, {
'url': 'https://www.daserste.de/unterhaltung/serie/in-aller-freundschaft-die-jungen-aerzte/Drehpause-100.html',
'only_matching': True,
}, {
'url': 'https://www.daserste.de/unterhaltung/film/filmmittwoch-im-ersten/videos/making-ofwendezeit-video-100.html',
'only_matching': True,
}]
def _real_extract(self, url):
mobj = self._match_valid_url(url)
display_id = mobj.group('id')
player_url = mobj.group('mainurl') + '~playerXml.xml'
doc = self._download_xml(player_url, display_id)
video_node = doc.find('./video')
upload_date = unified_strdate(xpath_text(
video_node, './broadcastDate'))
thumbnail = xpath_text(video_node, './/teaserImage//variant/url')
formats = []
for a in video_node.findall('.//asset'):
file_name = xpath_text(a, './fileName', default=None)
if not file_name:
continue
format_type = a.attrib.get('type')
format_url = url_or_none(file_name)
if format_url:
ext = determine_ext(file_name)
if ext == 'm3u8':
formats.extend(self._extract_m3u8_formats(
format_url, display_id, 'mp4', entry_protocol='m3u8_native',
m3u8_id=format_type or 'hls', fatal=False))
continue
elif ext == 'f4m':
formats.extend(self._extract_f4m_formats(
update_url_query(format_url, {'hdcore': '3.7.0'}),
display_id, f4m_id=format_type or 'hds', fatal=False))
continue
f = {
'format_id': format_type,
'width': int_or_none(xpath_text(a, './frameWidth')),
'height': int_or_none(xpath_text(a, './frameHeight')),
'vbr': int_or_none(xpath_text(a, './bitrateVideo')),
'abr': int_or_none(xpath_text(a, './bitrateAudio')),
'vcodec': xpath_text(a, './codecVideo'),
'tbr': int_or_none(xpath_text(a, './totalBitrate')),
}
server_prefix = xpath_text(a, './serverPrefix', default=None)
if server_prefix:
f.update({
'url': server_prefix,
'playpath': file_name,
})
else:
if not format_url:
continue
f['url'] = format_url
formats.append(f)
_SUB_FORMATS = (
('./dataTimedText', 'ttml'),
('./dataTimedTextNoOffset', 'ttml'),
('./dataTimedTextVtt', 'vtt'),
)
subtitles = {}
for subsel, subext in _SUB_FORMATS:
for node in video_node.findall(subsel):
subtitles.setdefault('de', []).append({
'url': node.attrib['url'],
'ext': subext,
})
return {
'id': xpath_text(video_node, './videoId', default=display_id),
'formats': formats,
'subtitles': subtitles,
'display_id': display_id,
'title': video_node.find('./title').text,
'duration': parse_duration(video_node.find('./duration').text),
'upload_date': upload_date,
'thumbnail': thumbnail,
}
class ARDBetaMediathekIE(InfoExtractor): class ARDBetaMediathekIE(InfoExtractor):
IE_NAME = 'ARDMediathek' IE_NAME = 'ARDMediathek'
_VALID_URL = r'''(?x)https?:// _VALID_URL = r'''(?x)https?://

View File

@ -42,6 +42,7 @@ def _id_to_pk(shortcode):
class InstagramBaseIE(InfoExtractor): class InstagramBaseIE(InfoExtractor):
_API_BASE_URL = 'https://i.instagram.com/api/v1'
_BASE_URL = 'https://www.instagram.com/' _BASE_URL = 'https://www.instagram.com/'
_APP_IDS = { _APP_IDS = {
'ios': '124024574287414', 'ios': '124024574287414',
@ -74,12 +75,6 @@ class InstagramBaseIE(InfoExtractor):
def _is_web_app(self): def _is_web_app(self):
return self._app_id == self._APP_IDS['web'] return self._app_id == self._APP_IDS['web']
@property
def _API_BASE_URL(self):
if not self._is_web_app:
return 'https://i.instagram.com/api/v1'
return 'https://www.instagram.com/api/v1'
@property @property
def _api_headers(self): def _api_headers(self):
return { return {
@ -92,8 +87,7 @@ class InstagramBaseIE(InfoExtractor):
@staticmethod @staticmethod
def _is_login_redirect(url): def _is_login_redirect(url):
path = urllib.parse.urlparse(url).path return urllib.parse.urlparse(url).path.startswith('/accounts/login')
return path.startswith('/accounts/login') or path == '/'
def _get_count(self, media, kind, *keys): def _get_count(self, media, kind, *keys):
return traverse_obj( return traverse_obj(

View File

@ -1,134 +1,80 @@
import datetime as dt
import json
from .common import InfoExtractor from .common import InfoExtractor
from ..utils import ( from ..utils import (
UserNotLive, ExtractorError,
clean_html,
extract_attributes,
int_or_none, int_or_none,
str_or_none, urljoin,
url_or_none,
)
from ..utils.traversal import (
find_element,
require,
traverse_obj,
) )
class ShowRoomLiveIE(InfoExtractor): class ShowRoomLiveIE(InfoExtractor):
IE_NAME = 'showroom:live' _WORKING = False
IE_DESC = 'SHOWROOM' _VALID_URL = r'https?://(?:www\.)?showroom-live\.com/(?!onlive|timetable|event|campaign|news|ranking|room)(?P<id>[^/?#&]+)'
_TEST = {
_VALID_URL = r'https?://(?:www\.)?showroom-live\.com/r/(?P<id>[\w-]+)' 'url': 'https://www.showroom-live.com/48_Nana_Okada',
_TESTS = [{
'url': 'https://www.showroom-live.com/r/48_Yui_Oguri',
'only_matching': True, 'only_matching': True,
}] }
def _real_extract(self, url): def _real_extract(self, url):
broadcaster_id = self._match_id(url) broadcaster_id = self._match_id(url)
webpage = self._download_webpage(
url, broadcaster_id, headers={'Accept-Language': 'ja'})
nuxt_data = self._search_nuxt_json(webpage, broadcaster_id)['data']
cookies = self._get_cookies(url) webpage = self._download_webpage(url, broadcaster_id)
sr_id = traverse_obj(cookies, ('sr_id', 'value', {str}, filter))
if not sr_id:
self.raise_login_required()
room_profile = traverse_obj(nuxt_data, ( room_id = self._search_regex(
f'roomProfile-{broadcaster_id}-{sr_id}', {dict})) (r'SrGlobal\.roomId\s*=\s*(\d+)',
start_timestamp = traverse_obj(room_profile, ('current_live_started_at', {int_or_none})) r'(?:profile|room)\?room_id\=(\d+)'), webpage, 'room_id')
is_live = traverse_obj(room_profile, ('is_onlive', {bool}))
if not is_live: room = self._download_json(
if start_timestamp: urljoin(url, f'/api/room/profile?room_id={room_id}'),
start_time = dt.datetime.fromtimestamp( broadcaster_id)
start_timestamp, dt.timezone.utc,
).astimezone().strftime('%Y-%m-%d %H:%M:%S %Z')
self.raise_no_formats( is_live = room.get('is_onlive')
f'Next livestream is scheduled to start at {start_time}', expected=True) if is_live is not True:
raise ExtractorError(f'{broadcaster_id} is offline', expected=True)
return { uploader = room.get('performer_name') or broadcaster_id
'id': broadcaster_id, title = room.get('room_name') or room.get('main_name') or uploader
'live_status': 'is_upcoming',
'release_timestamp': start_timestamp,
}
raise UserNotLive(video_id=broadcaster_id)
room_id = traverse_obj(room_profile, ('room_id', {str_or_none}))
room_name = traverse_obj(room_profile, (
('room_name', 'main_name'), {clean_html}, filter, any))
streaming_url_list = self._download_json( streaming_url_list = self._download_json(
'https://www.showroom-live.com/api/live/streaming_url', urljoin(url, f'/api/live/streaming_url?room_id={room_id}'),
broadcaster_id, query={'room_id': room_id}) broadcaster_id)['streaming_url_list']
m3u8_url = traverse_obj(streaming_url_list, (
'streaming_url_list', lambda _, v: v['type'] == 'hls_all', formats = []
'url', {url_or_none}, any, {require('m3u8 URL')})) for stream in streaming_url_list:
stream_url = stream.get('url')
if not stream_url:
continue
stream_type = stream.get('type')
if stream_type == 'hls':
m3u8_formats = self._extract_m3u8_formats(
stream_url, broadcaster_id, ext='mp4', m3u8_id='hls',
live=True)
for f in m3u8_formats:
f['quality'] = int_or_none(stream.get('quality', 100))
formats.extend(m3u8_formats)
elif stream_type == 'rtmp':
stream_name = stream.get('stream_name')
if not stream_name:
continue
formats.append({
'url': stream_url,
'play_path': stream_name,
'page_url': url,
'player_url': 'https://www.showroom-live.com/assets/swf/v3/ShowRoomLive.swf',
'rtmp_live': True,
'ext': 'flv',
'format_id': 'rtmp',
'format_note': stream.get('label'),
'quality': int_or_none(stream.get('quality', 100)),
})
return { return {
'title': room_name, 'id': str(room.get('live_id') or broadcaster_id),
'channel': room_name, 'title': title,
'channel_id': broadcaster_id, 'description': room.get('description'),
'formats': self._extract_m3u8_formats(m3u8_url, broadcaster_id, 'mp4'), 'timestamp': int_or_none(room.get('current_live_started_at')),
'is_live': is_live, 'uploader': uploader,
'release_timestamp': start_timestamp, 'uploader_id': broadcaster_id,
**traverse_obj(room_profile, { 'view_count': int_or_none(room.get('view_num')),
'id': ('live_id', {str_or_none}), 'formats': formats,
'channel_follower_count': ('follower_num', {int_or_none}), 'is_live': True,
'channel_is_verified': ('is_official', {bool}),
'description': ('description', {clean_html}, filter),
'genres': ('genre_name', {clean_html}, filter, all, filter),
'tags': ('live_tags', ..., 'name', {clean_html}, filter, all, filter),
'thumbnail': ('image_square', {url_or_none}),
'view_count': ('view_num', {int_or_none}),
}),
}
class ShowRoomVodIE(InfoExtractor):
IE_NAME = 'showroom:vod'
_VALID_URL = r'https?://(?:www\.)?showroom-live\.com/episode/watch\?(?:[^#]+&)?id=(?P<id>\w+)'
_TESTS = [{
'url': 'https://www.showroom-live.com/episode/watch?id=214',
'info_dict': {
'id': '214',
'ext': 'mp4',
'title': 'aaa',
},
}]
def _real_extract(self, url):
episode_id = self._match_id(url)
webpage = self._download_webpage(url, episode_id)
episode_data = traverse_obj(webpage, (
{find_element(id='episode-data', html=True)},
{extract_attributes}, 'data-episode', {json.loads}, {dict}))
streaming_url_list = self._download_json(
'https://www.showroom-live.com/api/episode/streaming_url',
episode_id, query={'episode_id': episode_id})
m3u8_url = traverse_obj(streaming_url_list, (
'streaming_url_list', 'hls_all',
'hls_all', {url_or_none}, {require('m3u8 URL')}))
return {
'id': episode_id,
'formats': self._extract_m3u8_formats(m3u8_url, episode_id, 'mp4'),
'thumbnail': traverse_obj(streaming_url_list, ('thumbnail_url', {url_or_none})),
**traverse_obj(episode_data, {
'title': ('title', {clean_html}, filter),
'description': ('description', {clean_html}, filter),
'duration': ('video_time', {int_or_none}),
'timestamp': ('display_started_at', {int_or_none}),
}),
**traverse_obj(episode_data, ('series', {
'series': ('name', {clean_html}, filter),
'series_id': ('id', {str_or_none}),
})),
} }

View File

@ -49,7 +49,7 @@ class VimeoBaseInfoExtractor(InfoExtractor):
'Cannot download embed-only video without embedding URL. Please call yt-dlp ' 'Cannot download embed-only video without embedding URL. Please call yt-dlp '
'with the URL of the page that embeds this video.') 'with the URL of the page that embeds this video.')
_DEFAULT_CLIENT = 'macos_basic' _DEFAULT_CLIENT = 'macos'
_DEFAULT_AUTHED_CLIENT = 'web' _DEFAULT_AUTHED_CLIENT = 'web'
_CLIENT_HEADERS = { _CLIENT_HEADERS = {
'Accept': 'application/vnd.vimeo.*+json; version=3.4.10', 'Accept': 'application/vnd.vimeo.*+json; version=3.4.10',
@ -59,6 +59,7 @@ class VimeoBaseInfoExtractor(InfoExtractor):
'android': { 'android': {
'CACHE_KEY': 'oauth-token-android', 'CACHE_KEY': 'oauth-token-android',
'CACHE_ONLY': True, 'CACHE_ONLY': True,
'VIEWER_JWT': False,
'REQUIRES_AUTH': False, 'REQUIRES_AUTH': False,
'AUTH': 'NzRmYTg5YjgxMWExY2JiNzUwZDg1MjhkMTYzZjQ4YWYyOGEyZGJlMTp4OGx2NFd3QnNvY1lkamI2UVZsdjdDYlNwSDUrdm50YzdNNThvWDcwN1JrenJGZC9tR1lReUNlRjRSVklZeWhYZVpRS0tBcU9YYzRoTGY2Z1dlVkJFYkdJc0dMRHpoZWFZbU0reDRqZ1dkZ1diZmdIdGUrNUM5RVBySlM0VG1qcw==', 'AUTH': 'NzRmYTg5YjgxMWExY2JiNzUwZDg1MjhkMTYzZjQ4YWYyOGEyZGJlMTp4OGx2NFd3QnNvY1lkamI2UVZsdjdDYlNwSDUrdm50YzdNNThvWDcwN1JrenJGZC9tR1lReUNlRjRSVklZeWhYZVpRS0tBcU9YYzRoTGY2Z1dlVkJFYkdJc0dMRHpoZWFZbU0reDRqZ1dkZ1diZmdIdGUrNUM5RVBySlM0VG1qcw==',
'USER_AGENT': 'com.vimeo.android.videoapp (OnePlus, ONEPLUS A6003, OnePlus, Android 14/34 Version 11.8.1) Kotlin VimeoNetworking/3.12.0', 'USER_AGENT': 'com.vimeo.android.videoapp (OnePlus, ONEPLUS A6003, OnePlus, Android 14/34 Version 11.8.1) Kotlin VimeoNetworking/3.12.0',
@ -70,8 +71,26 @@ class VimeoBaseInfoExtractor(InfoExtractor):
'resource_key', 'badge', 'upload', 'transcode', 'is_playable', 'has_audio', 'resource_key', 'badge', 'upload', 'transcode', 'is_playable', 'has_audio',
), ),
}, },
'macos_basic': { 'ios': {
'CACHE_KEY': 'oauth-token-ios',
'CACHE_ONLY': True,
'VIEWER_JWT': False,
'REQUIRES_AUTH': False,
'AUTH': 'MTMxNzViY2Y0NDE0YTQ5YzhjZTc0YmU0NjVjNDQxYzNkYWVjOWRlOTpHKzRvMmgzVUh4UkxjdU5FRW80cDNDbDhDWGR5dVJLNUJZZ055dHBHTTB4V1VzaG41bEx1a2hiN0NWYWNUcldSSW53dzRUdFRYZlJEZmFoTTArOTBUZkJHS3R4V2llYU04Qnl1bERSWWxUdXRidjNqR2J4SHFpVmtFSUcyRktuQw==',
'USER_AGENT': 'Vimeo/11.10.0 (com.vimeo; build:250424.164813.0; iOS 18.4.1) Alamofire/5.9.0 VimeoNetworking/5.0.0',
'VIDEOS_FIELDS': (
'uri', 'name', 'description', 'type', 'link', 'player_embed_url', 'duration',
'width', 'language', 'height', 'embed', 'created_time', 'modified_time', 'release_time',
'content_rating', 'content_rating_class', 'rating_mod_locked', 'license', 'config_url',
'embed_player_config_url', 'privacy', 'pictures', 'tags', 'stats', 'categories', 'uploader',
'metadata', 'user', 'files', 'download', 'app', 'play', 'status', 'resource_key', 'badge',
'upload', 'transcode', 'is_playable', 'has_audio',
),
},
'macos': {
'CACHE_KEY': 'oauth-token-macos',
'CACHE_ONLY': False, 'CACHE_ONLY': False,
'VIEWER_JWT': False,
'REQUIRES_AUTH': False, 'REQUIRES_AUTH': False,
'AUTH': 'NDc1N2JlN2Y5ZjZmMjU3NzE3NTRkZTg1NmY2YzU2MTI0OTFlNjJiYjpwVUNDWUlBZmZqSHhQcndBYWxGMzgyYys2NkN5d1JrREJZZXdPcEdsU05tdjFlVVo2aE1lYk9GcWE3ZW9KVldlYnFlOWh5Vno5UWtpUGJ5empYZFBpYkFwV0FFTnB5VWV4ZEh3aHZnRUNEL0VySnBzTmFraDdNbS9nMXhWanhIcw==', 'AUTH': 'NDc1N2JlN2Y5ZjZmMjU3NzE3NTRkZTg1NmY2YzU2MTI0OTFlNjJiYjpwVUNDWUlBZmZqSHhQcndBYWxGMzgyYys2NkN5d1JrREJZZXdPcEdsU05tdjFlVVo2aE1lYk9GcWE3ZW9KVldlYnFlOWh5Vno5UWtpUGJ5empYZFBpYkFwV0FFTnB5VWV4ZEh3aHZnRUNEL0VySnBzTmFraDdNbS9nMXhWanhIcw==',
'USER_AGENT': 'Vimeo/1.6.3 (com.vimeo.mac; build:251121.142637.0; macOS 13.7.8) Alamofire/5.9.0 VimeoNetworking/5.0.0', 'USER_AGENT': 'Vimeo/1.6.3 (com.vimeo.mac; build:251121.142637.0; macOS 13.7.8) Alamofire/5.9.0 VimeoNetworking/5.0.0',
@ -85,6 +104,7 @@ class VimeoBaseInfoExtractor(InfoExtractor):
}, },
'web': { 'web': {
'CACHE_ONLY': False, 'CACHE_ONLY': False,
'VIEWER_JWT': True,
'REQUIRES_AUTH': True, 'REQUIRES_AUTH': True,
'USER_AGENT': None, 'USER_AGENT': None,
'VIDEOS_FIELDS': ( 'VIDEOS_FIELDS': (
@ -162,8 +182,7 @@ class VimeoBaseInfoExtractor(InfoExtractor):
if self._LOGIN_REQUIRED: if self._LOGIN_REQUIRED:
self.raise_login_required() self.raise_login_required()
# Don't auto-load token from cache if the user has specified a client if self._DEFAULT_CLIENT != 'web':
if self._configuration_arg('client', [None], ie_key=VimeoIE)[0]:
return return
for client_name, client_config in self._CLIENT_CONFIGS.items(): for client_name, client_config in self._CLIENT_CONFIGS.items():
@ -348,13 +367,10 @@ class VimeoBaseInfoExtractor(InfoExtractor):
} }
def _fetch_oauth_token(self, client): def _fetch_oauth_token(self, client):
base_client, _, variant = client.partition('_')
if base_client == 'web':
return f'jwt {self._fetch_viewer_info()["jwt"]}'
client_config = self._CLIENT_CONFIGS[client] client_config = self._CLIENT_CONFIGS[client]
if variant == 'basic':
return f'Basic {client_config["AUTH"]}' if client_config['VIEWER_JWT']:
return f'jwt {self._fetch_viewer_info()["jwt"]}'
cache_key = client_config['CACHE_KEY'] cache_key = client_config['CACHE_KEY']
@ -1197,9 +1213,6 @@ class VimeoIE(VimeoBaseInfoExtractor):
'If your IP address is located in Europe you could try using a VPN/proxy,', 'If your IP address is located in Europe you could try using a VPN/proxy,',
f'or else u{self._login_hint()[1:]}', f'or else u{self._login_hint()[1:]}',
delim=' '), method=None) delim=' '), method=None)
# XXX: Temporary while macos_basic is the default client
elif e.cause.status == 401 and self._get_requested_client() == 'macos_basic':
self.raise_login_required('The Vimeo extractor only works when logged-in')
else: else:
raise raise
@ -1208,10 +1221,6 @@ class VimeoIE(VimeoBaseInfoExtractor):
else: else:
info = self._parse_api_response(video, video_id, unlisted_hash) info = self._parse_api_response(video, video_id, unlisted_hash)
# XXX: Temporary while macos_basic is the default client
if not info.get('formats') and self._get_requested_client() == 'macos_basic':
self.raise_login_required('The Vimeo extractor only works when logged-in')
source_format = self._extract_original_format( source_format = self._extract_original_format(
f'https://vimeo.com/{video_id}', video_id, unlisted_hash) f'https://vimeo.com/{video_id}', video_id, unlisted_hash)
if source_format: if source_format:

View File

@ -99,7 +99,7 @@ INNERTUBE_CLIENTS = {
'INNERTUBE_CONTEXT': { 'INNERTUBE_CONTEXT': {
'client': { 'client': {
'clientName': 'WEB', 'clientName': 'WEB',
'clientVersion': '2.20260708.00.00', 'clientVersion': '2.20260114.08.00',
}, },
}, },
'INNERTUBE_CONTEXT_CLIENT_NAME': 1, 'INNERTUBE_CONTEXT_CLIENT_NAME': 1,
@ -107,12 +107,11 @@ INNERTUBE_CLIENTS = {
**WEB_PO_TOKEN_POLICIES, **WEB_PO_TOKEN_POLICIES,
}, },
# Safari UA returns pre-merged video+audio 144p/240p/360p/720p/1080p HLS formats # Safari UA returns pre-merged video+audio 144p/240p/360p/720p/1080p HLS formats
# Since 2026.07, HLS formats are only returned with some logged-in or "trusted" sessions
'web_safari': { 'web_safari': {
'INNERTUBE_CONTEXT': { 'INNERTUBE_CONTEXT': {
'client': { 'client': {
'clientName': 'WEB', 'clientName': 'WEB',
'clientVersion': '2.20260708.00.00', 'clientVersion': '2.20260114.08.00',
'userAgent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Safari/605.1.15,gzip(gfe)', 'userAgent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Safari/605.1.15,gzip(gfe)',
}, },
}, },
@ -124,7 +123,7 @@ INNERTUBE_CLIENTS = {
'INNERTUBE_CONTEXT': { 'INNERTUBE_CONTEXT': {
'client': { 'client': {
'clientName': 'WEB_EMBEDDED_PLAYER', 'clientName': 'WEB_EMBEDDED_PLAYER',
'clientVersion': '2.20260708.00.00', 'clientVersion': '1.20260115.01.00',
}, },
}, },
'INNERTUBE_CONTEXT_CLIENT_NAME': 56, 'INNERTUBE_CONTEXT_CLIENT_NAME': 56,
@ -135,7 +134,7 @@ INNERTUBE_CLIENTS = {
'INNERTUBE_CONTEXT': { 'INNERTUBE_CONTEXT': {
'client': { 'client': {
'clientName': 'WEB_REMIX', 'clientName': 'WEB_REMIX',
'clientVersion': '1.20260707.12.00', 'clientVersion': '1.20260114.03.00',
}, },
}, },
'INNERTUBE_CONTEXT_CLIENT_NAME': 67, 'INNERTUBE_CONTEXT_CLIENT_NAME': 67,
@ -165,7 +164,7 @@ INNERTUBE_CLIENTS = {
'INNERTUBE_CONTEXT': { 'INNERTUBE_CONTEXT': {
'client': { 'client': {
'clientName': 'WEB_CREATOR', 'clientName': 'WEB_CREATOR',
'clientVersion': '1.20260708.06.00', 'clientVersion': '1.20260114.05.00',
}, },
}, },
'INNERTUBE_CONTEXT_CLIENT_NAME': 62, 'INNERTUBE_CONTEXT_CLIENT_NAME': 62,
@ -194,9 +193,9 @@ INNERTUBE_CLIENTS = {
'INNERTUBE_CONTEXT': { 'INNERTUBE_CONTEXT': {
'client': { 'client': {
'clientName': 'ANDROID', 'clientName': 'ANDROID',
'clientVersion': '21.26.364', 'clientVersion': '21.02.35',
'androidSdkVersion': 30, 'androidSdkVersion': 30,
'userAgent': 'com.google.android.youtube/21.26.364 (Linux; U; Android 11) gzip', 'userAgent': 'com.google.android.youtube/21.02.35 (Linux; U; Android 11) gzip',
'osName': 'Android', 'osName': 'Android',
'osVersion': '11', 'osVersion': '11',
}, },
@ -224,7 +223,6 @@ INNERTUBE_CLIENTS = {
}, },
# "Made for kids" videos aren't available with this client # "Made for kids" videos aren't available with this client
# Using a clientVersion>1.65 may return SABR streams only # Using a clientVersion>1.65 may return SABR streams only
# Since 2026.07, intermittent/selective POT enforcement has been observed for non-HLS formats
'android_vr': { 'android_vr': {
'INNERTUBE_CONTEXT': { 'INNERTUBE_CONTEXT': {
'client': { 'client': {
@ -240,24 +238,6 @@ INNERTUBE_CLIENTS = {
}, },
'INNERTUBE_CONTEXT_CLIENT_NAME': 28, 'INNERTUBE_CONTEXT_CLIENT_NAME': 28,
'REQUIRE_JS_PLAYER': False, 'REQUIRE_JS_PLAYER': False,
'GVS_PO_TOKEN_POLICY': {
StreamingProtocol.HTTPS: GvsPoTokenPolicy(
required=True,
recommended=True,
not_required_with_player_token=True,
),
StreamingProtocol.DASH: GvsPoTokenPolicy(
required=True,
recommended=True,
not_required_with_player_token=True,
),
StreamingProtocol.HLS: GvsPoTokenPolicy(
required=False,
recommended=True,
not_required_with_player_token=True,
),
},
'PLAYER_PO_TOKEN_POLICY': PlayerPoTokenPolicy(required=False, recommended=True),
}, },
# iOS clients have HLS live streams. Setting device model to get 60fps formats. # iOS clients have HLS live streams. Setting device model to get 60fps formats.
# See: https://github.com/TeamNewPipe/NewPipeExtractor/issues/680#issuecomment-1002724558 # See: https://github.com/TeamNewPipe/NewPipeExtractor/issues/680#issuecomment-1002724558
@ -265,10 +245,10 @@ INNERTUBE_CLIENTS = {
'INNERTUBE_CONTEXT': { 'INNERTUBE_CONTEXT': {
'client': { 'client': {
'clientName': 'IOS', 'clientName': 'IOS',
'clientVersion': '21.26.4', 'clientVersion': '21.02.3',
'deviceMake': 'Apple', 'deviceMake': 'Apple',
'deviceModel': 'iPhone16,2', 'deviceModel': 'iPhone16,2',
'userAgent': 'com.google.ios.youtube/21.26.4 (iPhone16,2; U; CPU iOS 18_3_2 like Mac OS X;)', 'userAgent': 'com.google.ios.youtube/21.02.3 (iPhone16,2; U; CPU iOS 18_3_2 like Mac OS X;)',
'osName': 'iPhone', 'osName': 'iPhone',
'osVersion': '18.3.2.22D82', 'osVersion': '18.3.2.22D82',
}, },
@ -290,29 +270,13 @@ INNERTUBE_CLIENTS = {
'PLAYER_PO_TOKEN_POLICY': PlayerPoTokenPolicy(required=False, recommended=True), 'PLAYER_PO_TOKEN_POLICY': PlayerPoTokenPolicy(required=False, recommended=True),
'REQUIRE_JS_PLAYER': False, 'REQUIRE_JS_PLAYER': False,
}, },
# "Made for kids" videos aren't available with this client
'visionos': {
'INNERTUBE_CONTEXT': {
'client': {
'clientName': 'VISIONOS',
'clientVersion': '1.02',
'deviceMake': 'Apple',
'deviceModel': 'RealityDevice17,1',
'userAgent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 15_7_3) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 Safari/605.1.15',
'osName': 'visionOS',
'osVersion': '26.5.23O471',
},
},
'INNERTUBE_CONTEXT_CLIENT_NAME': 101,
'REQUIRE_JS_PLAYER': False,
},
# mweb has 'ultralow' formats # mweb has 'ultralow' formats
# See: https://github.com/yt-dlp/yt-dlp/pull/557 # See: https://github.com/yt-dlp/yt-dlp/pull/557
'mweb': { 'mweb': {
'INNERTUBE_CONTEXT': { 'INNERTUBE_CONTEXT': {
'client': { 'client': {
'clientName': 'MWEB', 'clientName': 'MWEB',
'clientVersion': '2.20260708.05.00', 'clientVersion': '2.20260115.01.00',
# mweb previously did not require PO Token with this UA # mweb previously did not require PO Token with this UA
'userAgent': 'Mozilla/5.0 (iPad; CPU OS 16_7_10 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1,gzip(gfe)', 'userAgent': 'Mozilla/5.0 (iPad; CPU OS 16_7_10 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1,gzip(gfe)',
}, },
@ -343,7 +307,7 @@ INNERTUBE_CLIENTS = {
'INNERTUBE_CONTEXT': { 'INNERTUBE_CONTEXT': {
'client': { 'client': {
'clientName': 'TVHTML5', 'clientName': 'TVHTML5',
'clientVersion': '7.20260707.07.00', 'clientVersion': '7.20260114.12.00',
# See: https://github.com/youtube/cobalt/blob/main/cobalt/browser/user_agent/user_agent_platform_info.cc#L506 # See: https://github.com/youtube/cobalt/blob/main/cobalt/browser/user_agent/user_agent_platform_info.cc#L506
'userAgent': 'Mozilla/5.0 (ChromiumStylePlatform) Cobalt/25.lts.30.1034943-gold (unlike Gecko), Unknown_TV_Unknown_0/Unknown (Unknown, Unknown)', 'userAgent': 'Mozilla/5.0 (ChromiumStylePlatform) Cobalt/25.lts.30.1034943-gold (unlike Gecko), Unknown_TV_Unknown_0/Unknown (Unknown, Unknown)',
}, },
@ -355,11 +319,12 @@ INNERTUBE_CLIENTS = {
'INNERTUBE_CONTEXT': { 'INNERTUBE_CONTEXT': {
'client': { 'client': {
'clientName': 'TVHTML5', 'clientName': 'TVHTML5',
'clientVersion': '5.20260707', 'clientVersion': '5.20260114',
'userAgent': 'Mozilla/5.0 (ChromiumStylePlatform) Cobalt/Version', 'userAgent': 'Mozilla/5.0 (ChromiumStylePlatform) Cobalt/Version',
}, },
}, },
'INNERTUBE_CONTEXT_CLIENT_NAME': 7, 'INNERTUBE_CONTEXT_CLIENT_NAME': 7,
'REQUIRE_AUTH': True,
'SUPPORTS_COOKIES': True, 'SUPPORTS_COOKIES': True,
}, },
'tv_simply': { 'tv_simply': {

View File

@ -28,7 +28,6 @@ from .jsc._director import initialize_jsc_director
from .jsc.provider import JsChallengeRequest, JsChallengeType, NChallengeInput, SigChallengeInput from .jsc.provider import JsChallengeRequest, JsChallengeType, NChallengeInput, SigChallengeInput
from .pot._director import initialize_pot_director from .pot._director import initialize_pot_director
from .pot.provider import PoTokenContext, PoTokenRequest from .pot.provider import PoTokenContext, PoTokenRequest
from ...networking import HEADRequest
from ...utils import ( from ...utils import (
NO_DEFAULT, NO_DEFAULT,
ExtractorError, ExtractorError,
@ -140,13 +139,13 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
_RETURN_TYPE = 'video' # XXX: How to handle multifeed? _RETURN_TYPE = 'video' # XXX: How to handle multifeed?
_SUBTITLE_FORMATS = ('json3', 'srv1', 'srv2', 'srv3', 'ttml', 'srt', 'vtt') _SUBTITLE_FORMATS = ('json3', 'srv1', 'srv2', 'srv3', 'ttml', 'srt', 'vtt')
_DEFAULT_CLIENTS = ('visionos', 'android_vr', 'web') _DEFAULT_CLIENTS = ('android_vr', 'web_safari')
_DEFAULT_JSLESS_CLIENTS = ('visionos', 'android_vr') _DEFAULT_JSLESS_CLIENTS = ('android_vr',)
_DEFAULT_AUTHED_CLIENTS = ('tv_downgraded', 'web') _DEFAULT_AUTHED_CLIENTS = ('tv_downgraded', 'web_safari')
# Premium does not require POT (except for subtitles) # Premium does not require POT (except for subtitles)
_DEFAULT_PREMIUM_CLIENTS = ('tv_downgraded', 'web_creator', 'web') _DEFAULT_PREMIUM_CLIENTS = ('tv_downgraded', 'web_creator')
_WEBPAGE_CLIENTS = ('web', 'web_safari') _WEBPAGE_CLIENTS = ('web', 'web_safari')
_DEFAULT_WEBPAGE_CLIENT = 'web' _DEFAULT_WEBPAGE_CLIENT = 'web_safari'
_GEO_BYPASS = False _GEO_BYPASS = False
@ -1600,13 +1599,13 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
'info_dict': { 'info_dict': {
'id': 'brhfDfLdDZ8', 'id': 'brhfDfLdDZ8',
'ext': 'mp4', 'ext': 'mp4',
'title': 'Scientists React to Terrible Movie Science | Moonfall (2021)', 'title': 'This is the WORST Movie Science We\'ve Ever Seen',
'description': 'md5:8afd0a3cd69ec63438fc573580436f92', 'description': 'md5:8afd0a3cd69ec63438fc573580436f92',
'media_type': 'video', 'media_type': 'video',
'uploader': 'Sauce +', 'uploader': 'Open Sauce',
'uploader_id': '@sauceplusofficial', 'uploader_id': '@opensaucelive',
'uploader_url': 'https://www.youtube.com/@sauceplusofficial', 'uploader_url': 'https://www.youtube.com/@opensaucelive',
'channel': 'Sauce +', 'channel': 'Open Sauce',
'channel_id': 'UC2EiGVmCeD79l_vZ204DUSw', 'channel_id': 'UC2EiGVmCeD79l_vZ204DUSw',
'channel_url': 'https://www.youtube.com/channel/UC2EiGVmCeD79l_vZ204DUSw', 'channel_url': 'https://www.youtube.com/channel/UC2EiGVmCeD79l_vZ204DUSw',
'comment_count': int, 'comment_count': int,
@ -1614,17 +1613,15 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
'like_count': int, 'like_count': int,
'age_limit': 0, 'age_limit': 0,
'duration': 1664, 'duration': 1664,
'thumbnail': 'https://i.ytimg.com/vi/brhfDfLdDZ8/sddefault.jpg', 'thumbnail': 'https://i.ytimg.com/vi/brhfDfLdDZ8/hqdefault.jpg',
'categories': ['Entertainment'], 'categories': ['Entertainment'],
'tags': ['Moonfall', 'Bad Science', 'Open Sauce', 'Sauce+', 'The Backyard Scientist', 'William Osman', 'Allen Pan'], 'tags': ['Moonfall', 'Bad Science', 'Open Sauce', 'Sauce+', 'The Backyard Scientist', 'William Osman', 'Allen Pan'],
'creators': ['Sauce +', 'William Osman 2'], 'creators': ['Open Sauce', 'William Osman 2'],
'timestamp': 1759452918, 'timestamp': 1759452918,
'upload_date': '20251003', 'upload_date': '20251003',
'playable_in_embed': True, 'playable_in_embed': True,
'availability': 'public', 'availability': 'public',
'live_status': 'not_live', 'live_status': 'not_live',
'channel_follower_count': int,
'heatmap': 'count:100',
}, },
'params': {'skip_download': True}, 'params': {'skip_download': True},
}, { }, {
@ -1658,7 +1655,6 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
'playable_in_embed': True, 'playable_in_embed': True,
'availability': 'public', 'availability': 'public',
'live_status': 'not_live', 'live_status': 'not_live',
'channel_follower_count': int,
}, },
'params': {'skip_download': True}, 'params': {'skip_download': True},
}, { }, {
@ -2040,11 +2036,8 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
last_seq = last_seq_cache[cache_key] last_seq = last_seq_cache[cache_key]
else: else:
try: try:
urlh = self._request_webpage( urlh = self._request_webpage(base_url, None, note=False, errnote=False, fatal=False)
HEADRequest(base_url), None, except ExtractorError:
note=False, errnote='Fragment request failed')
except ExtractorError as e:
self.write_debug(e.msg)
urlh = None urlh = None
last_seq = try_get(urlh, lambda x: int_or_none(x.headers['X-Head-Seqnum'])) last_seq = try_get(urlh, lambda x: int_or_none(x.headers['X-Head-Seqnum']))
if urlh: if urlh:
@ -2907,10 +2900,6 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
def _is_unplayable(player_response): def _is_unplayable(player_response):
return traverse_obj(player_response, ('playabilityStatus', 'status')) == 'UNPLAYABLE' return traverse_obj(player_response, ('playabilityStatus', 'status')) == 'UNPLAYABLE'
@staticmethod
def _is_error_response(player_response):
return traverse_obj(player_response, ('playabilityStatus', 'status')) == 'ERROR'
def _extract_player_response(self, client, video_id, webpage_ytcfg, player_ytcfg, player_url, initial_pr, visitor_data, data_sync_id, po_token): def _extract_player_response(self, client, video_id, webpage_ytcfg, player_ytcfg, player_url, initial_pr, visitor_data, data_sync_id, po_token):
headers = self.generate_api_headers( headers = self.generate_api_headers(
ytcfg=player_ytcfg, ytcfg=player_ytcfg,
@ -3143,14 +3132,13 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
prs.append(pr) prs.append(pr)
if ( if (
# Is this a "made for kids" video that can't be downloaded with android_vr/visionos? # Is this a "made for kids" video that can't be downloaded with android_vr?
client in {'android_vr', 'visionos'} client == 'android_vr' and self._is_unplayable(pr)
and (self._is_unplayable(pr) or self._is_error_response(pr))
and webpage and 'made for kids' in webpage and webpage and 'made for kids' in webpage
# ...and is a JS runtime is available? # ...and is a JS runtime is available?
and any(p.is_available() for p in self._jsc_director.providers.values()) and any(p.is_available() for p in self._jsc_director.providers.values())
): ):
append_client('tv_downgraded') append_client('web_embedded')
# web_embedded can work around age-gate and age-verification for some embeddable videos # web_embedded can work around age-gate and age-verification for some embeddable videos
if self._is_agegated(pr) and variant != 'web_embedded': if self._is_agegated(pr) and variant != 'web_embedded':
@ -4471,16 +4459,13 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
vsir = get_first(contents, 'videoSecondaryInfoRenderer') vsir = get_first(contents, 'videoSecondaryInfoRenderer')
if vsir: if vsir:
vor = traverse_obj(vsir, ('owner', 'videoOwnerRenderer')) vor = traverse_obj(vsir, ('owner', 'videoOwnerRenderer'))
collab_view_models = traverse_obj(vor, ( collaborators = traverse_obj(vor, (
'attributedTitle', 'commandRuns', ..., 'onTap', 'innertubeCommand', 'showDialogCommand', 'attributedTitle', 'commandRuns', ..., 'onTap', 'innertubeCommand', 'showDialogCommand',
'panelLoadingStrategy', 'inlineContent', 'dialogViewModel', 'customContent', 'listViewModel', 'panelLoadingStrategy', 'inlineContent', 'dialogViewModel', 'customContent', 'listViewModel',
'listItems', ..., 'listItemViewModel', {dict})) 'listItems', ..., 'listItemViewModel', 'title', 'content', {str}))
collaborators = traverse_obj(collab_view_models, (..., 'title', 'content', {str}))
info.update({ info.update({
'channel': self._get_text(vor, 'title') or (collaborators[0] if collaborators else None), 'channel': self._get_text(vor, 'title') or (collaborators[0] if collaborators else None),
'channel_follower_count': ( 'channel_follower_count': self._get_count(vor, 'subscriberCountText'),
self._get_count(vor, 'subscriberCountText')
or traverse_obj(collab_view_models, (0, 'rendererContext', 'accessibilityContext', 'label', {parse_count}))),
'creators': collaborators if collaborators else None, 'creators': collaborators if collaborators else None,
}) })

View File

@ -17,7 +17,7 @@ from .traversal import traverse_obj
def random_user_agent(): def random_user_agent():
USER_AGENT_TMPL = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{}.0.0.0 Safari/537.36' USER_AGENT_TMPL = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{}.0.0.0 Safari/537.36'
# Target versions released within the last ~6 months # Target versions released within the last ~6 months
CHROME_MAJOR_VERSION_RANGE = (145, 151) CHROME_MAJOR_VERSION_RANGE = (144, 150)
return USER_AGENT_TMPL.format(random.randint(*CHROME_MAJOR_VERSION_RANGE)) return USER_AGENT_TMPL.format(random.randint(*CHROME_MAJOR_VERSION_RANGE))