diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b3db8fec1b..ec5d4020ab 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,9 +21,6 @@ on: macos: default: true type: boolean - macos_legacy: - default: true - type: boolean windows: default: true type: boolean @@ -67,10 +64,6 @@ on: description: yt-dlp_macos, yt-dlp_macos.zip default: true type: boolean - macos_legacy: - description: yt-dlp_macos_legacy - default: true - type: boolean windows: description: yt-dlp.exe, yt-dlp_win.zip default: true @@ -208,7 +201,7 @@ jobs: python3.9 -m pip install -U pip wheel 'setuptools>=71.0.2' # XXX: Keep this in sync with pyproject.toml (it can't be accessed at this stage) and exclude secretstorage python3.9 -m pip install -U Pyinstaller mutagen pycryptodomex brotli certifi cffi \ - 'requests>=2.32.2,<3' 'urllib3>=1.26.17,<3' 'websockets>=13.0' + 'requests>=2.32.2,<3' 'urllib3>=2.0.2,<3' 'websockets>=13.0' run: | cd repo @@ -344,58 +337,6 @@ jobs: ~/yt-dlp-build-venv key: cache-reqs-${{ github.job }}-${{ github.ref }} - macos_legacy: - needs: process - if: inputs.macos_legacy - runs-on: macos-13 - - steps: - - uses: actions/checkout@v4 - - name: Install Python - # We need the official Python, because the GA ones only support newer macOS versions - env: - PYTHON_VERSION: 3.10.5 - MACOSX_DEPLOYMENT_TARGET: 10.9 # Used up by the Python build tools - run: | - # Hack to get the latest patch version. Uncomment if needed - #brew install python@3.10 - #export PYTHON_VERSION=$( $(brew --prefix)/opt/python@3.10/bin/python3 --version | cut -d ' ' -f 2 ) - curl "https://www.python.org/ftp/python/${PYTHON_VERSION}/python-${PYTHON_VERSION}-macos11.pkg" -o "python.pkg" - sudo installer -pkg python.pkg -target / - python3 --version - - name: Install Requirements - run: | - brew install coreutils - python3 devscripts/install_deps.py --user -o --include build - python3 devscripts/install_deps.py --user --include pyinstaller - - - name: Prepare - run: | - python3 devscripts/update-version.py -c "${{ inputs.channel }}" -r "${{ needs.process.outputs.origin }}" "${{ inputs.version }}" - python3 devscripts/make_lazy_extractors.py - - name: Build - run: | - python3 -m bundle.pyinstaller - mv dist/yt-dlp_macos dist/yt-dlp_macos_legacy - - - name: Verify --update-to - if: vars.UPDATE_TO_VERIFICATION - run: | - chmod +x ./dist/yt-dlp_macos_legacy - cp ./dist/yt-dlp_macos_legacy ./dist/yt-dlp_macos_legacy_downgraded - version="$(./dist/yt-dlp_macos_legacy --version)" - ./dist/yt-dlp_macos_legacy_downgraded -v --update-to yt-dlp/yt-dlp@2023.03.04 - downgraded_version="$(./dist/yt-dlp_macos_legacy_downgraded --version)" - [[ "$version" != "$downgraded_version" ]] - - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: build-bin-${{ github.job }} - path: | - dist/yt-dlp_macos_legacy - compression-level: 0 - windows: needs: process if: inputs.windows @@ -410,7 +351,7 @@ jobs: run: | # Custom pyinstaller built with https://github.com/yt-dlp/pyinstaller-builds python devscripts/install_deps.py -o --include build python devscripts/install_deps.py --include curl-cffi - python -m pip install -U "https://yt-dlp.github.io/Pyinstaller-Builds/x86_64/pyinstaller-6.13.0-py3-none-any.whl" + python -m pip install -U "https://yt-dlp.github.io/Pyinstaller-Builds/x64/pyinstaller-6.15.0-py3-none-any.whl" - name: Prepare run: | @@ -459,7 +400,7 @@ jobs: run: | python devscripts/install_deps.py -o --include build python devscripts/install_deps.py - python -m pip install -U "https://yt-dlp.github.io/Pyinstaller-Builds/i686/pyinstaller-6.13.0-py3-none-any.whl" + python -m pip install -U "https://yt-dlp.github.io/Pyinstaller-Builds/x86/pyinstaller-6.15.0-py3-none-any.whl" - name: Prepare run: | @@ -498,7 +439,6 @@ jobs: - linux_static - linux_arm - macos - - macos_legacy - windows - windows32 runs-on: ubuntu-latest @@ -530,27 +470,31 @@ jobs: lock 2023.11.16 win_x86_exe .+ Windows-(?:Vista|2008Server) lock 2024.10.22 py2exe .+ lock 2024.10.22 linux_(?:armv7l|aarch64)_exe .+-glibc2\.(?:[12]?\d|30)\b - lock 2024.10.22 (?!\w+_exe).+ Python 3\.8 + lock 2024.10.22 zip Python 3\.8 lock 2024.10.22 win(?:_x86)?_exe Python 3\.[78].+ Windows-(?:7-|2008ServerR2) + lock 2025.08.11 darwin_legacy_exe .+ lockV2 yt-dlp/yt-dlp 2022.08.18.36 .+ Python 3\.6 lockV2 yt-dlp/yt-dlp 2023.11.16 (?!win_x86_exe).+ Python 3\.7 lockV2 yt-dlp/yt-dlp 2023.11.16 win_x86_exe .+ Windows-(?:Vista|2008Server) lockV2 yt-dlp/yt-dlp 2024.10.22 py2exe .+ lockV2 yt-dlp/yt-dlp 2024.10.22 linux_(?:armv7l|aarch64)_exe .+-glibc2\.(?:[12]?\d|30)\b - lockV2 yt-dlp/yt-dlp 2024.10.22 (?!\w+_exe).+ Python 3\.8 + lockV2 yt-dlp/yt-dlp 2024.10.22 zip Python 3\.8 lockV2 yt-dlp/yt-dlp 2024.10.22 win(?:_x86)?_exe Python 3\.[78].+ Windows-(?:7-|2008ServerR2) + lockV2 yt-dlp/yt-dlp 2025.08.11 darwin_legacy_exe .+ lockV2 yt-dlp/yt-dlp-nightly-builds 2023.11.15.232826 (?!win_x86_exe).+ Python 3\.7 lockV2 yt-dlp/yt-dlp-nightly-builds 2023.11.15.232826 win_x86_exe .+ Windows-(?:Vista|2008Server) lockV2 yt-dlp/yt-dlp-nightly-builds 2024.10.22.051025 py2exe .+ lockV2 yt-dlp/yt-dlp-nightly-builds 2024.10.22.051025 linux_(?:armv7l|aarch64)_exe .+-glibc2\.(?:[12]?\d|30)\b - lockV2 yt-dlp/yt-dlp-nightly-builds 2024.10.22.051025 (?!\w+_exe).+ Python 3\.8 + lockV2 yt-dlp/yt-dlp-nightly-builds 2024.10.22.051025 zip Python 3\.8 lockV2 yt-dlp/yt-dlp-nightly-builds 2024.10.22.051025 win(?:_x86)?_exe Python 3\.[78].+ Windows-(?:7-|2008ServerR2) + lockV2 yt-dlp/yt-dlp-nightly-builds 2025.08.12.233030 darwin_legacy_exe .+ lockV2 yt-dlp/yt-dlp-master-builds 2023.11.15.232812 (?!win_x86_exe).+ Python 3\.7 lockV2 yt-dlp/yt-dlp-master-builds 2023.11.15.232812 win_x86_exe .+ Windows-(?:Vista|2008Server) lockV2 yt-dlp/yt-dlp-master-builds 2024.10.22.045052 py2exe .+ lockV2 yt-dlp/yt-dlp-master-builds 2024.10.22.060347 linux_(?:armv7l|aarch64)_exe .+-glibc2\.(?:[12]?\d|30)\b - lockV2 yt-dlp/yt-dlp-master-builds 2024.10.22.060347 (?!\w+_exe).+ Python 3\.8 + lockV2 yt-dlp/yt-dlp-master-builds 2024.10.22.060347 zip Python 3\.8 lockV2 yt-dlp/yt-dlp-master-builds 2024.10.22.060347 win(?:_x86)?_exe Python 3\.[78].+ Windows-(?:7-|2008ServerR2) + lockV2 yt-dlp/yt-dlp-master-builds 2025.08.12.232447 darwin_legacy_exe .+ EOF - name: Sign checksum files diff --git a/CONTRIBUTORS b/CONTRIBUTORS index f20b4ce172..629ef7f74a 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -4,6 +4,7 @@ coletdjnz/colethedj (collaborator) Ashish0804 (collaborator) bashonly (collaborator) Grub4K (collaborator) +seproDev (collaborator) h-h-h-h pauldubois98 nixxo @@ -403,7 +404,6 @@ rebane2001 road-master rohieb sdht0 -seproDev Hill-98 LXYan2333 mushbite @@ -793,3 +793,10 @@ moonshinerd R0hanW ShockedPlot7560 swayll +atsushi2965 +barryvan +injust +iribeirocampos +rolandcrosby +Sojiroh +tchebb diff --git a/Changelog.md b/Changelog.md index 7205b95aa3..61ad25f1eb 100644 --- a/Changelog.md +++ b/Changelog.md @@ -4,6 +4,79 @@ # To create a release, dispatch the https://github.com/yt-dlp/yt-dlp/actions/workflows/release.yml workflow on master --> +### 2025.08.11 + +#### Important changes +- **The minimum *recommended* Python version has been raised to 3.10** +Since Python 3.9 will reach end-of-life in October 2025, support for it will be dropped soon. [Read more](https://github.com/yt-dlp/yt-dlp/issues/13858) +- **darwin_legacy_exe builds are being discontinued** +This release's `yt-dlp_macos_legacy` binary will likely be the last one. [Read more](https://github.com/yt-dlp/yt-dlp/issues/13857) +- **linux_armv7l_exe builds are being discontinued** +This release's `yt-dlp_linux_armv7l` binary could be the last one. [Read more](https://github.com/yt-dlp/yt-dlp/issues/13976) + +#### Core changes +- [Deprecate `darwin_legacy_exe` support](https://github.com/yt-dlp/yt-dlp/commit/cc5a5caac5fbc0d605b52bde0778d6fd5f97b5ab) ([#13857](https://github.com/yt-dlp/yt-dlp/issues/13857)) by [bashonly](https://github.com/bashonly) +- [Deprecate `linux_armv7l_exe` support](https://github.com/yt-dlp/yt-dlp/commit/c76ce28e06c816eb5b261dfb6aff6e69dd9b7382) ([#13978](https://github.com/yt-dlp/yt-dlp/issues/13978)) by [bashonly](https://github.com/bashonly) +- [Raise minimum recommended Python version to 3.10](https://github.com/yt-dlp/yt-dlp/commit/23c658b9cbe34a151f8f921ab1320bb5d4e40a4d) ([#13859](https://github.com/yt-dlp/yt-dlp/issues/13859)) by [bashonly](https://github.com/bashonly) +- [Warn when yt-dlp is severely outdated](https://github.com/yt-dlp/yt-dlp/commit/662af5bb8307ec3ff8ab0857f1159922d64792f0) ([#13937](https://github.com/yt-dlp/yt-dlp/issues/13937)) by [seproDev](https://github.com/seproDev) +- **cookies**: [Load cookies with float `expires` timestamps](https://github.com/yt-dlp/yt-dlp/commit/28b68f687561468e0c664dcb430707458970019f) ([#13873](https://github.com/yt-dlp/yt-dlp/issues/13873)) by [bashonly](https://github.com/bashonly) +- **utils** + - [Add `WINDOWS_VT_MODE` to globals](https://github.com/yt-dlp/yt-dlp/commit/eed94c7306d4ecdba53ad8783b1463a9af5c97f1) ([#12460](https://github.com/yt-dlp/yt-dlp/issues/12460)) by [Grub4K](https://github.com/Grub4K) + - `parse_resolution`: [Support width-only pattern](https://github.com/yt-dlp/yt-dlp/commit/4385480795acda35667be008d0bf26b46e9d65b4) ([#13802](https://github.com/yt-dlp/yt-dlp/issues/13802)) by [doe1080](https://github.com/doe1080) + - `random_user_agent`: [Bump versions](https://github.com/yt-dlp/yt-dlp/commit/c59ad2b066bbccd3cc4eed580842f961bce7dd4a) ([#13543](https://github.com/yt-dlp/yt-dlp/issues/13543)) by [bashonly](https://github.com/bashonly) + +#### Extractor changes +- **archive.org**: [Fix metadata extraction](https://github.com/yt-dlp/yt-dlp/commit/42ca3d601ee10cef89d698f72e2b5d44fab4f013) ([#13880](https://github.com/yt-dlp/yt-dlp/issues/13880)) by [bashonly](https://github.com/bashonly) +- **digitalconcerthall**: [Fix formats extraction](https://github.com/yt-dlp/yt-dlp/commit/e8d2807296ccc603e031f5982623a8311f2a5119) ([#13948](https://github.com/yt-dlp/yt-dlp/issues/13948)) by [bashonly](https://github.com/bashonly) +- **eagleplatform**: [Remove extractors](https://github.com/yt-dlp/yt-dlp/commit/1fe83b0111277a6f214c5ec1819cfbf943508baf) ([#13469](https://github.com/yt-dlp/yt-dlp/issues/13469)) by [doe1080](https://github.com/doe1080) +- **fauliolive** + - [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/3e609b2cedd285739bf82c7af7853735092070a4) ([#13421](https://github.com/yt-dlp/yt-dlp/issues/13421)) by [CasperMcFadden95](https://github.com/CasperMcFadden95), [seproDev](https://github.com/seproDev) + - [Support Bahry TV](https://github.com/yt-dlp/yt-dlp/commit/daa1859be1b0e7d123da8b4e0988f2eb7bd47d15) ([#13850](https://github.com/yt-dlp/yt-dlp/issues/13850)) by [CasperMcFadden95](https://github.com/CasperMcFadden95) +- **fc2**: [Fix old video support](https://github.com/yt-dlp/yt-dlp/commit/cd31c319e3142622ec43c49485d196ed2835df05) ([#12633](https://github.com/yt-dlp/yt-dlp/issues/12633)) by [JChris246](https://github.com/JChris246), [seproDev](https://github.com/seproDev) +- **motherless**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/e8d49b1c7f11c7e282319395ca9c2a201304be41) ([#13960](https://github.com/yt-dlp/yt-dlp/issues/13960)) by [Grub4K](https://github.com/Grub4K) +- **n1info**: article: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/6539ee1947d7885d3606da6365fd858308435a63) ([#13865](https://github.com/yt-dlp/yt-dlp/issues/13865)) by [u-spec-png](https://github.com/u-spec-png) +- **neteasemusic**: [Support XFF](https://github.com/yt-dlp/yt-dlp/commit/e8c2bf798b6707d27fecde66161172da69c7cd72) ([#11044](https://github.com/yt-dlp/yt-dlp/issues/11044)) by [c-basalt](https://github.com/c-basalt) +- **niconico**: [Fix error handling & improve metadata extraction](https://github.com/yt-dlp/yt-dlp/commit/05e553e9d1f57655d65c9811d05df38261601b85) ([#13240](https://github.com/yt-dlp/yt-dlp/issues/13240)) by [doe1080](https://github.com/doe1080) +- **parlview**: [Rework extractor](https://github.com/yt-dlp/yt-dlp/commit/485de69dbfeb7de7bcf9f7fe16d6c6ba9e81e1a0) ([#13788](https://github.com/yt-dlp/yt-dlp/issues/13788)) by [barryvan](https://github.com/barryvan) +- **plyrembed**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/61d4cd0bc01be6ebe11fd53c2d3805d1a2058990) ([#13836](https://github.com/yt-dlp/yt-dlp/issues/13836)) by [seproDev](https://github.com/seproDev) +- **royalive**: [Support `en` URLs](https://github.com/yt-dlp/yt-dlp/commit/43dedbe6394bdd489193b15ee9690a62d1b82d94) ([#13908](https://github.com/yt-dlp/yt-dlp/issues/13908)) by [CasperMcFadden95](https://github.com/CasperMcFadden95) +- **rtve.es**: program: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/b831406a1d3be34c159835079d12bae624c43610) ([#12955](https://github.com/yt-dlp/yt-dlp/issues/12955)) by [meGAmeS1](https://github.com/meGAmeS1), [seproDev](https://github.com/seproDev) +- **shiey**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/6ff135c31914ea8b5545f8d187c60e852cfde9bc) ([#13354](https://github.com/yt-dlp/yt-dlp/issues/13354)) by [iribeirocampos](https://github.com/iribeirocampos) +- **sportdeuschland**: [Support embedded player URLs](https://github.com/yt-dlp/yt-dlp/commit/30302df22b7b431ce920e0f7298cd10be9989967) ([#13833](https://github.com/yt-dlp/yt-dlp/issues/13833)) by [InvalidUsernameException](https://github.com/InvalidUsernameException) +- **sproutvideo**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/59765ecbc08d18005de7143fbb1d1caf90239471) ([#13813](https://github.com/yt-dlp/yt-dlp/issues/13813)) by [bashonly](https://github.com/bashonly) +- **tbs**: [Fix truTV support](https://github.com/yt-dlp/yt-dlp/commit/0adeb1e54b2d7e95cd19999e71013877850f8f41) ([#9683](https://github.com/yt-dlp/yt-dlp/issues/9683)) by [bashonly](https://github.com/bashonly), [ischmidt20](https://github.com/ischmidt20) +- **tbsjp**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/71f30921a2023dbb25c53fd1bb1399cac803116d) ([#13485](https://github.com/yt-dlp/yt-dlp/issues/13485)) by [garret1317](https://github.com/garret1317) +- **tver** + - [Extract Streaks API info](https://github.com/yt-dlp/yt-dlp/commit/70d7687487252a08dbf8b2831743e7833472ba05) ([#13885](https://github.com/yt-dlp/yt-dlp/issues/13885)) by [bashonly](https://github.com/bashonly) + - [Support --ignore-no-formats-error when geo-blocked](https://github.com/yt-dlp/yt-dlp/commit/121647705a2fc6b968278723fe61801007e228a4) ([#13598](https://github.com/yt-dlp/yt-dlp/issues/13598)) by [arabcoders](https://github.com/arabcoders) +- **tvw**: news: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/682334e4b35112f7a5798decdcb5cb12230ef948) ([#12907](https://github.com/yt-dlp/yt-dlp/issues/12907)) by [fries1234](https://github.com/fries1234) +- **vimeo**: [Fix login support and require authentication](https://github.com/yt-dlp/yt-dlp/commit/afaf60d9fd5a0c7a85aeb1374fd97fbc13cd652c) ([#13823](https://github.com/yt-dlp/yt-dlp/issues/13823)) by [bashonly](https://github.com/bashonly) +- **yandexdisk**: [Support 360 URLs](https://github.com/yt-dlp/yt-dlp/commit/a6df5e8a58d6743dd230011389c986495ec509da) ([#13935](https://github.com/yt-dlp/yt-dlp/issues/13935)) by [Sojiroh](https://github.com/Sojiroh) +- **youtube** + - [Add player params to mweb client](https://github.com/yt-dlp/yt-dlp/commit/38c2bf40260f7788efb5a7f5e8eba8e5cb43f741) ([#13914](https://github.com/yt-dlp/yt-dlp/issues/13914)) by [coletdjnz](https://github.com/coletdjnz) + - [Update player params](https://github.com/yt-dlp/yt-dlp/commit/bf366517ef0b745490ee9e0f929254fa26b69647) ([#13979](https://github.com/yt-dlp/yt-dlp/issues/13979)) by [bashonly](https://github.com/bashonly) + +#### Downloader changes +- **dash**: [Re-extract if using --load-info-json with --live-from-start](https://github.com/yt-dlp/yt-dlp/commit/fe53ebe5b66a03c664708a4d6fd87b8c13a1bc7b) ([#13922](https://github.com/yt-dlp/yt-dlp/issues/13922)) by [bashonly](https://github.com/bashonly) +- **external**: [Work around ffmpeg's `file:` URL handling](https://github.com/yt-dlp/yt-dlp/commit/d399505fdf8292332bdc91d33859a0b0d08104fd) ([#13844](https://github.com/yt-dlp/yt-dlp/issues/13844)) by [bashonly](https://github.com/bashonly) +- **hls**: [Fix `--hls-split-continuity` support](https://github.com/yt-dlp/yt-dlp/commit/57186f958f164daa50203adcbf7ec74d541151cf) ([#13321](https://github.com/yt-dlp/yt-dlp/issues/13321)) by [tchebb](https://github.com/tchebb) + +#### Postprocessor changes +- **embedthumbnail**: [Fix ffmpeg args for embedding in mp3](https://github.com/yt-dlp/yt-dlp/commit/7e3f48d64d237281a97b3df1a61980c78a0302fe) ([#13720](https://github.com/yt-dlp/yt-dlp/issues/13720)) by [atsushi2965](https://github.com/atsushi2965) +- **xattrmetadata**: [Add macOS "Where from" attribute](https://github.com/yt-dlp/yt-dlp/commit/3e918d825d7ff367812658957b281b8cda8f9ebb) ([#12664](https://github.com/yt-dlp/yt-dlp/issues/12664)) by [rolandcrosby](https://github.com/rolandcrosby) (With fixes in [1e0c77d](https://github.com/yt-dlp/yt-dlp/commit/1e0c77ddcce335a1875ecc17d93ed6ff3fabd975) by [seproDev](https://github.com/seproDev)) + +#### Networking changes +- **Request Handler** + - curl_cffi: [Support `curl_cffi` 0.11.x, 0.12.x, 0.13.x](https://github.com/yt-dlp/yt-dlp/commit/e98695549e2eb8ce4a59abe16b5afa8adc075bbe) ([#13989](https://github.com/yt-dlp/yt-dlp/issues/13989)) by [bashonly](https://github.com/bashonly) + - requests: [Bump minimum required version of urllib3 to 2.0.2](https://github.com/yt-dlp/yt-dlp/commit/8175f3738fe4db3bc629d36bb72b927d4286d3f9) ([#13939](https://github.com/yt-dlp/yt-dlp/issues/13939)) by [bashonly](https://github.com/bashonly) + +#### Misc. changes +- **build**: [Use `macos-14` runner for `macos` builds](https://github.com/yt-dlp/yt-dlp/commit/66aa21dc5a3b79059c38f3ad1d05dc9b29187701) ([#13814](https://github.com/yt-dlp/yt-dlp/issues/13814)) by [bashonly](https://github.com/bashonly) +- **ci**: [Bump supported PyPy version to 3.11](https://github.com/yt-dlp/yt-dlp/commit/62e2a9c0d55306906f18da2927e05e1cbc31473c) ([#13877](https://github.com/yt-dlp/yt-dlp/issues/13877)) by [bashonly](https://github.com/bashonly) +- **cleanup** + - [Move embed tests to dedicated extractors](https://github.com/yt-dlp/yt-dlp/commit/1c6068af997cfc0e28061fc00f4d6091e1de57da) ([#13782](https://github.com/yt-dlp/yt-dlp/issues/13782)) by [doe1080](https://github.com/doe1080) + - Miscellaneous: [5e4ceb3](https://github.com/yt-dlp/yt-dlp/commit/5e4ceb35cf997af0dbf100e1de37f4e2bcbaa0b7) by [bashonly](https://github.com/bashonly), [injust](https://github.com/injust), [seproDev](https://github.com/seproDev) + ### 2025.07.21 #### Important changes diff --git a/README.md b/README.md index 12f68e98d8..aa8b1d4f24 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,6 @@ File|Description [yt-dlp_linux_aarch64](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_linux_aarch64)|Linux standalone aarch64 (64-bit) binary [yt-dlp_win.zip](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_win.zip)|Unpackaged Windows executable (no auto-update) [yt-dlp_macos.zip](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_macos.zip)|Unpackaged MacOS (10.15+) executable (no auto-update) -[yt-dlp_macos_legacy](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_macos_legacy)|MacOS (10.9+) standalone x64 executable #### Misc @@ -171,6 +170,9 @@ yt-dlp --update-to nightly python3 -m pip install -U --pre "yt-dlp[default]" ``` +When running a yt-dlp version that is older than 90 days, you will see a warning message suggesting to update to the latest version. +You can suppress this warning by adding `--no-update` to your command or configuration file. + ## DEPENDENCIES Python versions 3.9+ (CPython) and 3.11+ (PyPy) are supported. Other versions and implementations may or may not work correctly. @@ -208,7 +210,7 @@ The following provide support for impersonating browser requests. This may be re * [**mutagen**](https://github.com/quodlibet/mutagen)\* - For `--embed-thumbnail` in certain formats. Licensed under [GPLv2+](https://github.com/quodlibet/mutagen/blob/master/COPYING) * [**AtomicParsley**](https://github.com/wez/atomicparsley) - For `--embed-thumbnail` in `mp4`/`m4a` files when `mutagen`/`ffmpeg` cannot. Licensed under [GPLv2+](https://github.com/wez/atomicparsley/blob/master/COPYING) -* [**xattr**](https://github.com/xattr/xattr), [**pyxattr**](https://github.com/iustin/pyxattr) or [**setfattr**](http://savannah.nongnu.org/projects/attr) - For writing xattr metadata (`--xattr`) on **Mac** and **BSD**. Licensed under [MIT](https://github.com/xattr/xattr/blob/master/LICENSE.txt), [LGPL2.1](https://github.com/iustin/pyxattr/blob/master/COPYING) and [GPLv2+](http://git.savannah.nongnu.org/cgit/attr.git/tree/doc/COPYING) respectively +* [**xattr**](https://github.com/xattr/xattr), [**pyxattr**](https://github.com/iustin/pyxattr) or [**setfattr**](http://savannah.nongnu.org/projects/attr) - For writing xattr metadata (`--xattrs`) on **Mac** and **BSD**. Licensed under [MIT](https://github.com/xattr/xattr/blob/master/LICENSE.txt), [LGPL2.1](https://github.com/iustin/pyxattr/blob/master/COPYING) and [GPLv2+](http://git.savannah.nongnu.org/cgit/attr.git/tree/doc/COPYING) respectively ### Misc @@ -2367,7 +2369,6 @@ These are aliases that are no longer documented for various reasons --dump-headers --print-traffic --dump-intermediate-pages --dump-pages --force-write-download-archive --force-write-archive - --load-info --load-info-json --no-clean-infojson --no-clean-info-json --no-split-tracks --no-split-chapters --no-write-srt --no-write-subs diff --git a/devscripts/bash-completion.in b/devscripts/bash-completion.in index bb66c20956..994bb4e721 100644 --- a/devscripts/bash-completion.in +++ b/devscripts/bash-completion.in @@ -6,7 +6,7 @@ __yt_dlp() prev="${COMP_WORDS[COMP_CWORD-1]}" opts="{{flags}}" keywords=":ytfavorites :ytrecommended :ytsubscriptions :ytwatchlater :ythistory" - fileopts="-a|--batch-file|--download-archive|--cookies|--load-info" + fileopts="-a|--batch-file|--download-archive|--cookies|--load-info-json" diropts="--cache-dir" if [[ ${prev} =~ ${fileopts} ]]; then diff --git a/devscripts/changelog_override.json b/devscripts/changelog_override.json index c22ea94bfc..9b808a7481 100644 --- a/devscripts/changelog_override.json +++ b/devscripts/changelog_override.json @@ -272,5 +272,26 @@ "action": "add", "when": "959ac99e98c3215437e573c22d64be42d361e863", "short": "[priority] Security: [[CVE-2025-54072](https://nvd.nist.gov/vuln/detail/CVE-2025-54072)] [Fix `--exec` placeholder expansion on Windows](https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-45hg-7f49-5h56)\n - When `--exec` is used on Windows, the filepath expanded from `{}` (or the default placeholder) is now properly escaped" + }, + { + "action": "change", + "when": "b831406a1d3be34c159835079d12bae624c43610", + "short": "[ie/rtve.es:program] Add extractor (#12955)", + "authors": ["meGAmeS1", "seproDev"] + }, + { + "action": "add", + "when": "23c658b9cbe34a151f8f921ab1320bb5d4e40a4d", + "short": "[priority] **The minimum *recommended* Python version has been raised to 3.10**\nSince Python 3.9 will reach end-of-life in October 2025, support for it will be dropped soon. [Read more](https://github.com/yt-dlp/yt-dlp/issues/13858)" + }, + { + "action": "add", + "when": "cc5a5caac5fbc0d605b52bde0778d6fd5f97b5ab", + "short": "[priority] **darwin_legacy_exe builds are being discontinued**\nThis release's `yt-dlp_macos_legacy` binary will likely be the last one. [Read more](https://github.com/yt-dlp/yt-dlp/issues/13857)" + }, + { + "action": "add", + "when": "c76ce28e06c816eb5b261dfb6aff6e69dd9b7382", + "short": "[priority] **linux_armv7l_exe builds are being discontinued**\nThis release's `yt-dlp_linux_armv7l` binary could be the last one. [Read more](https://github.com/yt-dlp/yt-dlp/issues/13976)" } ] diff --git a/devscripts/cli_to_api.py b/devscripts/cli_to_api.py index 9c2710e09f..cc86b413f4 100755 --- a/devscripts/cli_to_api.py +++ b/devscripts/cli_to_api.py @@ -20,6 +20,7 @@ def parse_patched_options(opts): 'fragment_retries': 0, 'extract_flat': False, 'concat_playlist': 'never', + 'update_self': False, }) yt_dlp.options.create_parser = lambda: patched_parser try: diff --git a/pyproject.toml b/pyproject.toml index 41d5ec3b0f..d8c3d9e822 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,11 +15,11 @@ description = "A feature-rich command-line audio/video downloader" readme = "README.md" requires-python = ">=3.9" keywords = [ + "cli", + "downloader", "youtube-dl", - "video-downloader", "youtube-downloader", "sponsorblock", - "youtube-dlc", "yt-dlp", ] license = {file = "LICENSE"} @@ -51,11 +51,11 @@ default = [ "mutagen", "pycryptodomex", "requests>=2.32.2,<3", - "urllib3>=1.26.17,<3", + "urllib3>=2.0.2,<3", "websockets>=13.0", ] curl-cffi = [ - "curl-cffi>=0.5.10,!=0.6.*,!=0.7.*,!=0.8.*,!=0.9.*,<0.11; implementation_name=='cpython'", + "curl-cffi>=0.5.10,!=0.6.*,!=0.7.*,!=0.8.*,!=0.9.*,<0.14; implementation_name=='cpython'", ] secretstorage = [ "cffi", diff --git a/setup.cfg b/setup.cfg index 20d40cd303..a556eb29f5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,7 +16,7 @@ remove-unused-variables = true [tox:tox] skipsdist = true -envlist = py{39,310,311,312,313},pypy310 +envlist = py{39,310,311,312,313},pypy311 skip_missing_interpreters = true [testenv] # tox diff --git a/supportedsites.md b/supportedsites.md index 3e0bef4bcf..26d5dab42c 100644 --- a/supportedsites.md +++ b/supportedsites.md @@ -12,7 +12,7 @@ The only reliable way to check if a site is supported is to try it. - **17live:vod** - **1News**: 1news.co.nz article videos - **1tv**: Первый канал - - **20min** + - **20min**: (**Currently broken**) - **23video** - **247sports**: (**Currently broken**) - **24tv.ua** @@ -44,10 +44,10 @@ The only reliable way to check if a site is supported is to try it. - **ADN**: [*animationdigitalnetwork*](## "netrc machine") Animation Digital Network - **ADNSeason**: [*animationdigitalnetwork*](## "netrc machine") Animation Digital Network - **AdobeConnect** - - **adobetv** - - **adobetv:channel** - - **adobetv:embed** - - **adobetv:show** + - **adobetv**: (**Currently broken**) + - **adobetv:channel**: (**Currently broken**) + - **adobetv:embed**: (**Currently broken**) + - **adobetv:show**: (**Currently broken**) - **adobetv:video** - **AdultSwim** - **aenetworks**: A+E Networks: A&E, Lifetime, History.com, FYI Network and History Vault @@ -285,7 +285,6 @@ The only reliable way to check if a site is supported is to try it. - **Clipchamp** - **Clippit** - **ClipRs**: (**Currently broken**) - - **ClipYouEmbed** - **CloserToTruth**: (**Currently broken**) - **CloudflareStream** - **CloudyCDN** @@ -396,7 +395,6 @@ The only reliable way to check if a site is supported is to try it. - **dw:article**: (**Currently broken**) - **dzen.ru**: Дзен (dzen) formerly Яндекс.Дзен (Yandex Zen) - **dzen.ru:channel** - - **EaglePlatform** - **EbaumsWorld** - **Ebay** - **egghead:course**: egghead.io course @@ -447,6 +445,7 @@ The only reliable way to check if a site is supported is to try it. - **fancode:live**: [*fancode*](## "netrc machine") (**Currently broken**) - **fancode:vod**: [*fancode*](## "netrc machine") (**Currently broken**) - **Fathom** + - **FaulioLive** - **faz.net** - **fc2**: [*fc2*](## "netrc machine") - **fc2:embed** @@ -728,7 +727,7 @@ The only reliable way to check if a site is supported is to try it. - **Liputan6** - **ListenNotes** - **LiTV** - - **LiveJournal** + - **LiveJournal**: (**Currently broken**) - **livestream** - **livestream:original** - **Livestreamfails** @@ -1056,7 +1055,7 @@ The only reliable way to check if a site is supported is to try it. - **ParamountPressExpress** - **Parler**: Posts on parler.com - **parliamentlive.tv**: UK parliament videos - - **Parlview**: (**Currently broken**) + - **Parlview** - **parti:livestream** - **parti:video** - **patreon** @@ -1105,6 +1104,7 @@ The only reliable way to check if a site is supported is to try it. - **pluralsight:course** - **PlutoTV**: (**Currently broken**) - **PlVideo**: Платформа + - **PlyrEmbed** - **PodbayFM** - **PodbayFMChannel** - **Podchaser** @@ -1258,6 +1258,7 @@ The only reliable way to check if a site is supported is to try it. - **rtve.es:alacarta**: RTVE a la carta and Play - **rtve.es:audio**: RTVE audio - **rtve.es:live**: RTVE.es live streams + - **rtve.es:program**: RTVE.es programs - **rtve.es:television** - **rtvslo.si** - **rtvslo.si:show** @@ -1275,7 +1276,7 @@ The only reliable way to check if a site is supported is to try it. - **rutube:playlist**: Rutube playlists - **rutube:tags**: Rutube tags - **RUTV**: RUTV.RU - - **Ruutu** + - **Ruutu**: (**Currently broken**) - **Ruv** - **ruv.is:spila** - **S4C** @@ -1326,6 +1327,7 @@ The only reliable way to check if a site is supported is to try it. - **SharePoint** - **ShareVideosEmbed** - **ShemarooMe** + - **Shiey** - **ShowRoomLive** - **ShugiinItvLive**: 衆議院インターネット審議中継 - **ShugiinItvLiveRoom**: 衆議院インターネット審議中継 (中継) @@ -1383,7 +1385,7 @@ The only reliable way to check if a site is supported is to try it. - **SpankBangPlaylist** - **Spiegel** - **Sport5** - - **SportBox** + - **SportBox**: (**Currently broken**) - **SportDeutschland** - **spotify**: Spotify episodes (**Currently broken**) - **spotify:show**: Spotify shows (**Currently broken**) @@ -1524,7 +1526,6 @@ The only reliable way to check if a site is supported is to try it. - **TrueID** - **TruNews** - **Truth** - - **TruTV** - **Tube8**: (**Currently broken**) - **TubeTuGraz**: [*tubetugraz*](## "netrc machine") tube.tugraz.at - **TubeTuGrazSeries**: [*tubetugraz*](## "netrc machine") @@ -1569,6 +1570,7 @@ The only reliable way to check if a site is supported is to try it. - **TVPlayer** - **TVPlayHome** - **tvw** + - **tvw:news** - **tvw:tvchannels** - **Tweakers** - **TwitCasting** @@ -1624,7 +1626,7 @@ The only reliable way to check if a site is supported is to try it. - **vice**: (**Currently broken**) - **vice:article**: (**Currently broken**) - **vice:show**: (**Currently broken**) - - **Viddler** + - **Viddler**: (**Currently broken**) - **Videa** - **video.arnes.si**: Arnes Video - **video.google:search**: Google Video search; "gvsearch:" prefix diff --git a/test/test_update.py b/test/test_update.py index 23c12d38c1..b4979bc92c 100644 --- a/test/test_update.py +++ b/test/test_update.py @@ -84,8 +84,9 @@ lock 2023.11.16 (?!win_x86_exe).+ Python 3\.7 lock 2023.11.16 win_x86_exe .+ Windows-(?:Vista|2008Server) lock 2024.10.22 py2exe .+ lock 2024.10.22 linux_(?:armv7l|aarch64)_exe .+-glibc2\.(?:[12]?\d|30)\b -lock 2024.10.22 (?!\w+_exe).+ Python 3\.8 +lock 2024.10.22 zip Python 3\.8 lock 2024.10.22 win(?:_x86)?_exe Python 3\.[78].+ Windows-(?:7-|2008ServerR2) +lock 2025.08.11 darwin_legacy_exe .+ ''' TEST_LOCKFILE_V2_TMPL = r'''%s @@ -94,20 +95,23 @@ lockV2 yt-dlp/yt-dlp 2023.11.16 (?!win_x86_exe).+ Python 3\.7 lockV2 yt-dlp/yt-dlp 2023.11.16 win_x86_exe .+ Windows-(?:Vista|2008Server) lockV2 yt-dlp/yt-dlp 2024.10.22 py2exe .+ lockV2 yt-dlp/yt-dlp 2024.10.22 linux_(?:armv7l|aarch64)_exe .+-glibc2\.(?:[12]?\d|30)\b -lockV2 yt-dlp/yt-dlp 2024.10.22 (?!\w+_exe).+ Python 3\.8 +lockV2 yt-dlp/yt-dlp 2024.10.22 zip Python 3\.8 lockV2 yt-dlp/yt-dlp 2024.10.22 win(?:_x86)?_exe Python 3\.[78].+ Windows-(?:7-|2008ServerR2) +lockV2 yt-dlp/yt-dlp 2025.08.11 darwin_legacy_exe .+ lockV2 yt-dlp/yt-dlp-nightly-builds 2023.11.15.232826 (?!win_x86_exe).+ Python 3\.7 lockV2 yt-dlp/yt-dlp-nightly-builds 2023.11.15.232826 win_x86_exe .+ Windows-(?:Vista|2008Server) lockV2 yt-dlp/yt-dlp-nightly-builds 2024.10.22.051025 py2exe .+ lockV2 yt-dlp/yt-dlp-nightly-builds 2024.10.22.051025 linux_(?:armv7l|aarch64)_exe .+-glibc2\.(?:[12]?\d|30)\b -lockV2 yt-dlp/yt-dlp-nightly-builds 2024.10.22.051025 (?!\w+_exe).+ Python 3\.8 +lockV2 yt-dlp/yt-dlp-nightly-builds 2024.10.22.051025 zip Python 3\.8 lockV2 yt-dlp/yt-dlp-nightly-builds 2024.10.22.051025 win(?:_x86)?_exe Python 3\.[78].+ Windows-(?:7-|2008ServerR2) +lockV2 yt-dlp/yt-dlp-nightly-builds 2025.08.12.233030 darwin_legacy_exe .+ lockV2 yt-dlp/yt-dlp-master-builds 2023.11.15.232812 (?!win_x86_exe).+ Python 3\.7 lockV2 yt-dlp/yt-dlp-master-builds 2023.11.15.232812 win_x86_exe .+ Windows-(?:Vista|2008Server) lockV2 yt-dlp/yt-dlp-master-builds 2024.10.22.045052 py2exe .+ lockV2 yt-dlp/yt-dlp-master-builds 2024.10.22.060347 linux_(?:armv7l|aarch64)_exe .+-glibc2\.(?:[12]?\d|30)\b -lockV2 yt-dlp/yt-dlp-master-builds 2024.10.22.060347 (?!\w+_exe).+ Python 3\.8 +lockV2 yt-dlp/yt-dlp-master-builds 2024.10.22.060347 zip Python 3\.8 lockV2 yt-dlp/yt-dlp-master-builds 2024.10.22.060347 win(?:_x86)?_exe Python 3\.[78].+ Windows-(?:7-|2008ServerR2) +lockV2 yt-dlp/yt-dlp-master-builds 2025.08.12.232447 darwin_legacy_exe .+ ''' TEST_LOCKFILE_V2 = TEST_LOCKFILE_V2_TMPL % TEST_LOCKFILE_COMMENT @@ -217,6 +221,10 @@ class TestUpdate(unittest.TestCase): test( # linux_aarch64_exe w/glibc2.3 should only update to glibc<2.31 lock lockfile, 'linux_aarch64_exe Python 3.8.0 (CPython aarch64 64bit) - Linux-6.5.0-1025-azure-aarch64-with-glibc2.3 (OpenSSL', '2025.01.01', '2024.10.22') + test(lockfile, 'darwin_legacy_exe Python 3.10.5', '2025.08.11', '2025.08.11') + test(lockfile, 'darwin_legacy_exe Python 3.10.5', '2025.08.11', '2025.08.11', exact=True) + test(lockfile, 'darwin_legacy_exe Python 3.10.5', '2025.08.12', '2025.08.11') + test(lockfile, 'darwin_legacy_exe Python 3.10.5', '2025.08.12', None, exact=True) # Forks can block updates to non-numeric tags rather than lock test(TEST_LOCKFILE_FORK, 'zip Python 3.6.3', 'pr0000', None, repo='fork/yt-dlp') diff --git a/test/test_youtube_signature.py b/test/test_youtube_signature.py index 4562467534..684a6175db 100644 --- a/test/test_youtube_signature.py +++ b/test/test_youtube_signature.py @@ -138,6 +138,16 @@ _SIG_TESTS = [ 'gN7a-hudCuAuPH6fByOk1_GNXN0yNMHShjZXS2VOgsEItAJz0tipeavEOmNdYN-wUtcEqD3bCXjc0iyKfAyZxCBGgIARwsSdQfJ2CJtt', 'JC2JfQdSswRAIgGBCxZyAfKyi0cjXCb3DqEctUw-NYdNmOEvaepit0zJAtIEsgOV2SXZjhSHMNy0NXNG_1kOyBf6HPuAuCduh-a', ), + ( + 'https://www.youtube.com/s/player/010fbc8d/player_es5.vflset/en_US/base.js', + 'gN7a-hudCuAuPH6fByOk1_GNXN0yNMHShjZXS2VOgsEItAJz0tipeavEOmNdYN-wUtcEqD3bCXjc0iyKfAyZxCBGgIARwsSdQfJ2CJtt', + 'ttJC2JfQdSswRAIgGBCxZyAfKyi0cjXCb3DqEctUw-NYdNmOEvaepit2zJAsIEggOVaSXZjhSHMNy0NXNG_1kOyBf6HPuAuCduh-', + ), + ( + 'https://www.youtube.com/s/player/010fbc8d/player_es6.vflset/en_US/base.js', + 'gN7a-hudCuAuPH6fByOk1_GNXN0yNMHShjZXS2VOgsEItAJz0tipeavEOmNdYN-wUtcEqD3bCXjc0iyKfAyZxCBGgIARwsSdQfJ2CJtt', + 'ttJC2JfQdSswRAIgGBCxZyAfKyi0cjXCb3DqEctUw-NYdNmOEvaepit2zJAsIEggOVaSXZjhSHMNy0NXNG_1kOyBf6HPuAuCduh-', + ), ] _NSIG_TESTS = [ @@ -377,6 +387,14 @@ _NSIG_TESTS = [ 'https://www.youtube.com/s/player/ef259203/player_ias_tce.vflset/en_US/base.js', 'rPqBC01nJpqhhi2iA2U', 'hY7dbiKFT51UIA', ), + ( + 'https://www.youtube.com/s/player/010fbc8d/player_es5.vflset/en_US/base.js', + '0hlOAlqjFszVvF4Z', 'R-H23bZGAsRFTg', + ), + ( + 'https://www.youtube.com/s/player/010fbc8d/player_es6.vflset/en_US/base.js', + '0hlOAlqjFszVvF4Z', 'R-H23bZGAsRFTg', + ), ] diff --git a/yt_dlp/YoutubeDL.py b/yt_dlp/YoutubeDL.py index a9f347bf4a..5ef2be21e5 100644 --- a/yt_dlp/YoutubeDL.py +++ b/yt_dlp/YoutubeDL.py @@ -73,6 +73,7 @@ from .postprocessor.ffmpeg import resolve_mapping as resolve_recode_mapping from .update import ( REPOSITORY, _get_system_deprecation, + _get_outdated_warning, _make_label, current_git_head, detect_variant, @@ -504,6 +505,7 @@ class YoutubeDL: force_keyframes_at_cuts: Re-encode the video when downloading ranges to get precise cuts noprogress: Do not print the progress bar live_from_start: Whether to download livestreams videos from the start + warn_when_outdated: Emit a warning if the yt-dlp version is older than 90 days The following parameters are not used by YoutubeDL itself, they are used by the downloader (see yt_dlp/downloader/common.py): @@ -613,7 +615,7 @@ class YoutubeDL: 'player_url', 'protocol', 'fragment_base_url', 'fragments', 'is_from_start', 'is_dash_periods', 'request_data', 'preference', 'language', 'language_preference', 'quality', 'source_preference', 'cookies', 'http_headers', 'stretched_ratio', 'no_resume', 'has_drm', 'extra_param_to_segment_url', 'extra_param_to_key_url', - 'hls_aes', 'downloader_options', 'page_url', 'app', 'play_path', 'tc_url', 'flash_version', + 'hls_aes', 'downloader_options', 'impersonate', 'page_url', 'app', 'play_path', 'tc_url', 'flash_version', 'rtmp_live', 'rtmp_conn', 'rtmp_protocol', 'rtmp_real_time', } _deprecated_multivalue_fields = { @@ -703,6 +705,9 @@ class YoutubeDL: system_deprecation = _get_system_deprecation() if system_deprecation: self.deprecated_feature(system_deprecation.replace('\n', '\n ')) + elif self.params.get('warn_when_outdated'): + if outdated_warning := _get_outdated_warning(): + self.report_warning(outdated_warning) if self.params.get('allow_unplayable_formats'): self.report_warning( @@ -749,8 +754,6 @@ class YoutubeDL: if self.params.get('geo_verification_proxy') is None: self.params['geo_verification_proxy'] = self.params['cn_verification_proxy'] - check_deprecated('autonumber', '--auto-number', '-o "%(autonumber)s-%(title)s.%(ext)s"') - check_deprecated('usetitle', '--title', '-o "%(title)s-%(id)s.%(ext)s"') check_deprecated('useid', '--id', '-o "%(id)s.%(ext)s"') for msg in self.params.get('_warnings', []): diff --git a/yt_dlp/__init__.py b/yt_dlp/__init__.py index 2e7646b7ec..3277cbfa1a 100644 --- a/yt_dlp/__init__.py +++ b/yt_dlp/__init__.py @@ -500,6 +500,14 @@ def validate_options(opts): 'To let yt-dlp download and merge the best available formats, simply do not pass any format selection', 'If you know what you are doing and want only the best pre-merged format, use "-f b" instead to suppress this warning'))) + # Common mistake: -f mp4 + if opts.format == 'mp4': + warnings.append('.\n '.join(( + '"-f mp4" selects the best pre-merged mp4 format which is often not what\'s intended', + 'Pre-merged mp4 formats are not available from all sites, or may only be available in lower quality', + 'To prioritize the best h264 video and aac audio in an mp4 container, use "-t mp4" instead', + 'If you know what you are doing and want a pre-merged mp4 format, use "-f b[ext=mp4]" instead to suppress this warning'))) + # --(postprocessor/downloader)-args without name def report_args_compat(name, value, key1, key2=None, where=None): if key1 in value and key2 not in value: @@ -971,6 +979,7 @@ def parse_options(argv=None): 'geo_bypass': opts.geo_bypass, 'geo_bypass_country': opts.geo_bypass_country, 'geo_bypass_ip_block': opts.geo_bypass_ip_block, + 'warn_when_outdated': opts.update_self is None, '_warnings': warnings, '_deprecation_warnings': deprecation_warnings, 'compat_opts': opts.compat_opts, @@ -1030,6 +1039,7 @@ def _real_main(argv=None): (ImpersonateTarget('safari'), 'curl_cffi'), (ImpersonateTarget('firefox'), 'curl_cffi>=0.10'), (ImpersonateTarget('edge'), 'curl_cffi'), + (ImpersonateTarget('tor'), 'curl_cffi>=0.11'), ] available_targets = ydl._get_available_impersonate_targets() diff --git a/yt_dlp/downloader/dash.py b/yt_dlp/downloader/dash.py index afc79b6caf..bf8652d8b0 100644 --- a/yt_dlp/downloader/dash.py +++ b/yt_dlp/downloader/dash.py @@ -3,7 +3,7 @@ import urllib.parse from . import get_suitable_downloader from .fragment import FragmentFD -from ..utils import update_url_query, urljoin +from ..utils import ReExtractInfo, update_url_query, urljoin class DashSegmentsFD(FragmentFD): @@ -28,6 +28,11 @@ class DashSegmentsFD(FragmentFD): requested_formats = [{**info_dict, **fmt} for fmt in info_dict.get('requested_formats', [])] args = [] for fmt in requested_formats or [info_dict]: + # Re-extract if --load-info-json is used and 'fragments' was originally a generator + # See https://github.com/yt-dlp/yt-dlp/issues/13906 + if isinstance(fmt['fragments'], str): + raise ReExtractInfo('the stream needs to be re-extracted', expected=True) + try: fragment_count = 1 if self.params.get('test') else len(fmt['fragments']) except TypeError: diff --git a/yt_dlp/extractor/common.py b/yt_dlp/extractor/common.py index 4a4b5416d0..a96fb9c4cb 100644 --- a/yt_dlp/extractor/common.py +++ b/yt_dlp/extractor/common.py @@ -243,7 +243,7 @@ class InfoExtractor: * extra_param_to_segment_url A query string to append to each fragment's URL, or to update each existing query string with. If it is an HLS stream with an AES-128 decryption key, - the query paramaters will be passed to the key URI as well, + the query parameters will be passed to the key URI as well, unless there is an `extra_param_to_key_url` given, or unless an external key URI is provided via `hls_aes`. Only applied by the native HLS/DASH downloaders. @@ -419,7 +419,7 @@ class InfoExtractor: __post_extractor: A function to be called just before the metadata is written to either disk, logger or console. The function must return a dict which will be added to the info_dict. - This is usefull for additional information that is + This is useful for additional information that is time-consuming to extract. Note that the fields thus extracted will not be available to output template and match_filter. So, only "comments" and "comment_count" are @@ -1527,11 +1527,11 @@ class InfoExtractor: r'>\s*(?:18\s+U(?:\.S\.C\.|SC)\s+)?(?:§+\s*)?2257\b', ] - age_limit = 0 + age_limit = None for marker in AGE_LIMIT_MARKERS: mobj = re.search(marker, html) if mobj: - age_limit = max(age_limit, int(traverse_obj(mobj, 1, default=18))) + age_limit = max(age_limit or 0, int(traverse_obj(mobj, 1, default=18))) return age_limit def _media_rating_search(self, html): @@ -2968,7 +2968,7 @@ class InfoExtractor: else: codecs = parse_codecs(codec_str) if content_type not in ('video', 'audio', 'text'): - if mime_type == 'image/jpeg': + if mime_type in ('image/avif', 'image/jpeg'): content_type = mime_type elif codecs.get('vcodec', 'none') != 'none': content_type = 'video' @@ -3028,14 +3028,14 @@ class InfoExtractor: 'manifest_url': mpd_url, 'filesize': filesize, } - elif content_type == 'image/jpeg': + elif content_type in ('image/avif', 'image/jpeg'): # See test case in VikiIE # https://www.viki.com/videos/1175236v-choosing-spouse-by-lottery-episode-1 f = { 'format_id': format_id, 'ext': 'mhtml', 'manifest_url': mpd_url, - 'format_note': 'DASH storyboards (jpeg)', + 'format_note': f'DASH storyboards ({mimetype2ext(mime_type)})', 'acodec': 'none', 'vcodec': 'none', } @@ -3177,7 +3177,7 @@ class InfoExtractor: 'url': mpd_url or base_url, 'fragment_base_url': base_url, 'fragments': [], - 'protocol': 'http_dash_segments' if mime_type != 'image/jpeg' else 'mhtml', + 'protocol': 'mhtml' if mime_type in ('image/avif', 'image/jpeg') else 'http_dash_segments', }) if 'initialization_url' in representation_ms_info: initialization_url = representation_ms_info['initialization_url'] @@ -3192,7 +3192,7 @@ class InfoExtractor: else: # Assuming direct URL to unfragmented media. f['url'] = base_url - if content_type in ('video', 'audio', 'image/jpeg'): + if content_type in ('video', 'audio', 'image/avif', 'image/jpeg'): f['manifest_stream_number'] = stream_numbers[f['url']] stream_numbers[f['url']] += 1 period_entry['formats'].append(f) diff --git a/yt_dlp/extractor/digitalconcerthall.py b/yt_dlp/extractor/digitalconcerthall.py index 4c4fe470da..be16f5e873 100644 --- a/yt_dlp/extractor/digitalconcerthall.py +++ b/yt_dlp/extractor/digitalconcerthall.py @@ -4,6 +4,7 @@ from .common import InfoExtractor from ..networking.exceptions import HTTPError from ..utils import ( ExtractorError, + determine_ext, jwt_decode_hs256, parse_codecs, try_get, @@ -222,11 +223,18 @@ class DigitalConcertHallIE(InfoExtractor): raise formats = [] - for m3u8_url in traverse_obj(stream_info, ('channel', ..., 'stream', ..., 'url', {url_or_none})): - formats.extend(self._extract_m3u8_formats(m3u8_url, video_id, 'mp4', m3u8_id='hls', fatal=False)) - for fmt in formats: - if fmt.get('format_note') and fmt.get('vcodec') == 'none': - fmt.update(parse_codecs(fmt['format_note'])) + for fmt_url in traverse_obj(stream_info, ('channel', ..., 'stream', ..., 'url', {url_or_none})): + ext = determine_ext(fmt_url) + if ext == 'm3u8': + fmts = self._extract_m3u8_formats(fmt_url, video_id, 'mp4', m3u8_id='hls', fatal=False) + for fmt in fmts: + if fmt.get('format_note') and fmt.get('vcodec') == 'none': + fmt.update(parse_codecs(fmt['format_note'])) + formats.extend(fmts) + elif ext == 'mpd': + formats.extend(self._extract_mpd_formats(fmt_url, video_id, mpd_id='dash', fatal=False)) + else: + self.report_warning(f'Skipping unsupported format extension "{ext}"') yield { 'id': video_id, diff --git a/yt_dlp/extractor/generic.py b/yt_dlp/extractor/generic.py index bc34aafa7a..b3a27f31e8 100644 --- a/yt_dlp/extractor/generic.py +++ b/yt_dlp/extractor/generic.py @@ -121,7 +121,6 @@ class GenericIE(InfoExtractor): 'id': 'cauky-lidi-70-dil-babis-predstavil-pohadky-prymulanek-nebo-andrejovy-nove-saty-ac867', 'ext': 'mp4', 'title': 'čauky lidi 70 finall', - 'age_limit': 0, 'description': 'md5:47b2673a5b76780d9d329783e1fbf5aa', 'direct': True, 'duration': 318.0, @@ -244,7 +243,6 @@ class GenericIE(InfoExtractor): 'id': 'paris-d-moll', 'ext': 'mp4', 'title': 'Paris d-moll', - 'age_limit': 0, 'description': 'md5:319e37ea5542293db37e1e13072fe330', 'thumbnail': r're:https?://www\.filmarkivet\.se/wp-content/uploads/.+\.jpg', }, @@ -255,7 +253,6 @@ class GenericIE(InfoExtractor): 'info_dict': { 'id': '60413035', 'title': 'Etter ett års planlegging, klaffet endelig alt: - Jeg måtte ta en liten dans', - 'age_limit': 0, 'description': 'md5:bbb4e12e42e78609a74fd421b93b1239', 'thumbnail': r're:https?://www\.dagbladet\.no/images/.+', }, @@ -267,7 +264,6 @@ class GenericIE(InfoExtractor): 'info_dict': { 'id': 'single_clip', 'title': 'Single Clip player examples', - 'age_limit': 0, }, 'playlist_count': 3, }, { @@ -324,7 +320,6 @@ class GenericIE(InfoExtractor): 'id': 'videos-1', 'ext': 'mp4', 'title': 'Videos & Audio - King Machine (1)', - 'age_limit': 0, 'description': 'Browse King Machine videos & audio for sweet media. Your eyes will thank you.', 'thumbnail': r're:https?://media\.indiedb\.com/cache/images/.+\.jpg', '_old_archive_ids': ['generic videos'], @@ -363,7 +358,6 @@ class GenericIE(InfoExtractor): 'id': '21217', 'ext': 'mp4', 'title': '40 ночей (2016) - BogMedia.org', - 'age_limit': 0, 'description': 'md5:4e6d7d622636eb7948275432eb256dc3', 'display_id': '40-nochey-2016', 'thumbnail': r're:https?://bogmedia\.org/contents/videos_screenshots/.+\.jpg', @@ -378,7 +372,6 @@ class GenericIE(InfoExtractor): 'id': '18485', 'ext': 'mp4', 'title': 'Клип: Ленинград - ЗОЖ скачать, смотреть онлайн | Youix.com', - 'age_limit': 0, 'display_id': 'leningrad-zoj', 'thumbnail': r're:https?://youix\.com/contents/videos_screenshots/.+\.jpg', }, @@ -419,7 +412,6 @@ class GenericIE(InfoExtractor): 'id': '105', 'ext': 'mp4', 'title': 'Kelis - 4th Of July / Embed Player', - 'age_limit': 0, 'display_id': 'kelis-4th-of-july', 'thumbnail': r're:https?://www\.kvs-demo\.com/contents/videos_screenshots/.+\.jpg', }, @@ -430,9 +422,8 @@ class GenericIE(InfoExtractor): 'info_dict': { 'id': 'beltzlaw-1', 'ext': 'mp4', - 'title': 'Beltz Law Group | Dallas Traffic Ticket, Accident & Criminal Attorney (1)', - 'age_limit': 0, - 'description': 'md5:5bdf23fcb76801dc3b31e74cabf82147', + 'title': str, + 'description': str, 'thumbnail': r're:https?://beltzlaw\.com/wp-content/uploads/.+\.jpg', 'timestamp': int, # varies 'upload_date': str, @@ -447,7 +438,6 @@ class GenericIE(InfoExtractor): 'id': 'cine-1', 'ext': 'webm', 'title': 'CINE.AR (1)', - 'age_limit': 0, 'description': 'md5:a4e58f9e2291c940e485f34251898c4a', 'thumbnail': r're:https?://cine\.ar/img/.+\.png', '_old_archive_ids': ['generic cine'], @@ -461,7 +451,6 @@ class GenericIE(InfoExtractor): 'id': 'ipy2AcGL', 'ext': 'mp4', 'title': 'Hoe een bladvlo dit verwoestende Japanse onkruid moet vernietigen', - 'age_limit': 0, 'description': 'md5:6a9d644bab0dc2dc06849c2505d8383d', 'duration': 111.0, 'thumbnail': r're:https?://images\.nu\.nl/.+\.jpg', @@ -477,7 +466,6 @@ class GenericIE(InfoExtractor): 'id': 'porsche-911-gt3-rs-rij-impressie-2', 'ext': 'mp4', 'title': 'Test: Porsche 911 GT3 RS - AutoWeek', - 'age_limit': 0, 'description': 'md5:a17b5bd84288448d8f11b838505718fc', 'direct': True, 'thumbnail': r're:https?://images\.autoweek\.nl/.+', @@ -493,7 +481,6 @@ class GenericIE(InfoExtractor): 'id': 'k6gl2kt2eq', 'ext': 'mp4', 'title': 'Breezy HR\'s ATS helps you find & hire employees sooner', - 'age_limit': 0, 'average_rating': 4.5, 'description': 'md5:eee75fdd3044c538003f3be327ba01e1', 'duration': 60.1, @@ -509,7 +496,6 @@ class GenericIE(InfoExtractor): 'id': 'videojs_hls_test', 'ext': 'mp4', 'title': 'video', - 'age_limit': 0, 'duration': 1800, }, 'params': {'skip_download': 'm3u8'}, diff --git a/yt_dlp/extractor/motherless.py b/yt_dlp/extractor/motherless.py index 86551950b7..e236ec3db8 100644 --- a/yt_dlp/extractor/motherless.py +++ b/yt_dlp/extractor/motherless.py @@ -51,23 +51,7 @@ class MotherlessIE(InfoExtractor): 'skip': '404', }, { 'url': 'http://motherless.com/g/cosplay/633979F', - 'md5': '0b2a43f447a49c3e649c93ad1fafa4a0', - 'info_dict': { - 'id': '633979F', - 'ext': 'mp4', - 'title': 'Turtlette', - 'categories': ['superheroine heroine superher'], - 'upload_date': '20140827', - 'uploader_id': 'shade0230', - 'thumbnail': r're:https?://.*\.jpg', - 'age_limit': 18, - 'like_count': int, - 'comment_count': int, - 'view_count': int, - }, - 'params': { - 'nocheckcertificate': True, - }, + 'expected_exception': 'ExtractorError', }, { 'url': 'http://motherless.com/8B4BBC1', 'info_dict': { @@ -113,8 +97,10 @@ class MotherlessIE(InfoExtractor): webpage = self._download_webpage(url, video_id) if any(p in webpage for p in ( - '404 - MOTHERLESS.COM<', - ">The page you're looking for cannot be found.<")): + '<title>404 - MOTHERLESS.COM<', + ">The page you're looking for cannot be found.<", + '<div class="error-page', + )): raise ExtractorError(f'Video {video_id} does not exist', expected=True) if '>The content you are trying to view is for friends only.' in webpage: @@ -183,6 +169,9 @@ class MotherlessPaginatedIE(InfoExtractor): def _correct_path(self, url, item_id): raise NotImplementedError('This method must be implemented by subclasses') + def _correct_title(self, title, /): + return title.partition(' - Videos')[0] if title else None + def _extract_entries(self, webpage, base): for mobj in re.finditer(r'href="[^"]*(?P<href>/[A-F0-9]+)"\s+title="(?P<title>[^"]+)', webpage): @@ -205,7 +194,7 @@ class MotherlessPaginatedIE(InfoExtractor): return self.playlist_result( OnDemandPagedList(get_page, self._PAGE_SIZE), item_id, - remove_end(self._html_extract_title(webpage), ' | MOTHERLESS.COM ™')) + self._correct_title(self._html_extract_title(webpage))) class MotherlessGroupIE(MotherlessPaginatedIE): @@ -214,7 +203,7 @@ class MotherlessGroupIE(MotherlessPaginatedIE): 'url': 'http://motherless.com/gv/movie_scenes', 'info_dict': { 'id': 'movie_scenes', - 'title': 'Movie Scenes - Videos - Hot and sexy scenes from "regular" movies... Beautiful actresses fully', + 'title': 'Movie Scenes', }, 'playlist_mincount': 540, }, { @@ -230,7 +219,7 @@ class MotherlessGroupIE(MotherlessPaginatedIE): 'id': 'beautiful_cock', 'title': 'Beautiful Cock', }, - 'playlist_mincount': 2040, + 'playlist_mincount': 371, }] def _correct_path(self, url, item_id): @@ -245,14 +234,14 @@ class MotherlessGalleryIE(MotherlessPaginatedIE): 'id': '338999F', 'title': 'Random', }, - 'playlist_mincount': 171, + 'playlist_mincount': 100, }, { 'url': 'https://motherless.com/GVABD6213', 'info_dict': { 'id': 'ABD6213', 'title': 'Cuties', }, - 'playlist_mincount': 2, + 'playlist_mincount': 1, }, { 'url': 'https://motherless.com/GVBCF7622', 'info_dict': { @@ -266,9 +255,12 @@ class MotherlessGalleryIE(MotherlessPaginatedIE): 'id': '035DE2F', 'title': 'General', }, - 'playlist_mincount': 420, + 'playlist_mincount': 234, }] + def _correct_title(self, title, /): + return remove_end(title, ' | MOTHERLESS.COM ™') + def _correct_path(self, url, item_id): return urllib.parse.urljoin(url, f'/GV{item_id}') @@ -279,14 +271,14 @@ class MotherlessUploaderIE(MotherlessPaginatedIE): 'url': 'https://motherless.com/u/Mrgo4hrs2023', 'info_dict': { 'id': 'Mrgo4hrs2023', - 'title': "Mrgo4hrs2023's Uploads - Videos", + 'title': "Mrgo4hrs2023's Uploads", }, 'playlist_mincount': 32, }, { 'url': 'https://motherless.com/u/Happy_couple?t=v', 'info_dict': { 'id': 'Happy_couple', - 'title': "Happy_couple's Uploads - Videos", + 'title': "Happy_couple's Uploads", }, 'playlist_mincount': 8, }] diff --git a/yt_dlp/extractor/n1.py b/yt_dlp/extractor/n1.py index b4371c299e..cc2ea392a7 100644 --- a/yt_dlp/extractor/n1.py +++ b/yt_dlp/extractor/n1.py @@ -145,7 +145,9 @@ class N1InfoIIE(InfoExtractor): webpage = self._download_webpage(url, video_id) title = self._og_search_title(webpage) or self._html_extract_title(webpage) - timestamp = unified_timestamp(self._og_search_property('published_time', webpage, default=None) or self._html_search_meta('article:published_time', webpage)) + timestamp = unified_timestamp( + self._og_search_property('published_time', webpage, default=None) + or self._html_search_meta('article:published_time', webpage)) plugin_data = re.findall(r'\$bp\("(?:Brid|TargetVideo)_\d+",\s(.+)\);', webpage) entries = [] if plugin_data: diff --git a/yt_dlp/extractor/tiktok.py b/yt_dlp/extractor/tiktok.py index d9280cec14..18407a0820 100644 --- a/yt_dlp/extractor/tiktok.py +++ b/yt_dlp/extractor/tiktok.py @@ -65,7 +65,7 @@ class TikTokBaseIE(InfoExtractor): @functools.cached_property def _DEVICE_ID(self): - return self._KNOWN_DEVICE_ID or str(random.randint(7250000000000000000, 7351147085025500000)) + return self._KNOWN_DEVICE_ID or str(random.randint(7250000000000000000, 7325099899999994577)) @functools.cached_property def _API_HOSTNAME(self): @@ -942,7 +942,6 @@ class TikTokUserIE(TikTokBaseIE): 'id': 'MS4wLjABAAAAM3R2BtjzVT-uAtstkl2iugMzC6AtnpkojJbjiOdDDrdsTiTR75-8lyWJCY5VvDrZ', }, }] - _USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:115.0) Gecko/20100101 Firefox/115.0' _API_BASE_URL = 'https://www.tiktok.com/api/creator/item_list/' def _build_web_query(self, sec_uid, cursor): @@ -986,9 +985,23 @@ class TikTokUserIE(TikTokBaseIE): cursor = int(time.time() * 1E3) for page in itertools.count(1): - response = self._download_json( - self._API_BASE_URL, display_id, f'Downloading page {page}', - query=self._build_web_query(sec_uid, cursor), headers={'User-Agent': self._USER_AGENT}) + for retry in self.RetryManager(): + response = self._download_json( + self._API_BASE_URL, display_id, f'Downloading page {page}', + query=self._build_web_query(sec_uid, cursor)) + + # Avoid infinite loop caused by bad device_id + # See: https://github.com/yt-dlp/yt-dlp/issues/14031 + current_batch = sorted(traverse_obj(response, ('itemList', ..., 'id', {str}))) + if current_batch and current_batch == sorted(seen_ids): + message = 'TikTok API keeps sending the same page' + if self._KNOWN_DEVICE_ID: + raise ExtractorError( + f'{message}. Try again with a different device_id', expected=True) + # The user didn't pass a device_id so we can reset it and retry + del self._DEVICE_ID + retry.error = ExtractorError( + f'{message}. Taking measures to avoid an infinite loop', expected=True) for video in traverse_obj(response, ('itemList', lambda _, v: v['id'])): video_id = video['id'] @@ -1008,42 +1021,52 @@ class TikTokUserIE(TikTokBaseIE): cursor = old_cursor - 7 * 86_400_000 # In case 'hasMorePrevious' is wrong, break if we have gone back before TikTok existed if cursor < 1472706000000 or not traverse_obj(response, 'hasMorePrevious'): - break + return - def _get_sec_uid(self, user_url, user_name, msg): + # User directly passed sec_uid via prefix URL, bypassing our private account detection + if not user_name and not seen_ids: + self.raise_login_required( + 'This user\'s account is likely private. Log into an account that has access') + + def _extract_sec_uid_from_embed(self, user_name): webpage = self._download_webpage( - user_url, user_name, fatal=False, headers={'User-Agent': 'Mozilla/5.0'}, - note=f'Downloading {msg} webpage', errnote=f'Unable to download {msg} webpage') or '' - return (traverse_obj(self._get_universal_data(webpage, user_name), - ('webapp.user-detail', 'userInfo', 'user', 'secUid', {str})) - or traverse_obj(self._get_sigi_state(webpage, user_name), - ('LiveRoom', 'liveRoomUserInfo', 'user', 'secUid', {str}), - ('UserModule', 'users', ..., 'secUid', {str}, any))) + f'https://www.tiktok.com/embed/@{user_name}', user_name, + 'Downloading user embed page', errnote=False, fatal=False) + if not webpage: + self.report_warning('This user\'s account is either private or has embedding disabled') + return None + + data = traverse_obj(self._search_json( + r'<script[^>]+\bid=[\'"]__FRONTITY_CONNECT_STATE__[\'"][^>]*>', + webpage, 'data', user_name, default={}), + ('source', 'data', f'/embed/@{user_name}', {dict})) + + for aweme_id in traverse_obj(data, ('videoList', ..., 'id', {str})): + webpage_url = self._create_url(user_name, aweme_id) + video_data, _ = self._extract_web_data_and_status(webpage_url, aweme_id, fatal=False) + sec_uid = self._parse_aweme_video_web( + video_data, webpage_url, aweme_id, extract_flat=True).get('channel_id') + if sec_uid: + return sec_uid + + return None def _real_extract(self, url): user_name, sec_uid = self._match_id(url), None if mobj := re.fullmatch(r'MS4wLjABAAAA[\w-]{64}', user_name): user_name, sec_uid = None, mobj.group(0) else: - sec_uid = (self._get_sec_uid(self._UPLOADER_URL_FORMAT % user_name, user_name, 'user') - or self._get_sec_uid(self._UPLOADER_URL_FORMAT % f'{user_name}/live', user_name, 'live')) - - if not sec_uid: webpage = self._download_webpage( - f'https://www.tiktok.com/embed/@{user_name}', user_name, - note='Downloading user embed page', fatal=False) or '' - data = traverse_obj(self._search_json( - r'<script[^>]+\bid=[\'"]__FRONTITY_CONNECT_STATE__[\'"][^>]*>', - webpage, 'data', user_name, default={}), - ('source', 'data', f'/embed/@{user_name}', {dict})) - - for aweme_id in traverse_obj(data, ('videoList', ..., 'id', {str})): - webpage_url = self._create_url(user_name, aweme_id) - video_data, _ = self._extract_web_data_and_status(webpage_url, aweme_id, fatal=False) - sec_uid = self._parse_aweme_video_web( - video_data, webpage_url, aweme_id, extract_flat=True).get('channel_id') - if sec_uid: - break + self._UPLOADER_URL_FORMAT % user_name, user_name, + 'Downloading user webpage', 'Unable to download user webpage', + fatal=False, headers={'User-Agent': 'Mozilla/5.0'}) or '' + detail = traverse_obj( + self._get_universal_data(webpage, user_name), ('webapp.user-detail', {dict})) or {} + if detail.get('statusCode') == 10222: + self.raise_login_required( + 'This user\'s account is private. Log into an account that has access') + sec_uid = traverse_obj(detail, ( + 'userInfo', 'user', 'secUid', {str})) or self._extract_sec_uid_from_embed(user_name) if not sec_uid: raise ExtractorError( diff --git a/yt_dlp/extractor/weibo.py b/yt_dlp/extractor/weibo.py index 420ac38299..dc8a2cd753 100644 --- a/yt_dlp/extractor/weibo.py +++ b/yt_dlp/extractor/weibo.py @@ -8,6 +8,7 @@ from ..utils import ( int_or_none, make_archive_id, mimetype2ext, + parse_qs, parse_resolution, str_or_none, strip_jsonp, @@ -52,13 +53,16 @@ class WeiboBaseIE(InfoExtractor): '_rand': random.random(), }) - def _weibo_download_json(self, url, video_id, *args, fatal=True, note='Downloading JSON metadata', **kwargs): - # XXX: Always fatal; _download_webpage_handle only returns False (not a tuple) on error - webpage, urlh = self._download_webpage_handle(url, video_id, *args, fatal=fatal, note=note, **kwargs) + def _weibo_download_json(self, url, video_id, note='Downloading JSON metadata', data=None, headers=None, query=None): + headers = { + 'Referer': 'https://weibo.com/', + **(headers or {}), + } + webpage, urlh = self._download_webpage_handle(url, video_id, note=note, data=data, headers=headers, query=query) if urllib.parse.urlparse(urlh.url).netloc == 'passport.weibo.com': self._update_visitor_cookies(urlh.url, video_id) - webpage = self._download_webpage(url, video_id, *args, fatal=fatal, note=note, **kwargs) - return self._parse_json(webpage, video_id, fatal=fatal) + webpage = self._download_webpage(url, video_id, note=note, data=data, headers=headers, query=query) + return self._parse_json(webpage, video_id) def _extract_formats(self, video_info): media_info = traverse_obj(video_info, ('page_info', 'media_info')) @@ -189,7 +193,8 @@ class WeiboIE(WeiboBaseIE): def _real_extract(self, url): video_id = self._match_id(url) - meta = self._weibo_download_json(f'https://weibo.com/ajax/statuses/show?id={video_id}', video_id) + meta = self._weibo_download_json( + 'https://weibo.com/ajax/statuses/show', video_id, query={'id': video_id}) mix_media_info = traverse_obj(meta, ('mix_media_info', 'items', ...)) if not mix_media_info: return self._parse_video_info(meta) @@ -205,7 +210,11 @@ class WeiboIE(WeiboBaseIE): class WeiboVideoIE(WeiboBaseIE): - _VALID_URL = r'https?://(?:www\.)?weibo\.com/tv/show/(?P<id>\d+:\d+)' + _VIDEO_ID_RE = r'\d+:(?:[\da-f]{32}|\d{16,})' + _VALID_URL = [ + fr'https?://(?:www\.)?weibo\.com/tv/show/(?P<id>{_VIDEO_ID_RE})', + fr'https?://video\.weibo\.com/show/?\?(?:[^#]+&)?fid=(?P<id>{_VIDEO_ID_RE})', + ] _TESTS = [{ 'url': 'https://weibo.com/tv/show/1034:4797699866951785?from=old_pc_videoshow', 'info_dict': { @@ -227,6 +236,49 @@ class WeiboVideoIE(WeiboBaseIE): 'repost_count': int, '_old_archive_ids': ['weibomobile 4797700463137878'], }, + }, { + 'url': 'https://weibo.com/tv/show/1034:633c288cc043d0ca7808030f1157da64', + 'info_dict': { + 'id': '4189191225395228', + 'ext': 'mp4', + 'display_id': 'FBqgOmDxO', + 'title': '柴犬柴犬的秒拍视频', + 'alt_title': '柴犬柴犬的秒拍视频', + 'description': '午睡当然是要甜甜蜜蜜的啦![坏笑] Instagram:shibainu.gaku http://t.cn/RHbmjzW \u200B\u200B\u200B', + 'uploader': '柴犬柴犬', + 'uploader_id': '5926682210', + 'uploader_url': 'https://weibo.com/u/5926682210', + 'view_count': int, + 'like_count': int, + 'repost_count': int, + 'duration': 53, + 'thumbnail': 'https://wx1.sinaimg.cn/large/006t5KMygy1fmu31fsqbej30hs0hstav.jpg', + 'timestamp': 1514264429, + 'upload_date': '20171226', + '_old_archive_ids': ['weibomobile 4189191225395228'], + }, + }, { + 'url': 'https://video.weibo.com/show?fid=1034:4967272104787984', + 'info_dict': { + 'id': '4967273022359838', + 'ext': 'mp4', + 'display_id': 'Nse4S9TTU', + 'title': '#张婧仪[超话]#📸#婧仪的相册集#  早收工的一天,小张@张婧仪 变身可可爱爱小导游,来次说走就走的泉州City Walk[举手]', + 'alt_title': '#张婧仪[超话]#📸#婧仪的相册集# \n早收工的一天,小张@张婧仪 变身可可爱爱小导游,来次说走就走的泉州City Walk[举手]', + 'description': '#张婧仪[超话]#📸#婧仪的相册集# \n早收工的一天,小张@张婧仪 变身可可爱爱小导游,来次说走就走的泉州City Walk[举手] http://t.cn/A6WTpbEu \u200B\u200B\u200B', + 'uploader': '张婧仪工作室', + 'uploader_id': '7610808848', + 'uploader_url': 'https://weibo.com/u/7610808848', + 'view_count': int, + 'like_count': int, + 'repost_count': int, + 'duration': 85, + 'thumbnail': 'https://wx2.sinaimg.cn/orj480/008j4b3qly1hjsce01gnqj30u00gvwf8.jpg', + 'tags': ['婧仪的相册集'], + 'timestamp': 1699773545, + 'upload_date': '20231112', + '_old_archive_ids': ['weibomobile 4967273022359838'], + }, }] def _real_extract(self, url): @@ -234,8 +286,8 @@ class WeiboVideoIE(WeiboBaseIE): post_data = f'data={{"Component_Play_Playinfo":{{"oid":"{video_id}"}}}}'.encode() video_info = self._weibo_download_json( - f'https://weibo.com/tv/api/component?page=%2Ftv%2Fshow%2F{video_id.replace(":", "%3A")}', - video_id, headers={'Referer': url}, data=post_data)['data']['Component_Play_Playinfo'] + 'https://weibo.com/tv/api/component', video_id, data=post_data, headers={'Referer': url}, + query={'page': f'/tv/show/{video_id}'})['data']['Component_Play_Playinfo'] return self.url_result(f'https://weibo.com/0/{video_info["mid"]}', WeiboIE) @@ -250,6 +302,38 @@ class WeiboUserIE(WeiboBaseIE): 'uploader': '萧影殿下', }, 'playlist_mincount': 195, + }, { + 'url': 'https://weibo.com/u/7610808848?tabtype=newVideo&layerid=4967273022359838', + 'info_dict': { + 'id': '7610808848', + 'title': '张婧仪工作室的视频', + 'description': '张婧仪工作室的全部视频', + 'uploader': '张婧仪工作室', + }, + 'playlist_mincount': 61, + }, { + 'url': 'https://weibo.com/u/7610808848?tabtype=newVideo&layerid=4967273022359838', + 'info_dict': { + 'id': '4967273022359838', + 'ext': 'mp4', + 'display_id': 'Nse4S9TTU', + 'title': '#张婧仪[超话]#📸#婧仪的相册集#  早收工的一天,小张@张婧仪 变身可可爱爱小导游,来次说走就走的泉州City Walk[举手]', + 'alt_title': '#张婧仪[超话]#📸#婧仪的相册集# \n早收工的一天,小张@张婧仪 变身可可爱爱小导游,来次说走就走的泉州City Walk[举手]', + 'description': '#张婧仪[超话]#📸#婧仪的相册集# \n早收工的一天,小张@张婧仪 变身可可爱爱小导游,来次说走就走的泉州City Walk[举手] http://t.cn/A6WTpbEu \u200B\u200B\u200B', + 'uploader': '张婧仪工作室', + 'uploader_id': '7610808848', + 'uploader_url': 'https://weibo.com/u/7610808848', + 'view_count': int, + 'like_count': int, + 'repost_count': int, + 'duration': 85, + 'thumbnail': 'https://wx2.sinaimg.cn/orj480/008j4b3qly1hjsce01gnqj30u00gvwf8.jpg', + 'tags': ['婧仪的相册集'], + 'timestamp': 1699773545, + 'upload_date': '20231112', + '_old_archive_ids': ['weibomobile 4967273022359838'], + }, + 'params': {'noplaylist': True}, }] def _fetch_page(self, uid, cursor=0, page=1): @@ -270,6 +354,11 @@ class WeiboUserIE(WeiboBaseIE): def _real_extract(self, url): uid = self._match_id(url) + params = {k: v[-1] for k, v in parse_qs(url).items()} + video_id = params.get('layerid') if params.get('tabtype') == 'newVideo' else None + if not self._yes_playlist(uid, video_id): + return self.url_result(f'https://weibo.com/{uid}/{video_id}', WeiboIE, video_id) + first_page = self._fetch_page(uid) uploader = traverse_obj(first_page, ('list', ..., 'user', 'screen_name', {str}), get_all=False) metainfo = { diff --git a/yt_dlp/extractor/yandexdisk.py b/yt_dlp/extractor/yandexdisk.py index 3214816701..654858fcca 100644 --- a/yt_dlp/extractor/yandexdisk.py +++ b/yt_dlp/extractor/yandexdisk.py @@ -16,7 +16,7 @@ class YandexDiskIE(InfoExtractor): _VALID_URL = r'''(?x)https?:// (?P<domain> yadi\.sk| - disk\.yandex\. + disk\.(?:360\.)?yandex\. (?: az| by| @@ -51,6 +51,9 @@ class YandexDiskIE(InfoExtractor): }, { 'url': 'https://yadi.sk/public?hash=5DZ296JK9GWCLp02f6jrObjnctjRxMs8L6%2B%2FuhNqk38%3D', 'only_matching': True, + }, { + 'url': 'https://disk.360.yandex.ru/i/TM2xsIVsgjY4uw', + 'only_matching': True, }] def _real_extract(self, url): diff --git a/yt_dlp/extractor/youtube/_base.py b/yt_dlp/extractor/youtube/_base.py index f7dadd013d..d28550794a 100644 --- a/yt_dlp/extractor/youtube/_base.py +++ b/yt_dlp/extractor/youtube/_base.py @@ -105,7 +105,7 @@ INNERTUBE_CLIENTS = { 'INNERTUBE_CONTEXT_CLIENT_NAME': 1, 'SUPPORTS_COOKIES': True, **WEB_PO_TOKEN_POLICIES, - 'PLAYER_PARAMS': '8AEB', + 'PLAYER_PARAMS': '8AEB2AMB', }, # Safari UA returns pre-merged video+audio 144p/240p/360p/720p/1080p HLS formats 'web_safari': { @@ -119,7 +119,7 @@ INNERTUBE_CLIENTS = { 'INNERTUBE_CONTEXT_CLIENT_NAME': 1, 'SUPPORTS_COOKIES': True, **WEB_PO_TOKEN_POLICIES, - 'PLAYER_PARAMS': '8AEB', + 'PLAYER_PARAMS': '8AEB2AMB', }, 'web_embedded': { 'INNERTUBE_CONTEXT': { @@ -282,7 +282,7 @@ INNERTUBE_CLIENTS = { '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)', }, }, - 'PLAYER_PARAMS': '8AEB', + 'PLAYER_PARAMS': '8AEB2AMB', 'INNERTUBE_CONTEXT_CLIENT_NAME': 2, 'GVS_PO_TOKEN_POLICY': { StreamingProtocol.HTTPS: GvsPoTokenPolicy( @@ -314,7 +314,7 @@ INNERTUBE_CLIENTS = { }, 'INNERTUBE_CONTEXT_CLIENT_NAME': 7, 'SUPPORTS_COOKIES': True, - 'PLAYER_PARAMS': '8AEB', + 'PLAYER_PARAMS': '8AEB2AMB', }, 'tv_simply': { 'INNERTUBE_CONTEXT': { diff --git a/yt_dlp/extractor/youtube/_tab.py b/yt_dlp/extractor/youtube/_tab.py index 226e5ede3b..5870786978 100644 --- a/yt_dlp/extractor/youtube/_tab.py +++ b/yt_dlp/extractor/youtube/_tab.py @@ -566,6 +566,7 @@ class YoutubeTabBaseInfoExtractor(YoutubeBaseInfoExtractor): 'gridContinuation': (self._grid_entries, None), 'itemSectionContinuation': (self._post_thread_continuation_entries, None), 'sectionListContinuation': (extract_entries, None), # for feeds + 'lockupViewModel': (self._grid_entries, 'items'), # for playlists tab } continuation_items = traverse_obj(response, ( @@ -1026,7 +1027,7 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor): 'info_dict': { 'id': 'UCqj7Cz7revf5maW9g5pgNcg', 'title': 'Igor Kleiner - Playlists', - 'description': 'md5:15d7dd9e333cb987907fcb0d604b233a', + 'description': r're:(?s)Добро пожаловать на мой канал! Здесь вы найдете видео .{504}/a1/50b/10a$', 'uploader': 'Igor Kleiner ', 'uploader_id': '@IgorDataScience', 'uploader_url': 'https://www.youtube.com/@IgorDataScience', @@ -1043,7 +1044,7 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor): 'info_dict': { 'id': 'UCqj7Cz7revf5maW9g5pgNcg', 'title': 'Igor Kleiner - Playlists', - 'description': 'md5:15d7dd9e333cb987907fcb0d604b233a', + 'description': r're:(?s)Добро пожаловать на мой канал! Здесь вы найдете видео .{504}/a1/50b/10a$', 'uploader': 'Igor Kleiner ', 'uploader_id': '@IgorDataScience', 'uploader_url': 'https://www.youtube.com/@IgorDataScience', @@ -1093,7 +1094,7 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor): 'url': 'https://www.youtube.com/c/ChristophLaimer/playlists', 'only_matching': True, }, { - # TODO: fix availability extraction + # TODO: fix availability and view_count extraction 'note': 'basic, single video playlist', 'url': 'https://www.youtube.com/playlist?list=PLt5yu3-wZAlSLRHmI1qNm0wjyVNWw1pCU', 'info_dict': { @@ -1215,6 +1216,7 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor): 'uploader': 'lex will', }, 'playlist_mincount': 18, + 'skip': 'This Community isn\'t available', }, { # TODO: fix channel_is_verified extraction 'note': 'Search tab', @@ -1399,7 +1401,7 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor): }, { 'url': 'https://www.youtube.com/channel/UCoMdktPbSTixAyNGwb-UYkQ/live', 'info_dict': { - 'id': 'YDvsBbKfLPA', # This will keep changing + 'id': 'VFGoUmo74wE', # This will keep changing 'ext': 'mp4', 'title': str, 'upload_date': r're:\d{8}', @@ -1578,6 +1580,7 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor): 'playlist_count': 50, 'expected_warnings': ['YouTube Music is not directly supported'], }, { + # TODO: fix test suite, 208163447408c78673b08c172beafe5c310fb167 broke this test 'note': 'unlisted single video playlist', 'url': 'https://www.youtube.com/playlist?list=PLt5yu3-wZAlQLfIN0MMgp0wVV6MP3bM4_', 'info_dict': { @@ -1597,19 +1600,19 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor): }, 'playlist': [{ 'info_dict': { - 'title': 'youtube-dl test video "\'/\\ä↭𝕐', - 'id': 'BaW_jenozKc', + 'title': 'Big Buck Bunny 60fps 4K - Official Blender Foundation Short Film', + 'id': 'aqz-KE-bpKQ', '_type': 'url', 'ie_key': 'Youtube', - 'duration': 10, - 'channel_id': 'UCLqxVugv74EIW3VWh2NOa3Q', - 'channel_url': 'https://www.youtube.com/channel/UCLqxVugv74EIW3VWh2NOa3Q', + 'duration': 635, + 'channel_id': 'UCSMOQeBJ2RAnuFungnQOxLg', + 'channel_url': 'https://www.youtube.com/channel/UCSMOQeBJ2RAnuFungnQOxLg', 'view_count': int, - 'url': 'https://www.youtube.com/watch?v=BaW_jenozKc', - 'channel': 'Philipp Hagemeister', - 'uploader_id': '@PhilippHagemeister', - 'uploader_url': 'https://www.youtube.com/@PhilippHagemeister', - 'uploader': 'Philipp Hagemeister', + 'url': 'https://www.youtube.com/watch?v=aqz-KE-bpKQ', + 'channel': 'Blender', + 'uploader_id': '@BlenderOfficial', + 'uploader_url': 'https://www.youtube.com/@BlenderOfficial', + 'uploader': 'Blender', }, }], 'playlist_count': 1, @@ -1675,7 +1678,6 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor): 'url': 'https://www.youtube.com/channel/UCwVVpHQ2Cs9iGJfpdFngePQ', 'only_matching': True, }, { - # TODO: fix metadata extraction 'note': 'collaborative playlist (uploader name in the form "by <uploader> and x other(s)")', 'url': 'https://www.youtube.com/playlist?list=PLx-_-Kk4c89oOHEDQAojOXzEzemXxoqx6', 'info_dict': { @@ -1694,6 +1696,7 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor): 'uploader': 'pukkandan', }, 'playlist_mincount': 2, + 'skip': 'https://github.com/yt-dlp/yt-dlp/issues/13690', }, { 'note': 'translated tab name', 'url': 'https://www.youtube.com/channel/UCiu-3thuViMebBjw_5nWYrA/playlists', @@ -1801,7 +1804,7 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor): 'title': 'Not Just Bikes - Shorts', 'tags': 'count:10', 'channel_url': 'https://www.youtube.com/channel/UC0intLFzLaudFG-xAvUEO-A', - 'description': 'md5:1d9fc1bad7f13a487299d1fe1712e031', + 'description': 'md5:295758591d0d43d8594277be54584da7', 'channel_follower_count': int, 'channel_id': 'UC0intLFzLaudFG-xAvUEO-A', 'channel': 'Not Just Bikes', @@ -1822,7 +1825,7 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor): 'channel_url': 'https://www.youtube.com/channel/UC3eYAvjCVwNHgkaGbXX3sig', 'channel': '中村悠一', 'channel_follower_count': int, - 'description': 'md5:e8fd705073a594f27d6d6d020da560dc', + 'description': 'md5:76b312b48a26c3b0e4d90e2dfc1b417d', 'uploader_url': 'https://www.youtube.com/@Yuichi-Nakamura', 'uploader_id': '@Yuichi-Nakamura', 'uploader': '中村悠一', @@ -1865,12 +1868,13 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor): 'tags': [], }, 'playlist_mincount': 30, + 'skip': 'The channel/playlist does not exist and the URL redirected to youtube.com home page', }, { # Trending Gaming Tab. tab id is empty 'url': 'https://www.youtube.com/feed/trending?bp=4gIcGhpnYW1pbmdfY29ycHVzX21vc3RfcG9wdWxhcg%3D%3D', 'info_dict': { 'id': 'trending', - 'title': 'trending - Gaming', + 'title': 'trending', 'tags': [], }, 'playlist_mincount': 30, @@ -2018,7 +2022,7 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor): 'uploader': 'A Himitsu', 'channel_id': 'UCgFwu-j5-xNJml2FtTrrB3A', 'tags': 'count:12', - 'description': 'I make music', + 'description': 'Music producer, sometimes.', 'channel_url': 'https://www.youtube.com/channel/UCgFwu-j5-xNJml2FtTrrB3A', 'channel_follower_count': int, 'channel_is_verified': True, @@ -2304,19 +2308,20 @@ class YoutubePlaylistIE(YoutubeBaseInfoExtractor): ) IE_NAME = 'youtube:playlist' _TESTS = [{ + # TODO: fix availability extraction 'note': 'issue #673', 'url': 'PLBB231211A4F62143', 'info_dict': { - 'title': '[OLD]Team Fortress 2 (Class-based LP)', + 'title': 'Team Fortress 2 [2010 Version]', 'id': 'PLBB231211A4F62143', - 'uploader': 'Wickman', - 'uploader_id': '@WickmanVT', + 'uploader': 'Wickman Wish', + 'uploader_id': '@WickmanWish', 'description': 'md5:8fa6f52abb47a9552002fa3ddfc57fc2', 'view_count': int, - 'uploader_url': 'https://www.youtube.com/@WickmanVT', + 'uploader_url': 'https://www.youtube.com/@WickmanWish', 'modified_date': r're:\d{8}', 'channel_id': 'UCKSpbfbl5kRQpTdL7kMc-1Q', - 'channel': 'Wickman', + 'channel': 'Wickman Wish', 'tags': [], 'channel_url': 'https://www.youtube.com/channel/UCKSpbfbl5kRQpTdL7kMc-1Q', 'availability': 'public', @@ -2331,6 +2336,7 @@ class YoutubePlaylistIE(YoutubeBaseInfoExtractor): 'playlist_count': 2, 'skip': 'This playlist is private', }, { + # TODO: fix availability extraction 'note': 'embedded', 'url': 'https://www.youtube.com/embed/videoseries?list=PL6IaIsEjSbf96XFRuNccS_RuEXwNdsoEu', 'playlist_count': 4, @@ -2351,6 +2357,7 @@ class YoutubePlaylistIE(YoutubeBaseInfoExtractor): }, 'expected_warnings': [r'[Uu]navailable videos? (is|are|will be) hidden', 'Retrying', 'Giving up'], }, { + # TODO: fix availability extraction 'url': 'http://www.youtube.com/embed/_xDOZElKyNU?list=PLsyOSbh5bs16vubvKePAQ1x3PhKavfBIl', 'playlist_mincount': 455, 'info_dict': { diff --git a/yt_dlp/extractor/youtube/_video.py b/yt_dlp/extractor/youtube/_video.py index 9ff727657c..14582c5f98 100644 --- a/yt_dlp/extractor/youtube/_video.py +++ b/yt_dlp/extractor/youtube/_video.py @@ -1817,6 +1817,8 @@ class YoutubeIE(YoutubeBaseInfoExtractor): _PLAYER_JS_VARIANT_MAP = { 'main': 'player_ias.vflset/en_US/base.js', 'tce': 'player_ias_tce.vflset/en_US/base.js', + 'es5': 'player_es5.vflset/en_US/base.js', + 'es6': 'player_es6.vflset/en_US/base.js', 'tv': 'tv-player-ias.vflset/tv-player-ias.js', 'tv_es6': 'tv-player-es6.vflset/tv-player-es6.js', 'phone': 'player-plasma-ias-phone-en_US.vflset/base.js', diff --git a/yt_dlp/networking/_curlcffi.py b/yt_dlp/networking/_curlcffi.py index 747879da87..90570417bd 100644 --- a/yt_dlp/networking/_curlcffi.py +++ b/yt_dlp/networking/_curlcffi.py @@ -33,9 +33,9 @@ if curl_cffi is None: curl_cffi_version = tuple(map(int, re.split(r'[^\d]+', curl_cffi.__version__)[:3])) -if curl_cffi_version != (0, 5, 10) and not (0, 10) <= curl_cffi_version: +if curl_cffi_version != (0, 5, 10) and not (0, 10) <= curl_cffi_version < (0, 14): curl_cffi._yt_dlp__version = f'{curl_cffi.__version__} (unsupported)' - raise ImportError('Only curl_cffi versions 0.5.10 and 0.10.x are supported') + raise ImportError('Only curl_cffi versions 0.5.10, 0.10.x, 0.11.x, 0.12.x, 0.13.x are supported') import curl_cffi.requests from curl_cffi.const import CurlECode, CurlOpt @@ -120,8 +120,8 @@ BROWSER_TARGETS: dict[tuple[int, ...], dict[str, ImpersonateTarget]] = { 'chrome110': ImpersonateTarget('chrome', '110', 'windows', '10'), 'edge99': ImpersonateTarget('edge', '99', 'windows', '10'), 'edge101': ImpersonateTarget('edge', '101', 'windows', '10'), - 'safari15_3': ImpersonateTarget('safari', '15.3', 'macos', '11'), - 'safari15_5': ImpersonateTarget('safari', '15.5', 'macos', '12'), + 'safari153': ImpersonateTarget('safari', '15.3', 'macos', '11'), + 'safari155': ImpersonateTarget('safari', '15.5', 'macos', '12'), }, (0, 7): { 'chrome116': ImpersonateTarget('chrome', '116', 'windows', '10'), @@ -129,12 +129,12 @@ BROWSER_TARGETS: dict[tuple[int, ...], dict[str, ImpersonateTarget]] = { 'chrome120': ImpersonateTarget('chrome', '120', 'macos', '14'), 'chrome123': ImpersonateTarget('chrome', '123', 'macos', '14'), 'chrome124': ImpersonateTarget('chrome', '124', 'macos', '14'), - 'safari17_0': ImpersonateTarget('safari', '17.0', 'macos', '14'), - 'safari17_2_ios': ImpersonateTarget('safari', '17.2', 'ios', '17.2'), + 'safari170': ImpersonateTarget('safari', '17.0', 'macos', '14'), + 'safari172_ios': ImpersonateTarget('safari', '17.2', 'ios', '17.2'), }, (0, 9): { - 'safari15_3': ImpersonateTarget('safari', '15.3', 'macos', '14'), - 'safari15_5': ImpersonateTarget('safari', '15.5', 'macos', '14'), + 'safari153': ImpersonateTarget('safari', '15.3', 'macos', '14'), + 'safari155': ImpersonateTarget('safari', '15.5', 'macos', '14'), 'chrome119': ImpersonateTarget('chrome', '119', 'macos', '14'), 'chrome120': ImpersonateTarget('chrome', '120', 'macos', '14'), 'chrome123': ImpersonateTarget('chrome', '123', 'macos', '14'), @@ -143,12 +143,33 @@ BROWSER_TARGETS: dict[tuple[int, ...], dict[str, ImpersonateTarget]] = { 'chrome131_android': ImpersonateTarget('chrome', '131', 'android', '14'), 'chrome133a': ImpersonateTarget('chrome', '133', 'macos', '15'), 'firefox133': ImpersonateTarget('firefox', '133', 'macos', '14'), - 'safari18_0': ImpersonateTarget('safari', '18.0', 'macos', '15'), - 'safari18_0_ios': ImpersonateTarget('safari', '18.0', 'ios', '18.0'), + 'safari180': ImpersonateTarget('safari', '18.0', 'macos', '15'), + 'safari180_ios': ImpersonateTarget('safari', '18.0', 'ios', '18.0'), }, (0, 10): { 'firefox135': ImpersonateTarget('firefox', '135', 'macos', '14'), }, + (0, 11): { + 'tor145': ImpersonateTarget('tor', '14.5', 'macos', '14'), + 'safari184': ImpersonateTarget('safari', '18.4', 'macos', '15'), + 'safari184_ios': ImpersonateTarget('safari', '18.4', 'ios', '18.4'), + 'chrome136': ImpersonateTarget('chrome', '136', 'macos', '15'), + }, + (0, 12): { + 'safari260': ImpersonateTarget('safari', '26.0', 'macos', '26'), + 'safari260_ios': ImpersonateTarget('safari', '26.0', 'ios', '26.0'), + }, +} + +# Needed for curl_cffi < 0.11 +# See: https://github.com/lexiforest/curl_cffi/commit/d2f15c7a31506a08d217fcc04ae7570c39f5f5bb +_TARGETS_COMPAT_LOOKUP = { + 'safari153': 'safari15_3', + 'safari155': 'safari15_5', + 'safari170': 'safari17_0', + 'safari172_ios': 'safari17_2_ios', + 'safari180': 'safari18_0', + 'safari180_ios': 'safari18_0_ios', } @@ -159,16 +180,19 @@ class CurlCFFIRH(ImpersonateRequestHandler, InstanceStoreMixin): _SUPPORTED_FEATURES = (Features.NO_PROXY, Features.ALL_PROXY) _SUPPORTED_PROXY_SCHEMES = ('http', 'https', 'socks4', 'socks4a', 'socks5', 'socks5h') _SUPPORTED_IMPERSONATE_TARGET_MAP = { - target: name if curl_cffi_version >= (0, 9) else curl_cffi.requests.BrowserType[name] - for name, target in dict(sorted(itertools.chain.from_iterable( + target: ( + name if curl_cffi_version >= (0, 11) + else _TARGETS_COMPAT_LOOKUP.get(name, name) if curl_cffi_version >= (0, 9) + else curl_cffi.requests.BrowserType[_TARGETS_COMPAT_LOOKUP.get(name, name)] + ) for name, target in dict(sorted(itertools.chain.from_iterable( targets.items() for version, targets in BROWSER_TARGETS.items() if curl_cffi_version >= version ), key=lambda x: ( # deprioritize mobile targets since they give very different behavior x[1].os not in ('ios', 'android'), - # prioritize edge < firefox < safari < chrome - ('edge', 'firefox', 'safari', 'chrome').index(x[1].client), + # prioritize tor < edge < firefox < safari < chrome + ('tor', 'edge', 'firefox', 'safari', 'chrome').index(x[1].client), # prioritize newest version float(x[1].version) if x[1].version else 0, # group by os name diff --git a/yt_dlp/networking/_requests.py b/yt_dlp/networking/_requests.py index 6582038fcb..1526d2a599 100644 --- a/yt_dlp/networking/_requests.py +++ b/yt_dlp/networking/_requests.py @@ -1,6 +1,5 @@ from __future__ import annotations -import contextlib import functools import http.client import logging @@ -20,9 +19,9 @@ if urllib3 is None: urllib3_version = tuple(int_or_none(x, default=0) for x in urllib3.__version__.split('.')) -if urllib3_version < (1, 26, 17): +if urllib3_version < (2, 0, 2): urllib3._yt_dlp__version = f'{urllib3.__version__} (unsupported)' - raise ImportError('Only urllib3 >= 1.26.17 is supported') + raise ImportError('Only urllib3 >= 2.0.2 is supported') if requests.__build__ < 0x023202: requests._yt_dlp__version = f'{requests.__version__} (unsupported)' @@ -101,27 +100,10 @@ class Urllib3PercentREOverride: # https://github.com/urllib3/urllib3/commit/a2697e7c6b275f05879b60f593c5854a816489f0 import urllib3.util.url -if hasattr(urllib3.util.url, 'PERCENT_RE'): - urllib3.util.url.PERCENT_RE = Urllib3PercentREOverride(urllib3.util.url.PERCENT_RE) -elif hasattr(urllib3.util.url, '_PERCENT_RE'): # urllib3 >= 2.0.0 +if hasattr(urllib3.util.url, '_PERCENT_RE'): # was 'PERCENT_RE' in urllib3 < 2.0.0 urllib3.util.url._PERCENT_RE = Urllib3PercentREOverride(urllib3.util.url._PERCENT_RE) else: - warnings.warn('Failed to patch PERCENT_RE in urllib3 (does the attribute exist?)' + bug_reports_message()) - -''' -Workaround for issue in urllib.util.ssl_.py: ssl_wrap_context does not pass -server_hostname to SSLContext.wrap_socket if server_hostname is an IP, -however this is an issue because we set check_hostname to True in our SSLContext. - -Monkey-patching IS_SECURETRANSPORT forces ssl_wrap_context to pass server_hostname regardless. - -This has been fixed in urllib3 2.0+. -See: https://github.com/urllib3/urllib3/issues/517 -''' - -if urllib3_version < (2, 0, 0): - with contextlib.suppress(Exception): - urllib3.util.IS_SECURETRANSPORT = urllib3.util.ssl_.IS_SECURETRANSPORT = True + warnings.warn('Failed to patch _PERCENT_RE in urllib3 (does the attribute exist?)' + bug_reports_message()) # Requests will not automatically handle no_proxy by default diff --git a/yt_dlp/options.py b/yt_dlp/options.py index 13ba445df3..29b37b4255 100644 --- a/yt_dlp/options.py +++ b/yt_dlp/options.py @@ -1526,7 +1526,7 @@ def create_parser(): action='store_false', dest='getcomments', help='Do not retrieve video comments unless the extraction is known to be quick (Alias: --no-get-comments)') filesystem.add_option( - '--load-info-json', '--load-info', + '--load-info-json', dest='load_info_filename', metavar='FILE', help='JSON file containing the video information (created with the "--write-info-json" option)') filesystem.add_option( diff --git a/yt_dlp/postprocessor/xattrpp.py b/yt_dlp/postprocessor/xattrpp.py index fd83d783ba..52404b7d7a 100644 --- a/yt_dlp/postprocessor/xattrpp.py +++ b/yt_dlp/postprocessor/xattrpp.py @@ -1,4 +1,5 @@ import os +import sys from .common import PostProcessor from ..utils import ( @@ -54,6 +55,9 @@ class XAttrMetadataPP(PostProcessor): if infoname == 'upload_date': value = hyphenate_date(value) elif xattrname == 'com.apple.metadata:kMDItemWhereFroms': + # Colon in xattr name throws errors on Windows/NTFS and Linux + if sys.platform != 'darwin': + continue value = self.APPLE_PLIST_TEMPLATE % value write_xattr(info['filepath'], xattrname, value.encode()) diff --git a/yt_dlp/update.py b/yt_dlp/update.py index 30cbf538e9..dd948cd521 100644 --- a/yt_dlp/update.py +++ b/yt_dlp/update.py @@ -2,6 +2,7 @@ from __future__ import annotations import atexit import contextlib +import datetime as dt import functools import hashlib import json @@ -57,26 +58,30 @@ def _get_variant_and_executable_path(): """@returns (variant, executable_path)""" if getattr(sys, 'frozen', False): path = sys.executable + # py2exe is unsupported but we should still correctly identify it for debugging purposes if not hasattr(sys, '_MEIPASS'): return 'py2exe', path - elif sys._MEIPASS == os.path.dirname(path): + if sys._MEIPASS == os.path.dirname(path): return f'{sys.platform}_dir', path - elif sys.platform == 'darwin': + if sys.platform == 'darwin': + # darwin_legacy_exe is no longer supported, but still identify it to block updates machine = '_legacy' if version_tuple(platform.mac_ver()[0]) < (10, 15) else '' - else: - machine = f'_{platform.machine().lower()}' - is_64bits = sys.maxsize > 2**32 - # Ref: https://en.wikipedia.org/wiki/Uname#Examples - if machine[1:] in ('x86', 'x86_64', 'amd64', 'i386', 'i686'): - machine = '_x86' if not is_64bits else '' - # platform.machine() on 32-bit raspbian OS may return 'aarch64', so check "64-bitness" - # See: https://github.com/yt-dlp/yt-dlp/issues/11813 - elif machine[1:] == 'aarch64' and not is_64bits: - machine = '_armv7l' - # sys.executable returns a /tmp/ path for staticx builds (linux_static) - # Ref: https://staticx.readthedocs.io/en/latest/usage.html#run-time-information - if static_exe_path := os.getenv('STATICX_PROG_PATH'): - path = static_exe_path + return f'darwin{machine}_exe', path + + machine = f'_{platform.machine().lower()}' + is_64bits = sys.maxsize > 2**32 + # Ref: https://en.wikipedia.org/wiki/Uname#Examples + if machine[1:] in ('x86', 'x86_64', 'amd64', 'i386', 'i686'): + machine = '_x86' if not is_64bits else '' + # platform.machine() on 32-bit raspbian OS may return 'aarch64', so check "64-bitness" + # See: https://github.com/yt-dlp/yt-dlp/issues/11813 + elif machine[1:] == 'aarch64' and not is_64bits: + machine = '_armv7l' + # sys.executable returns a /tmp/ path for staticx builds (linux_static) + # Ref: https://staticx.readthedocs.io/en/latest/usage.html#run-time-information + if static_exe_path := os.getenv('STATICX_PROG_PATH'): + path = static_exe_path + return f'{remove_end(sys.platform, "32")}{machine}_exe', path path = os.path.dirname(__file__) @@ -110,7 +115,6 @@ _FILE_SUFFIXES = { 'win_exe': '.exe', 'win_x86_exe': '_x86.exe', 'darwin_exe': '_macos', - 'darwin_legacy_exe': '_macos_legacy', 'linux_exe': '_linux', 'linux_aarch64_exe': '_linux_aarch64', 'linux_armv7l_exe': '_linux_armv7l', @@ -146,11 +150,15 @@ def _get_system_deprecation(): STOP_MSG = 'You may stop receiving updates on this version at any time!' variant = detect_variant() - # Temporary until macos_legacy executable builds are discontinued - if variant == 'darwin_legacy_exe': + # Temporary until linux_armv7l executable builds are discontinued + if variant == 'linux_armv7l_exe': return EXE_MSG_TMPL.format( - f'{variant} (the PyInstaller-bundled executable for macOS versions older than 10.15)', - 'issues/13856', STOP_MSG) + f'{variant} (the PyInstaller-bundled executable for the Linux armv7l platform)', + 'issues/13976', STOP_MSG) + + # Temporary until linux_aarch64_exe is built with Python >=3.10 instead of Python 3.9 + if variant == 'linux_aarch64_exe': + return None if sys.version_info > MIN_RECOMMENDED: return None @@ -161,14 +169,23 @@ def _get_system_deprecation(): if sys.version_info < MIN_SUPPORTED: return f'Python version {major}.{minor} is no longer supported! {PYTHON_MSG}' - # Temporary until aarch64/armv7l build flow is bumped to Ubuntu 22.04 and Python 3.10 - if variant in ('linux_aarch64_exe', 'linux_armv7l_exe'): - libc_ver = version_tuple(os.confstr('CS_GNU_LIBC_VERSION').partition(' ')[2]) - if libc_ver < (2, 35): - return EXE_MSG_TMPL.format('system glibc version < 2.35', 'issues/13858', STOP_MSG) + return f'Support for Python version {major}.{minor} has been deprecated. {PYTHON_MSG}' + + +def _get_outdated_warning(): + # Only yt-dlp guarantees a stable release at least every 90 days + if not ORIGIN.startswith('yt-dlp/'): return None - return f'Support for Python version {major}.{minor} has been deprecated. {PYTHON_MSG}' + with contextlib.suppress(Exception): + last_updated = dt.date(*version_tuple(__version__)[:3]) + if last_updated < dt.datetime.now(dt.timezone.utc).date() - dt.timedelta(days=90): + return ('\n '.join(( + f'Your yt-dlp version ({__version__}) is older than 90 days!', + 'It is strongly recommended to always use the latest version.', + f'{is_non_updateable() or """Run "yt-dlp --update" or "yt-dlp -U" to update"""}.', + 'To suppress this warning, add --no-update to your command/config.'))) + return None def _sha256_file(path): diff --git a/yt_dlp/version.py b/yt_dlp/version.py index 868429ffb2..d2d51136e8 100644 --- a/yt_dlp/version.py +++ b/yt_dlp/version.py @@ -1,8 +1,8 @@ # Autogenerated by devscripts/update-version.py -__version__ = '2025.07.21' +__version__ = '2025.08.11' -RELEASE_GIT_HEAD = '9951fdd0d08b655cb1af8cd7f32a3fb7e2b1324e' +RELEASE_GIT_HEAD = '5e4ceb35cf997af0dbf100e1de37f4e2bcbaa0b7' VARIANT = None @@ -12,4 +12,4 @@ CHANNEL = 'stable' ORIGIN = 'yt-dlp/yt-dlp' -_pkg_version = '2025.07.21' +_pkg_version = '2025.08.11'