mirror of
https://github.com/yt-dlp/yt-dlp.git
synced 2024-11-02 06:27:21 +00:00
merge 'master'
This commit is contained in:
commit
628ce197eb
67
.github/workflows/build.yml
vendored
67
.github/workflows/build.yml
vendored
|
@ -72,7 +72,7 @@ on:
|
||||||
default: true
|
default: true
|
||||||
type: boolean
|
type: boolean
|
||||||
windows:
|
windows:
|
||||||
description: yt-dlp.exe, yt-dlp_min.exe, yt-dlp_win.zip
|
description: yt-dlp.exe, yt-dlp_win.zip
|
||||||
default: true
|
default: true
|
||||||
type: boolean
|
type: boolean
|
||||||
windows32:
|
windows32:
|
||||||
|
@ -199,22 +199,24 @@ jobs:
|
||||||
GITHUB_WORKFLOW: build
|
GITHUB_WORKFLOW: build
|
||||||
githubToken: ${{ github.token }} # To cache image
|
githubToken: ${{ github.token }} # To cache image
|
||||||
arch: ${{ matrix.architecture }}
|
arch: ${{ matrix.architecture }}
|
||||||
distro: ubuntu18.04 # Standalone executable should be built on minimum supported OS
|
distro: ubuntu20.04 # Standalone executable should be built on minimum supported OS
|
||||||
dockerRunArgs: --volume "${PWD}/repo:/repo"
|
dockerRunArgs: --volume "${PWD}/repo:/repo"
|
||||||
install: | # Installing Python 3.10 from the Deadsnakes repo raises errors
|
install: | # Installing Python 3.10 from the Deadsnakes repo raises errors
|
||||||
apt update
|
apt update
|
||||||
apt -y install zlib1g-dev libffi-dev python3.8 python3.8-dev python3.8-distutils python3-pip
|
apt -y install zlib1g-dev libffi-dev python3.9 python3.9-dev python3.9-distutils python3-pip \
|
||||||
python3.8 -m pip install -U pip setuptools wheel
|
python3-secretstorage # Cannot build cryptography wheel in virtual armv7 environment
|
||||||
# Cannot access any files from the repo directory at this stage
|
python3.9 -m pip install -U pip wheel 'setuptools>=71.0.2'
|
||||||
python3.8 -m pip install -U Pyinstaller mutagen pycryptodomex websockets brotli certifi secretstorage cffi
|
# 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'
|
||||||
|
|
||||||
run: |
|
run: |
|
||||||
cd repo
|
cd repo
|
||||||
python3.8 devscripts/install_deps.py -o --include build
|
python3.9 devscripts/install_deps.py -o --include build
|
||||||
python3.8 devscripts/install_deps.py --include pyinstaller --include secretstorage # Cached version may be out of date
|
python3.9 devscripts/install_deps.py --include pyinstaller # Cached versions may be out of date
|
||||||
python3.8 devscripts/update-version.py -c "${{ inputs.channel }}" -r "${{ needs.process.outputs.origin }}" "${{ inputs.version }}"
|
python3.9 devscripts/update-version.py -c "${{ inputs.channel }}" -r "${{ needs.process.outputs.origin }}" "${{ inputs.version }}"
|
||||||
python3.8 devscripts/make_lazy_extractors.py
|
python3.9 devscripts/make_lazy_extractors.py
|
||||||
python3.8 -m bundle.pyinstaller
|
python3.9 -m bundle.pyinstaller
|
||||||
|
|
||||||
if ${{ vars.UPDATE_TO_VERIFICATION && 'true' || 'false' }}; then
|
if ${{ vars.UPDATE_TO_VERIFICATION && 'true' || 'false' }}; then
|
||||||
arch="${{ (matrix.architecture == 'armv7' && 'armv7l') || matrix.architecture }}"
|
arch="${{ (matrix.architecture == 'armv7' && 'armv7l') || matrix.architecture }}"
|
||||||
|
@ -240,7 +242,7 @@ jobs:
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
actions: write # For cleaning up cache
|
actions: write # For cleaning up cache
|
||||||
runs-on: macos-12
|
runs-on: macos-13
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
@ -346,7 +348,7 @@ jobs:
|
||||||
macos_legacy:
|
macos_legacy:
|
||||||
needs: process
|
needs: process
|
||||||
if: inputs.macos_legacy
|
if: inputs.macos_legacy
|
||||||
runs-on: macos-12
|
runs-on: macos-13
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
@ -403,13 +405,13 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v5
|
||||||
with: # 3.8 is used for Win7 support
|
with:
|
||||||
python-version: "3.8"
|
python-version: "3.10"
|
||||||
- name: Install Requirements
|
- name: Install Requirements
|
||||||
run: | # Custom pyinstaller built with https://github.com/yt-dlp/pyinstaller-builds
|
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 -o --include build
|
||||||
python devscripts/install_deps.py --include curl-cffi
|
python devscripts/install_deps.py --include curl-cffi
|
||||||
python -m pip install -U "https://yt-dlp.github.io/Pyinstaller-Builds/x86_64/pyinstaller-6.7.0-py3-none-any.whl"
|
python -m pip install -U "https://yt-dlp.github.io/Pyinstaller-Builds/x86_64/pyinstaller-6.10.0-py3-none-any.whl"
|
||||||
|
|
||||||
- name: Prepare
|
- name: Prepare
|
||||||
run: |
|
run: |
|
||||||
|
@ -419,22 +421,12 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
python -m bundle.pyinstaller
|
python -m bundle.pyinstaller
|
||||||
python -m bundle.pyinstaller --onedir
|
python -m bundle.pyinstaller --onedir
|
||||||
Move-Item ./dist/yt-dlp.exe ./dist/yt-dlp_real.exe
|
|
||||||
Compress-Archive -Path ./dist/yt-dlp/* -DestinationPath ./dist/yt-dlp_win.zip
|
Compress-Archive -Path ./dist/yt-dlp/* -DestinationPath ./dist/yt-dlp_win.zip
|
||||||
|
|
||||||
- name: Install Requirements (py2exe)
|
|
||||||
run: |
|
|
||||||
python devscripts/install_deps.py --include py2exe
|
|
||||||
- name: Build (py2exe)
|
|
||||||
run: |
|
|
||||||
python -m bundle.py2exe
|
|
||||||
Move-Item ./dist/yt-dlp.exe ./dist/yt-dlp_min.exe
|
|
||||||
Move-Item ./dist/yt-dlp_real.exe ./dist/yt-dlp.exe
|
|
||||||
|
|
||||||
- name: Verify --update-to
|
- name: Verify --update-to
|
||||||
if: vars.UPDATE_TO_VERIFICATION
|
if: vars.UPDATE_TO_VERIFICATION
|
||||||
run: |
|
run: |
|
||||||
foreach ($name in @("yt-dlp","yt-dlp_min")) {
|
foreach ($name in @("yt-dlp")) {
|
||||||
Copy-Item "./dist/${name}.exe" "./dist/${name}_downgraded.exe"
|
Copy-Item "./dist/${name}.exe" "./dist/${name}_downgraded.exe"
|
||||||
$version = & "./dist/${name}.exe" --version
|
$version = & "./dist/${name}.exe" --version
|
||||||
& "./dist/${name}_downgraded.exe" -v --update-to yt-dlp/yt-dlp@2023.03.04
|
& "./dist/${name}_downgraded.exe" -v --update-to yt-dlp/yt-dlp@2023.03.04
|
||||||
|
@ -450,7 +442,6 @@ jobs:
|
||||||
name: build-bin-${{ github.job }}
|
name: build-bin-${{ github.job }}
|
||||||
path: |
|
path: |
|
||||||
dist/yt-dlp.exe
|
dist/yt-dlp.exe
|
||||||
dist/yt-dlp_min.exe
|
|
||||||
dist/yt-dlp_win.zip
|
dist/yt-dlp_win.zip
|
||||||
compression-level: 0
|
compression-level: 0
|
||||||
|
|
||||||
|
@ -463,13 +454,13 @@ jobs:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "3.8"
|
python-version: "3.10"
|
||||||
architecture: "x86"
|
architecture: "x86"
|
||||||
- name: Install Requirements
|
- name: Install Requirements
|
||||||
run: |
|
run: |
|
||||||
python devscripts/install_deps.py -o --include build
|
python devscripts/install_deps.py -o --include build
|
||||||
python devscripts/install_deps.py
|
python devscripts/install_deps.py
|
||||||
python -m pip install -U "https://yt-dlp.github.io/Pyinstaller-Builds/i686/pyinstaller-6.7.0-py3-none-any.whl"
|
python -m pip install -U "https://yt-dlp.github.io/Pyinstaller-Builds/i686/pyinstaller-6.10.0-py3-none-any.whl"
|
||||||
|
|
||||||
- name: Prepare
|
- name: Prepare
|
||||||
run: |
|
run: |
|
||||||
|
@ -537,13 +528,29 @@ jobs:
|
||||||
lock 2022.08.18.36 .+ Python 3\.6
|
lock 2022.08.18.36 .+ Python 3\.6
|
||||||
lock 2023.11.16 (?!win_x86_exe).+ Python 3\.7
|
lock 2023.11.16 (?!win_x86_exe).+ Python 3\.7
|
||||||
lock 2023.11.16 win_x86_exe .+ Windows-(?:Vista|2008Server)
|
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 win(?:_x86)?_exe Python 3\.[78].+ Windows-(?:7-|2008ServerR2)
|
||||||
lockV2 yt-dlp/yt-dlp 2022.08.18.36 .+ Python 3\.6
|
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).+ Python 3\.7
|
||||||
lockV2 yt-dlp/yt-dlp 2023.11.16 win_x86_exe .+ Windows-(?:Vista|2008Server)
|
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 win(?:_x86)?_exe Python 3\.[78].+ Windows-(?:7-|2008ServerR2)
|
||||||
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).+ 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 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 win(?:_x86)?_exe Python 3\.[78].+ Windows-(?:7-|2008ServerR2)
|
||||||
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).+ 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 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 win(?:_x86)?_exe Python 3\.[78].+ Windows-(?:7-|2008ServerR2)
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
- name: Sign checksum files
|
- name: Sign checksum files
|
||||||
|
|
14
.github/workflows/core.yml
vendored
14
.github/workflows/core.yml
vendored
|
@ -36,16 +36,20 @@ jobs:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
# CPython 3.8 is in quick-test
|
# CPython 3.9 is in quick-test
|
||||||
python-version: ['3.9', '3.10', '3.11', '3.12', pypy-3.8, pypy-3.10]
|
python-version: ['3.10', '3.11', '3.12', '3.13', pypy-3.10]
|
||||||
include:
|
include:
|
||||||
# atleast one of each CPython/PyPy tests must be in windows
|
# atleast one of each CPython/PyPy tests must be in windows
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
python-version: '3.8'
|
python-version: '3.9'
|
||||||
|
- os: windows-latest
|
||||||
|
python-version: '3.10'
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
python-version: '3.12'
|
python-version: '3.12'
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
python-version: pypy-3.9
|
python-version: '3.13'
|
||||||
|
- os: windows-latest
|
||||||
|
python-version: pypy-3.10
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
@ -59,4 +63,4 @@ jobs:
|
||||||
continue-on-error: False
|
continue-on-error: False
|
||||||
run: |
|
run: |
|
||||||
python3 -m yt_dlp -v || true # Print debug head
|
python3 -m yt_dlp -v || true # Print debug head
|
||||||
python3 ./devscripts/run_tests.py core
|
python3 ./devscripts/run_tests.py --pytest-args '--reruns 2 --reruns-delay 3.0' core
|
||||||
|
|
6
.github/workflows/download.yml
vendored
6
.github/workflows/download.yml
vendored
|
@ -28,13 +28,13 @@ jobs:
|
||||||
fail-fast: true
|
fail-fast: true
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
python-version: ['3.10', '3.11', '3.12', pypy-3.8, pypy-3.10]
|
python-version: ['3.10', '3.11', '3.12', '3.13', pypy-3.10]
|
||||||
include:
|
include:
|
||||||
# atleast one of each CPython/PyPy tests must be in windows
|
# atleast one of each CPython/PyPy tests must be in windows
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
python-version: '3.8'
|
python-version: '3.9'
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
python-version: pypy-3.9
|
python-version: pypy-3.10
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
|
10
.github/workflows/quick-test.yml
vendored
10
.github/workflows/quick-test.yml
vendored
|
@ -10,17 +10,17 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python 3.8
|
- name: Set up Python 3.9
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: '3.8'
|
python-version: '3.9'
|
||||||
- name: Install test requirements
|
- name: Install test requirements
|
||||||
run: python3 ./devscripts/install_deps.py --include test
|
run: python3 ./devscripts/install_deps.py -o --include test
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
run: |
|
run: |
|
||||||
python3 -m yt_dlp -v || true
|
python3 -m yt_dlp -v || true
|
||||||
python3 ./devscripts/run_tests.py core
|
python3 ./devscripts/run_tests.py --pytest-args '--reruns 2 --reruns-delay 3.0' core
|
||||||
check:
|
check:
|
||||||
name: Code check
|
name: Code check
|
||||||
if: "!contains(github.event.head_commit.message, 'ci skip all')"
|
if: "!contains(github.event.head_commit.message, 'ci skip all')"
|
||||||
|
@ -29,7 +29,7 @@ jobs:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: '3.8'
|
python-version: '3.9'
|
||||||
- name: Install dev dependencies
|
- name: Install dev dependencies
|
||||||
run: python3 ./devscripts/install_deps.py -o --include static-analysis
|
run: python3 ./devscripts/install_deps.py -o --include static-analysis
|
||||||
- name: Make lazy extractors
|
- name: Make lazy extractors
|
||||||
|
|
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
|
@ -204,7 +204,7 @@ jobs:
|
||||||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
git add -u
|
git add -u
|
||||||
git commit -m "Release ${{ env.version }}" \
|
git commit -m "Release ${{ env.version }}" \
|
||||||
-m "Created by: ${{ github.event.sender.login }}" -m ":ci skip all :ci run dl"
|
-m "Created by: ${{ github.event.sender.login }}" -m ":ci skip all"
|
||||||
git push origin --force ${{ github.event.ref }}:release
|
git push origin --force ${{ github.event.ref }}:release
|
||||||
|
|
||||||
- name: Get target commitish
|
- name: Get target commitish
|
||||||
|
@ -325,7 +325,7 @@ jobs:
|
||||||
"(https://github.com/yt-dlp/yt-dlp-master-builds/releases/latest \"Master builds\")"' || '' }} > ./RELEASE_NOTES
|
"(https://github.com/yt-dlp/yt-dlp-master-builds/releases/latest \"Master builds\")"' || '' }} > ./RELEASE_NOTES
|
||||||
printf '\n\n' >> ./RELEASE_NOTES
|
printf '\n\n' >> ./RELEASE_NOTES
|
||||||
cat >> ./RELEASE_NOTES << EOF
|
cat >> ./RELEASE_NOTES << EOF
|
||||||
#### A description of the various files are in the [README](https://github.com/${{ github.repository }}#release-files)
|
#### A description of the various files is in the [README](https://github.com/${{ github.repository }}#release-files)
|
||||||
---
|
---
|
||||||
$(python ./devscripts/make_changelog.py -vv --collapsible)
|
$(python ./devscripts/make_changelog.py -vv --collapsible)
|
||||||
EOF
|
EOF
|
||||||
|
|
|
@ -37,14 +37,18 @@ # OPENING AN ISSUE
|
||||||
**Please include the full output of yt-dlp when run with `-vU`**, i.e. **add** `-vU` flag to **your command line**, copy the **whole** output and post it in the issue body wrapped in \`\`\` for better formatting. It should look similar to this:
|
**Please include the full output of yt-dlp when run with `-vU`**, i.e. **add** `-vU` flag to **your command line**, copy the **whole** output and post it in the issue body wrapped in \`\`\` for better formatting. It should look similar to this:
|
||||||
```
|
```
|
||||||
$ yt-dlp -vU <your command line>
|
$ yt-dlp -vU <your command line>
|
||||||
[debug] Command-line config: ['-v', 'demo.com']
|
[debug] Command-line config: ['-vU', 'https://www.example.com/']
|
||||||
[debug] Encodings: locale UTF-8, fs utf-8, out utf-8, pref UTF-8
|
[debug] Encodings: locale cp65001, fs utf-8, pref cp65001, out utf-8, error utf-8, screen utf-8
|
||||||
[debug] yt-dlp version 2021.09.25 (zip)
|
[debug] yt-dlp version nightly@... from yt-dlp/yt-dlp-nightly-builds [1a176d874] (win_exe)
|
||||||
[debug] Python version 3.8.10 (CPython 64bit) - Linux-5.4.0-74-generic-x86_64-with-glibc2.29
|
[debug] Python 3.10.11 (CPython AMD64 64bit) - Windows-10-10.0.20348-SP0 (OpenSSL 1.1.1t 7 Feb 2023)
|
||||||
[debug] exe versions: ffmpeg 4.2.4, ffprobe 4.2.4
|
[debug] exe versions: ffmpeg 7.0.2 (setts), ffprobe 7.0.2
|
||||||
|
[debug] Optional libraries: Cryptodome-3.21.0, brotli-1.1.0, certifi-2024.08.30, curl_cffi-0.5.10, mutagen-1.47.0, requests-2.32.3, sqlite3-3.40.1, urllib3-2.2.3, websockets-13.1
|
||||||
[debug] Proxy map: {}
|
[debug] Proxy map: {}
|
||||||
Current Build Hash 25cc412d1d3c0725a1f2f5b7e4682f6fb40e6d15f7024e96f7afd572e9919535
|
[debug] Request Handlers: urllib, requests, websockets, curl_cffi
|
||||||
yt-dlp is up to date (2021.09.25)
|
[debug] Loaded 1838 extractors
|
||||||
|
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
|
||||||
|
Latest version: nightly@... from yt-dlp/yt-dlp-nightly-builds
|
||||||
|
yt-dlp is up to date (nightly@... from yt-dlp/yt-dlp-nightly-builds)
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
**Do not post screenshots of verbose logs; only plain text is acceptable.**
|
**Do not post screenshots of verbose logs; only plain text is acceptable.**
|
||||||
|
@ -233,7 +237,7 @@ ## Adding support for a new site
|
||||||
# * MD5 checksum; start the string with 'md5:', e.g.
|
# * MD5 checksum; start the string with 'md5:', e.g.
|
||||||
# 'description': 'md5:098f6bcd4621d373cade4e832627b4f6',
|
# 'description': 'md5:098f6bcd4621d373cade4e832627b4f6',
|
||||||
# * A regular expression; start the string with 're:', e.g.
|
# * A regular expression; start the string with 're:', e.g.
|
||||||
# 'thumbnail': r're:^https?://.*\.jpg$',
|
# 'thumbnail': r're:https?://.*\.jpg$',
|
||||||
# * A count of elements in a list; start the string with 'count:', e.g.
|
# * A count of elements in a list; start the string with 'count:', e.g.
|
||||||
# 'tags': 'count:10',
|
# 'tags': 'count:10',
|
||||||
# * Any Python type, e.g.
|
# * Any Python type, e.g.
|
||||||
|
@ -268,7 +272,7 @@ ## Adding support for a new site
|
||||||
|
|
||||||
You can use `hatch fmt` to automatically fix problems. Rules that the linter/formatter enforces should not be disabled with `# noqa` unless a maintainer requests it. The only exception allowed is for old/printf-style string formatting in GraphQL query templates (use `# noqa: UP031`).
|
You can use `hatch fmt` to automatically fix problems. Rules that the linter/formatter enforces should not be disabled with `# noqa` unless a maintainer requests it. The only exception allowed is for old/printf-style string formatting in GraphQL query templates (use `# noqa: UP031`).
|
||||||
|
|
||||||
1. Make sure your code works under all [Python](https://www.python.org/) versions supported by yt-dlp, namely CPython and PyPy for Python 3.8 and above. Backward compatibility is not required for even older versions of Python.
|
1. Make sure your code works under all [Python](https://www.python.org/) versions supported by yt-dlp, namely CPython >=3.9 and PyPy >=3.10. Backward compatibility is not required for even older versions of Python.
|
||||||
1. When the tests pass, [add](https://git-scm.com/docs/git-add) the new files, [commit](https://git-scm.com/docs/git-commit) them and [push](https://git-scm.com/docs/git-push) the result, like this:
|
1. When the tests pass, [add](https://git-scm.com/docs/git-add) the new files, [commit](https://git-scm.com/docs/git-commit) them and [push](https://git-scm.com/docs/git-push) the result, like this:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
|
@ -302,10 +306,9 @@ ### Mandatory and optional metafields
|
||||||
For extraction to work yt-dlp relies on metadata your extractor extracts and provides to yt-dlp expressed by an [information dictionary](yt_dlp/extractor/common.py#L119-L440) or simply *info dict*. Only the following meta fields in the *info dict* are considered mandatory for a successful extraction process by yt-dlp:
|
For extraction to work yt-dlp relies on metadata your extractor extracts and provides to yt-dlp expressed by an [information dictionary](yt_dlp/extractor/common.py#L119-L440) or simply *info dict*. Only the following meta fields in the *info dict* are considered mandatory for a successful extraction process by yt-dlp:
|
||||||
|
|
||||||
- `id` (media identifier)
|
- `id` (media identifier)
|
||||||
- `title` (media title)
|
|
||||||
- `url` (media download URL) or `formats`
|
- `url` (media download URL) or `formats`
|
||||||
|
|
||||||
The aforementioned metafields are the critical data that the extraction does not make any sense without and if any of them fail to be extracted then the extractor is considered completely broken. While all extractors must return a `title`, they must also allow it's extraction to be non-fatal.
|
The aforementioned metadata fields are the critical data without which extraction does not make any sense. If any of them fail to be extracted, then the extractor is considered broken. All other metadata extraction should be completely non-fatal.
|
||||||
|
|
||||||
For pornographic sites, appropriate `age_limit` must also be returned.
|
For pornographic sites, appropriate `age_limit` must also be returned.
|
||||||
|
|
||||||
|
|
31
CONTRIBUTORS
31
CONTRIBUTORS
|
@ -657,3 +657,34 @@ luvyana
|
||||||
szantnerb
|
szantnerb
|
||||||
hugepower
|
hugepower
|
||||||
scribblemaniac
|
scribblemaniac
|
||||||
|
Codenade
|
||||||
|
Demon000
|
||||||
|
Deukhoofd
|
||||||
|
grqz
|
||||||
|
hibes
|
||||||
|
Khaoklong51
|
||||||
|
kieraneglin
|
||||||
|
lengzuo
|
||||||
|
naglis
|
||||||
|
ndyanx
|
||||||
|
otovalek
|
||||||
|
quad
|
||||||
|
rakslice
|
||||||
|
sahilsinghss73
|
||||||
|
tony-hn
|
||||||
|
xingchensong
|
||||||
|
BallzCrasher
|
||||||
|
coreywright
|
||||||
|
eric321
|
||||||
|
poyhen
|
||||||
|
tetra-fox
|
||||||
|
444995
|
||||||
|
63427083
|
||||||
|
allendema
|
||||||
|
DarkZeros
|
||||||
|
DTrombett
|
||||||
|
imranh2
|
||||||
|
KarboniteKream
|
||||||
|
mikkovedru
|
||||||
|
pktiuk
|
||||||
|
rubyevadestaxes
|
||||||
|
|
162
Changelog.md
162
Changelog.md
|
@ -4,6 +4,168 @@ # Changelog
|
||||||
# To create a release, dispatch the https://github.com/yt-dlp/yt-dlp/actions/workflows/release.yml workflow on master
|
# To create a release, dispatch the https://github.com/yt-dlp/yt-dlp/actions/workflows/release.yml workflow on master
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
### 2024.10.22
|
||||||
|
|
||||||
|
#### Important changes
|
||||||
|
- **Following this release, yt-dlp's Python dependencies *must* be installed using the `default` group**
|
||||||
|
If you're installing yt-dlp with pip/pipx or requiring yt-dlp in your own Python project, you'll need to specify `yt-dlp[default]` if you want to also install yt-dlp's optional dependencies (which were previously included by default). [Read more](https://github.com/yt-dlp/yt-dlp/pull/11255)
|
||||||
|
- **py2exe is no longer supported**
|
||||||
|
This release's `yt-dlp_min.exe` will be the last, and it's actually a PyInstaller-bundled executable so that yt-dlp users updating their py2exe build with `-U` will be automatically migrated. [Read more](https://github.com/yt-dlp/yt-dlp/issues/10087)
|
||||||
|
|
||||||
|
#### Core changes
|
||||||
|
- [Add extractor helpers](https://github.com/yt-dlp/yt-dlp/commit/d710a6ca7c622705c0c8c8a3615916f531137d5d) ([#10653](https://github.com/yt-dlp/yt-dlp/issues/10653)) by [Grub4K](https://github.com/Grub4K)
|
||||||
|
- [Add option `--plugin-dirs`](https://github.com/yt-dlp/yt-dlp/commit/0f593dca9fa995d88eb763170a932da61c8f24dc) ([#11277](https://github.com/yt-dlp/yt-dlp/issues/11277)) by [coletdjnz](https://github.com/coletdjnz), [imranh2](https://github.com/imranh2)
|
||||||
|
- **cookies**: [Fix compatibility for Python <=3.9 in traceback](https://github.com/yt-dlp/yt-dlp/commit/c5f0f58efd8c3930de8202c15a5c53b1b635bd51) by [Grub4K](https://github.com/Grub4K)
|
||||||
|
- **utils**
|
||||||
|
- `Popen`: [Reset PyInstaller environment](https://github.com/yt-dlp/yt-dlp/commit/fbc66e3ab35743cc847a21223c67d88bb463cd9c) ([#11258](https://github.com/yt-dlp/yt-dlp/issues/11258)) by [bashonly](https://github.com/bashonly), [Grub4K](https://github.com/Grub4K)
|
||||||
|
- `sanitize_path`: [Reimplement function](https://github.com/yt-dlp/yt-dlp/commit/85b87c991af25dcb35630fa94580fd418e78ee33) ([#11198](https://github.com/yt-dlp/yt-dlp/issues/11198)) by [Grub4K](https://github.com/Grub4K)
|
||||||
|
|
||||||
|
#### Extractor changes
|
||||||
|
- **adobepass**: [Use newer user-agent for provider redirect request](https://github.com/yt-dlp/yt-dlp/commit/dcfeea4dd5e5686821350baa6c7767a011944867) ([#11250](https://github.com/yt-dlp/yt-dlp/issues/11250)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **afreecatv**: [Adapt extractors to new sooplive.co.kr domain](https://github.com/yt-dlp/yt-dlp/commit/46fe60ff19395698a87113b2944453779e04ab9d) ([#11266](https://github.com/yt-dlp/yt-dlp/issues/11266)) by [63427083](https://github.com/63427083), [bashonly](https://github.com/bashonly)
|
||||||
|
- **cda**: [Support folders](https://github.com/yt-dlp/yt-dlp/commit/c4d95f67ddc522297bb1fea875255cf94b34d595) ([#10786](https://github.com/yt-dlp/yt-dlp/issues/10786)) by [pktiuk](https://github.com/pktiuk)
|
||||||
|
- **cwtv**: [Fix extraction](https://github.com/yt-dlp/yt-dlp/commit/9d43dcb2c5c38f443f84dfc126cd32720e1a1ad6) ([#11230](https://github.com/yt-dlp/yt-dlp/issues/11230)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **drtv**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/f4338714241b11d9d43768ae71a25f5e952f677d) ([#11141](https://github.com/yt-dlp/yt-dlp/issues/11141)) by [444995](https://github.com/444995)
|
||||||
|
- **funk**: [Extend `_VALID_URL`](https://github.com/yt-dlp/yt-dlp/commit/8de431ec97a4b62b73df8f686b6e21e462775336) ([#11269](https://github.com/yt-dlp/yt-dlp/issues/11269)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- **gem.cbc.ca**: [Fix formats extraction](https://github.com/yt-dlp/yt-dlp/commit/40054cb4a7ebbea30d335d444e6f58b298a3baa0) ([#11196](https://github.com/yt-dlp/yt-dlp/issues/11196)) by [DavidSkrundz](https://github.com/DavidSkrundz)
|
||||||
|
- **generic**: [Impersonate browser by default](https://github.com/yt-dlp/yt-dlp/commit/edfd095b1917701c5046bd51f9542897c17d41a7) ([#11206](https://github.com/yt-dlp/yt-dlp/issues/11206)) by [Grub4K](https://github.com/Grub4K)
|
||||||
|
- **imgur**
|
||||||
|
- [Fix thumbnail extraction](https://github.com/yt-dlp/yt-dlp/commit/87408ccfd772ddf31a8323d8151c24f9577cbc9f) ([#11298](https://github.com/yt-dlp/yt-dlp/issues/11298)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- [Support new URL format](https://github.com/yt-dlp/yt-dlp/commit/5af774d7a36c00bea618c7047c9326532cd3f616) ([#11075](https://github.com/yt-dlp/yt-dlp/issues/11075)) by [Deer-Spangle](https://github.com/Deer-Spangle)
|
||||||
|
- **patreon**: campaign: [Stricter URL matching](https://github.com/yt-dlp/yt-dlp/commit/babb70960595e2146f06f81affc29c7e713e34e2) ([#11235](https://github.com/yt-dlp/yt-dlp/issues/11235)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **reddit**: [Detect and raise when login is required](https://github.com/yt-dlp/yt-dlp/commit/cba7868502f04175fecf9ab3e363296aee7ebec2) ([#11202](https://github.com/yt-dlp/yt-dlp/issues/11202)) by [pzhlkj6612](https://github.com/pzhlkj6612)
|
||||||
|
- **substack**: [Resolve podcast file extensions](https://github.com/yt-dlp/yt-dlp/commit/3148c1822f66533998278f0a1cf842b9bea1526a) ([#11275](https://github.com/yt-dlp/yt-dlp/issues/11275)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **telecinco**: [Fix extractors](https://github.com/yt-dlp/yt-dlp/commit/0b7ec08816fb196cd41d392f8331b4eb8366c4f8) ([#11142](https://github.com/yt-dlp/yt-dlp/issues/11142)) by [bashonly](https://github.com/bashonly), [DarkZeros](https://github.com/DarkZeros)
|
||||||
|
- **tubitv**: [Strip extra whitespace from titles](https://github.com/yt-dlp/yt-dlp/commit/e68b4c19af122876561a41f2dd8093fae7b417c7) ([#10795](https://github.com/yt-dlp/yt-dlp/issues/10795)) by [allendema](https://github.com/allendema)
|
||||||
|
- **tver**: [Support series URLs](https://github.com/yt-dlp/yt-dlp/commit/ceaea731b6e314dbbdfb2e358d7677785ed0b4fc) ([#9507](https://github.com/yt-dlp/yt-dlp/issues/9507)) by [pzhlkj6612](https://github.com/pzhlkj6612), [vvto33](https://github.com/vvto33)
|
||||||
|
- **twitter**: spaces: [Allow extraction when not logged in](https://github.com/yt-dlp/yt-dlp/commit/679c68240a26481ea7c07cc0c014745631ea8481) ([#11289](https://github.com/yt-dlp/yt-dlp/issues/11289)) by [rubyevadestaxes](https://github.com/rubyevadestaxes)
|
||||||
|
- **weverse**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/5310fa87f6cb7f66bf42e2520878952fbf6b1652) ([#11215](https://github.com/yt-dlp/yt-dlp/issues/11215)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **youtube**
|
||||||
|
- [Fix `comment_count` extraction](https://github.com/yt-dlp/yt-dlp/commit/7af1ddaaf2a6a0a750373a9ab53c7770af4f9fe4) ([#11274](https://github.com/yt-dlp/yt-dlp/issues/11274)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [Remove broken `android_producer` client](https://github.com/yt-dlp/yt-dlp/commit/fed53d70bdb7d3e37ef63dd7fcf0ef74356167fd) ([#11297](https://github.com/yt-dlp/yt-dlp/issues/11297)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [Remove broken age-restriction workaround](https://github.com/yt-dlp/yt-dlp/commit/ec2f4bf0823a13043f98f5bd0bf6677837bf09dc) ([#11297](https://github.com/yt-dlp/yt-dlp/issues/11297)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [Support logging in with OAuth](https://github.com/yt-dlp/yt-dlp/commit/b8635c1d4779da195e71aa281f73aaad702c935e) ([#11001](https://github.com/yt-dlp/yt-dlp/issues/11001)) by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
|
||||||
|
#### Misc. changes
|
||||||
|
- **build**
|
||||||
|
- [Migrate `py2exe` builds to `win_exe`](https://github.com/yt-dlp/yt-dlp/commit/a886cf3e900f4a2ec00af705f883539269545609) ([#11256](https://github.com/yt-dlp/yt-dlp/issues/11256)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [Use `macos-13` image for macOS builds](https://github.com/yt-dlp/yt-dlp/commit/64d84d75ca8c19ec06558cc7c511f5f4f7a822bc) ([#11236](https://github.com/yt-dlp/yt-dlp/issues/11236)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- `make_lazy_extractors`: [Force running without plugins](https://github.com/yt-dlp/yt-dlp/commit/1a830394a21a81a3e9918f9e175abc9fbb21f089) ([#11205](https://github.com/yt-dlp/yt-dlp/issues/11205)) by [Grub4K](https://github.com/Grub4K)
|
||||||
|
- **cleanup**: Miscellaneous: [67adeb7](https://github.com/yt-dlp/yt-dlp/commit/67adeb7bab00662ba55d473e405b301abb42fe61) by [bashonly](https://github.com/bashonly), [DTrombett](https://github.com/DTrombett), [grqz](https://github.com/grqz), [Grub4K](https://github.com/Grub4K), [KarboniteKream](https://github.com/KarboniteKream), [mikkovedru](https://github.com/mikkovedru), [seproDev](https://github.com/seproDev)
|
||||||
|
- **test**: [Allow running tests explicitly](https://github.com/yt-dlp/yt-dlp/commit/16eb28026a2ddf5608d0a628ef15949b8d3805a9) ([#11203](https://github.com/yt-dlp/yt-dlp/issues/11203)) by [Grub4K](https://github.com/Grub4K)
|
||||||
|
|
||||||
|
### 2024.10.07
|
||||||
|
|
||||||
|
#### Core changes
|
||||||
|
- **cookies**: [Fix cookie load error handling](https://github.com/yt-dlp/yt-dlp/commit/e59c82a74cda5139eb3928c75b0bd45484dbe7f0) ([#11140](https://github.com/yt-dlp/yt-dlp/issues/11140)) by [Grub4K](https://github.com/Grub4K)
|
||||||
|
|
||||||
|
#### Extractor changes
|
||||||
|
- **applepodcasts**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/6328e2e67a4e126e08af382e6a387073082d5c5f) ([#10903](https://github.com/yt-dlp/yt-dlp/issues/10903)) by [coreywright](https://github.com/coreywright)
|
||||||
|
- **cwtv**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/4b7bec66d8100978b82bb24110ed44e2a7749931) ([#11135](https://github.com/yt-dlp/yt-dlp/issues/11135)) by [kclauhk](https://github.com/kclauhk)
|
||||||
|
- **instagram**
|
||||||
|
- [Do not hardcode user-agent](https://github.com/yt-dlp/yt-dlp/commit/079a7bc334281d3c13d347770ae5f9f2b7da471a) ([#11155](https://github.com/yt-dlp/yt-dlp/issues/11155)) by [poyhen](https://github.com/poyhen)
|
||||||
|
- [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/cf85cba5d9496bd2689e1070005b4d1b4cd3dc6d) ([#11156](https://github.com/yt-dlp/yt-dlp/issues/11156)) by [tetra-fox](https://github.com/tetra-fox)
|
||||||
|
- **noodlemagazine**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/ccb23e1bac9768d1c70535beb744e668ed4a2720) ([#11144](https://github.com/yt-dlp/yt-dlp/issues/11144)) by [BallzCrasher](https://github.com/BallzCrasher)
|
||||||
|
- **patreon**: [Extract all m3u8 formats for locked posts](https://github.com/yt-dlp/yt-dlp/commit/f91645aceaf13926cf35be2c1dfef61b3aab97fb) ([#11138](https://github.com/yt-dlp/yt-dlp/issues/11138)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **youtube**: [Change default player clients to `ios,mweb`](https://github.com/yt-dlp/yt-dlp/commit/de2062753a188060d76f587e45becce61fe399f9) ([#11190](https://github.com/yt-dlp/yt-dlp/issues/11190)) by [seproDev](https://github.com/seproDev)
|
||||||
|
|
||||||
|
#### Postprocessor changes
|
||||||
|
- **xattrmetadata**: [Try to write each attribute](https://github.com/yt-dlp/yt-dlp/commit/3a193346eeb27ac2959ff30c370adb899ec94732) ([#11115](https://github.com/yt-dlp/yt-dlp/issues/11115)) by [eric321](https://github.com/eric321)
|
||||||
|
|
||||||
|
#### Misc. changes
|
||||||
|
- **ci**: [Rerun failed tests](https://github.com/yt-dlp/yt-dlp/commit/b31b81d85f00601710d4fac590c3e4efb4133283) ([#11143](https://github.com/yt-dlp/yt-dlp/issues/11143)) by [Grub4K](https://github.com/Grub4K)
|
||||||
|
- **cleanup**: Miscellaneous: [1a176d8](https://github.com/yt-dlp/yt-dlp/commit/1a176d874e6772cd898ce507379ea388e96ee3f7) by [bashonly](https://github.com/bashonly)
|
||||||
|
|
||||||
|
### 2024.09.27
|
||||||
|
|
||||||
|
#### Important changes
|
||||||
|
- **The minimum *recommended* Python version has been raised to 3.9**
|
||||||
|
Since Python 3.8 will reach end-of-life in October 2024, support for it will be dropped soon. [Read more](https://github.com/yt-dlp/yt-dlp/issues/10086)
|
||||||
|
|
||||||
|
#### Core changes
|
||||||
|
- [Allow `none` arg to negate `--convert-subs` and `--convert-thumbnails`](https://github.com/yt-dlp/yt-dlp/commit/c08e0b20b5edd8957b8318716bc14e896d1b96f4) ([#11066](https://github.com/yt-dlp/yt-dlp/issues/11066)) by [kieraneglin](https://github.com/kieraneglin)
|
||||||
|
- [Fix format sorting bug with vp9.2 vcodec](https://github.com/yt-dlp/yt-dlp/commit/8f4ea14680c7865d8ffac10a9174205d1d84ada7) ([#10884](https://github.com/yt-dlp/yt-dlp/issues/10884)) by [rakslice](https://github.com/rakslice)
|
||||||
|
- [Raise minimum recommended Python version to 3.9](https://github.com/yt-dlp/yt-dlp/commit/cca534cd9e6850c70244f225a4a1895ef4bcdbec) ([#11098](https://github.com/yt-dlp/yt-dlp/issues/11098)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **cookies**: [Improve error message for Windows `--cookies-from-browser chrome` issue](https://github.com/yt-dlp/yt-dlp/commit/b397a64691421ace5df09457c2a764821a2dc6f2) ([#11090](https://github.com/yt-dlp/yt-dlp/issues/11090)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- **utils**: `mimetype2ext`: [Recognize `aacp` as `aac`](https://github.com/yt-dlp/yt-dlp/commit/cc85596d5b59f0c14e9381b3675f619c1e12e597) ([#10860](https://github.com/yt-dlp/yt-dlp/issues/10860)) by [bashonly](https://github.com/bashonly)
|
||||||
|
|
||||||
|
#### Extractor changes
|
||||||
|
- [Fix JW Player format parsing](https://github.com/yt-dlp/yt-dlp/commit/409f8e9e3b4bde81ef76fc563256f876d2ff8099) ([#10956](https://github.com/yt-dlp/yt-dlp/issues/10956)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- [Handle decode errors when reading responses](https://github.com/yt-dlp/yt-dlp/commit/325001317d97f4545d66fac44c4ba772c6f45f22) ([#10868](https://github.com/yt-dlp/yt-dlp/issues/10868)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **abc.net.au**: iview, showseries: [Fix extraction](https://github.com/yt-dlp/yt-dlp/commit/7f909046f4dc0fba472b4963145aef6e0d42491b) ([#11101](https://github.com/yt-dlp/yt-dlp/issues/11101)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **adn**: [Fix extractors](https://github.com/yt-dlp/yt-dlp/commit/cc88a54bb1ef285154775f8a6a413335ce4c71ce) ([#10749](https://github.com/yt-dlp/yt-dlp/issues/10749)) by [infanf](https://github.com/infanf)
|
||||||
|
- **asobistage**: [Support redirected URLs](https://github.com/yt-dlp/yt-dlp/commit/a7d3235c84dac57a127cbe0ff38f7f7c2fdd8fa0) ([#10768](https://github.com/yt-dlp/yt-dlp/issues/10768)) by [pzhlkj6612](https://github.com/pzhlkj6612)
|
||||||
|
- **bandcamp**: user: [Fix extraction](https://github.com/yt-dlp/yt-dlp/commit/5d0176547f16a3642cd71627126e9dfc24981e20) ([#10328](https://github.com/yt-dlp/yt-dlp/issues/10328)) by [bashonly](https://github.com/bashonly), [quad](https://github.com/quad)
|
||||||
|
- **beacon**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/b4760c778d0c92c6e3f2bc8346cd72c8f08595ae) ([#9901](https://github.com/yt-dlp/yt-dlp/issues/9901)) by [Deukhoofd](https://github.com/Deukhoofd)
|
||||||
|
- **bilibili**
|
||||||
|
- [Fix chapters and subtitles extraction](https://github.com/yt-dlp/yt-dlp/commit/a2000bc85730c950351d78bb818493dc39dca3cb) ([#11099](https://github.com/yt-dlp/yt-dlp/issues/11099)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [Fix festival URL support](https://github.com/yt-dlp/yt-dlp/commit/b43bd864851f2862e26caa85461c5d825d49d463) ([#10740](https://github.com/yt-dlp/yt-dlp/issues/10740)) by [bashonly](https://github.com/bashonly), [grqz](https://github.com/grqz)
|
||||||
|
- **biliintl**: [Fix referer header](https://github.com/yt-dlp/yt-dlp/commit/a06bb586795ebab87a2356923acfc674d6f0e152) ([#11003](https://github.com/yt-dlp/yt-dlp/issues/11003)) by [Khaoklong51](https://github.com/Khaoklong51)
|
||||||
|
- **dropbox**: [Fix password-protected video support](https://github.com/yt-dlp/yt-dlp/commit/63da31b3b29af90062d8a72a905ffe4b5e499042) ([#10735](https://github.com/yt-dlp/yt-dlp/issues/10735)) by [ndyanx](https://github.com/ndyanx)
|
||||||
|
- **ertgr**: [Fix video extraction](https://github.com/yt-dlp/yt-dlp/commit/416686ed0cf792ec44ab059f3b229dd776077e14) ([#11091](https://github.com/yt-dlp/yt-dlp/issues/11091)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- **eurosport**: [Support local URL variants](https://github.com/yt-dlp/yt-dlp/commit/f0bb28504c8c2b75ee3e5796aed50de2a7f90a1b) ([#10785](https://github.com/yt-dlp/yt-dlp/issues/10785)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- **facebook**
|
||||||
|
- ads: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/d62fef7e07d454c0d2ba2d69fb96d691dba1ded0) ([#10704](https://github.com/yt-dlp/yt-dlp/issues/10704)) by [kclauhk](https://github.com/kclauhk)
|
||||||
|
- reel: [Improve metadata extraction](https://github.com/yt-dlp/yt-dlp/commit/0e1b941c6b2caa688b0d3332e723d16dbafa4311) by [lengzuo](https://github.com/lengzuo)
|
||||||
|
- **germanupa**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/124f058b546d652a359c67025bb479789bfbef0b) ([#10538](https://github.com/yt-dlp/yt-dlp/issues/10538)) by [grqz](https://github.com/grqz)
|
||||||
|
- **hgtvde**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/a555389c9bb32e589e00b4664974423fb7b04dcd) ([#10992](https://github.com/yt-dlp/yt-dlp/issues/10992)) by [bashonly](https://github.com/bashonly), [rdamas](https://github.com/rdamas)
|
||||||
|
- **huya**: video: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/25c1cdaa2650563494d3bf00a38f72d0d9486bff) ([#10686](https://github.com/yt-dlp/yt-dlp/issues/10686)) by [hugepower](https://github.com/hugepower)
|
||||||
|
- **iprima**: [Fix zoom URL support](https://github.com/yt-dlp/yt-dlp/commit/4a27b8f092f7f7c10b7a334d3535c97c2af02f0a) ([#10959](https://github.com/yt-dlp/yt-dlp/issues/10959)) by [otovalek](https://github.com/otovalek)
|
||||||
|
- **khanacademy**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/0fba08485b6445b72b5b63ae23ca2a73fa5d967f) ([#10913](https://github.com/yt-dlp/yt-dlp/issues/10913)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- **kick**
|
||||||
|
- clips: [Support new URL format](https://github.com/yt-dlp/yt-dlp/commit/0aa4426e9a35f7f8e184f1f2082b3b313c1448f7) ([#11107](https://github.com/yt-dlp/yt-dlp/issues/11107)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- vod: [Support new URL format](https://github.com/yt-dlp/yt-dlp/commit/173d54c151b987409e3eb09552d8d89ed8fc50f7) ([#10988](https://github.com/yt-dlp/yt-dlp/issues/10988)) by [bashonly](https://github.com/bashonly), [grqz](https://github.com/grqz)
|
||||||
|
- **kika**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/e6f48ca80821939c1fd11ec2a0cdbf2fba9b258a) ([#5788](https://github.com/yt-dlp/yt-dlp/issues/5788)) by [1100101](https://github.com/1100101)
|
||||||
|
- **lnkgo**: [Remove extractor](https://github.com/yt-dlp/yt-dlp/commit/fa83d0b36bc43d30fe9241c1e923f4614864b758) ([#10904](https://github.com/yt-dlp/yt-dlp/issues/10904)) by [naglis](https://github.com/naglis)
|
||||||
|
- **loom**: [Fix m3u8 formats extraction](https://github.com/yt-dlp/yt-dlp/commit/7509d692b37a7ec6230ea75bfe1e44a8de5eefce) ([#10760](https://github.com/yt-dlp/yt-dlp/issues/10760)) by [kclauhk](https://github.com/kclauhk)
|
||||||
|
- **mediaklikk**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/e2b3634e299be9c16a247ece3b1858d83889c324) ([#11083](https://github.com/yt-dlp/yt-dlp/issues/11083)) by [szantnerb](https://github.com/szantnerb)
|
||||||
|
- **mojevideo**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/28b0ecba2af5b4919f198474b3d00a76ef322c31) ([#11019](https://github.com/yt-dlp/yt-dlp/issues/11019)) by [04-pasha-04](https://github.com/04-pasha-04), [pzhlkj6612](https://github.com/pzhlkj6612)
|
||||||
|
- **niconico**: [Fix m3u8 formats extraction](https://github.com/yt-dlp/yt-dlp/commit/eabb4680fdb09ba1f48d174a700a2e3b43f82add) ([#11103](https://github.com/yt-dlp/yt-dlp/issues/11103)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **nzz**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/4a9bc8c3630378bc29f0266126b503f6190c0430) ([#10461](https://github.com/yt-dlp/yt-dlp/issues/10461)) by [1-Byte](https://github.com/1-Byte)
|
||||||
|
- **patreoncampaign**: [Support API URLs](https://github.com/yt-dlp/yt-dlp/commit/232e6db30c474d1b387e405342f34173ceeaf832) ([#10734](https://github.com/yt-dlp/yt-dlp/issues/10734)) by [bashonly](https://github.com/bashonly), [hibes](https://github.com/hibes)
|
||||||
|
- **pinterest**: [Extend `_VALID_URL`](https://github.com/yt-dlp/yt-dlp/commit/c8c078fe28b0ffc15ef9646346c00c592fe71a78) ([#10867](https://github.com/yt-dlp/yt-dlp/issues/10867)) by [bashonly](https://github.com/bashonly), [sahilsinghss73](https://github.com/sahilsinghss73)
|
||||||
|
- **radiko**: [Extract unique `id` values](https://github.com/yt-dlp/yt-dlp/commit/c8d096c5ce111411fbdbe2abb8fed54f317a6182) ([#10726](https://github.com/yt-dlp/yt-dlp/issues/10726)) by [garret1317](https://github.com/garret1317)
|
||||||
|
- **rtp**: [Support more subpages](https://github.com/yt-dlp/yt-dlp/commit/d02df303d8e49390599db9f34482697e4d1cf5b2) ([#10787](https://github.com/yt-dlp/yt-dlp/issues/10787)) by [Demon000](https://github.com/Demon000)
|
||||||
|
- **rumblechannel**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/ad0b857f459a6d390fbf124183916218c52f223a) ([#11049](https://github.com/yt-dlp/yt-dlp/issues/11049)) by [tony-hn](https://github.com/tony-hn)
|
||||||
|
- **rutube**: [Support livestreams](https://github.com/yt-dlp/yt-dlp/commit/41be32e78c3845000dbac188ffb90ea3ea7c4dfa) ([#10844](https://github.com/yt-dlp/yt-dlp/issues/10844)) by [pzhlkj6612](https://github.com/pzhlkj6612)
|
||||||
|
- **samplefocus**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/46f4c80bc363ee8116c33d37f65202e6c3470954) ([#10947](https://github.com/yt-dlp/yt-dlp/issues/10947)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- **screenrec**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/36f9e602ad55679764bc75a4f67f7562b1d6adcf) ([#10917](https://github.com/yt-dlp/yt-dlp/issues/10917)) by [naglis](https://github.com/naglis)
|
||||||
|
- **sen**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/41a241ca6ffb95b3d9aaf4f42106ca8cba9af1a6) ([#10952](https://github.com/yt-dlp/yt-dlp/issues/10952)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- **servus**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/300c91274f7ea5b1b0528fc5ee11cf1a61d4079e) ([#10944](https://github.com/yt-dlp/yt-dlp/issues/10944)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- **snapchatspotlight**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/b37417e4f934fd8909788b493d017777155b0ae5) ([#11030](https://github.com/yt-dlp/yt-dlp/issues/11030)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- **svtpage**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/5a8a05aebb49693e78e1123015837ed5e961ff76) ([#11010](https://github.com/yt-dlp/yt-dlp/issues/11010)) by [diman8](https://github.com/diman8)
|
||||||
|
- **tenplay**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/d8d473002b654ab0e7b97ead869f58b4361eeae1) ([#10928](https://github.com/yt-dlp/yt-dlp/issues/10928)) by [aarubui](https://github.com/aarubui)
|
||||||
|
- **tiktok**: [Fix web formats extraction](https://github.com/yt-dlp/yt-dlp/commit/3ad0b7f422d547204df687b6d0b2d9110fff3990) ([#11074](https://github.com/yt-dlp/yt-dlp/issues/11074)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **twitter**: spaces: [Support video spaces](https://github.com/yt-dlp/yt-dlp/commit/bef1d4d6fc9493fda7f75e2289c07c507d10092f) ([#10789](https://github.com/yt-dlp/yt-dlp/issues/10789)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **vidflex**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/e978c312d6550a6ae4c9df18001afb1b420cb72f) ([#10002](https://github.com/yt-dlp/yt-dlp/issues/10002)) by [pzhlkj6612](https://github.com/pzhlkj6612)
|
||||||
|
- **vimeo**
|
||||||
|
- [Always try to extract original format](https://github.com/yt-dlp/yt-dlp/commit/4115c24d157c5b5f63089d75c4e0f51d1f8b4489) ([#10721](https://github.com/yt-dlp/yt-dlp/issues/10721)) by [bashonly](https://github.com/bashonly) (With fixes in [e8e6a98](https://github.com/yt-dlp/yt-dlp/commit/e8e6a982a1b659eed434d225d7922f632bac6568) by [seproDev](https://github.com/seproDev))
|
||||||
|
- [Fix HLS audio format sorting](https://github.com/yt-dlp/yt-dlp/commit/a1b4ac2b8ed8e6eaa56044d439f1e0d00c2ba218) ([#11082](https://github.com/yt-dlp/yt-dlp/issues/11082)) by [fireattack](https://github.com/fireattack)
|
||||||
|
- **watchespn**: [Improve auth support](https://github.com/yt-dlp/yt-dlp/commit/7adff8caf152dcf96d03aff69ed8545c0a63567c) ([#10910](https://github.com/yt-dlp/yt-dlp/issues/10910)) by [ischmidt20](https://github.com/ischmidt20)
|
||||||
|
- **wistia**: [Support password-protected videos](https://github.com/yt-dlp/yt-dlp/commit/9f5c9a90898c5a1e672922d9cd799716c73cee34) ([#11100](https://github.com/yt-dlp/yt-dlp/issues/11100)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **ximalaya**: [Add VIP support](https://github.com/yt-dlp/yt-dlp/commit/3dfd720d098b4d49d69cfc77e6376f22bcd90934) ([#10832](https://github.com/yt-dlp/yt-dlp/issues/10832)) by [seproDev](https://github.com/seproDev), [xingchensong](https://github.com/xingchensong)
|
||||||
|
- **xinpianchang**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/3aa0156e05662923d130ddbc1c82596e38c01a00) ([#10950](https://github.com/yt-dlp/yt-dlp/issues/10950)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- **yleareena**: [Support podcasts](https://github.com/yt-dlp/yt-dlp/commit/48d629d461e05b1b19f5e53dc959bb9ebe95da42) ([#11104](https://github.com/yt-dlp/yt-dlp/issues/11104)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **youtube**
|
||||||
|
- [Add `po_token`, `visitor_data`, `data_sync_id` extractor args](https://github.com/yt-dlp/yt-dlp/commit/3a3bd00037e9908e87da4fa9f2ad772aa34dc60e) ([#10648](https://github.com/yt-dlp/yt-dlp/issues/10648)) by [bashonly](https://github.com/bashonly), [coletdjnz](https://github.com/coletdjnz), [seproDev](https://github.com/seproDev) (With fixes in [fa2be9a](https://github.com/yt-dlp/yt-dlp/commit/fa2be9a7c63babede07480151363e54eee5702bd) by [bashonly](https://github.com/bashonly))
|
||||||
|
- [Support excluding `player_client`s in extractor-arg](https://github.com/yt-dlp/yt-dlp/commit/49f3741a820ed142f6866317c2e7d247b130960e) ([#10710](https://github.com/yt-dlp/yt-dlp/issues/10710)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- clip: [Prioritize `https` formats](https://github.com/yt-dlp/yt-dlp/commit/1d84b780cf33a1d84756825ac23f990a905703df) ([#11102](https://github.com/yt-dlp/yt-dlp/issues/11102)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- tab: [Fix shorts tab extraction](https://github.com/yt-dlp/yt-dlp/commit/9431777b4c37129a6093080c77ca59960afbb9d7) ([#10938](https://github.com/yt-dlp/yt-dlp/issues/10938)) by [seproDev](https://github.com/seproDev)
|
||||||
|
|
||||||
|
#### Networking changes
|
||||||
|
- [Fix handler not being added to RequestError](https://github.com/yt-dlp/yt-dlp/commit/d1c4d88b2d912e8da5e76db455562ca63b1af690) ([#10955](https://github.com/yt-dlp/yt-dlp/issues/10955)) by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
- [Pin `curl-cffi` version to < 0.7.2](https://github.com/yt-dlp/yt-dlp/commit/5bb1aa04dafce13ba9de707ea53169fab58b5207) ([#11092](https://github.com/yt-dlp/yt-dlp/issues/11092)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **Request Handler**: websockets: [Upgrade websockets to 13.0](https://github.com/yt-dlp/yt-dlp/commit/6f9e6537434562d513d0c9b68ced8a61ade94a64) ([#10815](https://github.com/yt-dlp/yt-dlp/issues/10815)) by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
|
||||||
|
#### Misc. changes
|
||||||
|
- **build**
|
||||||
|
- [Bump PyInstaller version pin to `>=6.10.0`](https://github.com/yt-dlp/yt-dlp/commit/fb8b7f226d251e521a89b23c415e249e5b788e5c) ([#10709](https://github.com/yt-dlp/yt-dlp/issues/10709)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [Pin `delocate` version for `macos`](https://github.com/yt-dlp/yt-dlp/commit/7e41628ff523b3fe373b0981a5db441358980dab) ([#10901](https://github.com/yt-dlp/yt-dlp/issues/10901)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **ci**
|
||||||
|
- [Add comment sanitization workflow](https://github.com/yt-dlp/yt-dlp/commit/b6200bdcf3a9415ae36859188f9a57e3e461c696) ([#10915](https://github.com/yt-dlp/yt-dlp/issues/10915)) by [bashonly](https://github.com/bashonly), [Grub4K](https://github.com/Grub4K)
|
||||||
|
- [Add issue tracker anti-spam protection](https://github.com/yt-dlp/yt-dlp/commit/ad9a8115aa29a1a95c961b16fcf129a228d98f50) ([#10861](https://github.com/yt-dlp/yt-dlp/issues/10861)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **cleanup**: Miscellaneous: [c6387ab](https://github.com/yt-dlp/yt-dlp/commit/c6387abc1af9842bb0541288a5610abba9b1ab51) by [bashonly](https://github.com/bashonly), [Codenade](https://github.com/Codenade), [coletdjnz](https://github.com/coletdjnz), [grqz](https://github.com/grqz), [Grub4K](https://github.com/Grub4K), [pzhlkj6612](https://github.com/pzhlkj6612), [seproDev](https://github.com/seproDev)
|
||||||
|
|
||||||
### 2024.08.06
|
### 2024.08.06
|
||||||
|
|
||||||
#### Core changes
|
#### Core changes
|
||||||
|
|
178
README.md
178
README.md
|
@ -4,7 +4,7 @@
|
||||||
[![YT-DLP](https://raw.githubusercontent.com/yt-dlp/yt-dlp/master/.github/banner.svg)](#readme)
|
[![YT-DLP](https://raw.githubusercontent.com/yt-dlp/yt-dlp/master/.github/banner.svg)](#readme)
|
||||||
|
|
||||||
[![Release version](https://img.shields.io/github/v/release/yt-dlp/yt-dlp?color=brightgreen&label=Download&style=for-the-badge)](#installation "Installation")
|
[![Release version](https://img.shields.io/github/v/release/yt-dlp/yt-dlp?color=brightgreen&label=Download&style=for-the-badge)](#installation "Installation")
|
||||||
[![PyPi](https://img.shields.io/badge/-PyPi-blue.svg?logo=pypi&labelColor=555555&style=for-the-badge)](https://pypi.org/project/yt-dlp "PyPi")
|
[![PyPI](https://img.shields.io/badge/-PyPI-blue.svg?logo=pypi&labelColor=555555&style=for-the-badge)](https://pypi.org/project/yt-dlp "PyPI")
|
||||||
[![Donate](https://img.shields.io/badge/_-Donate-red.svg?logo=githubsponsors&labelColor=555555&style=for-the-badge)](Collaborators.md#collaborators "Donate")
|
[![Donate](https://img.shields.io/badge/_-Donate-red.svg?logo=githubsponsors&labelColor=555555&style=for-the-badge)](Collaborators.md#collaborators "Donate")
|
||||||
[![Matrix](https://img.shields.io/matrix/yt-dlp:matrix.org?color=brightgreen&labelColor=555555&label=&logo=element&style=for-the-badge)](https://matrix.to/#/#yt-dlp:matrix.org "Matrix")
|
[![Matrix](https://img.shields.io/matrix/yt-dlp:matrix.org?color=brightgreen&labelColor=555555&label=&logo=element&style=for-the-badge)](https://matrix.to/#/#yt-dlp:matrix.org "Matrix")
|
||||||
[![Discord](https://img.shields.io/discord/807245652072857610?color=blue&labelColor=555555&label=&logo=discord&style=for-the-badge)](https://discord.gg/H5MNcFW63r "Discord")
|
[![Discord](https://img.shields.io/discord/807245652072857610?color=blue&labelColor=555555&label=&logo=discord&style=for-the-badge)](https://discord.gg/H5MNcFW63r "Discord")
|
||||||
|
@ -81,7 +81,7 @@ # INSTALLATION
|
||||||
[![Windows](https://img.shields.io/badge/-Windows_x64-blue.svg?style=for-the-badge&logo=windows)](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe)
|
[![Windows](https://img.shields.io/badge/-Windows_x64-blue.svg?style=for-the-badge&logo=windows)](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe)
|
||||||
[![Unix](https://img.shields.io/badge/-Linux/BSD-red.svg?style=for-the-badge&logo=linux)](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp)
|
[![Unix](https://img.shields.io/badge/-Linux/BSD-red.svg?style=for-the-badge&logo=linux)](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp)
|
||||||
[![MacOS](https://img.shields.io/badge/-MacOS-lightblue.svg?style=for-the-badge&logo=apple)](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_macos)
|
[![MacOS](https://img.shields.io/badge/-MacOS-lightblue.svg?style=for-the-badge&logo=apple)](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_macos)
|
||||||
[![PyPi](https://img.shields.io/badge/-PyPi-blue.svg?logo=pypi&labelColor=555555&style=for-the-badge)](https://pypi.org/project/yt-dlp)
|
[![PyPI](https://img.shields.io/badge/-PyPI-blue.svg?logo=pypi&labelColor=555555&style=for-the-badge)](https://pypi.org/project/yt-dlp)
|
||||||
[![Source Tarball](https://img.shields.io/badge/-Source_tar-green.svg?style=for-the-badge)](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.tar.gz)
|
[![Source Tarball](https://img.shields.io/badge/-Source_tar-green.svg?style=for-the-badge)](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.tar.gz)
|
||||||
[![Other variants](https://img.shields.io/badge/-Other-grey.svg?style=for-the-badge)](#release-files)
|
[![Other variants](https://img.shields.io/badge/-Other-grey.svg?style=for-the-badge)](#release-files)
|
||||||
[![All versions](https://img.shields.io/badge/-All_Versions-lightgrey.svg?style=for-the-badge)](https://github.com/yt-dlp/yt-dlp/releases)
|
[![All versions](https://img.shields.io/badge/-All_Versions-lightgrey.svg?style=for-the-badge)](https://github.com/yt-dlp/yt-dlp/releases)
|
||||||
|
@ -98,15 +98,14 @@ #### Recommended
|
||||||
File|Description
|
File|Description
|
||||||
:---|:---
|
:---|:---
|
||||||
[yt-dlp](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp)|Platform-independent [zipimport](https://docs.python.org/3/library/zipimport.html) binary. Needs Python (recommended for **Linux/BSD**)
|
[yt-dlp](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp)|Platform-independent [zipimport](https://docs.python.org/3/library/zipimport.html) binary. Needs Python (recommended for **Linux/BSD**)
|
||||||
[yt-dlp.exe](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe)|Windows (Win7 SP1+) standalone x64 binary (recommended for **Windows**)
|
[yt-dlp.exe](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe)|Windows (Win8+) standalone x64 binary (recommended for **Windows**)
|
||||||
[yt-dlp_macos](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_macos)|Universal MacOS (10.15+) standalone executable (recommended for **MacOS**)
|
[yt-dlp_macos](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_macos)|Universal MacOS (10.15+) standalone executable (recommended for **MacOS**)
|
||||||
|
|
||||||
#### Alternatives
|
#### Alternatives
|
||||||
|
|
||||||
File|Description
|
File|Description
|
||||||
:---|:---
|
:---|:---
|
||||||
[yt-dlp_x86.exe](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_x86.exe)|Windows (Win7 SP1+) standalone x86 (32-bit) binary
|
[yt-dlp_x86.exe](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_x86.exe)|Windows (Win8+) standalone x86 (32-bit) binary
|
||||||
[yt-dlp_min.exe](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_min.exe)|Windows (Win7 SP1+) standalone x64 binary built with `py2exe`<br/> ([Not recommended](#standalone-py2exe-builds-windows))
|
|
||||||
[yt-dlp_linux](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_linux)|Linux standalone x64 binary
|
[yt-dlp_linux](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_linux)|Linux standalone x64 binary
|
||||||
[yt-dlp_linux_armv7l](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_linux_armv7l)|Linux standalone armv7l (32-bit) binary
|
[yt-dlp_linux_armv7l](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_linux_armv7l)|Linux standalone armv7l (32-bit) binary
|
||||||
[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_linux_aarch64](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_linux_aarch64)|Linux standalone aarch64 (64-bit) binary
|
||||||
|
@ -173,11 +172,11 @@ # To install nightly with pip:
|
||||||
```
|
```
|
||||||
|
|
||||||
## DEPENDENCIES
|
## DEPENDENCIES
|
||||||
Python versions 3.8+ (CPython and PyPy) are supported. Other versions and implementations may or may not work correctly.
|
Python versions 3.9+ (CPython) and 3.10+ (PyPy) are supported. Other versions and implementations may or may not work correctly.
|
||||||
|
|
||||||
<!-- Python 3.5+ uses VC++14 and it is already embedded in the binary created
|
<!-- Python 3.5+ uses VC++14 and it is already embedded in the binary created
|
||||||
<!x-- https://www.microsoft.com/en-us/download/details.aspx?id=26999 --x>
|
<!x-- https://www.microsoft.com/en-us/download/details.aspx?id=26999 --x>
|
||||||
On windows, [Microsoft Visual C++ 2010 SP1 Redistributable Package (x86)](https://download.microsoft.com/download/1/6/5/165255E7-1014-4D0A-B094-B6A430A6BFFC/vcredist_x86.exe) is also necessary to run yt-dlp. You probably already have this, but if the executable throws an error due to missing `MSVCR100.dll` you need to install it manually.
|
On Windows, [Microsoft Visual C++ 2010 SP1 Redistributable Package (x86)](https://download.microsoft.com/download/1/6/5/165255E7-1014-4D0A-B094-B6A430A6BFFC/vcredist_x86.exe) is also necessary to run yt-dlp. You probably already have this, but if the executable throws an error due to missing `MSVCR100.dll` you need to install it manually.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
While all the other dependencies are optional, `ffmpeg` and `ffprobe` are highly recommended
|
While all the other dependencies are optional, `ffmpeg` and `ffprobe` are highly recommended
|
||||||
|
@ -200,7 +199,7 @@ #### Impersonation
|
||||||
|
|
||||||
The following provide support for impersonating browser requests. This may be required for some sites that employ TLS fingerprinting.
|
The following provide support for impersonating browser requests. This may be required for some sites that employ TLS fingerprinting.
|
||||||
|
|
||||||
* [**curl_cffi**](https://github.com/yifeikong/curl_cffi) (recommended) - Python binding for [curl-impersonate](https://github.com/lwthiker/curl-impersonate). Provides impersonation targets for Chrome, Edge and Safari. Licensed under [MIT](https://github.com/yifeikong/curl_cffi/blob/main/LICENSE)
|
* [**curl_cffi**](https://github.com/lexiforest/curl_cffi) (recommended) - Python binding for [curl-impersonate](https://github.com/lexiforest/curl-impersonate). Provides impersonation targets for Chrome, Edge and Safari. Licensed under [MIT](https://github.com/lexiforest/curl_cffi/blob/main/LICENSE)
|
||||||
* Can be installed with the `curl-cffi` group, e.g. `pip install "yt-dlp[default,curl-cffi]"`
|
* Can be installed with the `curl-cffi` group, e.g. `pip install "yt-dlp[default,curl-cffi]"`
|
||||||
* Currently included in `yt-dlp.exe`, `yt-dlp_linux` and `yt-dlp_macos` builds
|
* Currently included in `yt-dlp.exe`, `yt-dlp_linux` and `yt-dlp_macos` builds
|
||||||
|
|
||||||
|
@ -254,31 +253,19 @@ ### Standalone PyInstaller Builds
|
||||||
**Important**: Running `pyinstaller` directly **instead of** using `python -m bundle.pyinstaller` is **not** officially supported. This may or may not work correctly.
|
**Important**: Running `pyinstaller` directly **instead of** using `python -m bundle.pyinstaller` is **not** officially supported. This may or may not work correctly.
|
||||||
|
|
||||||
### Platform-independent Binary (UNIX)
|
### Platform-independent Binary (UNIX)
|
||||||
You will need the build tools `python` (3.8+), `zip`, `make` (GNU), `pandoc`\* and `pytest`\*.
|
You will need the build tools `python` (3.9+), `zip`, `make` (GNU), `pandoc`\* and `pytest`\*.
|
||||||
|
|
||||||
After installing these, simply run `make`.
|
After installing these, simply run `make`.
|
||||||
|
|
||||||
You can also run `make yt-dlp` instead to compile only the binary without updating any of the additional files. (The build tools marked with **\*** are not needed for this)
|
You can also run `make yt-dlp` instead to compile only the binary without updating any of the additional files. (The build tools marked with **\*** are not needed for this)
|
||||||
|
|
||||||
### Standalone Py2Exe Builds (Windows)
|
|
||||||
|
|
||||||
While we provide the option to build with [py2exe](https://www.py2exe.org), it is recommended to build [using PyInstaller](#standalone-pyinstaller-builds) instead since the py2exe builds **cannot contain `pycryptodomex`/`certifi`/`requests` and need VC++14** on the target computer to run.
|
|
||||||
|
|
||||||
If you wish to build it anyway, install Python (if it is not already installed) and you can run the following commands:
|
|
||||||
|
|
||||||
```
|
|
||||||
py devscripts/install_deps.py --include py2exe
|
|
||||||
py devscripts/make_lazy_extractors.py
|
|
||||||
py -m bundle.py2exe
|
|
||||||
```
|
|
||||||
|
|
||||||
### Related scripts
|
### Related scripts
|
||||||
|
|
||||||
* **`devscripts/install_deps.py`** - Install dependencies for yt-dlp.
|
* **`devscripts/install_deps.py`** - Install dependencies for yt-dlp.
|
||||||
* **`devscripts/update-version.py`** - Update the version number based on the current date.
|
* **`devscripts/update-version.py`** - Update the version number based on the current date.
|
||||||
* **`devscripts/set-variant.py`** - Set the build variant of the executable.
|
* **`devscripts/set-variant.py`** - Set the build variant of the executable.
|
||||||
* **`devscripts/make_changelog.py`** - Create a markdown changelog using short commit messages and update `CONTRIBUTORS` file.
|
* **`devscripts/make_changelog.py`** - Create a markdown changelog using short commit messages and update `CONTRIBUTORS` file.
|
||||||
* **`devscripts/make_lazy_extractors.py`** - Create lazy extractors. Running this before building the binaries (any variant) will improve their startup performance. Set the environment variable `YTDLP_NO_LAZY_EXTRACTORS=1` if you wish to forcefully disable lazy extractor loading.
|
* **`devscripts/make_lazy_extractors.py`** - Create lazy extractors. Running this before building the binaries (any variant) will improve their startup performance. Set the environment variable `YTDLP_NO_LAZY_EXTRACTORS` to something nonempty to forcefully disable lazy extractor loading.
|
||||||
|
|
||||||
Note: See their `--help` for more info.
|
Note: See their `--help` for more info.
|
||||||
|
|
||||||
|
@ -348,6 +335,13 @@ ## General Options:
|
||||||
containing directory ("-" for stdin). Can be
|
containing directory ("-" for stdin). Can be
|
||||||
used multiple times and inside other
|
used multiple times and inside other
|
||||||
configuration files
|
configuration files
|
||||||
|
--plugin-dirs PATH Path to an additional directory to search
|
||||||
|
for plugins. This option can be used
|
||||||
|
multiple times to add multiple directories.
|
||||||
|
Note that this currently only works for
|
||||||
|
extractor plugins; postprocessor plugins can
|
||||||
|
only be loaded from the default plugin
|
||||||
|
directories
|
||||||
--flat-playlist Do not extract the videos of a playlist,
|
--flat-playlist Do not extract the videos of a playlist,
|
||||||
only list them
|
only list them
|
||||||
--no-flat-playlist Fully extract the videos of a playlist
|
--no-flat-playlist Fully extract the videos of a playlist
|
||||||
|
@ -444,10 +438,10 @@ ## Video Selection:
|
||||||
E.g. "--date today-2weeks" downloads only
|
E.g. "--date today-2weeks" downloads only
|
||||||
videos uploaded on the same day two weeks ago
|
videos uploaded on the same day two weeks ago
|
||||||
--datebefore DATE Download only videos uploaded on or before
|
--datebefore DATE Download only videos uploaded on or before
|
||||||
this date. The date formats accepted is the
|
this date. The date formats accepted are the
|
||||||
same as --date
|
same as --date
|
||||||
--dateafter DATE Download only videos uploaded on or after
|
--dateafter DATE Download only videos uploaded on or after
|
||||||
this date. The date formats accepted is the
|
this date. The date formats accepted are the
|
||||||
same as --date
|
same as --date
|
||||||
--match-filters FILTER Generic video filter. Any "OUTPUT TEMPLATE"
|
--match-filters FILTER Generic video filter. Any "OUTPUT TEMPLATE"
|
||||||
field can be compared with a number or a
|
field can be compared with a number or a
|
||||||
|
@ -459,17 +453,17 @@ ## Video Selection:
|
||||||
conditions. Use a "\" to escape "&" or
|
conditions. Use a "\" to escape "&" or
|
||||||
quotes if needed. If used multiple times,
|
quotes if needed. If used multiple times,
|
||||||
the filter matches if at least one of the
|
the filter matches if at least one of the
|
||||||
conditions is met. E.g. --match-filter
|
conditions is met. E.g. --match-filters
|
||||||
!is_live --match-filter "like_count>?100 &
|
!is_live --match-filters "like_count>?100 &
|
||||||
description~='(?i)\bcats \& dogs\b'" matches
|
description~='(?i)\bcats \& dogs\b'" matches
|
||||||
only videos that are not live OR those that
|
only videos that are not live OR those that
|
||||||
have a like count more than 100 (or the like
|
have a like count more than 100 (or the like
|
||||||
field is not available) and also has a
|
field is not available) and also has a
|
||||||
description that contains the phrase "cats &
|
description that contains the phrase "cats &
|
||||||
dogs" (caseless). Use "--match-filter -" to
|
dogs" (caseless). Use "--match-filters -" to
|
||||||
interactively ask whether to download each
|
interactively ask whether to download each
|
||||||
video
|
video
|
||||||
--no-match-filters Do not use any --match-filter (default)
|
--no-match-filters Do not use any --match-filters (default)
|
||||||
--break-match-filters FILTER Same as "--match-filters" but stops the
|
--break-match-filters FILTER Same as "--match-filters" but stops the
|
||||||
download process when a video is rejected
|
download process when a video is rejected
|
||||||
--no-break-match-filters Do not use any --break-match-filters (default)
|
--no-break-match-filters Do not use any --break-match-filters (default)
|
||||||
|
@ -490,7 +484,7 @@ ## Video Selection:
|
||||||
encountering a file that is in the archive
|
encountering a file that is in the archive
|
||||||
(default)
|
(default)
|
||||||
--break-per-input Alters --max-downloads, --break-on-existing,
|
--break-per-input Alters --max-downloads, --break-on-existing,
|
||||||
--break-match-filter, and autonumber to
|
--break-match-filters, and autonumber to
|
||||||
reset per input URL
|
reset per input URL
|
||||||
--no-break-per-input --break-on-existing and similar options
|
--no-break-per-input --break-on-existing and similar options
|
||||||
terminates the entire download queue
|
terminates the entire download queue
|
||||||
|
@ -732,16 +726,16 @@ ## Verbosity and Simulation Options:
|
||||||
used. This option can be used multiple times
|
used. This option can be used multiple times
|
||||||
--print-to-file [WHEN:]TEMPLATE FILE
|
--print-to-file [WHEN:]TEMPLATE FILE
|
||||||
Append given template to the file. The
|
Append given template to the file. The
|
||||||
values of WHEN and TEMPLATE are same as that
|
values of WHEN and TEMPLATE are the same as
|
||||||
of --print. FILE uses the same syntax as the
|
that of --print. FILE uses the same syntax
|
||||||
output template. This option can be used
|
as the output template. This option can be
|
||||||
multiple times
|
used multiple times
|
||||||
-j, --dump-json Quiet, but print JSON information for each
|
-j, --dump-json Quiet, but print JSON information for each
|
||||||
video. Simulate unless --no-simulate is
|
video. Simulate unless --no-simulate is
|
||||||
used. See "OUTPUT TEMPLATE" for a
|
used. See "OUTPUT TEMPLATE" for a
|
||||||
description of available keys
|
description of available keys
|
||||||
-J, --dump-single-json Quiet, but print JSON information for each
|
-J, --dump-single-json Quiet, but print JSON information for each
|
||||||
url or infojson passed. Simulate unless
|
URL or infojson passed. Simulate unless
|
||||||
--no-simulate is used. If the URL refers to
|
--no-simulate is used. If the URL refers to
|
||||||
a playlist, the whole playlist information
|
a playlist, the whole playlist information
|
||||||
is dumped in a single line
|
is dumped in a single line
|
||||||
|
@ -816,9 +810,9 @@ ## Video Format Options:
|
||||||
--no-audio-multistreams Only one audio stream is downloaded for each
|
--no-audio-multistreams Only one audio stream is downloaded for each
|
||||||
output file (default)
|
output file (default)
|
||||||
--prefer-free-formats Prefer video formats with free containers
|
--prefer-free-formats Prefer video formats with free containers
|
||||||
over non-free ones of same quality. Use with
|
over non-free ones of the same quality. Use
|
||||||
"-S ext" to strictly prefer free containers
|
with "-S ext" to strictly prefer free
|
||||||
irrespective of quality
|
containers irrespective of quality
|
||||||
--no-prefer-free-formats Don't give any special preference to free
|
--no-prefer-free-formats Don't give any special preference to free
|
||||||
containers (default)
|
containers (default)
|
||||||
--check-formats Make sure formats are selected only from
|
--check-formats Make sure formats are selected only from
|
||||||
|
@ -843,15 +837,17 @@ ## Subtitle Options:
|
||||||
(default) (Alias: --no-write-automatic-subs)
|
(default) (Alias: --no-write-automatic-subs)
|
||||||
--list-subs List available subtitles of each video.
|
--list-subs List available subtitles of each video.
|
||||||
Simulate unless --no-simulate is used
|
Simulate unless --no-simulate is used
|
||||||
--sub-format FORMAT Subtitle format; accepts formats preference,
|
--sub-format FORMAT Subtitle format; accepts formats preference
|
||||||
e.g. "srt" or "ass/srt/best"
|
separated by "/", e.g. "srt" or "ass/srt/best"
|
||||||
--sub-langs LANGS Languages of the subtitles to download (can
|
--sub-langs LANGS Languages of the subtitles to download (can
|
||||||
be regex) or "all" separated by commas, e.g.
|
be regex) or "all" separated by commas, e.g.
|
||||||
--sub-langs "en.*,ja". You can prefix the
|
--sub-langs "en.*,ja" (where "en.*" is a
|
||||||
language code with a "-" to exclude it from
|
regex pattern that matches "en" followed by
|
||||||
the requested languages, e.g. --sub-langs
|
0 or more of any character). You can prefix
|
||||||
all,-live_chat. Use --list-subs for a list
|
the language code with a "-" to exclude it
|
||||||
of available language tags
|
from the requested languages, e.g. --sub-
|
||||||
|
langs all,-live_chat. Use --list-subs for a
|
||||||
|
list of available language tags
|
||||||
|
|
||||||
## Authentication Options:
|
## Authentication Options:
|
||||||
-u, --username USERNAME Login with this account ID
|
-u, --username USERNAME Login with this account ID
|
||||||
|
@ -899,9 +895,9 @@ ## Post-Processing Options:
|
||||||
necessary (currently supported: avi, flv,
|
necessary (currently supported: avi, flv,
|
||||||
gif, mkv, mov, mp4, webm, aac, aiff, alac,
|
gif, mkv, mov, mp4, webm, aac, aiff, alac,
|
||||||
flac, m4a, mka, mp3, ogg, opus, vorbis,
|
flac, m4a, mka, mp3, ogg, opus, vorbis,
|
||||||
wav). If target container does not support
|
wav). If the target container does not
|
||||||
the video/audio codec, remuxing will fail.
|
support the video/audio codec, remuxing will
|
||||||
You can specify multiple rules; e.g.
|
fail. You can specify multiple rules; e.g.
|
||||||
"aac>m4a/mov>mp4/mkv" will remux aac to m4a,
|
"aac>m4a/mov>mp4/mkv" will remux aac to m4a,
|
||||||
mov to mp4 and anything else to mkv
|
mov to mp4 and anything else to mkv
|
||||||
--recode-video FORMAT Re-encode the video into another format if
|
--recode-video FORMAT Re-encode the video into another format if
|
||||||
|
@ -969,29 +965,29 @@ ## Post-Processing Options:
|
||||||
are the same as that of --use-postprocessor
|
are the same as that of --use-postprocessor
|
||||||
(default: pre_process)
|
(default: pre_process)
|
||||||
--xattrs Write metadata to the video file's xattrs
|
--xattrs Write metadata to the video file's xattrs
|
||||||
(using dublin core and xdg standards)
|
(using Dublin Core and XDG standards)
|
||||||
--concat-playlist POLICY Concatenate videos in a playlist. One of
|
--concat-playlist POLICY Concatenate videos in a playlist. One of
|
||||||
"never", "always", or "multi_video"
|
"never", "always", or "multi_video"
|
||||||
(default; only when the videos form a single
|
(default; only when the videos form a single
|
||||||
show). All the video files must have same
|
show). All the video files must have the
|
||||||
codecs and number of streams to be
|
same codecs and number of streams to be
|
||||||
concatable. The "pl_video:" prefix can be
|
concatenable. The "pl_video:" prefix can be
|
||||||
used with "--paths" and "--output" to set
|
used with "--paths" and "--output" to set
|
||||||
the output filename for the concatenated
|
the output filename for the concatenated
|
||||||
files. See "OUTPUT TEMPLATE" for details
|
files. See "OUTPUT TEMPLATE" for details
|
||||||
--fixup POLICY Automatically correct known faults of the
|
--fixup POLICY Automatically correct known faults of the
|
||||||
file. One of never (do nothing), warn (only
|
file. One of never (do nothing), warn (only
|
||||||
emit a warning), detect_or_warn (the
|
emit a warning), detect_or_warn (the
|
||||||
default; fix file if we can, warn
|
default; fix the file if we can, warn
|
||||||
otherwise), force (try fixing even if file
|
otherwise), force (try fixing even if the
|
||||||
already exists)
|
file already exists)
|
||||||
--ffmpeg-location PATH Location of the ffmpeg binary; either the
|
--ffmpeg-location PATH Location of the ffmpeg binary; either the
|
||||||
path to the binary or its containing directory
|
path to the binary or its containing directory
|
||||||
--exec [WHEN:]CMD Execute a command, optionally prefixed with
|
--exec [WHEN:]CMD Execute a command, optionally prefixed with
|
||||||
when to execute it, separated by a ":".
|
when to execute it, separated by a ":".
|
||||||
Supported values of "WHEN" are the same as
|
Supported values of "WHEN" are the same as
|
||||||
that of --use-postprocessor (default:
|
that of --use-postprocessor (default:
|
||||||
after_move). Same syntax as the output
|
after_move). The same syntax as the output
|
||||||
template can be used to pass any field as
|
template can be used to pass any field as
|
||||||
arguments to the command. If no fields are
|
arguments to the command. If no fields are
|
||||||
passed, %(filepath,_filename|)q is appended
|
passed, %(filepath,_filename|)q is appended
|
||||||
|
@ -999,12 +995,16 @@ ## Post-Processing Options:
|
||||||
be used multiple times
|
be used multiple times
|
||||||
--no-exec Remove any previously defined --exec
|
--no-exec Remove any previously defined --exec
|
||||||
--convert-subs FORMAT Convert the subtitles to another format
|
--convert-subs FORMAT Convert the subtitles to another format
|
||||||
(currently supported: ass, lrc, srt, vtt)
|
(currently supported: ass, lrc, srt, vtt).
|
||||||
(Alias: --convert-subtitles)
|
Use "--convert-subs none" to disable
|
||||||
|
conversion (default) (Alias: --convert-
|
||||||
|
subtitles)
|
||||||
--convert-thumbnails FORMAT Convert the thumbnails to another format
|
--convert-thumbnails FORMAT Convert the thumbnails to another format
|
||||||
(currently supported: jpg, png, webp). You
|
(currently supported: jpg, png, webp). You
|
||||||
can specify multiple rules using similar
|
can specify multiple rules using similar
|
||||||
syntax as --remux-video
|
syntax as "--remux-video". Use "--convert-
|
||||||
|
thumbnails none" to disable conversion
|
||||||
|
(default)
|
||||||
--split-chapters Split video into multiple files based on
|
--split-chapters Split video into multiple files based on
|
||||||
internal chapters. The "chapter:" prefix can
|
internal chapters. The "chapter:" prefix can
|
||||||
be used with "--paths" and "--output" to set
|
be used with "--paths" and "--output" to set
|
||||||
|
@ -1025,7 +1025,7 @@ ## Post-Processing Options:
|
||||||
--no-force-keyframes-at-cuts Do not force keyframes around the chapters
|
--no-force-keyframes-at-cuts Do not force keyframes around the chapters
|
||||||
when cutting/splitting (default)
|
when cutting/splitting (default)
|
||||||
--use-postprocessor NAME[:ARGS]
|
--use-postprocessor NAME[:ARGS]
|
||||||
The (case sensitive) name of plugin
|
The (case-sensitive) name of plugin
|
||||||
postprocessors to be enabled, and
|
postprocessors to be enabled, and
|
||||||
(optionally) arguments to be passed to it,
|
(optionally) arguments to be passed to it,
|
||||||
separated by a colon ":". ARGS are a
|
separated by a colon ":". ARGS are a
|
||||||
|
@ -1038,8 +1038,8 @@ ## Post-Processing Options:
|
||||||
--print/--output), "before_dl" (before each
|
--print/--output), "before_dl" (before each
|
||||||
video download), "post_process" (after each
|
video download), "post_process" (after each
|
||||||
video download; default), "after_move"
|
video download; default), "after_move"
|
||||||
(after moving video file to its final
|
(after moving the video file to its final
|
||||||
locations), "after_video" (after downloading
|
location), "after_video" (after downloading
|
||||||
and processing all formats of a video), or
|
and processing all formats of a video), or
|
||||||
"playlist" (at end of playlist). This option
|
"playlist" (at end of playlist). This option
|
||||||
can be used multiple times to add different
|
can be used multiple times to add different
|
||||||
|
@ -1057,7 +1057,7 @@ ## SponsorBlock Options:
|
||||||
music_offtopic, poi_highlight, chapter, all
|
music_offtopic, poi_highlight, chapter, all
|
||||||
and default (=all). You can prefix the
|
and default (=all). You can prefix the
|
||||||
category with a "-" to exclude it. See [1]
|
category with a "-" to exclude it. See [1]
|
||||||
for description of the categories. E.g.
|
for descriptions of the categories. E.g.
|
||||||
--sponsorblock-mark all,-preview
|
--sponsorblock-mark all,-preview
|
||||||
[1] https://wiki.sponsor.ajay.app/w/Segment_Categories
|
[1] https://wiki.sponsor.ajay.app/w/Segment_Categories
|
||||||
--sponsorblock-remove CATS SponsorBlock categories to be removed from
|
--sponsorblock-remove CATS SponsorBlock categories to be removed from
|
||||||
|
@ -1089,7 +1089,7 @@ ## Extractor Options:
|
||||||
(Alias: --no-allow-dynamic-mpd)
|
(Alias: --no-allow-dynamic-mpd)
|
||||||
--hls-split-discontinuity Split HLS playlists to different formats at
|
--hls-split-discontinuity Split HLS playlists to different formats at
|
||||||
discontinuities such as ad breaks
|
discontinuities such as ad breaks
|
||||||
--no-hls-split-discontinuity Do not split HLS playlists to different
|
--no-hls-split-discontinuity Do not split HLS playlists into different
|
||||||
formats at discontinuities such as ad breaks
|
formats at discontinuities such as ad breaks
|
||||||
(default)
|
(default)
|
||||||
--extractor-args IE_KEY:ARGS Pass ARGS arguments to the IE_KEY extractor.
|
--extractor-args IE_KEY:ARGS Pass ARGS arguments to the IE_KEY extractor.
|
||||||
|
@ -1099,7 +1099,7 @@ ## Extractor Options:
|
||||||
|
|
||||||
# CONFIGURATION
|
# CONFIGURATION
|
||||||
|
|
||||||
You can configure yt-dlp by placing any supported command line option to a configuration file. The configuration is loaded from the following locations:
|
You can configure yt-dlp by placing any supported command line option in a configuration file. The configuration is loaded from the following locations:
|
||||||
|
|
||||||
1. **Main Configuration**:
|
1. **Main Configuration**:
|
||||||
* The file given to `--config-location`
|
* The file given to `--config-location`
|
||||||
|
@ -1144,7 +1144,7 @@ # Save all videos under YouTube directory in your home directory
|
||||||
-o ~/YouTube/%(title)s.%(ext)s
|
-o ~/YouTube/%(title)s.%(ext)s
|
||||||
```
|
```
|
||||||
|
|
||||||
**Note**: Options in configuration file are just the same options aka switches used in regular command line calls; thus there **must be no whitespace** after `-` or `--`, e.g. `-o` or `--proxy` but not `- o` or `-- proxy`. They must also be quoted when necessary, as if it were a UNIX shell.
|
**Note**: Options in a configuration file are just the same options aka switches used in regular command line calls; thus there **must be no whitespace** after `-` or `--`, e.g. `-o` or `--proxy` but not `- o` or `-- proxy`. They must also be quoted when necessary, as if it were a UNIX shell.
|
||||||
|
|
||||||
You can use `--ignore-config` if you want to disable all configuration files for a particular yt-dlp run. If `--ignore-config` is found inside any configuration file, no further configuration will be loaded. For example, having the option in the portable configuration file prevents loading of home, user, and system configurations. Additionally, (for backward compatibility) if `--ignore-config` is found inside the system configuration file, the user configuration is not loaded.
|
You can use `--ignore-config` if you want to disable all configuration files for a particular yt-dlp run. If `--ignore-config` is found inside any configuration file, no further configuration will be loaded. For example, having the option in the portable configuration file prevents loading of home, user, and system configurations. Additionally, (for backward compatibility) if `--ignore-config` is found inside the system configuration file, the user configuration is not loaded.
|
||||||
|
|
||||||
|
@ -1178,13 +1178,13 @@ ### Authentication with netrc
|
||||||
|
|
||||||
E.g. To use an encrypted `.netrc` file stored as `.authinfo.gpg`
|
E.g. To use an encrypted `.netrc` file stored as `.authinfo.gpg`
|
||||||
```
|
```
|
||||||
yt-dlp --netrc-cmd 'gpg --decrypt ~/.authinfo.gpg' https://www.youtube.com/watch?v=BaW_jenozKc
|
yt-dlp --netrc-cmd 'gpg --decrypt ~/.authinfo.gpg' 'https://www.youtube.com/watch?v=BaW_jenozKc'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### Notes about environment variables
|
### Notes about environment variables
|
||||||
* Environment variables are normally specified as `${VARIABLE}`/`$VARIABLE` on UNIX and `%VARIABLE%` on Windows; but is always shown as `${VARIABLE}` in this documentation
|
* Environment variables are normally specified as `${VARIABLE}`/`$VARIABLE` on UNIX and `%VARIABLE%` on Windows; but is always shown as `${VARIABLE}` in this documentation
|
||||||
* yt-dlp also allow using UNIX-style variables on Windows for path-like options; e.g. `--output`, `--config-location`
|
* yt-dlp also allows using UNIX-style variables on Windows for path-like options; e.g. `--output`, `--config-location`
|
||||||
* If unset, `${XDG_CONFIG_HOME}` defaults to `~/.config` and `${XDG_CACHE_HOME}` to `~/.cache`
|
* If unset, `${XDG_CONFIG_HOME}` defaults to `~/.config` and `${XDG_CACHE_HOME}` to `~/.cache`
|
||||||
* On Windows, `~` points to `${HOME}` if present; or, `${USERPROFILE}` or `${HOMEDRIVE}${HOMEPATH}` otherwise
|
* On Windows, `~` points to `${HOME}` if present; or, `${USERPROFILE}` or `${HOMEDRIVE}${HOMEPATH}` otherwise
|
||||||
* On Windows, `${USERPROFILE}` generally points to `C:\Users\<user name>` and `${APPDATA}` to `${USERPROFILE}\AppData\Roaming`
|
* On Windows, `${USERPROFILE}` generally points to `C:\Users\<user name>` and `${APPDATA}` to `${USERPROFILE}\AppData\Roaming`
|
||||||
|
@ -1265,7 +1265,7 @@ # OUTPUT TEMPLATE
|
||||||
- `like_count` (numeric): Number of positive ratings of the video
|
- `like_count` (numeric): Number of positive ratings of the video
|
||||||
- `dislike_count` (numeric): Number of negative ratings of the video
|
- `dislike_count` (numeric): Number of negative ratings of the video
|
||||||
- `repost_count` (numeric): Number of reposts of the video
|
- `repost_count` (numeric): Number of reposts of the video
|
||||||
- `average_rating` (numeric): Average rating give by users, the scale used depends on the webpage
|
- `average_rating` (numeric): Average rating given by users, the scale used depends on the webpage
|
||||||
- `comment_count` (numeric): Number of comments on the video (For some extractors, comments are only downloaded at the end, and so this field cannot be used)
|
- `comment_count` (numeric): Number of comments on the video (For some extractors, comments are only downloaded at the end, and so this field cannot be used)
|
||||||
- `age_limit` (numeric): Age restriction for the video (years)
|
- `age_limit` (numeric): Age restriction for the video (years)
|
||||||
- `live_status` (string): One of "not_live", "is_live", "is_upcoming", "was_live", "post_live" (was live, but VOD is not yet processed)
|
- `live_status` (string): One of "not_live", "is_live", "is_upcoming", "was_live", "post_live" (was live, but VOD is not yet processed)
|
||||||
|
@ -1295,7 +1295,7 @@ # OUTPUT TEMPLATE
|
||||||
- `webpage_url` (string): A URL to the video webpage which, if given to yt-dlp, should yield the same result again
|
- `webpage_url` (string): A URL to the video webpage which, if given to yt-dlp, should yield the same result again
|
||||||
- `webpage_url_basename` (string): The basename of the webpage URL
|
- `webpage_url_basename` (string): The basename of the webpage URL
|
||||||
- `webpage_url_domain` (string): The domain of the webpage URL
|
- `webpage_url_domain` (string): The domain of the webpage URL
|
||||||
- `original_url` (string): The URL given by the user (or same as `webpage_url` for playlist entries)
|
- `original_url` (string): The URL given by the user (or the same as `webpage_url` for playlist entries)
|
||||||
- `categories` (list): List of categories the video belongs to
|
- `categories` (list): List of categories the video belongs to
|
||||||
- `tags` (list): List of tags assigned to the video
|
- `tags` (list): List of tags assigned to the video
|
||||||
- `cast` (list): List of cast members
|
- `cast` (list): List of cast members
|
||||||
|
@ -1372,7 +1372,7 @@ # OUTPUT TEMPLATE
|
||||||
|
|
||||||
**Tip**: Look at the `-j` output to identify which fields are available for the particular URL
|
**Tip**: Look at the `-j` output to identify which fields are available for the particular URL
|
||||||
|
|
||||||
For numeric sequences you can use [numeric related formatting](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting); e.g. `%(view_count)05d` will result in a string with view count padded with zeros up to 5 characters, like in `00042`.
|
For numeric sequences, you can use [numeric related formatting](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting); e.g. `%(view_count)05d` will result in a string with view count padded with zeros up to 5 characters, like in `00042`.
|
||||||
|
|
||||||
Output templates can also contain arbitrary hierarchical path, e.g. `-o "%(playlist)s/%(playlist_index)s - %(title)s.%(ext)s"` which will result in downloading each video in a directory corresponding to this path template. Any missing directory will be automatically created for you.
|
Output templates can also contain arbitrary hierarchical path, e.g. `-o "%(playlist)s/%(playlist_index)s - %(title)s.%(ext)s"` which will result in downloading each video in a directory corresponding to this path template. Any missing directory will be automatically created for you.
|
||||||
|
|
||||||
|
@ -1414,7 +1414,7 @@ # Download entire series season keeping each series and each season in separate
|
||||||
|
|
||||||
# Download video as "C:\MyVideos\uploader\title.ext", subtitles as "C:\MyVideos\subs\uploader\title.ext"
|
# Download video as "C:\MyVideos\uploader\title.ext", subtitles as "C:\MyVideos\subs\uploader\title.ext"
|
||||||
# and put all temporary files in "C:\MyVideos\tmp"
|
# and put all temporary files in "C:\MyVideos\tmp"
|
||||||
$ yt-dlp -P "C:/MyVideos" -P "temp:tmp" -P "subtitle:subs" -o "%(uploader)s/%(title)s.%(ext)s" BaW_jenoz --write-subs
|
$ yt-dlp -P "C:/MyVideos" -P "temp:tmp" -P "subtitle:subs" -o "%(uploader)s/%(title)s.%(ext)s" BaW_jenozKc --write-subs
|
||||||
|
|
||||||
# Download video as "C:\MyVideos\uploader\title.ext" and subtitles as "C:\MyVideos\uploader\subs\title.ext"
|
# Download video as "C:\MyVideos\uploader\title.ext" and subtitles as "C:\MyVideos\uploader\subs\title.ext"
|
||||||
$ yt-dlp -P "C:/MyVideos" -o "%(uploader)s/%(title)s.%(ext)s" -o "subtitle:%(uploader)s/subs/%(title)s.%(ext)s" BaW_jenozKc --write-subs
|
$ yt-dlp -P "C:/MyVideos" -o "%(uploader)s/%(title)s.%(ext)s" -o "subtitle:%(uploader)s/subs/%(title)s.%(ext)s" BaW_jenozKc --write-subs
|
||||||
|
@ -1632,11 +1632,11 @@ # Download the best video (that also has audio) but no bigger than 50 MB,
|
||||||
# or the worst video (that also has audio) if there is no video under 50 MB
|
# or the worst video (that also has audio) if there is no video under 50 MB
|
||||||
$ yt-dlp -f "b[filesize<50M] / w"
|
$ yt-dlp -f "b[filesize<50M] / w"
|
||||||
|
|
||||||
# Download largest video (that also has audio) but no bigger than 50 MB,
|
# Download the largest video (that also has audio) but no bigger than 50 MB,
|
||||||
# or the smallest video (that also has audio) if there is no video under 50 MB
|
# or the smallest video (that also has audio) if there is no video under 50 MB
|
||||||
$ yt-dlp -f "b" -S "filesize:50M"
|
$ yt-dlp -f "b" -S "filesize:50M"
|
||||||
|
|
||||||
# Download best video (that also has audio) that is closest in size to 50 MB
|
# Download the best video (that also has audio) that is closest in size to 50 MB
|
||||||
$ yt-dlp -f "b" -S "filesize~50M"
|
$ yt-dlp -f "b" -S "filesize~50M"
|
||||||
|
|
||||||
|
|
||||||
|
@ -1692,7 +1692,7 @@ # MODIFYING METADATA
|
||||||
|
|
||||||
The general syntax of `--parse-metadata FROM:TO` is to give the name of a field or an [output template](#output-template) to extract data from, and the format to interpret it as, separated by a colon `:`. Either a [Python regular expression](https://docs.python.org/3/library/re.html#regular-expression-syntax) with named capture groups, a single field name, or a similar syntax to the [output template](#output-template) (only `%(field)s` formatting is supported) can be used for `TO`. The option can be used multiple times to parse and modify various fields.
|
The general syntax of `--parse-metadata FROM:TO` is to give the name of a field or an [output template](#output-template) to extract data from, and the format to interpret it as, separated by a colon `:`. Either a [Python regular expression](https://docs.python.org/3/library/re.html#regular-expression-syntax) with named capture groups, a single field name, or a similar syntax to the [output template](#output-template) (only `%(field)s` formatting is supported) can be used for `TO`. The option can be used multiple times to parse and modify various fields.
|
||||||
|
|
||||||
Note that these options preserve their relative order, allowing replacements to be made in parsed fields and viceversa. Also, any field thus created can be used in the [output template](#output-template) and will also affect the media file's metadata added when using `--embed-metadata`.
|
Note that these options preserve their relative order, allowing replacements to be made in parsed fields and vice versa. Also, any field thus created can be used in the [output template](#output-template) and will also affect the media file's metadata added when using `--embed-metadata`.
|
||||||
|
|
||||||
This option also has a few special uses:
|
This option also has a few special uses:
|
||||||
|
|
||||||
|
@ -1767,7 +1767,7 @@ # EXTRACTOR ARGUMENTS
|
||||||
#### 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.py](https://github.com/yt-dlp/yt-dlp/blob/c26f9b991a0681fd3ea548d535919cec1fbbd430/yt_dlp/extractor/youtube.py#L381-L390) for 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.py](https://github.com/yt-dlp/yt-dlp/blob/c26f9b991a0681fd3ea548d535919cec1fbbd430/yt_dlp/extractor/youtube.py#L381-L390) for 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 main clients are `web`, `ios` and `android`, with variants `_music` and `_creator` (e.g. `ios_creator`); and `mediaconnect`, `mweb`, `android_producer`, `android_testsuite`, `android_vr`, `web_safari`, `web_embedded`, `tv` and `tv_embedded` with no variants. By default, `ios,web_creator` is used, and `tv_embedded`, `web_creator` and `mediaconnect` are added as required for age-gated videos. Similarly, the music variants are added for `music.youtube.com` urls. Most `android` clients will be given lowest priority since their formats are broken. You can use `all` to use all the clients, and `default` for the default clients.
|
* `player_client`: Clients to extract video data from. The main clients are `web`, `ios` and `android`, with variants `_music` and `_creator` (e.g. `ios_creator`); and `mweb`, `mediaconnect`, `android_testsuite`, `android_vr`, `web_safari`, `web_embedded`, `tv` and `tv_embedded` with no variants. By default, `ios,mweb` is used, and `web_creator,mediaconnect` is added as needed for age-gated videos when account age verification is required. Similarly, the `_music` variants are added for `music.youtube.com` URLs. Some clients, such as `web` and `android`, require a `po_token` for their formats to be downloadable. Some clients, such as the `_creator` variants, will only work with authentication. You can use `all` to use all the clients, and `default` for the default clients. You can prefix a client with `-` to exclude it, e.g. `youtube:player_client=all,-web`
|
||||||
* `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). While these options can help reduce the number of requests needed or avoid some rate-limiting, they could cause some issues. See [#860](https://github.com/yt-dlp/yt-dlp/pull/860) 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). While these options can help reduce the number of requests needed or avoid some rate-limiting, they could cause some issues. See [#860](https://github.com/yt-dlp/yt-dlp/pull/860) for more details
|
||||||
* `player_params`: YouTube player parameters to use for player requests. Will overwrite any default ones set by yt-dlp.
|
* `player_params`: YouTube player parameters to use for player requests. Will overwrite any default ones set by yt-dlp.
|
||||||
* `comment_sort`: `top` or `new` (default) - choose comment sorting mode (on YouTube's side)
|
* `comment_sort`: `top` or `new` (default) - choose comment sorting mode (on YouTube's side)
|
||||||
|
@ -1791,6 +1791,7 @@ #### generic
|
||||||
* `key_query`: Passthrough the master m3u8 URL query to its HLS AES-128 decryption key URI if no value is provided, or else apply the query string given as `key_query=VALUE`. Note that this will have no effect if the key URI is provided via the `hls_key` extractor-arg. Does not apply to ffmpeg
|
* `key_query`: Passthrough the master m3u8 URL query to its HLS AES-128 decryption key URI if no value is provided, or else apply the query string given as `key_query=VALUE`. Note that this will have no effect if the key URI is provided via the `hls_key` extractor-arg. Does not apply to ffmpeg
|
||||||
* `hls_key`: An HLS AES-128 key URI *or* key (as hex), and optionally the IV (as hex), in the form of `(URI|KEY)[,IV]`; e.g. `generic:hls_key=ABCDEF1234567980,0xFEDCBA0987654321`. Passing any of these values will force usage of the native HLS downloader and override the corresponding values found in the m3u8 playlist
|
* `hls_key`: An HLS AES-128 key URI *or* key (as hex), and optionally the IV (as hex), in the form of `(URI|KEY)[,IV]`; e.g. `generic:hls_key=ABCDEF1234567980,0xFEDCBA0987654321`. Passing any of these values will force usage of the native HLS downloader and override the corresponding values found in the m3u8 playlist
|
||||||
* `is_live`: Bypass live HLS detection and manually set `live_status` - a value of `false` will set `not_live`, any other value (or no value) will set `is_live`
|
* `is_live`: Bypass live HLS detection and manually set `live_status` - a value of `false` will set `not_live`, any other value (or no value) will set `is_live`
|
||||||
|
* `impersonate`: Target(s) to try and impersonate with the initial webpage request; e.g. `generic:impersonate=safari,chrome-110`. Use `generic:impersonate` to impersonate any available target, and use `generic:impersonate=false` to disable impersonation (default)
|
||||||
|
|
||||||
#### funimation
|
#### funimation
|
||||||
* `language`: Audio languages to extract, e.g. `funimation:language=english,japanese`
|
* `language`: Audio languages to extract, e.g. `funimation:language=english,japanese`
|
||||||
|
@ -1893,6 +1894,7 @@ # PLUGINS
|
||||||
myplugin.py
|
myplugin.py
|
||||||
|
|
||||||
yt-dlp looks for these `yt_dlp_plugins` namespace folders in many locations (see below) and loads in plugins from **all** of them.
|
yt-dlp looks for these `yt_dlp_plugins` namespace folders in many locations (see below) and loads in plugins from **all** of them.
|
||||||
|
Set the environment variable `YTDLP_NO_PLUGINS` to something nonempty to disable loading plugins entirely.
|
||||||
|
|
||||||
See the [wiki for some known plugins](https://github.com/yt-dlp/yt-dlp/wiki/Plugins)
|
See the [wiki for some known plugins](https://github.com/yt-dlp/yt-dlp/wiki/Plugins)
|
||||||
|
|
||||||
|
@ -1920,7 +1922,7 @@ ## Installing Plugins
|
||||||
* Plugin packages can be installed and managed using `pip`. See [yt-dlp-sample-plugins](https://github.com/yt-dlp/yt-dlp-sample-plugins) for an example.
|
* Plugin packages can be installed and managed using `pip`. See [yt-dlp-sample-plugins](https://github.com/yt-dlp/yt-dlp-sample-plugins) for an example.
|
||||||
* Note: plugin files between plugin packages installed with pip must have unique filenames.
|
* Note: plugin files between plugin packages installed with pip must have unique filenames.
|
||||||
* Any path in `PYTHONPATH` is searched in for the `yt_dlp_plugins` namespace folder.
|
* Any path in `PYTHONPATH` is searched in for the `yt_dlp_plugins` namespace folder.
|
||||||
* Note: This does not apply for Pyinstaller/py2exe builds.
|
* Note: This does not apply for Pyinstaller builds.
|
||||||
|
|
||||||
|
|
||||||
`.zip`, `.egg` and `.whl` archives containing a `yt_dlp_plugins` namespace folder in their root are also supported as plugin packages.
|
`.zip`, `.egg` and `.whl` archives containing a `yt_dlp_plugins` namespace folder in their root are also supported as plugin packages.
|
||||||
|
@ -2154,9 +2156,9 @@ ### New features
|
||||||
* **YouTube improvements**:
|
* **YouTube improvements**:
|
||||||
* Supports Clips, Stories (`ytstories:<channel UCID>`), Search (including filters)**\***, YouTube Music Search, Channel-specific search, Search prefixes (`ytsearch:`, `ytsearchdate:`)**\***, Mixes, and Feeds (`:ytfav`, `:ytwatchlater`, `:ytsubs`, `:ythistory`, `:ytrec`, `:ytnotif`)
|
* Supports Clips, Stories (`ytstories:<channel UCID>`), Search (including filters)**\***, YouTube Music Search, Channel-specific search, Search prefixes (`ytsearch:`, `ytsearchdate:`)**\***, Mixes, and Feeds (`:ytfav`, `:ytwatchlater`, `:ytsubs`, `:ythistory`, `:ytrec`, `:ytnotif`)
|
||||||
* Fix for [n-sig based throttling](https://github.com/ytdl-org/youtube-dl/issues/29326) **\***
|
* Fix for [n-sig based throttling](https://github.com/ytdl-org/youtube-dl/issues/29326) **\***
|
||||||
* Supports some (but not all) age-gated content without cookies
|
|
||||||
* Download livestreams from the start using `--live-from-start` (*experimental*)
|
* Download livestreams from the start using `--live-from-start` (*experimental*)
|
||||||
* Channel URLs download all uploads of the channel, including shorts and live
|
* Channel URLs download all uploads of the channel, including shorts and live
|
||||||
|
* Support for [logging in with OAuth](https://github.com/yt-dlp/yt-dlp/wiki/Extractors#logging-in-with-oauth)
|
||||||
|
|
||||||
* **Cookies from browser**: Cookies can be automatically extracted from all major web browsers using `--cookies-from-browser BROWSER[+KEYRING][:PROFILE][::CONTAINER]`
|
* **Cookies from browser**: Cookies can be automatically extracted from all major web browsers using `--cookies-from-browser BROWSER[+KEYRING][:PROFILE][::CONTAINER]`
|
||||||
|
|
||||||
|
@ -2180,9 +2182,9 @@ ### New features
|
||||||
|
|
||||||
* **Output template improvements**: Output templates can now have date-time formatting, numeric offsets, object traversal etc. See [output template](#output-template) for details. Even more advanced operations can also be done with the help of `--parse-metadata` and `--replace-in-metadata`
|
* **Output template improvements**: Output templates can now have date-time formatting, numeric offsets, object traversal etc. See [output template](#output-template) for details. Even more advanced operations can also be done with the help of `--parse-metadata` and `--replace-in-metadata`
|
||||||
|
|
||||||
* **Other new options**: Many new options have been added such as `--alias`, `--print`, `--concat-playlist`, `--wait-for-video`, `--retry-sleep`, `--sleep-requests`, `--convert-thumbnails`, `--force-download-archive`, `--force-overwrites`, `--break-match-filter` etc
|
* **Other new options**: Many new options have been added such as `--alias`, `--print`, `--concat-playlist`, `--wait-for-video`, `--retry-sleep`, `--sleep-requests`, `--convert-thumbnails`, `--force-download-archive`, `--force-overwrites`, `--break-match-filters` etc
|
||||||
|
|
||||||
* **Improvements**: Regex and other operators in `--format`/`--match-filter`, multiple `--postprocessor-args` and `--downloader-args`, faster archive checking, more [format selection options](#format-selection), merge multi-video/audio, multiple `--config-locations`, `--exec` at different stages, etc
|
* **Improvements**: Regex and other operators in `--format`/`--match-filters`, multiple `--postprocessor-args` and `--downloader-args`, faster archive checking, more [format selection options](#format-selection), merge multi-video/audio, multiple `--config-locations`, `--exec` at different stages, etc
|
||||||
|
|
||||||
* **Plugins**: Extractors and PostProcessors can be loaded from an external file. See [plugins](#plugins) for details
|
* **Plugins**: Extractors and PostProcessors can be loaded from an external file. See [plugins](#plugins) for details
|
||||||
|
|
||||||
|
@ -2198,7 +2200,7 @@ ### Differences in default behavior
|
||||||
|
|
||||||
Some of yt-dlp's default options are different from that of youtube-dl and youtube-dlc:
|
Some of yt-dlp's default options are different from that of youtube-dl and youtube-dlc:
|
||||||
|
|
||||||
* yt-dlp supports only [Python 3.8+](## "Windows 7"), and *may* remove support for more versions as they [become EOL](https://devguide.python.org/versions/#python-release-cycle); while [youtube-dl still supports Python 2.6+ and 3.2+](https://github.com/ytdl-org/youtube-dl/issues/30568#issue-1118238743)
|
* yt-dlp supports only [Python 3.9+](## "Windows 8"), and will remove support for more versions as they [become EOL](https://devguide.python.org/versions/#python-release-cycle); while [youtube-dl still supports Python 2.6+ and 3.2+](https://github.com/ytdl-org/youtube-dl/issues/30568#issue-1118238743)
|
||||||
* The options `--auto-number` (`-A`), `--title` (`-t`) and `--literal` (`-l`), no longer work. See [removed options](#Removed) for details
|
* The options `--auto-number` (`-A`), `--title` (`-t`) and `--literal` (`-l`), no longer work. See [removed options](#Removed) for details
|
||||||
* `avconv` is not supported as an alternative to `ffmpeg`
|
* `avconv` is not supported as an alternative to `ffmpeg`
|
||||||
* yt-dlp stores config files in slightly different locations to youtube-dl. See [CONFIGURATION](#configuration) for a list of correct locations
|
* yt-dlp stores config files in slightly different locations to youtube-dl. See [CONFIGURATION](#configuration) for a list of correct locations
|
||||||
|
@ -2223,7 +2225,7 @@ ### Differences in default behavior
|
||||||
* `certifi` will be used for SSL root certificates, if installed. If you want to use system certificates (e.g. self-signed), use `--compat-options no-certifi`
|
* `certifi` will be used for SSL root certificates, if installed. If you want to use system certificates (e.g. self-signed), use `--compat-options no-certifi`
|
||||||
* yt-dlp's sanitization of invalid characters in filenames is different/smarter than in youtube-dl. You can use `--compat-options filename-sanitization` to revert to youtube-dl's behavior
|
* yt-dlp's sanitization of invalid characters in filenames is different/smarter than in youtube-dl. You can use `--compat-options filename-sanitization` to revert to youtube-dl's behavior
|
||||||
* ~~yt-dlp tries to parse the external downloader outputs into the standard progress output if possible (Currently implemented: [aria2c](https://github.com/yt-dlp/yt-dlp/issues/5931)). You can use `--compat-options no-external-downloader-progress` to get the downloader output as-is~~
|
* ~~yt-dlp tries to parse the external downloader outputs into the standard progress output if possible (Currently implemented: [aria2c](https://github.com/yt-dlp/yt-dlp/issues/5931)). You can use `--compat-options no-external-downloader-progress` to get the downloader output as-is~~
|
||||||
* yt-dlp versions between 2021.09.01 and 2023.01.02 applies `--match-filter` to nested playlists. This was an unintentional side-effect of [8f18ac](https://github.com/yt-dlp/yt-dlp/commit/8f18aca8717bb0dd49054555af8d386e5eda3a88) and is fixed in [d7b460](https://github.com/yt-dlp/yt-dlp/commit/d7b460d0e5fc710950582baed2e3fc616ed98a80). Use `--compat-options playlist-match-filter` to revert this
|
* yt-dlp versions between 2021.09.01 and 2023.01.02 applies `--match-filters` to nested playlists. This was an unintentional side-effect of [8f18ac](https://github.com/yt-dlp/yt-dlp/commit/8f18aca8717bb0dd49054555af8d386e5eda3a88) and is fixed in [d7b460](https://github.com/yt-dlp/yt-dlp/commit/d7b460d0e5fc710950582baed2e3fc616ed98a80). Use `--compat-options playlist-match-filter` to revert this
|
||||||
* yt-dlp versions between 2021.11.10 and 2023.06.21 estimated `filesize_approx` values for fragmented/manifest formats. This was added for convenience in [f2fe69](https://github.com/yt-dlp/yt-dlp/commit/f2fe69c7b0d208bdb1f6292b4ae92bc1e1a7444a), but was reverted in [0dff8e](https://github.com/yt-dlp/yt-dlp/commit/0dff8e4d1e6e9fb938f4256ea9af7d81f42fd54f) due to the potentially extreme inaccuracy of the estimated values. Use `--compat-options manifest-filesize-approx` to keep extracting the estimated values
|
* yt-dlp versions between 2021.11.10 and 2023.06.21 estimated `filesize_approx` values for fragmented/manifest formats. This was added for convenience in [f2fe69](https://github.com/yt-dlp/yt-dlp/commit/f2fe69c7b0d208bdb1f6292b4ae92bc1e1a7444a), but was reverted in [0dff8e](https://github.com/yt-dlp/yt-dlp/commit/0dff8e4d1e6e9fb938f4256ea9af7d81f42fd54f) due to the potentially extreme inaccuracy of the estimated values. Use `--compat-options manifest-filesize-approx` to keep extracting the estimated values
|
||||||
* yt-dlp uses modern http client backends such as `requests`. Use `--compat-options prefer-legacy-http-handler` to prefer the legacy http handler (`urllib`) to be used for standard http requests.
|
* yt-dlp uses modern http client backends such as `requests`. Use `--compat-options prefer-legacy-http-handler` to prefer the legacy http handler (`urllib`) to be used for standard http requests.
|
||||||
* The sub-modules `swfinterp`, `casefold` are removed.
|
* The sub-modules `swfinterp`, `casefold` are removed.
|
||||||
|
@ -2269,13 +2271,13 @@ #### Redundant options
|
||||||
--get-thumbnail --print thumbnail
|
--get-thumbnail --print thumbnail
|
||||||
-e, --get-title --print title
|
-e, --get-title --print title
|
||||||
-g, --get-url --print urls
|
-g, --get-url --print urls
|
||||||
--match-title REGEX --match-filter "title ~= (?i)REGEX"
|
--match-title REGEX --match-filters "title ~= (?i)REGEX"
|
||||||
--reject-title REGEX --match-filter "title !~= (?i)REGEX"
|
--reject-title REGEX --match-filters "title !~= (?i)REGEX"
|
||||||
--min-views COUNT --match-filter "view_count >=? COUNT"
|
--min-views COUNT --match-filters "view_count >=? COUNT"
|
||||||
--max-views COUNT --match-filter "view_count <=? COUNT"
|
--max-views COUNT --match-filters "view_count <=? COUNT"
|
||||||
--break-on-reject Use --break-match-filter
|
--break-on-reject Use --break-match-filters
|
||||||
--user-agent UA --add-header "User-Agent:UA"
|
--user-agent UA --add-headers "User-Agent:UA"
|
||||||
--referer URL --add-header "Referer:URL"
|
--referer URL --add-headers "Referer:URL"
|
||||||
--playlist-start NUMBER -I NUMBER:
|
--playlist-start NUMBER -I NUMBER:
|
||||||
--playlist-end NUMBER -I :NUMBER
|
--playlist-end NUMBER -I :NUMBER
|
||||||
--playlist-reverse -I ::-1
|
--playlist-reverse -I ::-1
|
||||||
|
|
|
@ -1,59 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
# Allow execution from anywhere
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
||||||
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
from py2exe import freeze
|
|
||||||
|
|
||||||
from devscripts.utils import read_version
|
|
||||||
|
|
||||||
VERSION = read_version()
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
warnings.warn(
|
|
||||||
'py2exe builds do not support pycryptodomex and needs VC++14 to run. '
|
|
||||||
'It is recommended to run "pyinst.py" to build using pyinstaller instead')
|
|
||||||
|
|
||||||
freeze(
|
|
||||||
console=[{
|
|
||||||
'script': './yt_dlp/__main__.py',
|
|
||||||
'dest_base': 'yt-dlp',
|
|
||||||
'icon_resources': [(1, 'devscripts/logo.ico')],
|
|
||||||
}],
|
|
||||||
version_info={
|
|
||||||
'version': VERSION,
|
|
||||||
'description': 'A feature-rich command-line audio/video downloader',
|
|
||||||
'comments': 'Official repository: <https://github.com/yt-dlp/yt-dlp>',
|
|
||||||
'product_name': 'yt-dlp',
|
|
||||||
'product_version': VERSION,
|
|
||||||
},
|
|
||||||
options={
|
|
||||||
'bundle_files': 0,
|
|
||||||
'compressed': 1,
|
|
||||||
'optimize': 2,
|
|
||||||
'dist_dir': './dist',
|
|
||||||
'excludes': [
|
|
||||||
# py2exe cannot import Crypto
|
|
||||||
'Crypto',
|
|
||||||
'Cryptodome',
|
|
||||||
# requests >=2.32.0 breaks py2exe builds due to certifi dependency
|
|
||||||
'requests',
|
|
||||||
'urllib3',
|
|
||||||
],
|
|
||||||
'dll_excludes': ['w9xpopen.exe', 'crypt32.dll'],
|
|
||||||
# Modules that are only imported dynamically must be added here
|
|
||||||
'includes': ['yt_dlp.compat._legacy', 'yt_dlp.compat._deprecated',
|
|
||||||
'yt_dlp.utils._legacy', 'yt_dlp.utils._deprecated'],
|
|
||||||
},
|
|
||||||
zipfile=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
|
@ -185,5 +185,36 @@
|
||||||
"action": "add",
|
"action": "add",
|
||||||
"when": "6075a029dba70a89675ae1250e7cdfd91f0eba41",
|
"when": "6075a029dba70a89675ae1250e7cdfd91f0eba41",
|
||||||
"short": "[priority] Security: [[ie/douyutv] Do not use dangerous javascript source/URL](https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-3v33-3wmw-3785)\n - A dependency on potentially malicious third-party JavaScript code has been removed from the Douyu extractors"
|
"short": "[priority] Security: [[ie/douyutv] Do not use dangerous javascript source/URL](https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-3v33-3wmw-3785)\n - A dependency on potentially malicious third-party JavaScript code has been removed from the Douyu extractors"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "add",
|
||||||
|
"when": "fb8b7f226d251e521a89b23c415e249e5b788e5c",
|
||||||
|
"short": "[priority] **The minimum *recommended* Python version has been raised to 3.9**\nSince Python 3.8 will reach end-of-life in October 2024, support for it will be dropped soon. [Read more](https://github.com/yt-dlp/yt-dlp/issues/10086)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "change",
|
||||||
|
"when": "b31b81d85f00601710d4fac590c3e4efb4133283",
|
||||||
|
"short": "[ci] Rerun failed tests (#11143)",
|
||||||
|
"authors": ["Grub4K"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "add",
|
||||||
|
"when": "a886cf3e900f4a2ec00af705f883539269545609",
|
||||||
|
"short": "[priority] **py2exe is no longer supported**\nThis release's `yt-dlp_min.exe` will be the last, and it's actually a PyInstaller-bundled executable so that yt-dlp users updating their py2exe build with `-U` will be automatically migrated. [Read more](https://github.com/yt-dlp/yt-dlp/issues/10087)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "add",
|
||||||
|
"when": "a886cf3e900f4a2ec00af705f883539269545609",
|
||||||
|
"short": "[priority] **Following this release, yt-dlp's Python dependencies *must* be installed using the `default` group**\nIf you're installing yt-dlp with pip/pipx or requiring yt-dlp in your own Python project, you'll need to specify `yt-dlp[default]` if you want to also install yt-dlp's optional dependencies (which were previously included by default). [Read more](https://github.com/yt-dlp/yt-dlp/pull/11255)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "add",
|
||||||
|
"when": "87884f15580910e4e0fe0e1db73508debc657471",
|
||||||
|
"short": "[priority] **Beginning with this release, yt-dlp's Python dependencies *must* be installed using the `default` group**\nIf you're installing yt-dlp with pip/pipx or requiring yt-dlp in your own Python project, you'll need to specify `yt-dlp[default]` if you want to also install yt-dlp's optional dependencies (which were previously included by default). [Read more](https://github.com/yt-dlp/yt-dlp/pull/11255)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "add",
|
||||||
|
"when": "d784464399b600ba9516bbcec6286f11d68974dd",
|
||||||
|
"short": "[priority] **The minimum *required* Python version has been raised to 3.9**\nPython 3.8 reached its end-of-life on 2024.10.07, and yt-dlp has now removed support for it. As an unfortunate side effect, the official `yt-dlp.exe` and `yt-dlp_x86.exe` binaries are no longer supported on Windows 7. [Read more](https://github.com/yt-dlp/yt-dlp/issues/10086)"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -32,14 +32,15 @@
|
||||||
placeholder: |
|
placeholder: |
|
||||||
[debug] Command-line config: ['-vU', 'https://www.youtube.com/watch?v=BaW_jenozKc']
|
[debug] Command-line config: ['-vU', 'https://www.youtube.com/watch?v=BaW_jenozKc']
|
||||||
[debug] Encodings: locale cp65001, fs utf-8, pref cp65001, out utf-8, error utf-8, screen utf-8
|
[debug] Encodings: locale cp65001, fs utf-8, pref cp65001, out utf-8, error utf-8, screen utf-8
|
||||||
[debug] yt-dlp version nightly@... from yt-dlp/yt-dlp [b634ba742] (win_exe)
|
[debug] yt-dlp version nightly@... from yt-dlp/yt-dlp-nightly-builds [1a176d874] (win_exe)
|
||||||
[debug] Python 3.8.10 (CPython 64bit) - Windows-10-10.0.22000-SP0
|
[debug] Python 3.10.11 (CPython AMD64 64bit) - Windows-10-10.0.20348-SP0 (OpenSSL 1.1.1t 7 Feb 2023)
|
||||||
[debug] exe versions: ffmpeg N-106550-g072101bd52-20220410 (fdk,setts), ffprobe N-106624-g391ce570c8-20220415, phantomjs 2.1.1
|
[debug] exe versions: ffmpeg 7.0.2 (setts), ffprobe 7.0.2
|
||||||
[debug] Optional libraries: Cryptodome-3.15.0, brotli-1.0.9, certifi-2022.06.15, mutagen-1.45.1, sqlite3-2.6.0, websockets-10.3
|
[debug] Optional libraries: Cryptodome-3.21.0, brotli-1.1.0, certifi-2024.08.30, curl_cffi-0.5.10, mutagen-1.47.0, requests-2.32.3, sqlite3-3.40.1, urllib3-2.2.3, websockets-13.1
|
||||||
[debug] Proxy map: {}
|
[debug] Proxy map: {}
|
||||||
[debug] Request Handlers: urllib, requests
|
[debug] Request Handlers: urllib, requests, websockets, curl_cffi
|
||||||
[debug] Loaded 1893 extractors
|
[debug] Loaded 1838 extractors
|
||||||
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp-nightly-builds/releases/latest
|
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
|
||||||
|
Latest version: nightly@... from yt-dlp/yt-dlp-nightly-builds
|
||||||
yt-dlp is up to date (nightly@... from yt-dlp/yt-dlp-nightly-builds)
|
yt-dlp is up to date (nightly@... from yt-dlp/yt-dlp-nightly-builds)
|
||||||
[youtube] Extracting URL: https://www.youtube.com/watch?v=BaW_jenozKc
|
[youtube] Extracting URL: https://www.youtube.com/watch?v=BaW_jenozKc
|
||||||
<more lines>
|
<more lines>
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
# Allow direct execution
|
# Allow direct execution
|
||||||
import os
|
import os
|
||||||
import shutil
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
@ -34,18 +33,14 @@ class {name}({bases}):
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
os.environ['YTDLP_NO_PLUGINS'] = 'true'
|
||||||
|
os.environ['YTDLP_NO_LAZY_EXTRACTORS'] = 'true'
|
||||||
|
|
||||||
lazy_extractors_filename = get_filename_args(default_outfile='yt_dlp/extractor/lazy_extractors.py')
|
lazy_extractors_filename = get_filename_args(default_outfile='yt_dlp/extractor/lazy_extractors.py')
|
||||||
if os.path.exists(lazy_extractors_filename):
|
|
||||||
os.remove(lazy_extractors_filename)
|
|
||||||
|
|
||||||
_ALL_CLASSES = get_all_ies() # Must be before import
|
from yt_dlp.extractor.extractors import _ALL_CLASSES
|
||||||
|
|
||||||
import yt_dlp.plugins
|
|
||||||
from yt_dlp.extractor.common import InfoExtractor, SearchInfoExtractor
|
from yt_dlp.extractor.common import InfoExtractor, SearchInfoExtractor
|
||||||
|
|
||||||
# Filter out plugins
|
|
||||||
_ALL_CLASSES = [cls for cls in _ALL_CLASSES if not cls.__module__.startswith(f'{yt_dlp.plugins.PACKAGE_NAME}.')]
|
|
||||||
|
|
||||||
DummyInfoExtractor = type('InfoExtractor', (InfoExtractor,), {'IE_NAME': NO_ATTR})
|
DummyInfoExtractor = type('InfoExtractor', (InfoExtractor,), {'IE_NAME': NO_ATTR})
|
||||||
module_src = '\n'.join((
|
module_src = '\n'.join((
|
||||||
MODULE_TEMPLATE,
|
MODULE_TEMPLATE,
|
||||||
|
@ -58,20 +53,6 @@ def main():
|
||||||
write_file(lazy_extractors_filename, f'{module_src}\n')
|
write_file(lazy_extractors_filename, f'{module_src}\n')
|
||||||
|
|
||||||
|
|
||||||
def get_all_ies():
|
|
||||||
PLUGINS_DIRNAME = 'ytdlp_plugins'
|
|
||||||
BLOCKED_DIRNAME = f'{PLUGINS_DIRNAME}_blocked'
|
|
||||||
if os.path.exists(PLUGINS_DIRNAME):
|
|
||||||
# os.rename cannot be used, e.g. in Docker. See https://github.com/yt-dlp/yt-dlp/pull/4958
|
|
||||||
shutil.move(PLUGINS_DIRNAME, BLOCKED_DIRNAME)
|
|
||||||
try:
|
|
||||||
from yt_dlp.extractor.extractors import _ALL_CLASSES
|
|
||||||
finally:
|
|
||||||
if os.path.exists(BLOCKED_DIRNAME):
|
|
||||||
shutil.move(BLOCKED_DIRNAME, PLUGINS_DIRNAME)
|
|
||||||
return _ALL_CLASSES
|
|
||||||
|
|
||||||
|
|
||||||
def extra_ie_code(ie, base=None):
|
def extra_ie_code(ie, base=None):
|
||||||
for var in STATIC_CLASS_PROPERTIES:
|
for var in STATIC_CLASS_PROPERTIES:
|
||||||
val = getattr(ie, var)
|
val = getattr(ie, var)
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
def parse_args():
|
def parse_args():
|
||||||
parser = argparse.ArgumentParser(description='Run selected yt-dlp tests')
|
parser = argparse.ArgumentParser(description='Run selected yt-dlp tests')
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'test', help='a extractor tests, or one of "core" or "download"', nargs='*')
|
'test', help='an extractor test, test path, or one of "core" or "download"', nargs='*')
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-k', help='run a test matching EXPRESSION. Same as "pytest -k"', metavar='EXPRESSION')
|
'-k', help='run a test matching EXPRESSION. Same as "pytest -k"', metavar='EXPRESSION')
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
|
@ -27,7 +27,6 @@ def parse_args():
|
||||||
def run_tests(*tests, pattern=None, ci=False):
|
def run_tests(*tests, pattern=None, ci=False):
|
||||||
run_core = 'core' in tests or (not pattern and not tests)
|
run_core = 'core' in tests or (not pattern and not tests)
|
||||||
run_download = 'download' in tests
|
run_download = 'download' in tests
|
||||||
tests = list(map(fix_test_name, tests))
|
|
||||||
|
|
||||||
pytest_args = args.pytest_args or os.getenv('HATCH_TEST_ARGS', '')
|
pytest_args = args.pytest_args or os.getenv('HATCH_TEST_ARGS', '')
|
||||||
arguments = ['pytest', '-Werror', '--tb=short', *shlex.split(pytest_args)]
|
arguments = ['pytest', '-Werror', '--tb=short', *shlex.split(pytest_args)]
|
||||||
|
@ -41,7 +40,9 @@ def run_tests(*tests, pattern=None, ci=False):
|
||||||
arguments.extend(['-m', 'download'])
|
arguments.extend(['-m', 'download'])
|
||||||
else:
|
else:
|
||||||
arguments.extend(
|
arguments.extend(
|
||||||
f'test/test_download.py::TestDownload::test_{test}' for test in tests)
|
test if '/' in test
|
||||||
|
else f'test/test_download.py::TestDownload::test_{fix_test_name(test)}'
|
||||||
|
for test in tests)
|
||||||
|
|
||||||
print(f'Running {arguments}', flush=True)
|
print(f'Running {arguments}', flush=True)
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -13,7 +13,7 @@ maintainers = [
|
||||||
]
|
]
|
||||||
description = "A feature-rich command-line audio/video downloader"
|
description = "A feature-rich command-line audio/video downloader"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.9"
|
||||||
keywords = [
|
keywords = [
|
||||||
"youtube-dl",
|
"youtube-dl",
|
||||||
"video-downloader",
|
"video-downloader",
|
||||||
|
@ -29,11 +29,11 @@ classifiers = [
|
||||||
"Environment :: Console",
|
"Environment :: Console",
|
||||||
"Programming Language :: Python",
|
"Programming Language :: Python",
|
||||||
"Programming Language :: Python :: 3 :: Only",
|
"Programming Language :: Python :: 3 :: Only",
|
||||||
"Programming Language :: Python :: 3.8",
|
|
||||||
"Programming Language :: Python :: 3.9",
|
"Programming Language :: Python :: 3.9",
|
||||||
"Programming Language :: Python :: 3.10",
|
"Programming Language :: Python :: 3.10",
|
||||||
"Programming Language :: Python :: 3.11",
|
"Programming Language :: Python :: 3.11",
|
||||||
"Programming Language :: Python :: 3.12",
|
"Programming Language :: Python :: 3.12",
|
||||||
|
"Programming Language :: Python :: 3.13",
|
||||||
"Programming Language :: Python :: Implementation",
|
"Programming Language :: Python :: Implementation",
|
||||||
"Programming Language :: Python :: Implementation :: CPython",
|
"Programming Language :: Python :: Implementation :: CPython",
|
||||||
"Programming Language :: Python :: Implementation :: PyPy",
|
"Programming Language :: Python :: Implementation :: PyPy",
|
||||||
|
@ -41,7 +41,10 @@ classifiers = [
|
||||||
"Operating System :: OS Independent",
|
"Operating System :: OS Independent",
|
||||||
]
|
]
|
||||||
dynamic = ["version"]
|
dynamic = ["version"]
|
||||||
dependencies = [
|
dependencies = []
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
default = [
|
||||||
"brotli; implementation_name=='cpython'",
|
"brotli; implementation_name=='cpython'",
|
||||||
"brotlicffi; implementation_name!='cpython'",
|
"brotlicffi; implementation_name!='cpython'",
|
||||||
"certifi",
|
"certifi",
|
||||||
|
@ -51,12 +54,9 @@ dependencies = [
|
||||||
"urllib3>=1.26.17,<3",
|
"urllib3>=1.26.17,<3",
|
||||||
"websockets>=13.0",
|
"websockets>=13.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
|
||||||
default = []
|
|
||||||
curl-cffi = [
|
curl-cffi = [
|
||||||
"curl-cffi==0.5.10; os_name=='nt' and implementation_name=='cpython'",
|
"curl-cffi==0.5.10; os_name=='nt' and implementation_name=='cpython'",
|
||||||
"curl-cffi>=0.5.10,!=0.6.*,<0.8; os_name!='nt' and implementation_name=='cpython'",
|
"curl-cffi>=0.5.10,!=0.6.*,<0.7.2; os_name!='nt' and implementation_name=='cpython'",
|
||||||
]
|
]
|
||||||
secretstorage = [
|
secretstorage = [
|
||||||
"cffi",
|
"cffi",
|
||||||
|
@ -76,16 +76,14 @@ dev = [
|
||||||
]
|
]
|
||||||
static-analysis = [
|
static-analysis = [
|
||||||
"autopep8~=2.0",
|
"autopep8~=2.0",
|
||||||
"ruff~=0.5.0",
|
"ruff~=0.7.0",
|
||||||
]
|
]
|
||||||
test = [
|
test = [
|
||||||
"pytest~=8.1",
|
"pytest~=8.1",
|
||||||
|
"pytest-rerunfailures~=14.0",
|
||||||
]
|
]
|
||||||
pyinstaller = [
|
pyinstaller = [
|
||||||
"pyinstaller>=6.7.0", # for compat with setuptools>=70
|
"pyinstaller>=6.10.0", # Windows temp cleanup fixed in 6.10.0
|
||||||
]
|
|
||||||
py2exe = [
|
|
||||||
"py2exe>=0.12",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
|
@ -162,7 +160,6 @@ lint-fix = "ruff check --fix {args:.}"
|
||||||
features = ["test"]
|
features = ["test"]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pytest-randomly~=3.15",
|
"pytest-randomly~=3.15",
|
||||||
"pytest-rerunfailures~=14.0",
|
|
||||||
"pytest-xdist[psutil]~=3.5",
|
"pytest-xdist[psutil]~=3.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -172,13 +169,11 @@ run-cov = "echo Code coverage not implemented && exit 1"
|
||||||
|
|
||||||
[[tool.hatch.envs.hatch-test.matrix]]
|
[[tool.hatch.envs.hatch-test.matrix]]
|
||||||
python = [
|
python = [
|
||||||
"3.8",
|
|
||||||
"3.9",
|
"3.9",
|
||||||
"3.10",
|
"3.10",
|
||||||
"3.11",
|
"3.11",
|
||||||
"3.12",
|
"3.12",
|
||||||
"pypy3.8",
|
"3.13",
|
||||||
"pypy3.9",
|
|
||||||
"pypy3.10",
|
"pypy3.10",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ remove-unused-variables = true
|
||||||
|
|
||||||
[tox:tox]
|
[tox:tox]
|
||||||
skipsdist = true
|
skipsdist = true
|
||||||
envlist = py{38,39,310,311,312},pypy{38,39,310}
|
envlist = py{39,310,311,312,313},pypy310
|
||||||
skip_missing_interpreters = true
|
skip_missing_interpreters = true
|
||||||
|
|
||||||
[testenv] # tox
|
[testenv] # tox
|
||||||
|
@ -29,7 +29,7 @@ setenv =
|
||||||
|
|
||||||
|
|
||||||
[isort]
|
[isort]
|
||||||
py_version = 38
|
py_version = 39
|
||||||
multi_line_output = VERTICAL_HANGING_INDENT
|
multi_line_output = VERTICAL_HANGING_INDENT
|
||||||
line_length = 80
|
line_length = 80
|
||||||
reverse_relative = true
|
reverse_relative = true
|
||||||
|
|
|
@ -45,10 +45,6 @@ # Supported sites
|
||||||
- **aenetworks:collection**
|
- **aenetworks:collection**
|
||||||
- **aenetworks:show**
|
- **aenetworks:show**
|
||||||
- **AeonCo**
|
- **AeonCo**
|
||||||
- **afreecatv**: [*afreecatv*](## "netrc machine") afreecatv.com
|
|
||||||
- **afreecatv:catchstory**: [*afreecatv*](## "netrc machine") afreecatv.com catch story
|
|
||||||
- **afreecatv:live**: [*afreecatv*](## "netrc machine") afreecatv.com livestreams
|
|
||||||
- **afreecatv:user**
|
|
||||||
- **AirTV**
|
- **AirTV**
|
||||||
- **AitubeKZVideo**
|
- **AitubeKZVideo**
|
||||||
- **AliExpressLive**
|
- **AliExpressLive**
|
||||||
|
@ -143,6 +139,7 @@ # Supported sites
|
||||||
- **BBVTV**: [*bbvtv*](## "netrc machine")
|
- **BBVTV**: [*bbvtv*](## "netrc machine")
|
||||||
- **BBVTVLive**: [*bbvtv*](## "netrc machine")
|
- **BBVTVLive**: [*bbvtv*](## "netrc machine")
|
||||||
- **BBVTVRecordings**: [*bbvtv*](## "netrc machine")
|
- **BBVTVRecordings**: [*bbvtv*](## "netrc machine")
|
||||||
|
- **BeaconTv**
|
||||||
- **BeatBumpPlaylist**
|
- **BeatBumpPlaylist**
|
||||||
- **BeatBumpVideo**
|
- **BeatBumpVideo**
|
||||||
- **Beatport**
|
- **Beatport**
|
||||||
|
@ -253,6 +250,7 @@ # Supported sites
|
||||||
- **CCMA**
|
- **CCMA**
|
||||||
- **CCTV**: 央视网
|
- **CCTV**: 央视网
|
||||||
- **CDA**: [*cdapl*](## "netrc machine")
|
- **CDA**: [*cdapl*](## "netrc machine")
|
||||||
|
- **CDAFolder**
|
||||||
- **Cellebrite**
|
- **Cellebrite**
|
||||||
- **CeskaTelevize**
|
- **CeskaTelevize**
|
||||||
- **CGTN**
|
- **CGTN**
|
||||||
|
@ -505,6 +503,7 @@ # Supported sites
|
||||||
- **gem.cbc.ca:playlist**
|
- **gem.cbc.ca:playlist**
|
||||||
- **Genius**
|
- **Genius**
|
||||||
- **GeniusLyrics**
|
- **GeniusLyrics**
|
||||||
|
- **Germanupa**: germanupa.de
|
||||||
- **GetCourseRu**: [*getcourseru*](## "netrc machine")
|
- **GetCourseRu**: [*getcourseru*](## "netrc machine")
|
||||||
- **GetCourseRuPlayer**
|
- **GetCourseRuPlayer**
|
||||||
- **Gettr**
|
- **Gettr**
|
||||||
|
@ -580,6 +579,7 @@ # Supported sites
|
||||||
- **HungamaAlbumPlaylist**
|
- **HungamaAlbumPlaylist**
|
||||||
- **HungamaSong**
|
- **HungamaSong**
|
||||||
- **huya:live**: huya.com
|
- **huya:live**: huya.com
|
||||||
|
- **huya:video**: 虎牙视频
|
||||||
- **Hypem**
|
- **Hypem**
|
||||||
- **Hytale**
|
- **Hytale**
|
||||||
- **Icareus**
|
- **Icareus**
|
||||||
|
@ -660,6 +660,7 @@ # Supported sites
|
||||||
- **kick:vod**
|
- **kick:vod**
|
||||||
- **Kicker**
|
- **Kicker**
|
||||||
- **KickStarter**
|
- **KickStarter**
|
||||||
|
- **Kika**: KiKA.de
|
||||||
- **kinja:embed**
|
- **kinja:embed**
|
||||||
- **KinoPoisk**
|
- **KinoPoisk**
|
||||||
- **Kommunetv**
|
- **Kommunetv**
|
||||||
|
@ -722,7 +723,6 @@ # Supported sites
|
||||||
- **livestream:original**
|
- **livestream:original**
|
||||||
- **Livestreamfails**
|
- **Livestreamfails**
|
||||||
- **Lnk**
|
- **Lnk**
|
||||||
- **LnkGo**
|
|
||||||
- **loc**: Library of Congress
|
- **loc**: Library of Congress
|
||||||
- **loom**
|
- **loom**
|
||||||
- **loom:folder**
|
- **loom:folder**
|
||||||
|
@ -756,7 +756,7 @@ # Supported sites
|
||||||
- **Masters**
|
- **Masters**
|
||||||
- **MatchTV**
|
- **MatchTV**
|
||||||
- **MBN**: mbn.co.kr (매일방송)
|
- **MBN**: mbn.co.kr (매일방송)
|
||||||
- **MDR**: MDR.DE and KiKA
|
- **MDR**: MDR.DE
|
||||||
- **MedalTV**
|
- **MedalTV**
|
||||||
- **media.ccc.de**
|
- **media.ccc.de**
|
||||||
- **media.ccc.de:lists**
|
- **media.ccc.de:lists**
|
||||||
|
@ -811,6 +811,7 @@ # Supported sites
|
||||||
- **MNetTVLive**: [*mnettv*](## "netrc machine")
|
- **MNetTVLive**: [*mnettv*](## "netrc machine")
|
||||||
- **MNetTVRecordings**: [*mnettv*](## "netrc machine")
|
- **MNetTVRecordings**: [*mnettv*](## "netrc machine")
|
||||||
- **MochaVideo**
|
- **MochaVideo**
|
||||||
|
- **Mojevideo**: mojevideo.sk
|
||||||
- **Mojvideo**
|
- **Mojvideo**
|
||||||
- **Monstercat**
|
- **Monstercat**
|
||||||
- **MonsterSirenHypergryphMusic**
|
- **MonsterSirenHypergryphMusic**
|
||||||
|
@ -1042,8 +1043,8 @@ # Supported sites
|
||||||
- **Parler**: Posts on parler.com
|
- **Parler**: Posts on parler.com
|
||||||
- **parliamentlive.tv**: UK parliament videos
|
- **parliamentlive.tv**: UK parliament videos
|
||||||
- **Parlview**: (**Currently broken**)
|
- **Parlview**: (**Currently broken**)
|
||||||
- **Patreon**
|
- **patreon**
|
||||||
- **PatreonCampaign**
|
- **patreon:campaign**
|
||||||
- **pbs**: Public Broadcasting Service (PBS) and member stations: PBS: Public Broadcasting Service, APT - Alabama Public Television (WBIQ), GPB/Georgia Public Broadcasting (WGTV), Mississippi Public Broadcasting (WMPN), Nashville Public Television (WNPT), WFSU-TV (WFSU), WSRE (WSRE), WTCI (WTCI), WPBA/Channel 30 (WPBA), Alaska Public Media (KAKM), Arizona PBS (KAET), KNME-TV/Channel 5 (KNME), Vegas PBS (KLVX), AETN/ARKANSAS ETV NETWORK (KETS), KET (WKLE), WKNO/Channel 10 (WKNO), LPB/LOUISIANA PUBLIC BROADCASTING (WLPB), OETA (KETA), Ozarks Public Television (KOZK), WSIU Public Broadcasting (WSIU), KEET TV (KEET), KIXE/Channel 9 (KIXE), KPBS San Diego (KPBS), KQED (KQED), KVIE Public Television (KVIE), PBS SoCal/KOCE (KOCE), ValleyPBS (KVPT), CONNECTICUT PUBLIC TELEVISION (WEDH), KNPB Channel 5 (KNPB), SOPTV (KSYS), Rocky Mountain PBS (KRMA), KENW-TV3 (KENW), KUED Channel 7 (KUED), Wyoming PBS (KCWC), Colorado Public Television / KBDI 12 (KBDI), KBYU-TV (KBYU), Thirteen/WNET New York (WNET), WGBH/Channel 2 (WGBH), WGBY (WGBY), NJTV Public Media NJ (WNJT), WLIW21 (WLIW), mpt/Maryland Public Television (WMPB), WETA Television and Radio (WETA), WHYY (WHYY), PBS 39 (WLVT), WVPT - Your Source for PBS and More! (WVPT), Howard University Television (WHUT), WEDU PBS (WEDU), WGCU Public Media (WGCU), WPBT2 (WPBT), WUCF TV (WUCF), WUFT/Channel 5 (WUFT), WXEL/Channel 42 (WXEL), WLRN/Channel 17 (WLRN), WUSF Public Broadcasting (WUSF), ETV (WRLK), UNC-TV (WUNC), PBS Hawaii - Oceanic Cable Channel 10 (KHET), Idaho Public Television (KAID), KSPS (KSPS), OPB (KOPB), KWSU/Channel 10 & KTNW/Channel 31 (KWSU), WILL-TV (WILL), Network Knowledge - WSEC/Springfield (WSEC), WTTW11 (WTTW), Iowa Public Television/IPTV (KDIN), Nine Network (KETC), PBS39 Fort Wayne (WFWA), WFYI Indianapolis (WFYI), Milwaukee Public Television (WMVS), WNIN (WNIN), WNIT Public Television (WNIT), WPT (WPNE), WVUT/Channel 22 (WVUT), WEIU/Channel 51 (WEIU), WQPT-TV (WQPT), WYCC PBS Chicago (WYCC), WIPB-TV (WIPB), WTIU (WTIU), CET (WCET), ThinkTVNetwork (WPTD), WBGU-TV (WBGU), WGVU TV (WGVU), NET1 (KUON), Pioneer Public Television (KWCM), SDPB Television (KUSD), TPT (KTCA), KSMQ (KSMQ), KPTS/Channel 8 (KPTS), KTWU/Channel 11 (KTWU), East Tennessee PBS (WSJK), WCTE-TV (WCTE), WLJT, Channel 11 (WLJT), WOSU TV (WOSU), WOUB/WOUC (WOUB), WVPB (WVPB), WKYU-PBS (WKYU), KERA 13 (KERA), MPBN (WCBB), Mountain Lake PBS (WCFE), NHPTV (WENH), Vermont PBS (WETK), witf (WITF), WQED Multimedia (WQED), WMHT Educational Telecommunications (WMHT), Q-TV (WDCQ), WTVS Detroit Public TV (WTVS), CMU Public Television (WCMU), WKAR-TV (WKAR), WNMU-TV Public TV 13 (WNMU), WDSE - WRPT (WDSE), WGTE TV (WGTE), Lakeland Public Television (KAWE), KMOS-TV - Channels 6.1, 6.2 and 6.3 (KMOS), MontanaPBS (KUSM), KRWG/Channel 22 (KRWG), KACV (KACV), KCOS/Channel 13 (KCOS), WCNY/Channel 24 (WCNY), WNED (WNED), WPBS (WPBS), WSKG Public TV (WSKG), WXXI (WXXI), WPSU (WPSU), WVIA Public Media Studios (WVIA), WTVI (WTVI), Western Reserve PBS (WNEO), WVIZ/PBS ideastream (WVIZ), KCTS 9 (KCTS), Basin PBS (KPBT), KUHT / Channel 8 (KUHT), KLRN (KLRN), KLRU (KLRU), WTJX Channel 12 (WTJX), WCVE PBS (WCVE), KBTC Public Television (KBTC)
|
- **pbs**: Public Broadcasting Service (PBS) and member stations: PBS: Public Broadcasting Service, APT - Alabama Public Television (WBIQ), GPB/Georgia Public Broadcasting (WGTV), Mississippi Public Broadcasting (WMPN), Nashville Public Television (WNPT), WFSU-TV (WFSU), WSRE (WSRE), WTCI (WTCI), WPBA/Channel 30 (WPBA), Alaska Public Media (KAKM), Arizona PBS (KAET), KNME-TV/Channel 5 (KNME), Vegas PBS (KLVX), AETN/ARKANSAS ETV NETWORK (KETS), KET (WKLE), WKNO/Channel 10 (WKNO), LPB/LOUISIANA PUBLIC BROADCASTING (WLPB), OETA (KETA), Ozarks Public Television (KOZK), WSIU Public Broadcasting (WSIU), KEET TV (KEET), KIXE/Channel 9 (KIXE), KPBS San Diego (KPBS), KQED (KQED), KVIE Public Television (KVIE), PBS SoCal/KOCE (KOCE), ValleyPBS (KVPT), CONNECTICUT PUBLIC TELEVISION (WEDH), KNPB Channel 5 (KNPB), SOPTV (KSYS), Rocky Mountain PBS (KRMA), KENW-TV3 (KENW), KUED Channel 7 (KUED), Wyoming PBS (KCWC), Colorado Public Television / KBDI 12 (KBDI), KBYU-TV (KBYU), Thirteen/WNET New York (WNET), WGBH/Channel 2 (WGBH), WGBY (WGBY), NJTV Public Media NJ (WNJT), WLIW21 (WLIW), mpt/Maryland Public Television (WMPB), WETA Television and Radio (WETA), WHYY (WHYY), PBS 39 (WLVT), WVPT - Your Source for PBS and More! (WVPT), Howard University Television (WHUT), WEDU PBS (WEDU), WGCU Public Media (WGCU), WPBT2 (WPBT), WUCF TV (WUCF), WUFT/Channel 5 (WUFT), WXEL/Channel 42 (WXEL), WLRN/Channel 17 (WLRN), WUSF Public Broadcasting (WUSF), ETV (WRLK), UNC-TV (WUNC), PBS Hawaii - Oceanic Cable Channel 10 (KHET), Idaho Public Television (KAID), KSPS (KSPS), OPB (KOPB), KWSU/Channel 10 & KTNW/Channel 31 (KWSU), WILL-TV (WILL), Network Knowledge - WSEC/Springfield (WSEC), WTTW11 (WTTW), Iowa Public Television/IPTV (KDIN), Nine Network (KETC), PBS39 Fort Wayne (WFWA), WFYI Indianapolis (WFYI), Milwaukee Public Television (WMVS), WNIN (WNIN), WNIT Public Television (WNIT), WPT (WPNE), WVUT/Channel 22 (WVUT), WEIU/Channel 51 (WEIU), WQPT-TV (WQPT), WYCC PBS Chicago (WYCC), WIPB-TV (WIPB), WTIU (WTIU), CET (WCET), ThinkTVNetwork (WPTD), WBGU-TV (WBGU), WGVU TV (WGVU), NET1 (KUON), Pioneer Public Television (KWCM), SDPB Television (KUSD), TPT (KTCA), KSMQ (KSMQ), KPTS/Channel 8 (KPTS), KTWU/Channel 11 (KTWU), East Tennessee PBS (WSJK), WCTE-TV (WCTE), WLJT, Channel 11 (WLJT), WOSU TV (WOSU), WOUB/WOUC (WOUB), WVPB (WVPB), WKYU-PBS (WKYU), KERA 13 (KERA), MPBN (WCBB), Mountain Lake PBS (WCFE), NHPTV (WENH), Vermont PBS (WETK), witf (WITF), WQED Multimedia (WQED), WMHT Educational Telecommunications (WMHT), Q-TV (WDCQ), WTVS Detroit Public TV (WTVS), CMU Public Television (WCMU), WKAR-TV (WKAR), WNMU-TV Public TV 13 (WNMU), WDSE - WRPT (WDSE), WGTE TV (WGTE), Lakeland Public Television (KAWE), KMOS-TV - Channels 6.1, 6.2 and 6.3 (KMOS), MontanaPBS (KUSM), KRWG/Channel 22 (KRWG), KACV (KACV), KCOS/Channel 13 (KCOS), WCNY/Channel 24 (WCNY), WNED (WNED), WPBS (WPBS), WSKG Public TV (WSKG), WXXI (WXXI), WPSU (WPSU), WVIA Public Media Studios (WVIA), WTVI (WTVI), Western Reserve PBS (WNEO), WVIZ/PBS ideastream (WVIZ), KCTS 9 (KCTS), Basin PBS (KPBT), KUHT / Channel 8 (KUHT), KLRN (KLRN), KLRU (KLRU), WTJX Channel 12 (WTJX), WCVE PBS (WCVE), KBTC Public Television (KBTC)
|
||||||
- **PBSKids**
|
- **PBSKids**
|
||||||
- **PearVideo**
|
- **PearVideo**
|
||||||
|
@ -1285,12 +1286,14 @@ # Supported sites
|
||||||
- **Screencast**
|
- **Screencast**
|
||||||
- **Screencastify**
|
- **Screencastify**
|
||||||
- **ScreencastOMatic**
|
- **ScreencastOMatic**
|
||||||
|
- **ScreenRec**
|
||||||
- **ScrippsNetworks**
|
- **ScrippsNetworks**
|
||||||
- **scrippsnetworks:watch**
|
- **scrippsnetworks:watch**
|
||||||
- **Scrolller**
|
- **Scrolller**
|
||||||
- **SCTE**: [*scte*](## "netrc machine") (**Currently broken**)
|
- **SCTE**: [*scte*](## "netrc machine") (**Currently broken**)
|
||||||
- **SCTECourse**: [*scte*](## "netrc machine") (**Currently broken**)
|
- **SCTECourse**: [*scte*](## "netrc machine") (**Currently broken**)
|
||||||
- **sejm**
|
- **sejm**
|
||||||
|
- **Sen**
|
||||||
- **SenalColombiaLive**: (**Currently broken**)
|
- **SenalColombiaLive**: (**Currently broken**)
|
||||||
- **SenateGov**
|
- **SenateGov**
|
||||||
- **SenateISVP**
|
- **SenateISVP**
|
||||||
|
@ -1327,11 +1330,16 @@ # Supported sites
|
||||||
- **SlidesLive**
|
- **SlidesLive**
|
||||||
- **Slutload**
|
- **Slutload**
|
||||||
- **Smotrim**
|
- **Smotrim**
|
||||||
|
- **SnapchatSpotlight**
|
||||||
- **Snotr**
|
- **Snotr**
|
||||||
- **Sohu**
|
- **Sohu**
|
||||||
- **SohuV**
|
- **SohuV**
|
||||||
- **SonyLIV**: [*sonyliv*](## "netrc machine")
|
- **SonyLIV**: [*sonyliv*](## "netrc machine")
|
||||||
- **SonyLIVSeries**
|
- **SonyLIVSeries**
|
||||||
|
- **soop**: [*afreecatv*](## "netrc machine") sooplive.co.kr
|
||||||
|
- **soop:catchstory**: [*afreecatv*](## "netrc machine") sooplive.co.kr catch story
|
||||||
|
- **soop:live**: [*afreecatv*](## "netrc machine") sooplive.co.kr livestreams
|
||||||
|
- **soop:user**: [*afreecatv*](## "netrc machine")
|
||||||
- **soundcloud**: [*soundcloud*](## "netrc machine")
|
- **soundcloud**: [*soundcloud*](## "netrc machine")
|
||||||
- **soundcloud:playlist**: [*soundcloud*](## "netrc machine")
|
- **soundcloud:playlist**: [*soundcloud*](## "netrc machine")
|
||||||
- **soundcloud:related**: [*soundcloud*](## "netrc machine")
|
- **soundcloud:related**: [*soundcloud*](## "netrc machine")
|
||||||
|
@ -1608,6 +1616,7 @@ # Supported sites
|
||||||
- **videomore:season**
|
- **videomore:season**
|
||||||
- **videomore:video**
|
- **videomore:video**
|
||||||
- **VideoPress**
|
- **VideoPress**
|
||||||
|
- **Vidflex**
|
||||||
- **Vidio**: [*vidio*](## "netrc machine")
|
- **Vidio**: [*vidio*](## "netrc machine")
|
||||||
- **VidioLive**: [*vidio*](## "netrc machine")
|
- **VidioLive**: [*vidio*](## "netrc machine")
|
||||||
- **VidioPremier**: [*vidio*](## "netrc machine")
|
- **VidioPremier**: [*vidio*](## "netrc machine")
|
||||||
|
@ -1736,7 +1745,7 @@ # Supported sites
|
||||||
- **XiaoHongShu**: 小红书
|
- **XiaoHongShu**: 小红书
|
||||||
- **ximalaya**: 喜马拉雅FM
|
- **ximalaya**: 喜马拉雅FM
|
||||||
- **ximalaya:album**: 喜马拉雅FM 专辑
|
- **ximalaya:album**: 喜马拉雅FM 专辑
|
||||||
- **xinpianchang**: xinpianchang.com (**Currently broken**)
|
- **Xinpianchang**: 新片场
|
||||||
- **XMinus**: (**Currently broken**)
|
- **XMinus**: (**Currently broken**)
|
||||||
- **XNXX**
|
- **XNXX**
|
||||||
- **Xstream**
|
- **Xstream**
|
||||||
|
@ -1770,24 +1779,24 @@ # Supported sites
|
||||||
- **YouPornStar**: YouPorn Pornstar, with description, sorting and pagination
|
- **YouPornStar**: YouPorn Pornstar, with description, sorting and pagination
|
||||||
- **YouPornTag**: YouPorn tag (porntags), with sorting, filtering and pagination
|
- **YouPornTag**: YouPorn tag (porntags), with sorting, filtering and pagination
|
||||||
- **YouPornVideos**: YouPorn video (browse) playlists, with sorting, filtering and pagination
|
- **YouPornVideos**: YouPorn video (browse) playlists, with sorting, filtering and pagination
|
||||||
- **youtube**: YouTube
|
- **youtube**: [*youtube*](## "netrc machine") YouTube
|
||||||
- **youtube:clip**
|
- **youtube:clip**: [*youtube*](## "netrc machine")
|
||||||
- **youtube:favorites**: YouTube liked videos; ":ytfav" keyword (requires cookies)
|
- **youtube:favorites**: [*youtube*](## "netrc machine") YouTube liked videos; ":ytfav" keyword (requires cookies)
|
||||||
- **youtube:history**: Youtube watch history; ":ythis" keyword (requires cookies)
|
- **youtube:history**: [*youtube*](## "netrc machine") Youtube watch history; ":ythis" keyword (requires cookies)
|
||||||
- **youtube:music:search_url**: YouTube music search URLs with selectable sections, e.g. #songs
|
- **youtube:music:search_url**: [*youtube*](## "netrc machine") YouTube music search URLs with selectable sections, e.g. #songs
|
||||||
- **youtube:notif**: YouTube notifications; ":ytnotif" keyword (requires cookies)
|
- **youtube:notif**: [*youtube*](## "netrc machine") YouTube notifications; ":ytnotif" keyword (requires cookies)
|
||||||
- **youtube:playlist**: YouTube playlists
|
- **youtube:playlist**: [*youtube*](## "netrc machine") YouTube playlists
|
||||||
- **youtube:recommended**: YouTube recommended videos; ":ytrec" keyword
|
- **youtube:recommended**: [*youtube*](## "netrc machine") YouTube recommended videos; ":ytrec" keyword
|
||||||
- **youtube:search**: YouTube search; "ytsearch:" prefix
|
- **youtube:search**: [*youtube*](## "netrc machine") YouTube search; "ytsearch:" prefix
|
||||||
- **youtube:search:date**: YouTube search, newest videos first; "ytsearchdate:" prefix
|
- **youtube:search:date**: [*youtube*](## "netrc machine") YouTube search, newest videos first; "ytsearchdate:" prefix
|
||||||
- **youtube:search_url**: YouTube search URLs with sorting and filter support
|
- **youtube:search_url**: [*youtube*](## "netrc machine") YouTube search URLs with sorting and filter support
|
||||||
- **youtube:shorts:pivot:audio**: YouTube Shorts audio pivot (Shorts using audio of a given video)
|
- **youtube:shorts:pivot:audio**: [*youtube*](## "netrc machine") YouTube Shorts audio pivot (Shorts using audio of a given video)
|
||||||
- **youtube:subscriptions**: YouTube subscriptions feed; ":ytsubs" keyword (requires cookies)
|
- **youtube:subscriptions**: [*youtube*](## "netrc machine") YouTube subscriptions feed; ":ytsubs" keyword (requires cookies)
|
||||||
- **youtube:tab**: YouTube Tabs
|
- **youtube:tab**: [*youtube*](## "netrc machine") YouTube Tabs
|
||||||
- **youtube:user**: YouTube user videos; "ytuser:" prefix
|
- **youtube:user**: [*youtube*](## "netrc machine") YouTube user videos; "ytuser:" prefix
|
||||||
- **youtube:watchlater**: Youtube watch later list; ":ytwatchlater" keyword (requires cookies)
|
- **youtube:watchlater**: [*youtube*](## "netrc machine") Youtube watch later list; ":ytwatchlater" keyword (requires cookies)
|
||||||
- **YoutubeLivestreamEmbed**: YouTube livestream embeds
|
- **YoutubeLivestreamEmbed**: [*youtube*](## "netrc machine") YouTube livestream embeds
|
||||||
- **YoutubeYtBe**: youtu.be
|
- **YoutubeYtBe**: [*youtube*](## "netrc machine") youtu.be
|
||||||
- **Zaiko**
|
- **Zaiko**
|
||||||
- **ZaikoETicket**
|
- **ZaikoETicket**
|
||||||
- **Zapiks**
|
- **Zapiks**
|
||||||
|
|
|
@ -236,6 +236,35 @@ def test_format_selection_video(self):
|
||||||
downloaded = ydl.downloaded_info_dicts[0]
|
downloaded = ydl.downloaded_info_dicts[0]
|
||||||
self.assertEqual(downloaded['format_id'], 'vid-vcodec-dot')
|
self.assertEqual(downloaded['format_id'], 'vid-vcodec-dot')
|
||||||
|
|
||||||
|
def test_format_selection_by_vcodec_sort(self):
|
||||||
|
formats = [
|
||||||
|
{'format_id': 'av1-format', 'ext': 'mp4', 'vcodec': 'av1', 'acodec': 'none', 'url': TEST_URL},
|
||||||
|
{'format_id': 'vp9-hdr-format', 'ext': 'mp4', 'vcodec': 'vp09.02.50.10.01.09.18.09.00', 'acodec': 'none', 'url': TEST_URL},
|
||||||
|
{'format_id': 'vp9-sdr-format', 'ext': 'mp4', 'vcodec': 'vp09.00.50.08', 'acodec': 'none', 'url': TEST_URL},
|
||||||
|
{'format_id': 'h265-format', 'ext': 'mp4', 'vcodec': 'h265', 'acodec': 'none', 'url': TEST_URL},
|
||||||
|
]
|
||||||
|
info_dict = _make_result(formats)
|
||||||
|
|
||||||
|
ydl = YDL({'format': 'bestvideo', 'format_sort': ['vcodec:vp9.2']})
|
||||||
|
ydl.process_ie_result(info_dict.copy())
|
||||||
|
downloaded = ydl.downloaded_info_dicts[0]
|
||||||
|
self.assertEqual(downloaded['format_id'], 'vp9-hdr-format')
|
||||||
|
|
||||||
|
ydl = YDL({'format': 'bestvideo', 'format_sort': ['vcodec:vp9']})
|
||||||
|
ydl.process_ie_result(info_dict.copy())
|
||||||
|
downloaded = ydl.downloaded_info_dicts[0]
|
||||||
|
self.assertEqual(downloaded['format_id'], 'vp9-sdr-format')
|
||||||
|
|
||||||
|
ydl = YDL({'format': 'bestvideo', 'format_sort': ['+vcodec:vp9.2']})
|
||||||
|
ydl.process_ie_result(info_dict.copy())
|
||||||
|
downloaded = ydl.downloaded_info_dicts[0]
|
||||||
|
self.assertEqual(downloaded['format_id'], 'vp9-hdr-format')
|
||||||
|
|
||||||
|
ydl = YDL({'format': 'bestvideo', 'format_sort': ['+vcodec:vp9']})
|
||||||
|
ydl.process_ie_result(info_dict.copy())
|
||||||
|
downloaded = ydl.downloaded_info_dicts[0]
|
||||||
|
self.assertEqual(downloaded['format_id'], 'vp9-sdr-format')
|
||||||
|
|
||||||
def test_format_selection_string_ops(self):
|
def test_format_selection_string_ops(self):
|
||||||
formats = [
|
formats = [
|
||||||
{'format_id': 'abc-cba', 'ext': 'mp4', 'url': TEST_URL},
|
{'format_id': 'abc-cba', 'ext': 'mp4', 'url': TEST_URL},
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
sys.path.append(str(TEST_DATA_DIR))
|
sys.path.append(str(TEST_DATA_DIR))
|
||||||
importlib.invalidate_caches()
|
importlib.invalidate_caches()
|
||||||
|
|
||||||
|
from yt_dlp.utils import Config
|
||||||
from yt_dlp.plugins import PACKAGE_NAME, directories, load_plugins
|
from yt_dlp.plugins import PACKAGE_NAME, directories, load_plugins
|
||||||
|
|
||||||
|
|
||||||
|
@ -68,6 +69,24 @@ def test_importing_zipped_module(self):
|
||||||
os.remove(zip_path)
|
os.remove(zip_path)
|
||||||
importlib.invalidate_caches() # reset the import caches
|
importlib.invalidate_caches() # reset the import caches
|
||||||
|
|
||||||
|
def test_plugin_dirs(self):
|
||||||
|
# Internal plugin dirs hack for CLI --plugin-dirs
|
||||||
|
# To be replaced with proper system later
|
||||||
|
custom_plugin_dir = TEST_DATA_DIR / 'plugin_packages'
|
||||||
|
Config._plugin_dirs = [str(custom_plugin_dir)]
|
||||||
|
importlib.invalidate_caches() # reset the import caches
|
||||||
|
|
||||||
|
try:
|
||||||
|
package = importlib.import_module(f'{PACKAGE_NAME}.extractor')
|
||||||
|
self.assertIn(custom_plugin_dir / 'testpackage' / PACKAGE_NAME / 'extractor', map(Path, package.__path__))
|
||||||
|
|
||||||
|
plugins_ie = load_plugins('extractor', 'IE')
|
||||||
|
self.assertIn('PackagePluginIE', plugins_ie.keys())
|
||||||
|
|
||||||
|
finally:
|
||||||
|
Config._plugin_dirs = []
|
||||||
|
importlib.invalidate_caches() # reset the import caches
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
@ -4,8 +4,18 @@
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from yt_dlp.utils import dict_get, int_or_none, str_or_none
|
from yt_dlp.utils import (
|
||||||
from yt_dlp.utils.traversal import traverse_obj
|
ExtractorError,
|
||||||
|
determine_ext,
|
||||||
|
dict_get,
|
||||||
|
int_or_none,
|
||||||
|
str_or_none,
|
||||||
|
)
|
||||||
|
from yt_dlp.utils.traversal import (
|
||||||
|
traverse_obj,
|
||||||
|
require,
|
||||||
|
subs_list_to_dict,
|
||||||
|
)
|
||||||
|
|
||||||
_TEST_DATA = {
|
_TEST_DATA = {
|
||||||
100: 100,
|
100: 100,
|
||||||
|
@ -420,6 +430,71 @@ def test_traversal_morsel(self):
|
||||||
assert traverse_obj(morsel, [(None,), any]) == morsel, \
|
assert traverse_obj(morsel, [(None,), any]) == morsel, \
|
||||||
'Morsel should not be implicitly changed to dict on usage'
|
'Morsel should not be implicitly changed to dict on usage'
|
||||||
|
|
||||||
|
def test_traversal_filter(self):
|
||||||
|
data = [None, False, True, 0, 1, 0.0, 1.1, '', 'str', {}, {0: 0}, [], [1]]
|
||||||
|
|
||||||
|
assert traverse_obj(data, [..., filter]) == [True, 1, 1.1, 'str', {0: 0}, [1]], \
|
||||||
|
'`filter` should filter falsy values'
|
||||||
|
|
||||||
|
|
||||||
|
class TestTraversalHelpers:
|
||||||
|
def test_traversal_require(self):
|
||||||
|
with pytest.raises(ExtractorError):
|
||||||
|
traverse_obj(_TEST_DATA, ['None', {require('value')}])
|
||||||
|
assert traverse_obj(_TEST_DATA, ['str', {require('value')}]) == 'str', \
|
||||||
|
'`require` should pass through non `None` values'
|
||||||
|
|
||||||
|
def test_subs_list_to_dict(self):
|
||||||
|
assert traverse_obj([
|
||||||
|
{'name': 'de', 'url': 'https://example.com/subs/de.vtt'},
|
||||||
|
{'name': 'en', 'url': 'https://example.com/subs/en1.ass'},
|
||||||
|
{'name': 'en', 'url': 'https://example.com/subs/en2.ass'},
|
||||||
|
], [..., {
|
||||||
|
'id': 'name',
|
||||||
|
'url': 'url',
|
||||||
|
}, all, {subs_list_to_dict}]) == {
|
||||||
|
'de': [{'url': 'https://example.com/subs/de.vtt'}],
|
||||||
|
'en': [
|
||||||
|
{'url': 'https://example.com/subs/en1.ass'},
|
||||||
|
{'url': 'https://example.com/subs/en2.ass'},
|
||||||
|
],
|
||||||
|
}, 'function should build subtitle dict from list of subtitles'
|
||||||
|
assert traverse_obj([
|
||||||
|
{'name': 'de', 'url': 'https://example.com/subs/de.ass'},
|
||||||
|
{'name': 'de'},
|
||||||
|
{'name': 'en', 'content': 'content'},
|
||||||
|
{'url': 'https://example.com/subs/en'},
|
||||||
|
], [..., {
|
||||||
|
'id': 'name',
|
||||||
|
'data': 'content',
|
||||||
|
'url': 'url',
|
||||||
|
}, all, {subs_list_to_dict}]) == {
|
||||||
|
'de': [{'url': 'https://example.com/subs/de.ass'}],
|
||||||
|
'en': [{'data': 'content'}],
|
||||||
|
}, 'subs with mandatory items missing should be filtered'
|
||||||
|
assert traverse_obj([
|
||||||
|
{'url': 'https://example.com/subs/de.ass', 'name': 'de'},
|
||||||
|
{'url': 'https://example.com/subs/en', 'name': 'en'},
|
||||||
|
], [..., {
|
||||||
|
'id': 'name',
|
||||||
|
'ext': ['url', {lambda x: determine_ext(x, default_ext=None)}],
|
||||||
|
'url': 'url',
|
||||||
|
}, all, {subs_list_to_dict(ext='ext')}]) == {
|
||||||
|
'de': [{'url': 'https://example.com/subs/de.ass', 'ext': 'ass'}],
|
||||||
|
'en': [{'url': 'https://example.com/subs/en', 'ext': 'ext'}],
|
||||||
|
}, '`ext` should set default ext but leave existing value untouched'
|
||||||
|
assert traverse_obj([
|
||||||
|
{'name': 'en', 'url': 'https://example.com/subs/en2', 'prio': True},
|
||||||
|
{'name': 'en', 'url': 'https://example.com/subs/en1', 'prio': False},
|
||||||
|
], [..., {
|
||||||
|
'id': 'name',
|
||||||
|
'quality': ['prio', {int}],
|
||||||
|
'url': 'url',
|
||||||
|
}, all, {subs_list_to_dict(ext='ext')}]) == {'en': [
|
||||||
|
{'url': 'https://example.com/subs/en1', 'ext': 'ext'},
|
||||||
|
{'url': 'https://example.com/subs/en2', 'ext': 'ext'},
|
||||||
|
]}, '`quality` key should sort subtitle list accordingly'
|
||||||
|
|
||||||
|
|
||||||
class TestDictGet:
|
class TestDictGet:
|
||||||
def test_dict_get(self):
|
def test_dict_get(self):
|
||||||
|
|
|
@ -82,16 +82,32 @@
|
||||||
lock 2022.08.18.36 .+ Python 3\.6
|
lock 2022.08.18.36 .+ Python 3\.6
|
||||||
lock 2023.11.16 (?!win_x86_exe).+ Python 3\.7
|
lock 2023.11.16 (?!win_x86_exe).+ Python 3\.7
|
||||||
lock 2023.11.16 win_x86_exe .+ Windows-(?:Vista|2008Server)
|
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 win(?:_x86)?_exe Python 3\.[78].+ Windows-(?:7-|2008ServerR2)
|
||||||
'''
|
'''
|
||||||
|
|
||||||
TEST_LOCKFILE_V2_TMPL = r'''%s
|
TEST_LOCKFILE_V2_TMPL = r'''%s
|
||||||
lockV2 yt-dlp/yt-dlp 2022.08.18.36 .+ Python 3\.6
|
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).+ Python 3\.7
|
||||||
lockV2 yt-dlp/yt-dlp 2023.11.16 win_x86_exe .+ Windows-(?:Vista|2008Server)
|
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 win(?:_x86)?_exe Python 3\.[78].+ Windows-(?:7-|2008ServerR2)
|
||||||
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).+ 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 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 win(?:_x86)?_exe Python 3\.[78].+ Windows-(?:7-|2008ServerR2)
|
||||||
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).+ 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 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 win(?:_x86)?_exe Python 3\.[78].+ Windows-(?:7-|2008ServerR2)
|
||||||
'''
|
'''
|
||||||
|
|
||||||
TEST_LOCKFILE_V2 = TEST_LOCKFILE_V2_TMPL % TEST_LOCKFILE_COMMENT
|
TEST_LOCKFILE_V2 = TEST_LOCKFILE_V2_TMPL % TEST_LOCKFILE_COMMENT
|
||||||
|
@ -145,43 +161,76 @@ def test(lockfile, identifier, input_tag, expect_tag, exact=False, repo='yt-dlp/
|
||||||
for lockfile in (TEST_LOCKFILE_V1, TEST_LOCKFILE_V2, TEST_LOCKFILE_ACTUAL, TEST_LOCKFILE_FORK):
|
for lockfile in (TEST_LOCKFILE_V1, TEST_LOCKFILE_V2, TEST_LOCKFILE_ACTUAL, TEST_LOCKFILE_FORK):
|
||||||
# Normal operation
|
# Normal operation
|
||||||
test(lockfile, 'zip Python 3.12.0', '2023.12.31', '2023.12.31')
|
test(lockfile, 'zip Python 3.12.0', '2023.12.31', '2023.12.31')
|
||||||
test(lockfile, 'zip stable Python 3.12.0', '2023.12.31', '2023.12.31', exact=True)
|
test(lockfile, 'zip Python 3.12.0', '2023.12.31', '2023.12.31', exact=True)
|
||||||
# Python 3.6 --update should update only to its lock
|
# py2exe should never update beyond 2024.10.22
|
||||||
|
test(lockfile, 'py2exe Python 3.8', '2025.01.01', '2024.10.22')
|
||||||
|
test(lockfile, 'py2exe Python 3.8', '2025.01.01', None, exact=True)
|
||||||
|
# Python 3.6 --update should update only to the py3.6 lock
|
||||||
test(lockfile, 'zip Python 3.6.0', '2023.11.16', '2022.08.18.36')
|
test(lockfile, 'zip Python 3.6.0', '2023.11.16', '2022.08.18.36')
|
||||||
# --update-to an exact version later than the lock should return None
|
# Python 3.6 --update-to an exact version later than the py3.6 lock should return None
|
||||||
test(lockfile, 'zip stable Python 3.6.0', '2023.11.16', None, exact=True)
|
test(lockfile, 'zip Python 3.6.0', '2023.11.16', None, exact=True)
|
||||||
# Python 3.7 should be able to update to its lock
|
# Python 3.7 should be able to update to the py3.7 lock
|
||||||
test(lockfile, 'zip Python 3.7.0', '2023.11.16', '2023.11.16')
|
test(lockfile, 'zip Python 3.7.0', '2023.11.16', '2023.11.16')
|
||||||
test(lockfile, 'zip stable Python 3.7.1', '2023.11.16', '2023.11.16', exact=True)
|
test(lockfile, 'zip Python 3.7.1', '2023.11.16', '2023.11.16', exact=True)
|
||||||
# Non-win_x86_exe builds on py3.7 must be locked
|
# Non-win_x86_exe builds on py3.7 must be locked at py3.7 lock
|
||||||
test(lockfile, 'zip Python 3.7.1', '2023.12.31', '2023.11.16')
|
test(lockfile, 'zip Python 3.7.1', '2023.12.31', '2023.11.16')
|
||||||
test(lockfile, 'zip stable Python 3.7.1', '2023.12.31', None, exact=True)
|
test(lockfile, 'zip Python 3.7.1', '2023.12.31', None, exact=True)
|
||||||
test( # Windows Vista w/ win_x86_exe must be locked
|
# Python 3.8 should only update to the py3.8 lock
|
||||||
lockfile, 'win_x86_exe stable Python 3.7.9 (CPython x86 32bit) - Windows-Vista-6.0.6003-SP2',
|
test(lockfile, 'zip Python 3.8.10', '2025.01.01', '2024.10.22')
|
||||||
|
test(lockfile, 'zip Python 3.8.110', '2025.01.01', None, exact=True)
|
||||||
|
test( # Windows Vista w/ win_x86_exe must be locked at Vista lock
|
||||||
|
lockfile, 'win_x86_exe Python 3.7.9 (CPython x86 32bit) - Windows-Vista-6.0.6003-SP2',
|
||||||
'2023.12.31', '2023.11.16')
|
'2023.12.31', '2023.11.16')
|
||||||
test( # Windows 2008Server w/ win_x86_exe must be locked
|
test( # Windows 2008Server w/ win_x86_exe must be locked at Vista lock
|
||||||
lockfile, 'win_x86_exe Python 3.7.9 (CPython x86 32bit) - Windows-2008Server',
|
lockfile, 'win_x86_exe Python 3.7.9 (CPython x86 32bit) - Windows-2008Server',
|
||||||
'2023.12.31', None, exact=True)
|
'2023.12.31', None, exact=True)
|
||||||
test( # Windows 7 w/ win_x86_exe py3.7 build should be able to update beyond lock
|
test( # Windows 7 w/ win_x86_exe py3.7 build should be able to update beyond py3.7 lock
|
||||||
lockfile, 'win_x86_exe stable Python 3.7.9 (CPython x86 32bit) - Windows-7-6.1.7601-SP1',
|
lockfile, 'win_x86_exe Python 3.7.9 (CPython x86 32bit) - Windows-7-6.1.7601-SP1',
|
||||||
'2023.12.31', '2023.12.31')
|
'2023.12.31', '2023.12.31', exact=True)
|
||||||
test( # Windows 8.1 w/ '2008Server' in platform string should be able to update beyond lock
|
test( # Windows 7 win_x86_exe should only update to Win7 lock
|
||||||
|
lockfile, 'win_x86_exe Python 3.7.9 (CPython x86 32bit) - Windows-7-6.1.7601-SP1',
|
||||||
|
'2025.01.01', '2024.10.22')
|
||||||
|
test( # Windows 2008ServerR2 win_exe should only update to Win7 lock
|
||||||
|
lockfile, 'win_exe Python 3.8.10 (CPython x86 32bit) - Windows-2008ServerR2',
|
||||||
|
'2025.12.31', '2024.10.22')
|
||||||
|
test( # Windows 8.1 w/ '2008Server' in platform string should be able to update beyond py3.7 lock
|
||||||
lockfile, 'win_x86_exe Python 3.7.9 (CPython x86 32bit) - Windows-post2008Server-6.2.9200',
|
lockfile, 'win_x86_exe Python 3.7.9 (CPython x86 32bit) - Windows-post2008Server-6.2.9200',
|
||||||
'2023.12.31', '2023.12.31', exact=True)
|
'2023.12.31', '2023.12.31', exact=True)
|
||||||
|
test( # win_exe built w/Python 3.8 on Windows>=8 should be able to update beyond py3.8 lock
|
||||||
|
lockfile, 'win_exe Python 3.8.10 (CPython AMD64 64bit) - Windows-10-10.0.20348-SP0',
|
||||||
|
'2025.01.01', '2025.01.01', exact=True)
|
||||||
|
test( # linux_armv7l_exe w/glibc2.7 should only update to glibc<2.31 lock
|
||||||
|
lockfile, 'linux_armv7l_exe Python 3.8.0 (CPython armv7l 32bit) - Linux-6.5.0-1025-azure-armv7l-with-glibc2.7',
|
||||||
|
'2025.01.01', '2024.10.22')
|
||||||
|
test( # linux_armv7l_exe w/Python 3.8 and glibc>=2.31 should be able to update beyond py3.8 and glibc<2.31 locks
|
||||||
|
lockfile, 'linux_armv7l_exe Python 3.8.0 (CPython armv7l 32bit) - Linux-6.5.0-1025-azure-armv7l-with-glibc2.31',
|
||||||
|
'2025.01.01', '2025.01.01')
|
||||||
|
test( # linux_armv7l_exe w/glibc2.30 should only update to glibc<2.31 lock
|
||||||
|
lockfile, 'linux_armv7l_exe Python 3.8.0 (CPython armv7l 64bit) - Linux-6.5.0-1025-azure-aarch64-with-glibc2.30 (OpenSSL',
|
||||||
|
'2025.01.01', '2024.10.22')
|
||||||
|
test( # linux_aarch64_exe w/glibc2.17 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.17',
|
||||||
|
'2025.01.01', '2024.10.22')
|
||||||
|
test( # linux_aarch64_exe w/glibc2.40 and glibc>=2.31 should be able to update beyond py3.8 and glibc<2.31 locks
|
||||||
|
lockfile, 'linux_aarch64_exe Python 3.8.0 (CPython aarch64 64bit) - Linux-6.5.0-1025-azure-aarch64-with-glibc2.40',
|
||||||
|
'2025.01.01', '2025.01.01')
|
||||||
|
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')
|
||||||
|
|
||||||
# Forks can block updates to non-numeric tags rather than lock
|
# 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')
|
test(TEST_LOCKFILE_FORK, 'zip Python 3.6.3', 'pr0000', None, repo='fork/yt-dlp')
|
||||||
test(TEST_LOCKFILE_FORK, 'zip stable Python 3.7.4', 'pr0000', 'pr0000', repo='fork/yt-dlp')
|
test(TEST_LOCKFILE_FORK, 'zip Python 3.7.4', 'pr0000', 'pr0000', repo='fork/yt-dlp')
|
||||||
test(TEST_LOCKFILE_FORK, 'zip stable Python 3.7.4', 'pr1234', None, repo='fork/yt-dlp')
|
test(TEST_LOCKFILE_FORK, 'zip Python 3.7.4', 'pr1234', None, repo='fork/yt-dlp')
|
||||||
test(TEST_LOCKFILE_FORK, 'zip Python 3.8.1', 'pr1234', 'pr1234', repo='fork/yt-dlp', exact=True)
|
test(TEST_LOCKFILE_FORK, 'zip Python 3.8.1', 'pr1234', 'pr1234', repo='fork/yt-dlp', exact=True)
|
||||||
test(
|
test(
|
||||||
TEST_LOCKFILE_FORK, 'win_x86_exe stable Python 3.7.9 (CPython x86 32bit) - Windows-Vista-6.0.6003-SP2',
|
TEST_LOCKFILE_FORK, 'win_x86_exe Python 3.7.9 (CPython x86 32bit) - Windows-Vista-6.0.6003-SP2',
|
||||||
'pr1234', None, repo='fork/yt-dlp')
|
'pr1234', None, repo='fork/yt-dlp')
|
||||||
test(
|
test(
|
||||||
TEST_LOCKFILE_FORK, 'win_x86_exe stable Python 3.7.9 (CPython x86 32bit) - Windows-7-6.1.7601-SP1',
|
TEST_LOCKFILE_FORK, 'win_x86_exe Python 3.7.9 (CPython x86 32bit) - Windows-7-6.1.7601-SP1',
|
||||||
'2023.12.31', '2023.12.31', repo='fork/yt-dlp')
|
'2023.12.31', '2023.12.31', repo='fork/yt-dlp')
|
||||||
test(TEST_LOCKFILE_FORK, 'zip Python 3.11.2', 'pr9999', None, repo='fork/yt-dlp', exact=True)
|
test(TEST_LOCKFILE_FORK, 'zip Python 3.11.2', 'pr9999', None, repo='fork/yt-dlp', exact=True)
|
||||||
test(TEST_LOCKFILE_FORK, 'zip stable Python 3.12.0', 'pr9999', 'pr9999', repo='fork/yt-dlp')
|
test(TEST_LOCKFILE_FORK, 'zip Python 3.12.0', 'pr9999', 'pr9999', repo='fork/yt-dlp')
|
||||||
|
|
||||||
def test_query_update(self):
|
def test_query_update(self):
|
||||||
ydl = FakeYDL()
|
ydl = FakeYDL()
|
||||||
|
|
|
@ -221,9 +221,10 @@ def test_sanitize_ids(self):
|
||||||
self.assertEqual(sanitize_filename('N0Y__7-UOdI', is_id=True), 'N0Y__7-UOdI')
|
self.assertEqual(sanitize_filename('N0Y__7-UOdI', is_id=True), 'N0Y__7-UOdI')
|
||||||
|
|
||||||
def test_sanitize_path(self):
|
def test_sanitize_path(self):
|
||||||
if sys.platform != 'win32':
|
with unittest.mock.patch('sys.platform', 'win32'):
|
||||||
return
|
self._test_sanitize_path()
|
||||||
|
|
||||||
|
def _test_sanitize_path(self):
|
||||||
self.assertEqual(sanitize_path('abc'), 'abc')
|
self.assertEqual(sanitize_path('abc'), 'abc')
|
||||||
self.assertEqual(sanitize_path('abc/def'), 'abc\\def')
|
self.assertEqual(sanitize_path('abc/def'), 'abc\\def')
|
||||||
self.assertEqual(sanitize_path('abc\\def'), 'abc\\def')
|
self.assertEqual(sanitize_path('abc\\def'), 'abc\\def')
|
||||||
|
@ -256,6 +257,11 @@ def test_sanitize_path(self):
|
||||||
self.assertEqual(sanitize_path('./abc'), 'abc')
|
self.assertEqual(sanitize_path('./abc'), 'abc')
|
||||||
self.assertEqual(sanitize_path('./../abc'), '..\\abc')
|
self.assertEqual(sanitize_path('./../abc'), '..\\abc')
|
||||||
|
|
||||||
|
self.assertEqual(sanitize_path('\\abc'), '\\abc')
|
||||||
|
self.assertEqual(sanitize_path('C:abc'), 'C:abc')
|
||||||
|
self.assertEqual(sanitize_path('C:abc\\..\\'), 'C:..')
|
||||||
|
self.assertEqual(sanitize_path('C:\\abc:%(title)s.%(ext)s'), 'C:\\abc#%(title)s.%(ext)s')
|
||||||
|
|
||||||
def test_sanitize_url(self):
|
def test_sanitize_url(self):
|
||||||
self.assertEqual(sanitize_url('//foo.bar'), 'http://foo.bar')
|
self.assertEqual(sanitize_url('//foo.bar'), 'http://foo.bar')
|
||||||
self.assertEqual(sanitize_url('httpss://foo.bar'), 'https://foo.bar')
|
self.assertEqual(sanitize_url('httpss://foo.bar'), 'https://foo.bar')
|
||||||
|
@ -921,6 +927,11 @@ def test_parse_codecs(self):
|
||||||
'acodec': 'none',
|
'acodec': 'none',
|
||||||
'dynamic_range': 'HDR10',
|
'dynamic_range': 'HDR10',
|
||||||
})
|
})
|
||||||
|
self.assertEqual(parse_codecs('vp09.02.50.10.01.09.18.09.00'), {
|
||||||
|
'vcodec': 'vp09.02.50.10.01.09.18.09.00',
|
||||||
|
'acodec': 'none',
|
||||||
|
'dynamic_range': 'HDR10',
|
||||||
|
})
|
||||||
self.assertEqual(parse_codecs('av01.0.12M.10.0.110.09.16.09.0'), {
|
self.assertEqual(parse_codecs('av01.0.12M.10.0.110.09.16.09.0'), {
|
||||||
'vcodec': 'av01.0.12M.10.0.110.09.16.09.0',
|
'vcodec': 'av01.0.12M.10.0.110.09.16.09.0',
|
||||||
'acodec': 'none',
|
'acodec': 'none',
|
||||||
|
|
5
test/testdata/plugin_packages/testpackage/yt_dlp_plugins/extractor/package.py
vendored
Normal file
5
test/testdata/plugin_packages/testpackage/yt_dlp_plugins/extractor/package.py
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from yt_dlp.extractor.common import InfoExtractor
|
||||||
|
|
||||||
|
|
||||||
|
class PackagePluginIE(InfoExtractor):
|
||||||
|
pass
|
|
@ -27,7 +27,7 @@
|
||||||
from .cache import Cache
|
from .cache import Cache
|
||||||
from .compat import urllib # isort: split
|
from .compat import urllib # isort: split
|
||||||
from .compat import compat_os_name, urllib_req_to_req
|
from .compat import compat_os_name, urllib_req_to_req
|
||||||
from .cookies import LenientSimpleCookie, load_cookies
|
from .cookies import CookieLoadError, LenientSimpleCookie, load_cookies
|
||||||
from .downloader import FFmpegFD, get_suitable_downloader, shorten_protocol_name
|
from .downloader import FFmpegFD, get_suitable_downloader, shorten_protocol_name
|
||||||
from .downloader.rtmp import rtmpdump_version
|
from .downloader.rtmp import rtmpdump_version
|
||||||
from .extractor import gen_extractor_classes, get_info_extractor
|
from .extractor import gen_extractor_classes, get_info_extractor
|
||||||
|
@ -154,7 +154,6 @@
|
||||||
try_get,
|
try_get,
|
||||||
url_basename,
|
url_basename,
|
||||||
variadic,
|
variadic,
|
||||||
version_tuple,
|
|
||||||
windows_enable_vt_mode,
|
windows_enable_vt_mode,
|
||||||
write_json_file,
|
write_json_file,
|
||||||
write_string,
|
write_string,
|
||||||
|
@ -251,7 +250,7 @@ class YoutubeDL:
|
||||||
format_sort_force: Force the given format_sort. see "Sorting Formats"
|
format_sort_force: Force the given format_sort. see "Sorting Formats"
|
||||||
for more details.
|
for more details.
|
||||||
prefer_free_formats: Whether to prefer video formats with free containers
|
prefer_free_formats: Whether to prefer video formats with free containers
|
||||||
over non-free ones of same quality.
|
over non-free ones of the same quality.
|
||||||
allow_multiple_video_streams: Allow multiple video streams to be merged
|
allow_multiple_video_streams: Allow multiple video streams to be merged
|
||||||
into a single file
|
into a single file
|
||||||
allow_multiple_audio_streams: Allow multiple audio streams to be merged
|
allow_multiple_audio_streams: Allow multiple audio streams to be merged
|
||||||
|
@ -285,7 +284,7 @@ class YoutubeDL:
|
||||||
rejecttitle: Reject downloads for matching titles.
|
rejecttitle: Reject downloads for matching titles.
|
||||||
logger: Log messages to a logging.Logger instance.
|
logger: Log messages to a logging.Logger instance.
|
||||||
logtostderr: Print everything to stderr instead of stdout.
|
logtostderr: Print everything to stderr instead of stdout.
|
||||||
consoletitle: Display progress in console window's titlebar.
|
consoletitle: Display progress in the console window's titlebar.
|
||||||
writedescription: Write the video description to a .description file
|
writedescription: Write the video description to a .description file
|
||||||
writeinfojson: Write the video description to a .info.json file
|
writeinfojson: Write the video description to a .info.json file
|
||||||
clean_infojson: Remove internal metadata from the infojson
|
clean_infojson: Remove internal metadata from the infojson
|
||||||
|
@ -513,7 +512,7 @@ class YoutubeDL:
|
||||||
The following options are used by the extractors:
|
The following options are used by the extractors:
|
||||||
extractor_retries: Number of times to retry for known errors (default: 3)
|
extractor_retries: Number of times to retry for known errors (default: 3)
|
||||||
dynamic_mpd: Whether to process dynamic DASH manifests (default: True)
|
dynamic_mpd: Whether to process dynamic DASH manifests (default: True)
|
||||||
hls_split_discontinuity: Split HLS playlists to different formats at
|
hls_split_discontinuity: Split HLS playlists into different formats at
|
||||||
discontinuities such as ad breaks (default: False)
|
discontinuities such as ad breaks (default: False)
|
||||||
extractor_args: A dictionary of arguments to be passed to the extractors.
|
extractor_args: A dictionary of arguments to be passed to the extractors.
|
||||||
See "EXTRACTOR ARGUMENTS" for details.
|
See "EXTRACTOR ARGUMENTS" for details.
|
||||||
|
@ -553,7 +552,7 @@ class YoutubeDL:
|
||||||
include_ads: - Doesn't work
|
include_ads: - Doesn't work
|
||||||
Download ads as well
|
Download ads as well
|
||||||
call_home: - Not implemented
|
call_home: - Not implemented
|
||||||
Boolean, true iff we are allowed to contact the
|
Boolean, true if we are allowed to contact the
|
||||||
yt-dlp servers for debugging.
|
yt-dlp servers for debugging.
|
||||||
post_hooks: - Register a custom postprocessor
|
post_hooks: - Register a custom postprocessor
|
||||||
A list of functions that get called as the final step
|
A list of functions that get called as the final step
|
||||||
|
@ -1624,7 +1623,7 @@ def wrapper(self, *args, **kwargs):
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
return func(self, *args, **kwargs)
|
return func(self, *args, **kwargs)
|
||||||
except (DownloadCancelled, LazyList.IndexError, PagedList.IndexError):
|
except (CookieLoadError, DownloadCancelled, LazyList.IndexError, PagedList.IndexError):
|
||||||
raise
|
raise
|
||||||
except ReExtractInfo as e:
|
except ReExtractInfo as e:
|
||||||
if e.expected:
|
if e.expected:
|
||||||
|
@ -3580,6 +3579,8 @@ def __download_wrapper(self, func):
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
try:
|
try:
|
||||||
res = func(*args, **kwargs)
|
res = func(*args, **kwargs)
|
||||||
|
except CookieLoadError:
|
||||||
|
raise
|
||||||
except UnavailableVideoError as e:
|
except UnavailableVideoError as e:
|
||||||
self.report_error(e)
|
self.report_error(e)
|
||||||
except DownloadCancelled as e:
|
except DownloadCancelled as e:
|
||||||
|
@ -4068,6 +4069,10 @@ def get_encoding(stream):
|
||||||
|
|
||||||
write_debug(f'Proxy map: {self.proxies}')
|
write_debug(f'Proxy map: {self.proxies}')
|
||||||
write_debug(f'Request Handlers: {", ".join(rh.RH_NAME for rh in self._request_director.handlers.values())}')
|
write_debug(f'Request Handlers: {", ".join(rh.RH_NAME for rh in self._request_director.handlers.values())}')
|
||||||
|
if os.environ.get('YTDLP_NO_PLUGINS'):
|
||||||
|
write_debug('Plugins are forcibly disabled')
|
||||||
|
return
|
||||||
|
|
||||||
for plugin_type, plugins in {'Extractor': plugin_ies, 'Post-Processor': plugin_pps}.items():
|
for plugin_type, plugins in {'Extractor': plugin_ies, 'Post-Processor': plugin_pps}.items():
|
||||||
display_list = ['{}{}'.format(
|
display_list = ['{}{}'.format(
|
||||||
klass.__name__, '' if klass.__name__ == name else f' as {name}')
|
klass.__name__, '' if klass.__name__ == name else f' as {name}')
|
||||||
|
@ -4083,17 +4088,6 @@ def get_encoding(stream):
|
||||||
if plugin_dirs:
|
if plugin_dirs:
|
||||||
write_debug(f'Plugin directories: {plugin_dirs}')
|
write_debug(f'Plugin directories: {plugin_dirs}')
|
||||||
|
|
||||||
# Not implemented
|
|
||||||
if False and self.params.get('call_home'):
|
|
||||||
ipaddr = self.urlopen('https://yt-dl.org/ip').read().decode()
|
|
||||||
write_debug(f'Public IP address: {ipaddr}')
|
|
||||||
latest_version = self.urlopen(
|
|
||||||
'https://yt-dl.org/latest/version').read().decode()
|
|
||||||
if version_tuple(latest_version) > version_tuple(__version__):
|
|
||||||
self.report_warning(
|
|
||||||
f'You are using an outdated version (newest version: {latest_version})! '
|
|
||||||
'See https://yt-dl.org/update if you need help updating.')
|
|
||||||
|
|
||||||
@functools.cached_property
|
@functools.cached_property
|
||||||
def proxies(self):
|
def proxies(self):
|
||||||
"""Global proxy configuration"""
|
"""Global proxy configuration"""
|
||||||
|
@ -4113,8 +4107,14 @@ def proxies(self):
|
||||||
@functools.cached_property
|
@functools.cached_property
|
||||||
def cookiejar(self):
|
def cookiejar(self):
|
||||||
"""Global cookiejar instance"""
|
"""Global cookiejar instance"""
|
||||||
|
try:
|
||||||
return load_cookies(
|
return load_cookies(
|
||||||
self.params.get('cookiefile'), self.params.get('cookiesfrombrowser'), self)
|
self.params.get('cookiefile'), self.params.get('cookiesfrombrowser'), self)
|
||||||
|
except CookieLoadError as error:
|
||||||
|
cause = error.__context__
|
||||||
|
# compat: <=py3.9: `traceback.format_exception` has a different signature
|
||||||
|
self.report_error(str(cause), tb=''.join(traceback.format_exception(None, cause, cause.__traceback__)))
|
||||||
|
raise
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _opener(self):
|
def _opener(self):
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
if sys.version_info < (3, 8):
|
if sys.version_info < (3, 9):
|
||||||
raise ImportError(
|
raise ImportError(
|
||||||
f'You are using an unsupported version of Python. Only Python versions 3.8 and above are supported by yt-dlp') # noqa: F541
|
f'You are using an unsupported version of Python. Only Python versions 3.9 and above are supported by yt-dlp') # noqa: F541
|
||||||
|
|
||||||
__license__ = 'The Unlicense'
|
__license__ = 'The Unlicense'
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from .compat import compat_os_name
|
from .compat import compat_os_name
|
||||||
from .cookies import SUPPORTED_BROWSERS, SUPPORTED_KEYRINGS
|
from .cookies import SUPPORTED_BROWSERS, SUPPORTED_KEYRINGS, CookieLoadError
|
||||||
from .downloader.external import get_external_downloader
|
from .downloader.external import get_external_downloader
|
||||||
from .extractor import list_extractor_classes
|
from .extractor import list_extractor_classes
|
||||||
from .extractor.adobepass import MSO_INFO
|
from .extractor.adobepass import MSO_INFO
|
||||||
|
@ -34,6 +34,7 @@
|
||||||
)
|
)
|
||||||
from .update import Updater
|
from .update import Updater
|
||||||
from .utils import (
|
from .utils import (
|
||||||
|
Config,
|
||||||
NO_DEFAULT,
|
NO_DEFAULT,
|
||||||
POSTPROCESS_WHEN,
|
POSTPROCESS_WHEN,
|
||||||
DateRange,
|
DateRange,
|
||||||
|
@ -235,6 +236,11 @@ def validate_minmax(min_val, max_val, min_name, max_name=None):
|
||||||
validate_regex('format sorting', f, FormatSorter.regex)
|
validate_regex('format sorting', f, FormatSorter.regex)
|
||||||
|
|
||||||
# Postprocessor formats
|
# Postprocessor formats
|
||||||
|
if opts.convertsubtitles == 'none':
|
||||||
|
opts.convertsubtitles = None
|
||||||
|
if opts.convertthumbnails == 'none':
|
||||||
|
opts.convertthumbnails = None
|
||||||
|
|
||||||
validate_regex('merge output format', opts.merge_output_format,
|
validate_regex('merge output format', opts.merge_output_format,
|
||||||
r'({0})(/({0}))*'.format('|'.join(map(re.escape, FFmpegMergerPP.SUPPORTED_EXTS))))
|
r'({0})(/({0}))*'.format('|'.join(map(re.escape, FFmpegMergerPP.SUPPORTED_EXTS))))
|
||||||
validate_regex('audio format', opts.audioformat, FFmpegExtractAudioPP.FORMAT_RE)
|
validate_regex('audio format', opts.audioformat, FFmpegExtractAudioPP.FORMAT_RE)
|
||||||
|
@ -962,6 +968,11 @@ def _real_main(argv=None):
|
||||||
|
|
||||||
parser, opts, all_urls, ydl_opts = parse_options(argv)
|
parser, opts, all_urls, ydl_opts = parse_options(argv)
|
||||||
|
|
||||||
|
# HACK: Set the plugin dirs early on
|
||||||
|
# TODO(coletdjnz): remove when plugin globals system is implemented
|
||||||
|
if opts.plugin_dirs is not None:
|
||||||
|
Config._plugin_dirs = list(map(expand_path, opts.plugin_dirs))
|
||||||
|
|
||||||
# Dump user agent
|
# Dump user agent
|
||||||
if opts.dump_user_agent:
|
if opts.dump_user_agent:
|
||||||
ua = traverse_obj(opts.headers, 'User-Agent', casesense=False, default=std_headers['User-Agent'])
|
ua = traverse_obj(opts.headers, 'User-Agent', casesense=False, default=std_headers['User-Agent'])
|
||||||
|
@ -1079,7 +1090,7 @@ def main(argv=None):
|
||||||
_IN_CLI = True
|
_IN_CLI = True
|
||||||
try:
|
try:
|
||||||
_exit(*variadic(_real_main(argv)))
|
_exit(*variadic(_real_main(argv)))
|
||||||
except DownloadError:
|
except (CookieLoadError, DownloadError):
|
||||||
_exit(1)
|
_exit(1)
|
||||||
except SameFileError as e:
|
except SameFileError as e:
|
||||||
_exit(f'ERROR: {e}')
|
_exit(f'ERROR: {e}')
|
||||||
|
|
|
@ -57,7 +57,7 @@ def __getattr__(attr):
|
||||||
callback(attr)
|
callback(attr)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@functools.lru_cache(maxsize=None)
|
@functools.cache
|
||||||
def from_child(attr):
|
def from_child(attr):
|
||||||
nonlocal child
|
nonlocal child
|
||||||
if attr not in allowed_attributes:
|
if attr not in allowed_attributes:
|
||||||
|
|
|
@ -5,8 +5,3 @@
|
||||||
|
|
||||||
passthrough_module(__name__, 'functools')
|
passthrough_module(__name__, 'functools')
|
||||||
del passthrough_module
|
del passthrough_module
|
||||||
|
|
||||||
try:
|
|
||||||
_ = cache # >= 3.9
|
|
||||||
except NameError:
|
|
||||||
cache = lru_cache(maxsize=None)
|
|
||||||
|
|
|
@ -34,6 +34,7 @@
|
||||||
from .minicurses import MultilinePrinter, QuietMultilinePrinter
|
from .minicurses import MultilinePrinter, QuietMultilinePrinter
|
||||||
from .utils import (
|
from .utils import (
|
||||||
DownloadError,
|
DownloadError,
|
||||||
|
YoutubeDLError,
|
||||||
Popen,
|
Popen,
|
||||||
error_to_str,
|
error_to_str,
|
||||||
expand_path,
|
expand_path,
|
||||||
|
@ -86,7 +87,12 @@ def _create_progress_bar(logger):
|
||||||
return printer
|
return printer
|
||||||
|
|
||||||
|
|
||||||
|
class CookieLoadError(YoutubeDLError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def load_cookies(cookie_file, browser_specification, ydl):
|
def load_cookies(cookie_file, browser_specification, ydl):
|
||||||
|
try:
|
||||||
cookie_jars = []
|
cookie_jars = []
|
||||||
if browser_specification is not None:
|
if browser_specification is not None:
|
||||||
browser_name, profile, keyring, container = _parse_browser_specification(*browser_specification)
|
browser_name, profile, keyring, container = _parse_browser_specification(*browser_specification)
|
||||||
|
@ -104,6 +110,8 @@ def load_cookies(cookie_file, browser_specification, ydl):
|
||||||
cookie_jars.append(jar)
|
cookie_jars.append(jar)
|
||||||
|
|
||||||
return _merge_cookie_jars(cookie_jars)
|
return _merge_cookie_jars(cookie_jars)
|
||||||
|
except Exception:
|
||||||
|
raise CookieLoadError('failed to load cookies')
|
||||||
|
|
||||||
|
|
||||||
def extract_cookies_from_browser(browser_name, profile=None, logger=YDLLogger(), *, keyring=None, container=None):
|
def extract_cookies_from_browser(browser_name, profile=None, logger=YDLLogger(), *, keyring=None, container=None):
|
||||||
|
@ -1053,8 +1061,9 @@ class DATA_BLOB(ctypes.Structure):
|
||||||
ctypes.byref(blob_out), # pDataOut
|
ctypes.byref(blob_out), # pDataOut
|
||||||
)
|
)
|
||||||
if not ret:
|
if not ret:
|
||||||
logger.warning('failed to decrypt with DPAPI', only_once=True)
|
message = 'Failed to decrypt with DPAPI. See https://github.com/yt-dlp/yt-dlp/issues/10927 for more info'
|
||||||
return None
|
logger.error(message)
|
||||||
|
raise DownloadError(message) # force exit
|
||||||
|
|
||||||
result = ctypes.string_at(blob_out.pbData, blob_out.cbData)
|
result = ctypes.string_at(blob_out.pbData, blob_out.cbData)
|
||||||
ctypes.windll.kernel32.LocalFree(blob_out.pbData)
|
ctypes.windll.kernel32.LocalFree(blob_out.pbData)
|
||||||
|
|
|
@ -508,7 +508,7 @@ def _call_downloader(self, tmpfilename, info_dict):
|
||||||
env = None
|
env = None
|
||||||
proxy = self.params.get('proxy')
|
proxy = self.params.get('proxy')
|
||||||
if proxy:
|
if proxy:
|
||||||
if not re.match(r'^[\da-zA-Z]+://', proxy):
|
if not re.match(r'[\da-zA-Z]+://', proxy):
|
||||||
proxy = f'http://{proxy}'
|
proxy = f'http://{proxy}'
|
||||||
|
|
||||||
if proxy.startswith('socks'):
|
if proxy.startswith('socks'):
|
||||||
|
@ -559,7 +559,7 @@ def _call_downloader(self, tmpfilename, info_dict):
|
||||||
|
|
||||||
selected_formats = info_dict.get('requested_formats') or [info_dict]
|
selected_formats = info_dict.get('requested_formats') or [info_dict]
|
||||||
for i, fmt in enumerate(selected_formats):
|
for i, fmt in enumerate(selected_formats):
|
||||||
is_http = re.match(r'^https?://', fmt['url'])
|
is_http = re.match(r'https?://', fmt['url'])
|
||||||
cookies = self.ydl.cookiejar.get_cookies_for_url(fmt['url']) if is_http else []
|
cookies = self.ydl.cookiejar.get_cookies_for_url(fmt['url']) if is_http else []
|
||||||
if cookies:
|
if cookies:
|
||||||
args.extend(['-cookies', ''.join(
|
args.extend(['-cookies', ''.join(
|
||||||
|
|
|
@ -363,7 +363,10 @@
|
||||||
)
|
)
|
||||||
from .ccma import CCMAIE
|
from .ccma import CCMAIE
|
||||||
from .cctv import CCTVIE
|
from .cctv import CCTVIE
|
||||||
from .cda import CDAIE
|
from .cda import (
|
||||||
|
CDAIE,
|
||||||
|
CDAFolderIE,
|
||||||
|
)
|
||||||
from .cellebrite import CellebriteIE
|
from .cellebrite import CellebriteIE
|
||||||
from .ceskatelevize import CeskaTelevizeIE
|
from .ceskatelevize import CeskaTelevizeIE
|
||||||
from .cgtn import CGTNIE
|
from .cgtn import CGTNIE
|
||||||
|
@ -730,6 +733,7 @@
|
||||||
GeniusIE,
|
GeniusIE,
|
||||||
GeniusLyricsIE,
|
GeniusLyricsIE,
|
||||||
)
|
)
|
||||||
|
from .germanupa import GermanupaIE
|
||||||
from .getcourseru import (
|
from .getcourseru import (
|
||||||
GetCourseRuIE,
|
GetCourseRuIE,
|
||||||
GetCourseRuPlayerIE,
|
GetCourseRuPlayerIE,
|
||||||
|
@ -1166,6 +1170,7 @@
|
||||||
)
|
)
|
||||||
from .mlssoccer import MLSSoccerIE
|
from .mlssoccer import MLSSoccerIE
|
||||||
from .mocha import MochaVideoIE
|
from .mocha import MochaVideoIE
|
||||||
|
from .mojevideo import MojevideoIE
|
||||||
from .mojvideo import MojvideoIE
|
from .mojvideo import MojvideoIE
|
||||||
from .monstercat import MonstercatIE
|
from .monstercat import MonstercatIE
|
||||||
from .motherless import (
|
from .motherless import (
|
||||||
|
@ -1879,6 +1884,7 @@
|
||||||
from .slideslive import SlidesLiveIE
|
from .slideslive import SlidesLiveIE
|
||||||
from .slutload import SlutloadIE
|
from .slutload import SlutloadIE
|
||||||
from .smotrim import SmotrimIE
|
from .smotrim import SmotrimIE
|
||||||
|
from .snapchat import SnapchatSpotlightIE
|
||||||
from .snotr import SnotrIE
|
from .snotr import SnotrIE
|
||||||
from .sohu import (
|
from .sohu import (
|
||||||
SohuIE,
|
SohuIE,
|
||||||
|
|
|
@ -387,17 +387,27 @@ class ABCIViewShowSeriesIE(InfoExtractor):
|
||||||
'thumbnail': r're:^https?://cdn\.iview\.abc\.net\.au/thumbs/.*\.jpg$',
|
'thumbnail': r're:^https?://cdn\.iview\.abc\.net\.au/thumbs/.*\.jpg$',
|
||||||
},
|
},
|
||||||
'playlist_count': 15,
|
'playlist_count': 15,
|
||||||
|
'skip': 'This program is not currently available in ABC iview',
|
||||||
|
}, {
|
||||||
|
'url': 'https://iview.abc.net.au/show/inbestigators',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '175343-1',
|
||||||
|
'title': 'Series 1',
|
||||||
|
'description': 'md5:b9976935a6450e5b78ce2a940a755685',
|
||||||
|
'series': 'The Inbestigators',
|
||||||
|
'season': 'Series 1',
|
||||||
|
'thumbnail': r're:^https?://cdn\.iview\.abc\.net\.au/thumbs/.+\.jpg',
|
||||||
|
},
|
||||||
|
'playlist_count': 17,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
show_id = self._match_id(url)
|
show_id = self._match_id(url)
|
||||||
webpage = self._download_webpage(url, show_id)
|
webpage = self._download_webpage(url, show_id)
|
||||||
webpage_data = self._search_regex(
|
video_data = self._search_json(
|
||||||
r'window\.__INITIAL_STATE__\s*=\s*[\'"](.+?)[\'"]\s*;',
|
r'window\.__INITIAL_STATE__\s*=\s*[\'"]', webpage, 'initial state', show_id,
|
||||||
webpage, 'initial state')
|
transform_source=lambda x: x.encode().decode('unicode_escape'),
|
||||||
video_data = self._parse_json(
|
end_pattern=r'[\'"]\s*;')['route']['pageData']['_embedded']
|
||||||
unescapeHTML(webpage_data).encode().decode('unicode_escape'), show_id)
|
|
||||||
video_data = video_data['route']['pageData']['_embedded']
|
|
||||||
|
|
||||||
highlight = try_get(video_data, lambda x: x['highlightVideo']['shareUrl'])
|
highlight = try_get(video_data, lambda x: x['highlightVideo']['shareUrl'])
|
||||||
if not self._yes_playlist(show_id, bool(highlight), video_label='highlight video'):
|
if not self._yes_playlist(show_id, bool(highlight), video_label='highlight video'):
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
|
|
||||||
class AcademicEarthCourseIE(InfoExtractor):
|
class AcademicEarthCourseIE(InfoExtractor):
|
||||||
_VALID_URL = r'^https?://(?:www\.)?academicearth\.org/playlists/(?P<id>[^?#/]+)'
|
_VALID_URL = r'https?://(?:www\.)?academicearth\.org/playlists/(?P<id>[^?#/]+)'
|
||||||
IE_NAME = 'AcademicEarth:Course'
|
IE_NAME = 'AcademicEarth:Course'
|
||||||
_TEST = {
|
_TEST = {
|
||||||
'url': 'http://academicearth.org/playlists/laws-of-nature/',
|
'url': 'http://academicearth.org/playlists/laws-of-nature/',
|
||||||
|
|
|
@ -1355,6 +1355,7 @@
|
||||||
class AdobePassIE(InfoExtractor): # XXX: Conventionally, base classes should end with BaseIE/InfoExtractor
|
class AdobePassIE(InfoExtractor): # XXX: Conventionally, base classes should end with BaseIE/InfoExtractor
|
||||||
_SERVICE_PROVIDER_TEMPLATE = 'https://sp.auth.adobe.com/adobe-services/%s'
|
_SERVICE_PROVIDER_TEMPLATE = 'https://sp.auth.adobe.com/adobe-services/%s'
|
||||||
_USER_AGENT = 'Mozilla/5.0 (X11; Linux i686; rv:47.0) Gecko/20100101 Firefox/47.0'
|
_USER_AGENT = 'Mozilla/5.0 (X11; Linux i686; rv:47.0) Gecko/20100101 Firefox/47.0'
|
||||||
|
_MODERN_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; rv:131.0) Gecko/20100101 Firefox/131.0'
|
||||||
_MVPD_CACHE = 'ap-mvpd'
|
_MVPD_CACHE = 'ap-mvpd'
|
||||||
|
|
||||||
_DOWNLOADING_LOGIN_PAGE = 'Downloading Provider Login Page'
|
_DOWNLOADING_LOGIN_PAGE = 'Downloading Provider Login Page'
|
||||||
|
@ -1454,7 +1455,11 @@ def extract_redirect_url(html, url=None, fatal=False):
|
||||||
'no_iframe': 'false',
|
'no_iframe': 'false',
|
||||||
'domain_name': 'adobe.com',
|
'domain_name': 'adobe.com',
|
||||||
'redirect_url': url,
|
'redirect_url': url,
|
||||||
})
|
}, headers={
|
||||||
|
# yt-dlp's default user-agent is usually too old for Comcast_SSO
|
||||||
|
# See: https://github.com/yt-dlp/yt-dlp/issues/10848
|
||||||
|
'User-Agent': self._MODERN_USER_AGENT,
|
||||||
|
} if mso_id == 'Comcast_SSO' else None)
|
||||||
elif not self._cookies_passed:
|
elif not self._cookies_passed:
|
||||||
raise_mvpd_required()
|
raise_mvpd_required()
|
||||||
|
|
||||||
|
|
|
@ -33,21 +33,21 @@ def _perform_login(self, username, password):
|
||||||
}
|
}
|
||||||
|
|
||||||
response = self._download_json(
|
response = self._download_json(
|
||||||
'https://login.afreecatv.com/app/LoginAction.php', None,
|
'https://login.sooplive.co.kr/app/LoginAction.php', None,
|
||||||
'Logging in', data=urlencode_postdata(login_form))
|
'Logging in', data=urlencode_postdata(login_form))
|
||||||
|
|
||||||
_ERRORS = {
|
_ERRORS = {
|
||||||
-4: 'Your account has been suspended due to a violation of our terms and policies.',
|
-4: 'Your account has been suspended due to a violation of our terms and policies.',
|
||||||
-5: 'https://member.afreecatv.com/app/user_delete_progress.php',
|
-5: 'https://member.sooplive.co.kr/app/user_delete_progress.php',
|
||||||
-6: 'https://login.afreecatv.com/membership/changeMember.php',
|
-6: 'https://login.sooplive.co.kr/membership/changeMember.php',
|
||||||
-8: "Hello! AfreecaTV here.\nThe username you have entered belongs to \n an account that requires a legal guardian's consent. \nIf you wish to use our services without restriction, \nplease make sure to go through the necessary verification process.",
|
-8: "Hello! Soop here.\nThe username you have entered belongs to \n an account that requires a legal guardian's consent. \nIf you wish to use our services without restriction, \nplease make sure to go through the necessary verification process.",
|
||||||
-9: 'https://member.afreecatv.com/app/pop_login_block.php',
|
-9: 'https://member.sooplive.co.kr/app/pop_login_block.php',
|
||||||
-11: 'https://login.afreecatv.com/afreeca/second_login.php',
|
-11: 'https://login.sooplive.co.kr/afreeca/second_login.php',
|
||||||
-12: 'https://member.afreecatv.com/app/user_security.php',
|
-12: 'https://member.sooplive.co.kr/app/user_security.php',
|
||||||
0: 'The username does not exist or you have entered the wrong password.',
|
0: 'The username does not exist or you have entered the wrong password.',
|
||||||
-1: 'The username does not exist or you have entered the wrong password.',
|
-1: 'The username does not exist or you have entered the wrong password.',
|
||||||
-3: 'You have entered your username/password incorrectly.',
|
-3: 'You have entered your username/password incorrectly.',
|
||||||
-7: 'You cannot use your Global AfreecaTV account to access Korean AfreecaTV.',
|
-7: 'You cannot use your Global Soop account to access Korean Soop.',
|
||||||
-10: 'Sorry for the inconvenience. \nYour account has been blocked due to an unauthorized access. \nPlease contact our Help Center for assistance.',
|
-10: 'Sorry for the inconvenience. \nYour account has been blocked due to an unauthorized access. \nPlease contact our Help Center for assistance.',
|
||||||
-32008: 'You have failed to log in. Please contact our Help Center.',
|
-32008: 'You have failed to log in. Please contact our Help Center.',
|
||||||
}
|
}
|
||||||
|
@ -61,76 +61,40 @@ def _perform_login(self, username, password):
|
||||||
|
|
||||||
def _call_api(self, endpoint, display_id, data=None, headers=None, query=None):
|
def _call_api(self, endpoint, display_id, data=None, headers=None, query=None):
|
||||||
return self._download_json(Request(
|
return self._download_json(Request(
|
||||||
f'https://api.m.afreecatv.com/{endpoint}',
|
f'https://api.m.sooplive.co.kr/{endpoint}',
|
||||||
data=data, headers=headers, query=query,
|
data=data, headers=headers, query=query,
|
||||||
extensions={'legacy_ssl': True}), display_id,
|
extensions={'legacy_ssl': True}), display_id,
|
||||||
'Downloading API JSON', 'Unable to download API JSON')
|
'Downloading API JSON', 'Unable to download API JSON')
|
||||||
|
|
||||||
|
|
||||||
class AfreecaTVIE(AfreecaTVBaseIE):
|
class AfreecaTVIE(AfreecaTVBaseIE):
|
||||||
IE_NAME = 'afreecatv'
|
IE_NAME = 'soop'
|
||||||
IE_DESC = 'afreecatv.com'
|
IE_DESC = 'sooplive.co.kr'
|
||||||
_VALID_URL = r'''(?x)
|
_VALID_URL = r'https?://vod\.(?:sooplive\.co\.kr|afreecatv\.com)/(?:PLAYER/STATION|player)/(?P<id>\d+)/?(?:$|[?#&])'
|
||||||
https?://
|
|
||||||
(?:
|
|
||||||
(?:(?:live|afbbs|www)\.)?afreeca(?:tv)?\.com(?::\d+)?
|
|
||||||
(?:
|
|
||||||
/app/(?:index|read_ucc_bbs)\.cgi|
|
|
||||||
/player/[Pp]layer\.(?:swf|html)
|
|
||||||
)\?.*?\bnTitleNo=|
|
|
||||||
vod\.afreecatv\.com/(PLAYER/STATION|player)/
|
|
||||||
)
|
|
||||||
(?P<id>\d+)/?(?:$|[?#&])
|
|
||||||
'''
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://live.afreecatv.com:8079/app/index.cgi?szType=read_ucc_bbs&szBjId=dailyapril&nStationNo=16711924&nBbsNo=18605867&nTitleNo=36164052&szSkin=',
|
'url': 'https://vod.sooplive.co.kr/player/96753363',
|
||||||
'md5': 'f72c89fe7ecc14c1b5ce506c4996046e',
|
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '36164052',
|
'id': '20230108_9FF5BEE1_244432674_1',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': '데일리 에이프릴 요정들의 시상식!',
|
'uploader_id': 'rlantnghks',
|
||||||
'thumbnail': 're:^https?://(?:video|st)img.afreecatv.com/.*$',
|
'uploader': '페이즈으',
|
||||||
'uploader': 'dailyapril',
|
'duration': 10840,
|
||||||
'uploader_id': 'dailyapril',
|
'thumbnail': r're:https?://videoimg\.sooplive\.co/.kr/.+',
|
||||||
'upload_date': '20160503',
|
'upload_date': '20230108',
|
||||||
|
'timestamp': 1673218805,
|
||||||
|
'title': '젠지 페이즈',
|
||||||
},
|
},
|
||||||
'skip': 'Video is gone',
|
'params': {
|
||||||
}, {
|
'skip_download': True,
|
||||||
'url': 'http://afbbs.afreecatv.com:8080/app/read_ucc_bbs.cgi?nStationNo=16711924&nTitleNo=36153164&szBjId=dailyapril&nBbsNo=18605867',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '36153164',
|
|
||||||
'title': "BJ유트루와 함께하는 '팅커벨 메이크업!'",
|
|
||||||
'thumbnail': 're:^https?://(?:video|st)img.afreecatv.com/.*$',
|
|
||||||
'uploader': 'dailyapril',
|
|
||||||
'uploader_id': 'dailyapril',
|
|
||||||
},
|
},
|
||||||
'playlist_count': 2,
|
|
||||||
'playlist': [{
|
|
||||||
'md5': 'd8b7c174568da61d774ef0203159bf97',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '36153164_1',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': "BJ유트루와 함께하는 '팅커벨 메이크업!'",
|
|
||||||
'upload_date': '20160502',
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'md5': '58f2ce7f6044e34439ab2d50612ab02b',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '36153164_2',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': "BJ유트루와 함께하는 '팅커벨 메이크업!'",
|
|
||||||
'upload_date': '20160502',
|
|
||||||
},
|
|
||||||
}],
|
|
||||||
'skip': 'Video is gone',
|
|
||||||
}, {
|
}, {
|
||||||
# non standard key
|
# non standard key
|
||||||
'url': 'http://vod.afreecatv.com/PLAYER/STATION/20515605',
|
'url': 'http://vod.sooplive.co.kr/PLAYER/STATION/20515605',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '20170411_BE689A0E_190960999_1_2_h',
|
'id': '20170411_BE689A0E_190960999_1_2_h',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': '혼자사는여자집',
|
'title': '혼자사는여자집',
|
||||||
'thumbnail': 're:^https?://(?:video|st)img.afreecatv.com/.*$',
|
'thumbnail': r're:https?://(?:video|st)img\.sooplive\.co\.kr/.+',
|
||||||
'uploader': '♥이슬이',
|
'uploader': '♥이슬이',
|
||||||
'uploader_id': 'dasl8121',
|
'uploader_id': 'dasl8121',
|
||||||
'upload_date': '20170411',
|
'upload_date': '20170411',
|
||||||
|
@ -142,12 +106,12 @@ class AfreecaTVIE(AfreecaTVBaseIE):
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
# adult content
|
# adult content
|
||||||
'url': 'https://vod.afreecatv.com/player/97267690',
|
'url': 'https://vod.sooplive.co.kr/player/97267690',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '20180327_27901457_202289533_1',
|
'id': '20180327_27901457_202289533_1',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': '[생]빨개요♥ (part 1)',
|
'title': '[생]빨개요♥ (part 1)',
|
||||||
'thumbnail': 're:^https?://(?:video|st)img.afreecatv.com/.*$',
|
'thumbnail': r're:https?://(?:video|st)img\.sooplive\.co\.kr/.+',
|
||||||
'uploader': '[SA]서아',
|
'uploader': '[SA]서아',
|
||||||
'uploader_id': 'bjdyrksu',
|
'uploader_id': 'bjdyrksu',
|
||||||
'upload_date': '20180327',
|
'upload_date': '20180327',
|
||||||
|
@ -157,36 +121,17 @@ class AfreecaTVIE(AfreecaTVBaseIE):
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
'skip': 'The VOD does not exist',
|
'skip': 'The VOD does not exist',
|
||||||
}, {
|
|
||||||
'url': 'http://www.afreecatv.com/player/Player.swf?szType=szBjId=djleegoon&nStationNo=11273158&nBbsNo=13161095&nTitleNo=36327652',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'https://vod.afreecatv.com/player/96753363',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '20230108_9FF5BEE1_244432674_1',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'uploader_id': 'rlantnghks',
|
|
||||||
'uploader': '페이즈으',
|
|
||||||
'duration': 10840,
|
|
||||||
'thumbnail': r're:https?://videoimg\.afreecatv\.com/.+',
|
|
||||||
'upload_date': '20230108',
|
|
||||||
'timestamp': 1673218805,
|
|
||||||
'title': '젠지 페이즈',
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
'skip_download': True,
|
|
||||||
},
|
|
||||||
}, {
|
}, {
|
||||||
# adult content
|
# adult content
|
||||||
'url': 'https://vod.afreecatv.com/player/70395877',
|
'url': 'https://vod.sooplive.co.kr/player/70395877',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}, {
|
}, {
|
||||||
# subscribers only
|
# subscribers only
|
||||||
'url': 'https://vod.afreecatv.com/player/104647403',
|
'url': 'https://vod.sooplive.co.kr/player/104647403',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}, {
|
}, {
|
||||||
# private
|
# private
|
||||||
'url': 'https://vod.afreecatv.com/player/81669846',
|
'url': 'https://vod.sooplive.co.kr/player/81669846',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
|
@ -262,11 +207,11 @@ def _real_extract(self, url):
|
||||||
|
|
||||||
|
|
||||||
class AfreecaTVCatchStoryIE(AfreecaTVBaseIE):
|
class AfreecaTVCatchStoryIE(AfreecaTVBaseIE):
|
||||||
IE_NAME = 'afreecatv:catchstory'
|
IE_NAME = 'soop:catchstory'
|
||||||
IE_DESC = 'afreecatv.com catch story'
|
IE_DESC = 'sooplive.co.kr catch story'
|
||||||
_VALID_URL = r'https?://vod\.afreecatv\.com/player/(?P<id>\d+)/catchstory'
|
_VALID_URL = r'https?://vod\.(?:sooplive\.co\.kr|afreecatv\.com)/player/(?P<id>\d+)/catchstory'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://vod.afreecatv.com/player/103247/catchstory',
|
'url': 'https://vod.sooplive.co.kr/player/103247/catchstory',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '103247',
|
'id': '103247',
|
||||||
},
|
},
|
||||||
|
@ -299,11 +244,11 @@ def _entries(data):
|
||||||
|
|
||||||
|
|
||||||
class AfreecaTVLiveIE(AfreecaTVBaseIE):
|
class AfreecaTVLiveIE(AfreecaTVBaseIE):
|
||||||
IE_NAME = 'afreecatv:live'
|
IE_NAME = 'soop:live'
|
||||||
IE_DESC = 'afreecatv.com livestreams'
|
IE_DESC = 'sooplive.co.kr livestreams'
|
||||||
_VALID_URL = r'https?://play\.afreeca(?:tv)?\.com/(?P<id>[^/]+)(?:/(?P<bno>\d+))?'
|
_VALID_URL = r'https?://play\.(?:sooplive\.co\.kr|afreecatv\.com)/(?P<id>[^/?#]+)(?:/(?P<bno>\d+))?'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://play.afreecatv.com/pyh3646/237852185',
|
'url': 'https://play.sooplive.co.kr/pyh3646/237852185',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '237852185',
|
'id': '237852185',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
|
@ -315,30 +260,30 @@ class AfreecaTVLiveIE(AfreecaTVBaseIE):
|
||||||
},
|
},
|
||||||
'skip': 'Livestream has ended',
|
'skip': 'Livestream has ended',
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://play.afreecatv.com/pyh3646/237852185',
|
'url': 'https://play.sooplive.co.kr/pyh3646/237852185',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://play.afreecatv.com/pyh3646',
|
'url': 'https://play.sooplive.co.kr/pyh3646',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
_LIVE_API_URL = 'https://live.afreecatv.com/afreeca/player_live_api.php'
|
_LIVE_API_URL = 'https://live.sooplive.co.kr/afreeca/player_live_api.php'
|
||||||
_WORKING_CDNS = [
|
_WORKING_CDNS = [
|
||||||
'gcp_cdn', # live-global-cdn-v02.afreecatv.com
|
'gcp_cdn', # live-global-cdn-v02.sooplive.co.kr
|
||||||
'gs_cdn_pc_app', # pc-app.stream.afreecatv.com
|
'gs_cdn_pc_app', # pc-app.stream.sooplive.co.kr
|
||||||
'gs_cdn_mobile_web', # mobile-web.stream.afreecatv.com
|
'gs_cdn_mobile_web', # mobile-web.stream.sooplive.co.kr
|
||||||
'gs_cdn_pc_web', # pc-web.stream.afreecatv.com
|
'gs_cdn_pc_web', # pc-web.stream.sooplive.co.kr
|
||||||
]
|
]
|
||||||
_BAD_CDNS = [
|
_BAD_CDNS = [
|
||||||
'gs_cdn', # chromecast.afreeca.gscdn.com (cannot resolve)
|
'gs_cdn', # chromecast.afreeca.gscdn.com (cannot resolve)
|
||||||
'gs_cdn_chromecast', # chromecast.stream.afreecatv.com (HTTP Error 400)
|
'gs_cdn_chromecast', # chromecast.stream.sooplive.co.kr (HTTP Error 400)
|
||||||
'azure_cdn', # live-global-cdn-v01.afreecatv.com (cannot resolve)
|
'azure_cdn', # live-global-cdn-v01.sooplive.co.kr (cannot resolve)
|
||||||
'aws_cf', # live-global-cdn-v03.afreecatv.com (cannot resolve)
|
'aws_cf', # live-global-cdn-v03.sooplive.co.kr (cannot resolve)
|
||||||
'kt_cdn', # kt.stream.afreecatv.com (HTTP Error 400)
|
'kt_cdn', # kt.stream.sooplive.co.kr (HTTP Error 400)
|
||||||
]
|
]
|
||||||
|
|
||||||
def _extract_formats(self, channel_info, broadcast_no, aid):
|
def _extract_formats(self, channel_info, broadcast_no, aid):
|
||||||
stream_base_url = channel_info.get('RMD') or 'https://livestream-manager.afreecatv.com'
|
stream_base_url = channel_info.get('RMD') or 'https://livestream-manager.sooplive.co.kr'
|
||||||
|
|
||||||
# If user has not passed CDN IDs, try API-provided CDN ID followed by other working CDN IDs
|
# If user has not passed CDN IDs, try API-provided CDN ID followed by other working CDN IDs
|
||||||
default_cdn_ids = orderedSet([
|
default_cdn_ids = orderedSet([
|
||||||
|
@ -358,7 +303,7 @@ def _extract_formats(self, channel_info, broadcast_no, aid):
|
||||||
try:
|
try:
|
||||||
return self._extract_m3u8_formats(
|
return self._extract_m3u8_formats(
|
||||||
m3u8_url, broadcast_no, 'mp4', m3u8_id='hls', query={'aid': aid},
|
m3u8_url, broadcast_no, 'mp4', m3u8_id='hls', query={'aid': aid},
|
||||||
headers={'Referer': 'https://play.afreecatv.com/'})
|
headers={'Referer': 'https://play.sooplive.co.kr/'})
|
||||||
except ExtractorError as e:
|
except ExtractorError as e:
|
||||||
if attempt == len(cdn_ids):
|
if attempt == len(cdn_ids):
|
||||||
raise
|
raise
|
||||||
|
@ -374,7 +319,13 @@ def _real_extract(self, url):
|
||||||
broadcaster_id = channel_info.get('BJID') or broadcaster_id
|
broadcaster_id = channel_info.get('BJID') or broadcaster_id
|
||||||
broadcast_no = channel_info.get('BNO') or broadcast_no
|
broadcast_no = channel_info.get('BNO') or broadcast_no
|
||||||
if not broadcast_no:
|
if not broadcast_no:
|
||||||
|
result = channel_info.get('RESULT')
|
||||||
|
if result == 0:
|
||||||
raise UserNotLive(video_id=broadcaster_id)
|
raise UserNotLive(video_id=broadcaster_id)
|
||||||
|
elif result == -6:
|
||||||
|
self.raise_login_required(
|
||||||
|
'This channel is streaming for subscribers only', method='password')
|
||||||
|
raise ExtractorError('Unable to extract broadcast number')
|
||||||
|
|
||||||
password = self.get_param('videopassword')
|
password = self.get_param('videopassword')
|
||||||
if channel_info.get('BPWD') == 'Y' and password is None:
|
if channel_info.get('BPWD') == 'Y' and password is None:
|
||||||
|
@ -403,7 +354,7 @@ def _real_extract(self, url):
|
||||||
formats = self._extract_formats(channel_info, broadcast_no, aid)
|
formats = self._extract_formats(channel_info, broadcast_no, aid)
|
||||||
|
|
||||||
station_info = traverse_obj(self._download_json(
|
station_info = traverse_obj(self._download_json(
|
||||||
'https://st.afreecatv.com/api/get_station_status.php', broadcast_no,
|
'https://st.sooplive.co.kr/api/get_station_status.php', broadcast_no,
|
||||||
'Downloading channel metadata', 'Unable to download channel metadata',
|
'Downloading channel metadata', 'Unable to download channel metadata',
|
||||||
query={'szBjId': broadcaster_id}, fatal=False), {dict}) or {}
|
query={'szBjId': broadcaster_id}, fatal=False), {dict}) or {}
|
||||||
|
|
||||||
|
@ -419,11 +370,11 @@ def _real_extract(self, url):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class AfreecaTVUserIE(InfoExtractor):
|
class AfreecaTVUserIE(AfreecaTVBaseIE):
|
||||||
IE_NAME = 'afreecatv:user'
|
IE_NAME = 'soop:user'
|
||||||
_VALID_URL = r'https?://bj\.afreeca(?:tv)?\.com/(?P<id>[^/]+)/vods/?(?P<slug_type>[^/]+)?'
|
_VALID_URL = r'https?://ch\.(?:sooplive\.co\.kr|afreecatv\.com)/(?P<id>[^/?#]+)/vods/?(?P<slug_type>[^/?#]+)?'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://bj.afreecatv.com/ryuryu24/vods/review',
|
'url': 'https://ch.sooplive.co.kr/ryuryu24/vods/review',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'_type': 'playlist',
|
'_type': 'playlist',
|
||||||
'id': 'ryuryu24',
|
'id': 'ryuryu24',
|
||||||
|
@ -431,7 +382,7 @@ class AfreecaTVUserIE(InfoExtractor):
|
||||||
},
|
},
|
||||||
'playlist_count': 218,
|
'playlist_count': 218,
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://bj.afreecatv.com/parang1995/vods/highlight',
|
'url': 'https://ch.sooplive.co.kr/parang1995/vods/highlight',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'_type': 'playlist',
|
'_type': 'playlist',
|
||||||
'id': 'parang1995',
|
'id': 'parang1995',
|
||||||
|
@ -439,7 +390,7 @@ class AfreecaTVUserIE(InfoExtractor):
|
||||||
},
|
},
|
||||||
'playlist_count': 997,
|
'playlist_count': 997,
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://bj.afreecatv.com/ryuryu24/vods',
|
'url': 'https://ch.sooplive.co.kr/ryuryu24/vods',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'_type': 'playlist',
|
'_type': 'playlist',
|
||||||
'id': 'ryuryu24',
|
'id': 'ryuryu24',
|
||||||
|
@ -447,7 +398,7 @@ class AfreecaTVUserIE(InfoExtractor):
|
||||||
},
|
},
|
||||||
'playlist_count': 221,
|
'playlist_count': 221,
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://bj.afreecatv.com/ryuryu24/vods/balloonclip',
|
'url': 'https://ch.sooplive.co.kr/ryuryu24/vods/balloonclip',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'_type': 'playlist',
|
'_type': 'playlist',
|
||||||
'id': 'ryuryu24',
|
'id': 'ryuryu24',
|
||||||
|
@ -459,12 +410,12 @@ class AfreecaTVUserIE(InfoExtractor):
|
||||||
|
|
||||||
def _fetch_page(self, user_id, user_type, page):
|
def _fetch_page(self, user_id, user_type, page):
|
||||||
page += 1
|
page += 1
|
||||||
info = self._download_json(f'https://bjapi.afreecatv.com/api/{user_id}/vods/{user_type}', user_id,
|
info = self._download_json(f'https://chapi.sooplive.co.kr/api/{user_id}/vods/{user_type}', user_id,
|
||||||
query={'page': page, 'per_page': self._PER_PAGE, 'orderby': 'reg_date'},
|
query={'page': page, 'per_page': self._PER_PAGE, 'orderby': 'reg_date'},
|
||||||
note=f'Downloading {user_type} video page {page}')
|
note=f'Downloading {user_type} video page {page}')
|
||||||
for item in info['data']:
|
for item in info['data']:
|
||||||
yield self.url_result(
|
yield self.url_result(
|
||||||
f'https://vod.afreecatv.com/player/{item["title_no"]}/', AfreecaTVIE, item['title_no'])
|
f'https://vod.sooplive.co.kr/player/{item["title_no"]}/', AfreecaTVIE, item['title_no'])
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
user_id, user_type = self._match_valid_url(url).group('id', 'slug_type')
|
user_id, user_type = self._match_valid_url(url).group('id', 'slug_type')
|
||||||
|
|
|
@ -1,27 +1,42 @@
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
clean_html,
|
|
||||||
clean_podcast_url,
|
clean_podcast_url,
|
||||||
get_element_by_class,
|
|
||||||
int_or_none,
|
int_or_none,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
try_get,
|
|
||||||
)
|
)
|
||||||
|
from ..utils.traversal import traverse_obj
|
||||||
|
|
||||||
|
|
||||||
class ApplePodcastsIE(InfoExtractor):
|
class ApplePodcastsIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://podcasts\.apple\.com/(?:[^/]+/)?podcast(?:/[^/]+){1,2}.*?\bi=(?P<id>\d+)'
|
_VALID_URL = r'https?://podcasts\.apple\.com/(?:[^/]+/)?podcast(?:/[^/]+){1,2}.*?\bi=(?P<id>\d+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
|
'url': 'https://podcasts.apple.com/us/podcast/ferreck-dawn-to-the-break-of-dawn-117/id1625658232?i=1000665010654',
|
||||||
|
'md5': '82cc219b8cc1dcf8bfc5a5e99b23b172',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '1000665010654',
|
||||||
|
'ext': 'mp3',
|
||||||
|
'title': 'Ferreck Dawn - To The Break of Dawn 117',
|
||||||
|
'episode': 'Ferreck Dawn - To The Break of Dawn 117',
|
||||||
|
'description': 'md5:1fc571102f79dbd0a77bfd71ffda23bc',
|
||||||
|
'upload_date': '20240812',
|
||||||
|
'timestamp': 1723449600,
|
||||||
|
'duration': 3596,
|
||||||
|
'series': 'Ferreck Dawn - To The Break of Dawn',
|
||||||
|
'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',
|
||||||
'md5': '41dc31cd650143e530d9423b6b5a344f',
|
'md5': 'baf8a6b8b8aa6062dbb4639ed73d0052',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '1000482637777',
|
'id': '1000482637777',
|
||||||
'ext': 'mp3',
|
'ext': 'mp3',
|
||||||
'title': '207 - Whitney Webb Returns',
|
'title': '207 - Whitney Webb Returns',
|
||||||
|
'episode': '207 - Whitney Webb Returns',
|
||||||
|
'episode_number': 207,
|
||||||
'description': 'md5:75ef4316031df7b41ced4e7b987f79c6',
|
'description': 'md5:75ef4316031df7b41ced4e7b987f79c6',
|
||||||
'upload_date': '20200705',
|
'upload_date': '20200705',
|
||||||
'timestamp': 1593932400,
|
'timestamp': 1593932400,
|
||||||
'duration': 6454,
|
'duration': 5369,
|
||||||
'series': 'The Tim Dillon Show',
|
'series': 'The Tim Dillon Show',
|
||||||
'thumbnail': 're:.+[.](png|jpe?g|webp)',
|
'thumbnail': 're:.+[.](png|jpe?g|webp)',
|
||||||
},
|
},
|
||||||
|
@ -39,47 +54,24 @@ class ApplePodcastsIE(InfoExtractor):
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
episode_id = self._match_id(url)
|
episode_id = self._match_id(url)
|
||||||
webpage = self._download_webpage(url, episode_id)
|
webpage = self._download_webpage(url, episode_id)
|
||||||
episode_data = {}
|
server_data = self._search_json(
|
||||||
ember_data = {}
|
r'<script [^>]*\bid=["\']serialized-server-data["\'][^>]*>', webpage,
|
||||||
# new page type 2021-11
|
'server data', episode_id, contains_pattern=r'\[{(?s:.+)}\]')[0]['data']
|
||||||
amp_data = self._parse_json(self._search_regex(
|
model_data = traverse_obj(server_data, (
|
||||||
r'(?s)id="shoebox-media-api-cache-amp-podcasts"[^>]*>\s*({.+?})\s*<',
|
'headerButtonItems', lambda _, v: v['$kind'] == 'bookmark' and v['modelType'] == 'EpisodeOffer',
|
||||||
webpage, 'AMP data', default='{}'), episode_id, fatal=False) or {}
|
'model', {dict}, any))
|
||||||
amp_data = try_get(amp_data,
|
|
||||||
lambda a: self._parse_json(
|
|
||||||
next(a[x] for x in iter(a) if episode_id in x),
|
|
||||||
episode_id),
|
|
||||||
dict) or {}
|
|
||||||
amp_data = amp_data.get('d') or []
|
|
||||||
episode_data = try_get(
|
|
||||||
amp_data,
|
|
||||||
lambda a: next(x for x in a
|
|
||||||
if x['type'] == 'podcast-episodes' and x['id'] == episode_id),
|
|
||||||
dict)
|
|
||||||
if not episode_data:
|
|
||||||
# try pre 2021-11 page type: TODO: consider deleting if no longer used
|
|
||||||
ember_data = self._parse_json(self._search_regex(
|
|
||||||
r'(?s)id="shoebox-ember-data-store"[^>]*>\s*({.+?})\s*<',
|
|
||||||
webpage, 'ember data'), episode_id) or {}
|
|
||||||
ember_data = ember_data.get(episode_id) or ember_data
|
|
||||||
episode_data = try_get(ember_data, lambda x: x['data'], dict)
|
|
||||||
episode = episode_data['attributes']
|
|
||||||
description = episode.get('description') or {}
|
|
||||||
|
|
||||||
series = None
|
|
||||||
for inc in (amp_data or ember_data.get('included') or []):
|
|
||||||
if inc.get('type') == 'media/podcast':
|
|
||||||
series = try_get(inc, lambda x: x['attributes']['name'])
|
|
||||||
series = series or clean_html(get_element_by_class('podcast-header__identity', webpage))
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': episode_id,
|
'id': episode_id,
|
||||||
'title': episode.get('name'),
|
**self._json_ld(
|
||||||
'url': clean_podcast_url(episode['assetUrl']),
|
traverse_obj(server_data, ('seoData', 'schemaContent', {dict}))
|
||||||
'description': description.get('standard') or description.get('short'),
|
or self._yield_json_ld(webpage, episode_id, fatal=False), episode_id, fatal=False),
|
||||||
'timestamp': parse_iso8601(episode.get('releaseDateTime')),
|
**traverse_obj(model_data, {
|
||||||
'duration': int_or_none(episode.get('durationInMilliseconds'), 1000),
|
'title': ('title', {str}),
|
||||||
'series': series,
|
'url': ('streamUrl', {clean_podcast_url}),
|
||||||
|
'timestamp': ('releaseDate', {parse_iso8601}),
|
||||||
|
'duration': ('duration', {int_or_none}),
|
||||||
|
}),
|
||||||
'thumbnail': self._og_search_thumbnail(webpage),
|
'thumbnail': self._og_search_thumbnail(webpage),
|
||||||
'vcodec': 'none',
|
'vcodec': 'none',
|
||||||
}
|
}
|
||||||
|
|
|
@ -231,7 +231,7 @@ def _real_extract(self, url):
|
||||||
|
|
||||||
class ARDBetaMediathekIE(InfoExtractor):
|
class ARDBetaMediathekIE(InfoExtractor):
|
||||||
IE_NAME = 'ARDMediathek'
|
IE_NAME = 'ARDMediathek'
|
||||||
_VALID_URL = r'''(?x)https://
|
_VALID_URL = r'''(?x)https?://
|
||||||
(?:(?:beta|www)\.)?ardmediathek\.de/
|
(?:(?:beta|www)\.)?ardmediathek\.de/
|
||||||
(?:[^/]+/)?
|
(?:[^/]+/)?
|
||||||
(?:player|live|video)/
|
(?:player|live|video)/
|
||||||
|
@ -470,7 +470,7 @@ def _real_extract(self, url):
|
||||||
|
|
||||||
|
|
||||||
class ARDMediathekCollectionIE(InfoExtractor):
|
class ARDMediathekCollectionIE(InfoExtractor):
|
||||||
_VALID_URL = r'''(?x)https://
|
_VALID_URL = r'''(?x)https?://
|
||||||
(?:(?:beta|www)\.)?ardmediathek\.de/
|
(?:(?:beta|www)\.)?ardmediathek\.de/
|
||||||
(?:[^/?#]+/)?
|
(?:[^/?#]+/)?
|
||||||
(?P<playlist>sendung|serie|sammlung)/
|
(?P<playlist>sendung|serie|sammlung)/
|
||||||
|
|
|
@ -46,6 +46,7 @@
|
||||||
|
|
||||||
|
|
||||||
class BilibiliBaseIE(InfoExtractor):
|
class BilibiliBaseIE(InfoExtractor):
|
||||||
|
_HEADERS = {'Referer': 'https://www.bilibili.com/'}
|
||||||
_FORMAT_ID_RE = re.compile(r'-(\d+)\.m4s\?')
|
_FORMAT_ID_RE = re.compile(r'-(\d+)\.m4s\?')
|
||||||
_WBI_KEY_CACHE_TIMEOUT = 30 # exact expire timeout is unclear, use 30s for one session
|
_WBI_KEY_CACHE_TIMEOUT = 30 # exact expire timeout is unclear, use 30s for one session
|
||||||
_wbi_key_cache = {}
|
_wbi_key_cache = {}
|
||||||
|
@ -192,7 +193,7 @@ def _get_subtitles(self, video_id, cid, aid=None):
|
||||||
video_info = self._download_json(
|
video_info = self._download_json(
|
||||||
'https://api.bilibili.com/x/player/v2', video_id,
|
'https://api.bilibili.com/x/player/v2', video_id,
|
||||||
query={'aid': aid, 'cid': cid} if aid else {'bvid': video_id, 'cid': cid},
|
query={'aid': aid, 'cid': cid} if aid else {'bvid': video_id, 'cid': cid},
|
||||||
note=f'Extracting subtitle info {cid}')
|
note=f'Extracting subtitle info {cid}', headers=self._HEADERS)
|
||||||
if traverse_obj(video_info, ('data', 'need_login_subtitle')):
|
if traverse_obj(video_info, ('data', 'need_login_subtitle')):
|
||||||
self.report_warning(
|
self.report_warning(
|
||||||
f'Subtitles are only available when logged in. {self._login_hint()}', only_once=True)
|
f'Subtitles are only available when logged in. {self._login_hint()}', only_once=True)
|
||||||
|
@ -207,7 +208,7 @@ def _get_subtitles(self, video_id, cid, aid=None):
|
||||||
def _get_chapters(self, aid, cid):
|
def _get_chapters(self, aid, cid):
|
||||||
chapters = aid and cid and self._download_json(
|
chapters = aid and cid and self._download_json(
|
||||||
'https://api.bilibili.com/x/player/v2', aid, query={'aid': aid, 'cid': cid},
|
'https://api.bilibili.com/x/player/v2', aid, query={'aid': aid, 'cid': cid},
|
||||||
note='Extracting chapters', fatal=False)
|
note='Extracting chapters', fatal=False, headers=self._HEADERS)
|
||||||
return traverse_obj(chapters, ('data', 'view_points', ..., {
|
return traverse_obj(chapters, ('data', 'view_points', ..., {
|
||||||
'title': 'content',
|
'title': 'content',
|
||||||
'start_time': 'from',
|
'start_time': 'from',
|
||||||
|
@ -1021,8 +1022,6 @@ def _real_extract(self, url):
|
||||||
|
|
||||||
|
|
||||||
class BilibiliCheeseBaseIE(BilibiliBaseIE):
|
class BilibiliCheeseBaseIE(BilibiliBaseIE):
|
||||||
_HEADERS = {'Referer': 'https://www.bilibili.com/'}
|
|
||||||
|
|
||||||
def _extract_episode(self, season_info, ep_id):
|
def _extract_episode(self, season_info, ep_id):
|
||||||
episode_info = traverse_obj(season_info, (
|
episode_info = traverse_obj(season_info, (
|
||||||
'episodes', lambda _, v: v['id'] == int(ep_id)), get_all=False)
|
'episodes', lambda _, v: v['id'] == int(ep_id)), get_all=False)
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
|
|
||||||
class CallinIE(InfoExtractor):
|
class CallinIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?callin\.com/(episode)/(?P<id>[-a-zA-Z]+)'
|
_VALID_URL = r'https?://(?:www\.)?callin\.com/episode/(?P<id>[-a-zA-Z]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.callin.com/episode/the-title-ix-regime-and-the-long-march-through-EBfXYSrsjc',
|
'url': 'https://www.callin.com/episode/the-title-ix-regime-and-the-long-march-through-EBfXYSrsjc',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import xml.etree.ElementTree
|
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..networking import HEADRequest
|
from ..networking import HEADRequest
|
||||||
|
@ -12,7 +11,6 @@
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
float_or_none,
|
float_or_none,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
join_nonempty,
|
|
||||||
js_to_json,
|
js_to_json,
|
||||||
mimetype2ext,
|
mimetype2ext,
|
||||||
orderedSet,
|
orderedSet,
|
||||||
|
@ -524,14 +522,13 @@ class CBCGemIE(InfoExtractor):
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
# This is a normal, public, TV show video
|
# This is a normal, public, TV show video
|
||||||
'url': 'https://gem.cbc.ca/media/schitts-creek/s06e01',
|
'url': 'https://gem.cbc.ca/media/schitts-creek/s06e01',
|
||||||
'md5': '93dbb31c74a8e45b378cf13bd3f6f11e',
|
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'schitts-creek/s06e01',
|
'id': 'schitts-creek/s06e01',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Smoke Signals',
|
'title': 'Smoke Signals',
|
||||||
'description': 'md5:929868d20021c924020641769eb3e7f1',
|
'description': 'md5:929868d20021c924020641769eb3e7f1',
|
||||||
'thumbnail': 'https://images.radio-canada.ca/v1/synps-cbc/episode/perso/cbc_schitts_creek_season_06e01_thumbnail_v01.jpg?im=Resize=(Size)',
|
'thumbnail': r're:https://images\.radio-canada\.ca/[^#?]+/cbc_schitts_creek_season_06e01_thumbnail_v01\.jpg',
|
||||||
'duration': 1314,
|
'duration': 1324,
|
||||||
'categories': ['comedy'],
|
'categories': ['comedy'],
|
||||||
'series': 'Schitt\'s Creek',
|
'series': 'Schitt\'s Creek',
|
||||||
'season': 'Season 6',
|
'season': 'Season 6',
|
||||||
|
@ -539,19 +536,21 @@ class CBCGemIE(InfoExtractor):
|
||||||
'episode': 'Smoke Signals',
|
'episode': 'Smoke Signals',
|
||||||
'episode_number': 1,
|
'episode_number': 1,
|
||||||
'episode_id': 'schitts-creek/s06e01',
|
'episode_id': 'schitts-creek/s06e01',
|
||||||
|
'upload_date': '20210618',
|
||||||
|
'timestamp': 1623988800,
|
||||||
|
'release_date': '20200107',
|
||||||
|
'release_timestamp': 1578427200,
|
||||||
},
|
},
|
||||||
'params': {'format': 'bv'},
|
'params': {'format': 'bv'},
|
||||||
'skip': 'Geo-restricted to Canada',
|
|
||||||
}, {
|
}, {
|
||||||
# This video requires an account in the browser, but works fine in yt-dlp
|
# This video requires an account in the browser, but works fine in yt-dlp
|
||||||
'url': 'https://gem.cbc.ca/media/schitts-creek/s01e01',
|
'url': 'https://gem.cbc.ca/media/schitts-creek/s01e01',
|
||||||
'md5': '297a9600f554f2258aed01514226a697',
|
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'schitts-creek/s01e01',
|
'id': 'schitts-creek/s01e01',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'The Cup Runneth Over',
|
'title': 'The Cup Runneth Over',
|
||||||
'description': 'md5:9bca14ea49ab808097530eb05a29e797',
|
'description': 'md5:9bca14ea49ab808097530eb05a29e797',
|
||||||
'thumbnail': 'https://images.radio-canada.ca/v1/synps-cbc/episode/perso/cbc_schitts_creek_season_01e01_thumbnail_v01.jpg?im=Resize=(Size)',
|
'thumbnail': r're:https://images\.radio-canada\.ca/[^#?]+/cbc_schitts_creek_season_01e01_thumbnail_v01\.jpg',
|
||||||
'series': 'Schitt\'s Creek',
|
'series': 'Schitt\'s Creek',
|
||||||
'season_number': 1,
|
'season_number': 1,
|
||||||
'season': 'Season 1',
|
'season': 'Season 1',
|
||||||
|
@ -560,9 +559,12 @@ class CBCGemIE(InfoExtractor):
|
||||||
'episode_id': 'schitts-creek/s01e01',
|
'episode_id': 'schitts-creek/s01e01',
|
||||||
'duration': 1309,
|
'duration': 1309,
|
||||||
'categories': ['comedy'],
|
'categories': ['comedy'],
|
||||||
|
'upload_date': '20210617',
|
||||||
|
'timestamp': 1623902400,
|
||||||
|
'release_date': '20151124',
|
||||||
|
'release_timestamp': 1448323200,
|
||||||
},
|
},
|
||||||
'params': {'format': 'bv'},
|
'params': {'format': 'bv'},
|
||||||
'skip': 'Geo-restricted to Canada',
|
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://gem.cbc.ca/nadiyas-family-favourites/s01e01',
|
'url': 'https://gem.cbc.ca/nadiyas-family-favourites/s01e01',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
@ -631,38 +633,6 @@ def _real_initialize(self):
|
||||||
return
|
return
|
||||||
self._claims_token = self.cache.load(self._NETRC_MACHINE, 'claims_token')
|
self._claims_token = self.cache.load(self._NETRC_MACHINE, 'claims_token')
|
||||||
|
|
||||||
def _find_secret_formats(self, formats, video_id):
|
|
||||||
""" Find a valid video url and convert it to the secret variant """
|
|
||||||
base_format = next((f for f in formats if f.get('vcodec') != 'none'), None)
|
|
||||||
if not base_format:
|
|
||||||
return
|
|
||||||
|
|
||||||
base_url = re.sub(r'(Manifest\(.*?),filter=[\w-]+(.*?\))', r'\1\2', base_format['url'])
|
|
||||||
url = re.sub(r'(Manifest\(.*?),format=[\w-]+(.*?\))', r'\1\2', base_url)
|
|
||||||
|
|
||||||
secret_xml = self._download_xml(url, video_id, note='Downloading secret XML', fatal=False)
|
|
||||||
if not isinstance(secret_xml, xml.etree.ElementTree.Element):
|
|
||||||
return
|
|
||||||
|
|
||||||
for child in secret_xml:
|
|
||||||
if child.attrib.get('Type') != 'video':
|
|
||||||
continue
|
|
||||||
for video_quality in child:
|
|
||||||
bitrate = int_or_none(video_quality.attrib.get('Bitrate'))
|
|
||||||
if not bitrate or 'Index' not in video_quality.attrib:
|
|
||||||
continue
|
|
||||||
height = int_or_none(video_quality.attrib.get('MaxHeight'))
|
|
||||||
|
|
||||||
yield {
|
|
||||||
**base_format,
|
|
||||||
'format_id': join_nonempty('sec', height),
|
|
||||||
# Note: \g<1> is necessary instead of \1 since bitrate is a number
|
|
||||||
'url': re.sub(r'(QualityLevels\()\d+(\))', fr'\g<1>{bitrate}\2', base_url),
|
|
||||||
'width': int_or_none(video_quality.attrib.get('MaxWidth')),
|
|
||||||
'tbr': bitrate / 1000.0,
|
|
||||||
'height': height,
|
|
||||||
}
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
video_info = self._download_json(
|
video_info = self._download_json(
|
||||||
|
@ -676,7 +646,6 @@ def _real_extract(self, url):
|
||||||
else:
|
else:
|
||||||
headers = {}
|
headers = {}
|
||||||
m3u8_info = self._download_json(video_info['playSession']['url'], video_id, headers=headers)
|
m3u8_info = self._download_json(video_info['playSession']['url'], video_id, headers=headers)
|
||||||
m3u8_url = m3u8_info.get('url')
|
|
||||||
|
|
||||||
if m3u8_info.get('errorCode') == 1:
|
if m3u8_info.get('errorCode') == 1:
|
||||||
self.raise_geo_restricted(countries=['CA'])
|
self.raise_geo_restricted(countries=['CA'])
|
||||||
|
@ -685,9 +654,9 @@ def _real_extract(self, url):
|
||||||
elif m3u8_info.get('errorCode') != 0:
|
elif m3u8_info.get('errorCode') != 0:
|
||||||
raise ExtractorError(f'{self.IE_NAME} said: {m3u8_info.get("errorCode")} - {m3u8_info.get("message")}')
|
raise ExtractorError(f'{self.IE_NAME} said: {m3u8_info.get("errorCode")} - {m3u8_info.get("message")}')
|
||||||
|
|
||||||
formats = self._extract_m3u8_formats(m3u8_url, video_id, m3u8_id='hls')
|
formats = self._extract_m3u8_formats(
|
||||||
|
m3u8_info['url'], video_id, 'mp4', m3u8_id='hls', query={'manifestType': ''})
|
||||||
self._remove_duplicate_formats(formats)
|
self._remove_duplicate_formats(formats)
|
||||||
formats.extend(self._find_secret_formats(formats, video_id))
|
|
||||||
|
|
||||||
for fmt in formats:
|
for fmt in formats:
|
||||||
if fmt.get('vcodec') == 'none':
|
if fmt.get('vcodec') == 'none':
|
||||||
|
@ -703,20 +672,21 @@ def _real_extract(self, url):
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'title': video_info['title'],
|
|
||||||
'description': video_info.get('description'),
|
|
||||||
'thumbnail': video_info.get('image'),
|
|
||||||
'series': video_info.get('series'),
|
|
||||||
'season_number': video_info.get('season'),
|
|
||||||
'season': f'Season {video_info.get("season")}',
|
|
||||||
'episode_number': video_info.get('episode'),
|
|
||||||
'episode': video_info.get('title'),
|
|
||||||
'episode_id': video_id,
|
'episode_id': video_id,
|
||||||
'duration': video_info.get('duration'),
|
|
||||||
'categories': [video_info.get('category')],
|
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
'release_timestamp': video_info.get('airDate'),
|
**traverse_obj(video_info, {
|
||||||
'timestamp': video_info.get('availableDate'),
|
'title': ('title', {str}),
|
||||||
|
'episode': ('title', {str}),
|
||||||
|
'description': ('description', {str}),
|
||||||
|
'thumbnail': ('image', {url_or_none}),
|
||||||
|
'series': ('series', {str}),
|
||||||
|
'season_number': ('season', {int_or_none}),
|
||||||
|
'episode_number': ('episode', {int_or_none}),
|
||||||
|
'duration': ('duration', {int_or_none}),
|
||||||
|
'categories': ('category', {str}, all),
|
||||||
|
'release_timestamp': ('airDate', {int_or_none(scale=1000)}),
|
||||||
|
'timestamp': ('availableDate', {int_or_none(scale=1000)}),
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
from ..compat import compat_ord
|
from ..compat import compat_ord
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
|
OnDemandPagedList,
|
||||||
float_or_none,
|
float_or_none,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
merge_dicts,
|
merge_dicts,
|
||||||
|
@ -351,3 +352,50 @@ def extract_format(page, version):
|
||||||
extract_format(webpage, resolution)
|
extract_format(webpage, resolution)
|
||||||
|
|
||||||
return merge_dicts(info_dict, info)
|
return merge_dicts(info_dict, info)
|
||||||
|
|
||||||
|
|
||||||
|
class CDAFolderIE(InfoExtractor):
|
||||||
|
_MAX_PAGE_SIZE = 36
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?cda\.pl/(?P<channel>\w+)/folder/(?P<id>\d+)'
|
||||||
|
_TESTS = [
|
||||||
|
{
|
||||||
|
'url': 'https://www.cda.pl/domino264/folder/31188385',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '31188385',
|
||||||
|
'title': 'SERIA DRUGA',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 13,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url': 'https://www.cda.pl/smiechawaTV/folder/2664592/vfilm',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '2664592',
|
||||||
|
'title': 'VideoDowcipy - wszystkie odcinki',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 71,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url': 'https://www.cda.pl/DeliciousBeauty/folder/19129979/vfilm',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '19129979',
|
||||||
|
'title': 'TESTY KOSMETYKÓW',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 139,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
folder_id, channel = self._match_valid_url(url).group('id', 'channel')
|
||||||
|
|
||||||
|
webpage = self._download_webpage(url, folder_id)
|
||||||
|
|
||||||
|
def extract_page_entries(page):
|
||||||
|
webpage = self._download_webpage(
|
||||||
|
f'https://www.cda.pl/{channel}/folder/{folder_id}/vfilm/{page + 1}', folder_id,
|
||||||
|
f'Downloading page {page + 1}', expected_status=404)
|
||||||
|
items = re.findall(r'<a[^>]+href="/video/([0-9a-z]+)"', webpage)
|
||||||
|
for video_id in items:
|
||||||
|
yield self.url_result(f'https://www.cda.pl/video/{video_id}', CDAIE, video_id)
|
||||||
|
|
||||||
|
return self.playlist_result(
|
||||||
|
OnDemandPagedList(extract_page_entries, self._MAX_PAGE_SIZE),
|
||||||
|
folder_id, self._og_search_title(webpage))
|
||||||
|
|
|
@ -333,7 +333,7 @@ class InfoExtractor:
|
||||||
like_count: Number of positive ratings of the video
|
like_count: Number of positive ratings of the video
|
||||||
dislike_count: Number of negative ratings of the video
|
dislike_count: Number of negative ratings of the video
|
||||||
repost_count: Number of reposts of the video
|
repost_count: Number of reposts of the video
|
||||||
average_rating: Average rating give by users, the scale used depends on the webpage
|
average_rating: Average rating given by users, the scale used depends on the webpage
|
||||||
comment_count: Number of comments on the video
|
comment_count: Number of comments on the video
|
||||||
comments: A list of comments, each with one or more of the following
|
comments: A list of comments, each with one or more of the following
|
||||||
properties (all but one of text or html optional):
|
properties (all but one of text or html optional):
|
||||||
|
@ -520,7 +520,7 @@ class InfoExtractor:
|
||||||
or _extract_from_webpage as necessary. While these are normally classmethods,
|
or _extract_from_webpage as necessary. While these are normally classmethods,
|
||||||
_extract_from_webpage is allowed to be an instance method.
|
_extract_from_webpage is allowed to be an instance method.
|
||||||
|
|
||||||
_extract_from_webpage may raise self.StopExtraction() to stop further
|
_extract_from_webpage may raise self.StopExtraction to stop further
|
||||||
processing of the webpage and obtain exclusive rights to it. This is useful
|
processing of the webpage and obtain exclusive rights to it. This is useful
|
||||||
when the extractor cannot reliably be matched using just the URL,
|
when the extractor cannot reliably be matched using just the URL,
|
||||||
e.g. invidious/peertube instances
|
e.g. invidious/peertube instances
|
||||||
|
@ -573,13 +573,13 @@ class InfoExtractor:
|
||||||
|
|
||||||
def _login_hint(self, method=NO_DEFAULT, netrc=None):
|
def _login_hint(self, method=NO_DEFAULT, netrc=None):
|
||||||
password_hint = f'--username and --password, --netrc-cmd, or --netrc ({netrc or self._NETRC_MACHINE}) to provide account credentials'
|
password_hint = f'--username and --password, --netrc-cmd, or --netrc ({netrc or self._NETRC_MACHINE}) to provide account credentials'
|
||||||
|
cookies_hint = 'See https://github.com/yt-dlp/yt-dlp/wiki/FAQ#how-do-i-pass-cookies-to-yt-dlp for how to manually pass cookies'
|
||||||
return {
|
return {
|
||||||
None: '',
|
None: '',
|
||||||
'any': f'Use --cookies, --cookies-from-browser, {password_hint}',
|
'any': f'Use --cookies, --cookies-from-browser, {password_hint}. {cookies_hint}',
|
||||||
'password': f'Use {password_hint}',
|
'password': f'Use {password_hint}',
|
||||||
'cookies': (
|
'cookies': f'Use --cookies-from-browser or --cookies for the authentication. {cookies_hint}',
|
||||||
'Use --cookies-from-browser or --cookies for the authentication. '
|
'session_cookies': f'Use --cookies for the authentication (--cookies-from-browser might not work). {cookies_hint}',
|
||||||
'See https://github.com/yt-dlp/yt-dlp/wiki/FAQ#how-do-i-pass-cookies-to-yt-dlp for how to manually pass cookies'),
|
|
||||||
}[method if method is not NO_DEFAULT else 'any' if self.supports_login() else 'cookies']
|
}[method if method is not NO_DEFAULT else 'any' if self.supports_login() else 'cookies']
|
||||||
|
|
||||||
def __init__(self, downloader=None):
|
def __init__(self, downloader=None):
|
||||||
|
@ -1710,7 +1710,7 @@ def traverse_json_ld(json_ld, at_top_level=True):
|
||||||
rating = traverse_obj(e, ('aggregateRating', 'ratingValue'), expected_type=float_or_none)
|
rating = traverse_obj(e, ('aggregateRating', 'ratingValue'), expected_type=float_or_none)
|
||||||
if rating is not None:
|
if rating is not None:
|
||||||
info['average_rating'] = rating
|
info['average_rating'] = rating
|
||||||
if is_type(e, 'TVEpisode', 'Episode'):
|
if is_type(e, 'TVEpisode', 'Episode', 'PodcastEpisode'):
|
||||||
episode_name = unescapeHTML(e.get('name'))
|
episode_name = unescapeHTML(e.get('name'))
|
||||||
info.update({
|
info.update({
|
||||||
'episode': episode_name,
|
'episode': episode_name,
|
||||||
|
@ -2077,7 +2077,7 @@ def _parse_m3u8_formats_and_subtitles(
|
||||||
has_drm = HlsFD._has_drm(m3u8_doc)
|
has_drm = HlsFD._has_drm(m3u8_doc)
|
||||||
|
|
||||||
def format_url(url):
|
def format_url(url):
|
||||||
return url if re.match(r'^https?://', url) else urllib.parse.urljoin(m3u8_url, url)
|
return url if re.match(r'https?://', url) else urllib.parse.urljoin(m3u8_url, url)
|
||||||
|
|
||||||
if self.get_param('hls_split_discontinuity', False):
|
if self.get_param('hls_split_discontinuity', False):
|
||||||
def _extract_m3u8_playlist_indices(manifest_url=None, m3u8_doc=None):
|
def _extract_m3u8_playlist_indices(manifest_url=None, m3u8_doc=None):
|
||||||
|
@ -2812,11 +2812,11 @@ def extract_Initialization(source):
|
||||||
base_url_e = element.find(_add_ns('BaseURL'))
|
base_url_e = element.find(_add_ns('BaseURL'))
|
||||||
if try_call(lambda: base_url_e.text) is not None:
|
if try_call(lambda: base_url_e.text) is not None:
|
||||||
base_url = base_url_e.text + base_url
|
base_url = base_url_e.text + base_url
|
||||||
if re.match(r'^https?://', base_url):
|
if re.match(r'https?://', base_url):
|
||||||
break
|
break
|
||||||
if mpd_base_url and base_url.startswith('/'):
|
if mpd_base_url and base_url.startswith('/'):
|
||||||
base_url = urllib.parse.urljoin(mpd_base_url, base_url)
|
base_url = urllib.parse.urljoin(mpd_base_url, base_url)
|
||||||
elif mpd_base_url and not re.match(r'^https?://', base_url):
|
elif mpd_base_url and not re.match(r'https?://', base_url):
|
||||||
if not mpd_base_url.endswith('/'):
|
if not mpd_base_url.endswith('/'):
|
||||||
mpd_base_url += '/'
|
mpd_base_url += '/'
|
||||||
base_url = mpd_base_url + base_url
|
base_url = mpd_base_url + base_url
|
||||||
|
@ -2906,7 +2906,7 @@ def prepare_template(template_name, identifiers):
|
||||||
}
|
}
|
||||||
|
|
||||||
def location_key(location):
|
def location_key(location):
|
||||||
return 'url' if re.match(r'^https?://', location) else 'path'
|
return 'url' if re.match(r'https?://', location) else 'path'
|
||||||
|
|
||||||
if 'segment_urls' not in representation_ms_info and 'media' in representation_ms_info:
|
if 'segment_urls' not in representation_ms_info and 'media' in representation_ms_info:
|
||||||
|
|
||||||
|
|
|
@ -6,12 +6,37 @@
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
smuggle_url,
|
smuggle_url,
|
||||||
str_or_none,
|
str_or_none,
|
||||||
|
update_url_query,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CWTVIE(InfoExtractor):
|
class CWTVIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?cw(?:tv(?:pr)?|seed)\.com/(?:shows/)?(?:[^/]+/)+[^?]*\?.*\b(?:play|watch)=(?P<id>[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12})'
|
_VALID_URL = r'https?://(?:www\.)?cw(?:tv(?:pr)?|seed)\.com/(?:shows/)?(?:[^/]+/)+[^?]*\?.*\b(?:play|watch)=(?P<id>[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12})'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
|
'url': 'https://www.cwtv.com/shows/all-american-homecoming/ready-or-not/?play=d848488f-f62a-40fd-af1f-6440b1821aab',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'd848488f-f62a-40fd-af1f-6440b1821aab',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Ready Or Not',
|
||||||
|
'description': 'Simone is concerned about changes taking place at Bringston; JR makes a decision about his future.',
|
||||||
|
'thumbnail': r're:^https?://.*\.jpe?g$',
|
||||||
|
'duration': 2547,
|
||||||
|
'timestamp': 1720519200,
|
||||||
|
'uploader': 'CWTV',
|
||||||
|
'chapters': 'count:6',
|
||||||
|
'series': 'All American: Homecoming',
|
||||||
|
'season_number': 3,
|
||||||
|
'episode_number': 1,
|
||||||
|
'age_limit': 0,
|
||||||
|
'upload_date': '20240709',
|
||||||
|
'season': 'Season 3',
|
||||||
|
'episode': 'Episode 1',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
# m3u8 download
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
'url': 'http://cwtv.com/shows/arrow/legends-of-yesterday/?play=6b15e985-9345-4f60-baf8-56e96be57c63',
|
'url': 'http://cwtv.com/shows/arrow/legends-of-yesterday/?play=6b15e985-9345-4f60-baf8-56e96be57c63',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '6b15e985-9345-4f60-baf8-56e96be57c63',
|
'id': '6b15e985-9345-4f60-baf8-56e96be57c63',
|
||||||
|
@ -69,13 +94,14 @@ class CWTVIE(InfoExtractor):
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
data = self._download_json(
|
data = self._download_json(
|
||||||
'http://images.cwtv.com/feed/mobileapp/video-meta/apiversion_8/guid_' + video_id,
|
f'https://images.cwtv.com/feed/mobileapp/video-meta/apiversion_12/guid_{video_id}', video_id)
|
||||||
video_id)
|
|
||||||
if data.get('result') != 'ok':
|
if data.get('result') != 'ok':
|
||||||
raise ExtractorError(data['msg'], expected=True)
|
raise ExtractorError(data['msg'], expected=True)
|
||||||
video_data = data['video']
|
video_data = data['video']
|
||||||
title = video_data['title']
|
title = video_data['title']
|
||||||
mpx_url = video_data.get('mpx_url') or f'http://link.theplatform.com/s/cwtv/media/guid/2703454149/{video_id}?formats=M3U'
|
mpx_url = update_url_query(
|
||||||
|
video_data.get('mpx_url') or f'https://link.theplatform.com/s/cwtv/media/guid/2703454149/{video_id}',
|
||||||
|
{'formats': 'M3U+none'})
|
||||||
|
|
||||||
season = str_or_none(video_data.get('season'))
|
season = str_or_none(video_data.get('season'))
|
||||||
episode = str_or_none(video_data.get('episode'))
|
episode = str_or_none(video_data.get('episode'))
|
||||||
|
|
|
@ -6,8 +6,10 @@
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
|
update_url,
|
||||||
update_url_query,
|
update_url_query,
|
||||||
url_basename,
|
url_basename,
|
||||||
|
urlencode_postdata,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -36,43 +38,58 @@ class DropboxIE(InfoExtractor):
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def _yield_decoded_parts(self, webpage):
|
||||||
|
for encoded in reversed(re.findall(r'registerStreamedPrefetch\s*\(\s*"[\w/+=]+"\s*,\s*"([\w/+=]+)"', webpage)):
|
||||||
|
yield base64.b64decode(encoded).decode('utf-8', 'ignore')
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = self._match_valid_url(url)
|
mobj = self._match_valid_url(url)
|
||||||
video_id = mobj.group('id')
|
video_id = mobj.group('id')
|
||||||
webpage = self._download_webpage(url, video_id)
|
webpage = self._download_webpage(url, video_id)
|
||||||
fn = urllib.parse.unquote(url_basename(url))
|
fn = urllib.parse.unquote(url_basename(url))
|
||||||
title = os.path.splitext(fn)[0]
|
title = os.path.splitext(fn)[0]
|
||||||
|
|
||||||
password = self.get_param('videopassword')
|
password = self.get_param('videopassword')
|
||||||
if (self._og_search_title(webpage) == 'Dropbox - Password Required'
|
|
||||||
or 'Enter the password for this link' in webpage):
|
|
||||||
|
|
||||||
|
for part in self._yield_decoded_parts(webpage):
|
||||||
|
if '/sm/password' in part:
|
||||||
|
webpage = self._download_webpage(
|
||||||
|
update_url('https://www.dropbox.com/sm/password', query=part.partition('?')[2]), video_id)
|
||||||
|
break
|
||||||
|
|
||||||
|
if (self._og_search_title(webpage, default=None) == 'Dropbox - Password Required'
|
||||||
|
or 'Enter the password for this link' in webpage):
|
||||||
if password:
|
if password:
|
||||||
content_id = self._search_regex(r'content_id=(.*?)["\']', webpage, 'content_id')
|
|
||||||
payload = f'is_xhr=true&t={self._get_cookies("https://www.dropbox.com").get("t").value}&content_id={content_id}&password={password}&url={url}'
|
|
||||||
response = self._download_json(
|
response = self._download_json(
|
||||||
'https://www.dropbox.com/sm/auth', video_id, 'POSTing video password', data=payload.encode(),
|
'https://www.dropbox.com/sm/auth', video_id, 'POSTing video password',
|
||||||
headers={'content-type': 'application/x-www-form-urlencoded; charset=UTF-8'})
|
headers={'content-type': 'application/x-www-form-urlencoded; charset=UTF-8'},
|
||||||
|
data=urlencode_postdata({
|
||||||
|
'is_xhr': 'true',
|
||||||
|
't': self._get_cookies('https://www.dropbox.com')['t'].value,
|
||||||
|
'content_id': self._search_regex(r'content_id=([\w.+=/-]+)["\']', webpage, 'content id'),
|
||||||
|
'password': password,
|
||||||
|
'url': url,
|
||||||
|
}))
|
||||||
|
|
||||||
if response.get('status') != 'authed':
|
if response.get('status') != 'authed':
|
||||||
raise ExtractorError('Authentication failed!', expected=True)
|
raise ExtractorError('Invalid password', expected=True)
|
||||||
webpage = self._download_webpage(url, video_id)
|
elif not self._get_cookies('https://dropbox.com').get('sm_auth'):
|
||||||
elif self._get_cookies('https://dropbox.com').get('sm_auth'):
|
|
||||||
webpage = self._download_webpage(url, video_id)
|
|
||||||
else:
|
|
||||||
raise ExtractorError('Password protected video, use --video-password <password>', expected=True)
|
raise ExtractorError('Password protected video, use --video-password <password>', expected=True)
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
formats, subtitles, has_anonymous_download = [], {}, False
|
formats, subtitles = [], {}
|
||||||
for encoded in reversed(re.findall(r'registerStreamedPrefetch\s*\(\s*"[\w/+=]+"\s*,\s*"([\w/+=]+)"', webpage)):
|
has_anonymous_download = False
|
||||||
decoded = base64.b64decode(encoded).decode('utf-8', 'ignore')
|
thumbnail = None
|
||||||
|
for part in self._yield_decoded_parts(webpage):
|
||||||
if not has_anonymous_download:
|
if not has_anonymous_download:
|
||||||
has_anonymous_download = self._search_regex(
|
has_anonymous_download = self._search_regex(
|
||||||
r'(anonymous:\tanonymous)', decoded, 'anonymous', default=False)
|
r'(anonymous:\tanonymous)', part, 'anonymous', default=False)
|
||||||
transcode_url = self._search_regex(
|
transcode_url = self._search_regex(
|
||||||
r'\n.(https://[^\x03\x08\x12\n]+\.m3u8)', decoded, 'transcode url', default=None)
|
r'\n.(https://[^\x03\x08\x12\n]+\.m3u8)', part, 'transcode url', default=None)
|
||||||
if not transcode_url:
|
if not transcode_url:
|
||||||
continue
|
continue
|
||||||
formats, subtitles = self._extract_m3u8_formats_and_subtitles(transcode_url, video_id, 'mp4')
|
formats, subtitles = self._extract_m3u8_formats_and_subtitles(transcode_url, video_id, 'mp4')
|
||||||
|
thumbnail = self._search_regex(
|
||||||
|
r'(https://www\.dropbox\.com/temp_thumb_from_token/[\w/?&=]+)', part, 'thumbnail', default=None)
|
||||||
break
|
break
|
||||||
|
|
||||||
# downloads enabled we can get the original file
|
# downloads enabled we can get the original file
|
||||||
|
@ -89,4 +106,5 @@ def _real_extract(self, url):
|
||||||
'title': title,
|
'title': title,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
'subtitles': subtitles,
|
'subtitles': subtitles,
|
||||||
|
'thumbnail': thumbnail,
|
||||||
}
|
}
|
||||||
|
|
|
@ -139,12 +139,11 @@ def _real_initialize(self):
|
||||||
return
|
return
|
||||||
|
|
||||||
token_response = self._download_json(
|
token_response = self._download_json(
|
||||||
'https://production.dr-massive.com/api/authorization/anonymous-sso', None,
|
'https://isl.dr-massive.com/api/authorization/anonymous-sso', None,
|
||||||
note='Downloading anonymous token', headers={
|
note='Downloading anonymous token', headers={
|
||||||
'content-type': 'application/json',
|
'content-type': 'application/json',
|
||||||
}, query={
|
}, query={
|
||||||
'device': 'web_browser',
|
'device': 'phone_android',
|
||||||
'ff': 'idp,ldp,rpt',
|
|
||||||
'lang': 'da',
|
'lang': 'da',
|
||||||
'supportFallbackToken': 'true',
|
'supportFallbackToken': 'true',
|
||||||
}, data=json.dumps({
|
}, data=json.dumps({
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
url_or_none,
|
url_or_none,
|
||||||
variadic,
|
variadic,
|
||||||
)
|
)
|
||||||
|
from ..utils.traversal import traverse_obj
|
||||||
|
|
||||||
|
|
||||||
class ERTFlixBaseIE(InfoExtractor):
|
class ERTFlixBaseIE(InfoExtractor):
|
||||||
|
@ -74,18 +75,17 @@ class ERTFlixCodenameIE(ERTFlixBaseIE):
|
||||||
|
|
||||||
def _extract_formats_and_subs(self, video_id):
|
def _extract_formats_and_subs(self, video_id):
|
||||||
media_info = self._call_api(video_id, codename=video_id)
|
media_info = self._call_api(video_id, codename=video_id)
|
||||||
formats, subs = [], {}
|
formats, subtitles = [], {}
|
||||||
for media_file in try_get(media_info, lambda x: x['MediaFiles'], list) or []:
|
for media in traverse_obj(media_info, (
|
||||||
for media in try_get(media_file, lambda x: x['Formats'], list) or []:
|
'MediaFiles', lambda _, v: v['RoleCodename'] == 'main',
|
||||||
fmt_url = url_or_none(try_get(media, lambda x: x['Url']))
|
'Formats', lambda _, v: url_or_none(v['Url']))):
|
||||||
if not fmt_url:
|
fmt_url = media['Url']
|
||||||
continue
|
|
||||||
ext = determine_ext(fmt_url)
|
ext = determine_ext(fmt_url)
|
||||||
if ext == 'm3u8':
|
if ext == 'm3u8':
|
||||||
formats_, subs_ = self._extract_m3u8_formats_and_subtitles(
|
fmts, subs = self._extract_m3u8_formats_and_subtitles(
|
||||||
fmt_url, video_id, m3u8_id='hls', ext='mp4', fatal=False)
|
fmt_url, video_id, m3u8_id='hls', ext='mp4', fatal=False)
|
||||||
elif ext == 'mpd':
|
elif ext == 'mpd':
|
||||||
formats_, subs_ = self._extract_mpd_formats_and_subtitles(
|
fmts, subs = self._extract_mpd_formats_and_subtitles(
|
||||||
fmt_url, video_id, mpd_id='dash', fatal=False)
|
fmt_url, video_id, mpd_id='dash', fatal=False)
|
||||||
else:
|
else:
|
||||||
formats.append({
|
formats.append({
|
||||||
|
@ -93,10 +93,10 @@ def _extract_formats_and_subs(self, video_id):
|
||||||
'format_id': str_or_none(media.get('Id')),
|
'format_id': str_or_none(media.get('Id')),
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
formats.extend(formats_)
|
formats.extend(fmts)
|
||||||
self._merge_subtitles(subs_, target=subs)
|
self._merge_subtitles(subs, target=subtitles)
|
||||||
|
|
||||||
return formats, subs
|
return formats, subtitles
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
|
|
|
@ -564,11 +564,12 @@ def extract_from_jsmods_instances(js_data):
|
||||||
js_data, lambda x: x['jsmods']['instances'], list) or [])
|
js_data, lambda x: x['jsmods']['instances'], list) or [])
|
||||||
|
|
||||||
def extract_dash_manifest(video, formats):
|
def extract_dash_manifest(video, formats):
|
||||||
dash_manifest = traverse_obj(video, 'dash_manifest', 'playlist', expected_type=str)
|
dash_manifest = traverse_obj(
|
||||||
|
video, 'dash_manifest', 'playlist', 'dash_manifest_xml_string', expected_type=str)
|
||||||
if dash_manifest:
|
if dash_manifest:
|
||||||
formats.extend(self._parse_mpd_formats(
|
formats.extend(self._parse_mpd_formats(
|
||||||
compat_etree_fromstring(urllib.parse.unquote_plus(dash_manifest)),
|
compat_etree_fromstring(urllib.parse.unquote_plus(dash_manifest)),
|
||||||
mpd_url=video.get('dash_manifest_url')))
|
mpd_url=url_or_none(video.get('dash_manifest_url'))))
|
||||||
|
|
||||||
def process_formats(info):
|
def process_formats(info):
|
||||||
# Downloads with browser's User-Agent are rate limited. Working around
|
# Downloads with browser's User-Agent are rate limited. Working around
|
||||||
|
@ -618,12 +619,13 @@ def parse_graphql_video(video):
|
||||||
video = video['creation_story']
|
video = video['creation_story']
|
||||||
video['owner'] = traverse_obj(video, ('short_form_video_context', 'video_owner'))
|
video['owner'] = traverse_obj(video, ('short_form_video_context', 'video_owner'))
|
||||||
video.update(reel_info)
|
video.update(reel_info)
|
||||||
|
fmt_data = traverse_obj(video, ('videoDeliveryLegacyFields', {dict})) or video
|
||||||
formats = []
|
formats = []
|
||||||
q = qualities(['sd', 'hd'])
|
q = qualities(['sd', 'hd'])
|
||||||
for key, format_id in (('playable_url', 'sd'), ('playable_url_quality_hd', 'hd'),
|
for key, format_id in (('playable_url', 'sd'), ('playable_url_quality_hd', 'hd'),
|
||||||
('playable_url_dash', ''), ('browser_native_hd_url', 'hd'),
|
('playable_url_dash', ''), ('browser_native_hd_url', 'hd'),
|
||||||
('browser_native_sd_url', 'sd')):
|
('browser_native_sd_url', 'sd')):
|
||||||
playable_url = video.get(key)
|
playable_url = fmt_data.get(key)
|
||||||
if not playable_url:
|
if not playable_url:
|
||||||
continue
|
continue
|
||||||
if determine_ext(playable_url) == 'mpd':
|
if determine_ext(playable_url) == 'mpd':
|
||||||
|
@ -635,7 +637,7 @@ def parse_graphql_video(video):
|
||||||
'quality': q(format_id) - 3,
|
'quality': q(format_id) - 3,
|
||||||
'url': playable_url,
|
'url': playable_url,
|
||||||
})
|
})
|
||||||
extract_dash_manifest(video, formats)
|
extract_dash_manifest(fmt_data, formats)
|
||||||
if not formats:
|
if not formats:
|
||||||
# Do not append false positive entry w/o any formats
|
# Do not append false positive entry w/o any formats
|
||||||
return
|
return
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
|
|
||||||
class FC2IE(InfoExtractor):
|
class FC2IE(InfoExtractor):
|
||||||
_VALID_URL = r'^(?:https?://video\.fc2\.com/(?:[^/]+/)*content/|fc2:)(?P<id>[^/]+)'
|
_VALID_URL = r'(?:https?://video\.fc2\.com/(?:[^/]+/)*content/|fc2:)(?P<id>[^/]+)'
|
||||||
IE_NAME = 'fc2'
|
IE_NAME = 'fc2'
|
||||||
_NETRC_MACHINE = 'fc2'
|
_NETRC_MACHINE = 'fc2'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
|
|
||||||
class FunkIE(InfoExtractor):
|
class FunkIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.|origin\.)?funk\.net/(?:channel|playlist)/[^/]+/(?P<display_id>[0-9a-z-]+)-(?P<id>\d+)'
|
_VALID_URL = r'https?://(?:(?:www|origin|play)\.)?funk\.net/(?:channel|playlist)/[^/?#]+/(?P<display_id>[0-9a-z-]+)-(?P<id>\d+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.funk.net/channel/ba-793/die-lustigsten-instrumente-aus-dem-internet-teil-2-1155821',
|
'url': 'https://www.funk.net/channel/ba-793/die-lustigsten-instrumente-aus-dem-internet-teil-2-1155821',
|
||||||
'md5': '8610449476156f338761a75391b0017d',
|
'md5': '8610449476156f338761a75391b0017d',
|
||||||
|
@ -27,6 +27,9 @@ class FunkIE(InfoExtractor):
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.funk.net/playlist/neuesteVideos/kameras-auf-dem-fusion-festival-1618699',
|
'url': 'https://www.funk.net/playlist/neuesteVideos/kameras-auf-dem-fusion-festival-1618699',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://play.funk.net/playlist/neuesteVideos/george-floyd-wenn-die-polizei-toetet-der-fall-2004391',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
|
|
|
@ -8,6 +8,9 @@
|
||||||
from .commonprotocols import RtmpIE
|
from .commonprotocols import RtmpIE
|
||||||
from .youtube import YoutubeIE
|
from .youtube import YoutubeIE
|
||||||
from ..compat import compat_etree_fromstring
|
from ..compat import compat_etree_fromstring
|
||||||
|
from ..cookies import LenientSimpleCookie
|
||||||
|
from ..networking.exceptions import HTTPError
|
||||||
|
from ..networking.impersonate import ImpersonateTarget
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
KNOWN_EXTENSIONS,
|
KNOWN_EXTENSIONS,
|
||||||
MEDIA_EXTENSIONS,
|
MEDIA_EXTENSIONS,
|
||||||
|
@ -2340,7 +2343,7 @@ def _real_extract(self, url):
|
||||||
default_search = 'fixup_error'
|
default_search = 'fixup_error'
|
||||||
|
|
||||||
if default_search in ('auto', 'auto_warning', 'fixup_error'):
|
if default_search in ('auto', 'auto_warning', 'fixup_error'):
|
||||||
if re.match(r'^[^\s/]+\.[^\s/]+/', url):
|
if re.match(r'[^\s/]+\.[^\s/]+/', url):
|
||||||
self.report_warning('The url doesn\'t specify the protocol, trying with http')
|
self.report_warning('The url doesn\'t specify the protocol, trying with http')
|
||||||
return self.url_result('http://' + url)
|
return self.url_result('http://' + url)
|
||||||
elif default_search != 'fixup_error':
|
elif default_search != 'fixup_error':
|
||||||
|
@ -2373,6 +2376,11 @@ def _real_extract(self, url):
|
||||||
else:
|
else:
|
||||||
video_id = self._generic_id(url)
|
video_id = self._generic_id(url)
|
||||||
|
|
||||||
|
# Do not impersonate by default; see https://github.com/yt-dlp/yt-dlp/issues/11335
|
||||||
|
impersonate = self._configuration_arg('impersonate', ['false'])
|
||||||
|
if 'false' in impersonate:
|
||||||
|
impersonate = None
|
||||||
|
|
||||||
# Some webservers may serve compressed content of rather big size (e.g. gzipped flac)
|
# Some webservers may serve compressed content of rather big size (e.g. gzipped flac)
|
||||||
# making it impossible to download only chunk of the file (yet we need only 512kB to
|
# making it impossible to download only chunk of the file (yet we need only 512kB to
|
||||||
# test whether it's HTML or not). According to yt-dlp default Accept-Encoding
|
# test whether it's HTML or not). According to yt-dlp default Accept-Encoding
|
||||||
|
@ -2381,10 +2389,29 @@ def _real_extract(self, url):
|
||||||
# to accept raw bytes and being able to download only a chunk.
|
# to accept raw bytes and being able to download only a chunk.
|
||||||
# It may probably better to solve this by checking Content-Type for application/octet-stream
|
# It may probably better to solve this by checking Content-Type for application/octet-stream
|
||||||
# after a HEAD request, but not sure if we can rely on this.
|
# after a HEAD request, but not sure if we can rely on this.
|
||||||
|
try:
|
||||||
full_response = self._request_webpage(url, video_id, headers=filter_dict({
|
full_response = self._request_webpage(url, video_id, headers=filter_dict({
|
||||||
'Accept-Encoding': 'identity',
|
'Accept-Encoding': 'identity',
|
||||||
'Referer': smuggled_data.get('referer'),
|
'Referer': smuggled_data.get('referer'),
|
||||||
}))
|
}), impersonate=impersonate)
|
||||||
|
except ExtractorError as e:
|
||||||
|
if not (isinstance(e.cause, HTTPError) and e.cause.status == 403
|
||||||
|
and e.cause.response.get_header('cf-mitigated') == 'challenge'
|
||||||
|
and e.cause.response.extensions.get('impersonate') is None):
|
||||||
|
raise
|
||||||
|
cf_cookie_domain = traverse_obj(
|
||||||
|
LenientSimpleCookie(e.cause.response.get_header('set-cookie')),
|
||||||
|
('__cf_bm', 'domain'))
|
||||||
|
if cf_cookie_domain:
|
||||||
|
self.write_debug(f'Clearing __cf_bm cookie for {cf_cookie_domain}')
|
||||||
|
self.cookiejar.clear(domain=cf_cookie_domain, path='/', name='__cf_bm')
|
||||||
|
msg = 'Got HTTP Error 403 caused by Cloudflare anti-bot challenge; '
|
||||||
|
if not self._downloader._impersonate_target_available(ImpersonateTarget()):
|
||||||
|
msg += ('see https://github.com/yt-dlp/yt-dlp#impersonation for '
|
||||||
|
'how to install the required impersonation dependency, and ')
|
||||||
|
raise ExtractorError(
|
||||||
|
f'{msg}try again with --extractor-args "generic:impersonate"', expected=True)
|
||||||
|
|
||||||
new_url = full_response.url
|
new_url = full_response.url
|
||||||
if new_url != extract_basic_auth(url)[0]:
|
if new_url != extract_basic_auth(url)[0]:
|
||||||
self.report_following_redirect(new_url)
|
self.report_following_redirect(new_url)
|
||||||
|
@ -2400,7 +2427,7 @@ def _real_extract(self, url):
|
||||||
|
|
||||||
# Check for direct link to a video
|
# Check for direct link to a video
|
||||||
content_type = full_response.headers.get('Content-Type', '').lower()
|
content_type = full_response.headers.get('Content-Type', '').lower()
|
||||||
m = re.match(r'^(?P<type>audio|video|application(?=/(?:ogg$|(?:vnd\.apple\.|x-)?mpegurl)))/(?P<format_id>[^;\s]+)', content_type)
|
m = re.match(r'(?P<type>audio|video|application(?=/(?:ogg$|(?:vnd\.apple\.|x-)?mpegurl)))/(?P<format_id>[^;\s]+)', content_type)
|
||||||
if m:
|
if m:
|
||||||
self.report_detected('direct video link')
|
self.report_detected('direct video link')
|
||||||
headers = filter_dict({'Referer': smuggled_data.get('referer')})
|
headers = filter_dict({'Referer': smuggled_data.get('referer')})
|
||||||
|
|
91
yt_dlp/extractor/germanupa.py
Normal file
91
yt_dlp/extractor/germanupa.py
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from .vimeo import VimeoIE
|
||||||
|
from ..utils import (
|
||||||
|
parse_qs,
|
||||||
|
traverse_obj,
|
||||||
|
url_or_none,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GermanupaIE(InfoExtractor):
|
||||||
|
IE_DESC = 'germanupa.de'
|
||||||
|
_VALID_URL = r'https?://germanupa\.de/mediathek/(?P<id>[\w-]+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://germanupa.de/mediathek/4-figma-beratung-deine-sprechstunde-fuer-figma-fragen',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '909179246',
|
||||||
|
'title': 'Tutorial: #4 Figma Beratung - Deine Sprechstunde für Figma-Fragen',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'uploader': 'German UPA',
|
||||||
|
'uploader_id': 'germanupa',
|
||||||
|
'thumbnail': 'https://i.vimeocdn.com/video/1792564420-7415283ccef8bf8702dab8c6b7515555ceeb7a1c11371ffcc133b8e887dbf70e-d_1280',
|
||||||
|
'uploader_url': 'https://vimeo.com/germanupa',
|
||||||
|
'duration': 3987,
|
||||||
|
},
|
||||||
|
'expected_warnings': ['Failed to parse XML: not well-formed'],
|
||||||
|
'params': {'skip_download': 'm3u8'},
|
||||||
|
}, {
|
||||||
|
'note': 'audio, uses GenericIE',
|
||||||
|
'url': 'https://germanupa.de/mediathek/live-vom-ux-festival-neuigkeiten-von-figma-jobmarkt-agenturszene-interview-zu-sustainable',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '1867346676',
|
||||||
|
'title': 'Live vom UX Festival: Neuigkeiten von Figma, Jobmarkt, Agenturszene & Interview zu Sustainable UX',
|
||||||
|
'ext': 'opus',
|
||||||
|
'timestamp': 1720545088,
|
||||||
|
'upload_date': '20240709',
|
||||||
|
'duration': 3910.557,
|
||||||
|
'like_count': int,
|
||||||
|
'description': 'md5:db2aed5ff131e177a7b33901e9a8db05',
|
||||||
|
'uploader': 'German UPA',
|
||||||
|
'repost_count': int,
|
||||||
|
'genres': ['Science'],
|
||||||
|
'license': 'all-rights-reserved',
|
||||||
|
'uploader_url': 'https://soundcloud.com/user-80097677',
|
||||||
|
'uploader_id': '471579486',
|
||||||
|
'view_count': int,
|
||||||
|
'comment_count': int,
|
||||||
|
'thumbnail': 'https://i1.sndcdn.com/artworks-oCti2e9GhaZFWBqY-48ybGw-original.jpg',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'note': 'Nur für Mitglieder/Just for members',
|
||||||
|
'url': 'https://germanupa.de/mediathek/ux-festival-2024-usability-tests-und-ai',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '986994430',
|
||||||
|
'title': 'UX Festival 2024 "Usability Tests und AI" von Lennart Weber',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'release_date': '20240719',
|
||||||
|
'uploader_url': 'https://vimeo.com/germanupa',
|
||||||
|
'timestamp': 1721373980,
|
||||||
|
'license': 'by-sa',
|
||||||
|
'like_count': int,
|
||||||
|
'thumbnail': 'https://i.vimeocdn.com/video/1904187064-2a672630c30f9ad787bd390bff3f51d7506a3e8416763ba6dbf465732b165c5c-d_1280',
|
||||||
|
'duration': 2146,
|
||||||
|
'release_timestamp': 1721373980,
|
||||||
|
'uploader': 'German UPA',
|
||||||
|
'uploader_id': 'germanupa',
|
||||||
|
'upload_date': '20240719',
|
||||||
|
'comment_count': int,
|
||||||
|
},
|
||||||
|
'expected_warnings': ['Failed to parse XML: not well-formed'],
|
||||||
|
'skip': 'login required',
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
|
param_url = traverse_obj(
|
||||||
|
self._search_regex(
|
||||||
|
r'<iframe[^>]+data-src\s*?=\s*?([\'"])(?P<url>https://germanupa\.de/media/oembed\?url=(?:(?!\1).)+)\1',
|
||||||
|
webpage, 'embedded video', default=None, group='url'),
|
||||||
|
({parse_qs}, 'url', 0, {url_or_none}))
|
||||||
|
|
||||||
|
if not param_url:
|
||||||
|
if self._search_regex(
|
||||||
|
r'<div[^>]+class\s*?=\s*?([\'"])(?:(?!\1).)*login-wrapper(?:(?!\1).)*\1',
|
||||||
|
webpage, 'login wrapper', default=None):
|
||||||
|
self.raise_login_required('This video is only available for members')
|
||||||
|
return self.url_result(url, 'Generic') # Fall back to generic to extract audio
|
||||||
|
|
||||||
|
real_url = param_url.replace('https://vimeo.com/', 'https://player.vimeo.com/video/')
|
||||||
|
return self.url_result(VimeoIE._smuggle_referrer(real_url, url), VimeoIE, video_id)
|
|
@ -52,7 +52,7 @@ class GetCourseRuIE(InfoExtractor):
|
||||||
_BASE_URL_RE = rf'https?://(?:(?!player02\.)[^.]+\.getcourse\.(?:ru|io)|{"|".join(map(re.escape, _DOMAINS))})'
|
_BASE_URL_RE = rf'https?://(?:(?!player02\.)[^.]+\.getcourse\.(?:ru|io)|{"|".join(map(re.escape, _DOMAINS))})'
|
||||||
_VALID_URL = [
|
_VALID_URL = [
|
||||||
rf'{_BASE_URL_RE}/(?!pl/|teach/)(?P<id>[^?#]+)',
|
rf'{_BASE_URL_RE}/(?!pl/|teach/)(?P<id>[^?#]+)',
|
||||||
rf'{_BASE_URL_RE}/(:?pl/)?teach/control/lesson/view\?(?:[^#]+&)?id=(?P<id>\d+)',
|
rf'{_BASE_URL_RE}/(?:pl/)?teach/control/lesson/view\?(?:[^#]+&)?id=(?P<id>\d+)',
|
||||||
]
|
]
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://academymel.online/3video_1',
|
'url': 'http://academymel.online/3video_1',
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
|
|
||||||
class GolemIE(InfoExtractor):
|
class GolemIE(InfoExtractor):
|
||||||
_VALID_URL = r'^https?://video\.golem\.de/.+?/(?P<id>.+?)/'
|
_VALID_URL = r'https?://video\.golem\.de/.+?/(?P<id>.+?)/'
|
||||||
_TEST = {
|
_TEST = {
|
||||||
'url': 'http://video.golem.de/handy/14095/iphone-6-und-6-plus-test.html',
|
'url': 'http://video.golem.de/handy/14095/iphone-6-und-6-plus-test.html',
|
||||||
'md5': 'c1a2c0a3c863319651c7c992c5ee29bf',
|
'md5': 'c1a2c0a3c863319651c7c992c5ee29bf',
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
|
|
||||||
class HRFernsehenIE(InfoExtractor):
|
class HRFernsehenIE(InfoExtractor):
|
||||||
IE_NAME = 'hrfernsehen'
|
IE_NAME = 'hrfernsehen'
|
||||||
_VALID_URL = r'^https?://www\.(?:hr-fernsehen|hessenschau)\.de/.*,video-(?P<id>[0-9]{6})\.html'
|
_VALID_URL = r'https?://www\.(?:hr-fernsehen|hessenschau)\.de/.*,video-(?P<id>[0-9]{6})\.html'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.hessenschau.de/tv-sendung/hessenschau-vom-26082020,video-130546.html',
|
'url': 'https://www.hessenschau.de/tv-sendung/hessenschau-vom-26082020,video-130546.html',
|
||||||
'md5': '5c4e0ba94677c516a2f65a84110fc536',
|
'md5': '5c4e0ba94677c516a2f65a84110fc536',
|
||||||
|
|
|
@ -37,7 +37,7 @@ def get_description(s):
|
||||||
|
|
||||||
|
|
||||||
class ImgurIE(ImgurBaseIE):
|
class ImgurIE(ImgurBaseIE):
|
||||||
_VALID_URL = r'https?://(?:i\.)?imgur\.com/(?!(?:a|gallery|t|topic|r)/)(?P<id>[a-zA-Z0-9]+)'
|
_VALID_URL = r'https?://(?:i\.)?imgur\.com/(?!(?:a|gallery|t|topic|r)/)(?:[^/?#]+-)?(?P<id>[a-zA-Z0-9]+)'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://imgur.com/A61SaA1',
|
'url': 'https://imgur.com/A61SaA1',
|
||||||
|
@ -54,6 +54,22 @@ class ImgurIE(ImgurBaseIE):
|
||||||
'like_count': int,
|
'like_count': int,
|
||||||
'thumbnail': 'https://i.imgur.com/A61SaA1h.jpg',
|
'thumbnail': 'https://i.imgur.com/A61SaA1h.jpg',
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
# Test with URL slug
|
||||||
|
'url': 'https://imgur.com/mrw-gifv-is-up-running-without-any-bugs-A61SaA1',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'A61SaA1',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'MRW gifv is up and running without any bugs',
|
||||||
|
'timestamp': 1416446068,
|
||||||
|
'upload_date': '20141120',
|
||||||
|
'dislike_count': int,
|
||||||
|
'comment_count': int,
|
||||||
|
'release_timestamp': 1416446068,
|
||||||
|
'release_date': '20141120',
|
||||||
|
'like_count': int,
|
||||||
|
'thumbnail': 'https://i.imgur.com/A61SaA1h.jpg',
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://i.imgur.com/A61SaA1.gifv',
|
'url': 'https://i.imgur.com/A61SaA1.gifv',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
@ -92,6 +108,7 @@ class ImgurIE(ImgurBaseIE):
|
||||||
'comment_count': int,
|
'comment_count': int,
|
||||||
'release_timestamp': 1710491255,
|
'release_timestamp': 1710491255,
|
||||||
'release_date': '20240315',
|
'release_date': '20240315',
|
||||||
|
'thumbnail': 'https://i.imgur.com/zV03bd5h.jpg',
|
||||||
},
|
},
|
||||||
}]
|
}]
|
||||||
|
|
||||||
|
@ -208,7 +225,10 @@ def og_get_size(media_type):
|
||||||
}), get_all=False),
|
}), get_all=False),
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
'thumbnail': url_or_none(search('thumbnailUrl')),
|
'thumbnails': [{
|
||||||
|
'url': thumbnail_url,
|
||||||
|
'http_headers': {'Accept': '*/*'},
|
||||||
|
}] if (thumbnail_url := search(['thumbnailUrl', 'twitter:image', 'og:image'])) else None,
|
||||||
'http_headers': {'Accept': '*/*'},
|
'http_headers': {'Accept': '*/*'},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -252,17 +272,9 @@ def _real_extract(self, url):
|
||||||
|
|
||||||
class ImgurGalleryIE(ImgurGalleryBaseIE):
|
class ImgurGalleryIE(ImgurGalleryBaseIE):
|
||||||
IE_NAME = 'imgur:gallery'
|
IE_NAME = 'imgur:gallery'
|
||||||
_VALID_URL = r'https?://(?:i\.)?imgur\.com/(?:gallery|(?:t(?:opic)?|r)/[^/?#]+)/(?P<id>[a-zA-Z0-9]+)'
|
_VALID_URL = r'https?://(?:i\.)?imgur\.com/(?:gallery|(?:t(?:opic)?|r)/[^/?#]+)/(?:[^/?#]+-)?(?P<id>[a-zA-Z0-9]+)'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://imgur.com/gallery/Q95ko',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'Q95ko',
|
|
||||||
'title': 'Adding faces make every GIF better',
|
|
||||||
},
|
|
||||||
'playlist_count': 25,
|
|
||||||
'skip': 'Zoinks! You\'ve taken a wrong turn.',
|
|
||||||
}, {
|
|
||||||
# TODO: static images - replace with animated/video gallery
|
# TODO: static images - replace with animated/video gallery
|
||||||
'url': 'http://imgur.com/topic/Aww/ll5Vk',
|
'url': 'http://imgur.com/topic/Aww/ll5Vk',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
@ -280,7 +292,27 @@ class ImgurGalleryIE(ImgurGalleryBaseIE):
|
||||||
'release_timestamp': 1358554297,
|
'release_timestamp': 1358554297,
|
||||||
'thumbnail': 'https://i.imgur.com/YcAQlkxh.jpg',
|
'thumbnail': 'https://i.imgur.com/YcAQlkxh.jpg',
|
||||||
'release_date': '20130119',
|
'release_date': '20130119',
|
||||||
'uploader_url': 'https://i.imgur.com/u3R4I2S_d.png?maxwidth=290&fidelity=grand',
|
'uploader_url': 'https://i.imgur.com/N5Flb2v_d.png?maxwidth=290&fidelity=grand',
|
||||||
|
'comment_count': int,
|
||||||
|
'dislike_count': int,
|
||||||
|
'like_count': int,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
# Test with slug
|
||||||
|
'url': 'https://imgur.com/gallery/classic-steve-carell-gif-cracks-me-up-everytime-repost-downvotes-YcAQlkx',
|
||||||
|
'add_ies': ['Imgur'],
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'YcAQlkx',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Classic Steve Carell gif...cracks me up everytime....damn the repost downvotes....',
|
||||||
|
'timestamp': 1358554297,
|
||||||
|
'upload_date': '20130119',
|
||||||
|
'uploader_id': '1648642',
|
||||||
|
'uploader': 'wittyusernamehere',
|
||||||
|
'release_timestamp': 1358554297,
|
||||||
|
'release_date': '20130119',
|
||||||
|
'thumbnail': 'https://i.imgur.com/YcAQlkxh.jpg',
|
||||||
|
'uploader_url': 'https://i.imgur.com/N5Flb2v_d.png?maxwidth=290&fidelity=grand',
|
||||||
'comment_count': int,
|
'comment_count': int,
|
||||||
'dislike_count': int,
|
'dislike_count': int,
|
||||||
'like_count': int,
|
'like_count': int,
|
||||||
|
@ -317,6 +349,13 @@ class ImgurGalleryIE(ImgurGalleryBaseIE):
|
||||||
'title': 'Penguins !',
|
'title': 'Penguins !',
|
||||||
},
|
},
|
||||||
'playlist_count': 3,
|
'playlist_count': 3,
|
||||||
|
}, {
|
||||||
|
'url': 'https://imgur.com/t/unmuted/penguins-penguins-6lAn9VQ',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '6lAn9VQ',
|
||||||
|
'title': 'Penguins !',
|
||||||
|
},
|
||||||
|
'playlist_count': 3,
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://imgur.com/t/unmuted/kx2uD3C',
|
'url': 'https://imgur.com/t/unmuted/kx2uD3C',
|
||||||
'add_ies': ['Imgur'],
|
'add_ies': ['Imgur'],
|
||||||
|
@ -357,7 +396,7 @@ class ImgurGalleryIE(ImgurGalleryBaseIE):
|
||||||
|
|
||||||
class ImgurAlbumIE(ImgurGalleryBaseIE):
|
class ImgurAlbumIE(ImgurGalleryBaseIE):
|
||||||
IE_NAME = 'imgur:album'
|
IE_NAME = 'imgur:album'
|
||||||
_VALID_URL = r'https?://(?:i\.)?imgur\.com/a/(?P<id>[a-zA-Z0-9]+)'
|
_VALID_URL = r'https?://(?:i\.)?imgur\.com/a/(?:[^/?#]+-)?(?P<id>[a-zA-Z0-9]+)'
|
||||||
_GALLERY = False
|
_GALLERY = False
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
# TODO: only static images - replace with animated/video gallery
|
# TODO: only static images - replace with animated/video gallery
|
||||||
|
@ -372,6 +411,14 @@ class ImgurAlbumIE(ImgurGalleryBaseIE):
|
||||||
'title': 'enen-no-shouboutai',
|
'title': 'enen-no-shouboutai',
|
||||||
},
|
},
|
||||||
'playlist_count': 2,
|
'playlist_count': 2,
|
||||||
|
}, {
|
||||||
|
# Test with URL slug
|
||||||
|
'url': 'https://imgur.com/a/enen-no-shouboutai-iX265HX',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'iX265HX',
|
||||||
|
'title': 'enen-no-shouboutai',
|
||||||
|
},
|
||||||
|
'playlist_count': 2,
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://imgur.com/a/8pih2Ed',
|
'url': 'https://imgur.com/a/8pih2Ed',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
|
|
|
@ -48,7 +48,6 @@ class InstagramBaseIE(InfoExtractor):
|
||||||
'X-IG-WWW-Claim': '0',
|
'X-IG-WWW-Claim': '0',
|
||||||
'Origin': 'https://www.instagram.com',
|
'Origin': 'https://www.instagram.com',
|
||||||
'Accept': '*/*',
|
'Accept': '*/*',
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def _perform_login(self, username, password):
|
def _perform_login(self, username, password):
|
||||||
|
@ -435,10 +434,10 @@ def _real_extract(self, url):
|
||||||
'X-Requested-With': 'XMLHttpRequest',
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
'Referer': url,
|
'Referer': url,
|
||||||
}, query={
|
}, query={
|
||||||
'query_hash': '9f8827793ef34641b2fb195d4d41151c',
|
'doc_id': '8845758582119845',
|
||||||
'variables': json.dumps(variables, separators=(',', ':')),
|
'variables': json.dumps(variables, separators=(',', ':')),
|
||||||
})
|
})
|
||||||
media.update(traverse_obj(general_info, ('data', 'shortcode_media')) or {})
|
media.update(traverse_obj(general_info, ('data', 'xdt_shortcode_media')) or {})
|
||||||
|
|
||||||
if not general_info:
|
if not general_info:
|
||||||
self.report_warning('General metadata extraction failed (some metadata might be missing).', video_id)
|
self.report_warning('General metadata extraction failed (some metadata might be missing).', video_id)
|
||||||
|
|
|
@ -194,11 +194,14 @@ def _real_extract(self, url):
|
||||||
|
|
||||||
|
|
||||||
class SangiinInstructionIE(InfoExtractor):
|
class SangiinInstructionIE(InfoExtractor):
|
||||||
_VALID_URL = r'^https?://www\.webtv\.sangiin\.go\.jp/webtv/index\.php'
|
_VALID_URL = r'https?://www\.webtv\.sangiin\.go\.jp/webtv/index\.php'
|
||||||
IE_DESC = False # this shouldn't be listed as a supported site
|
IE_DESC = False # this shouldn't be listed as a supported site
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
raise ExtractorError('Copy the link from the botton below the video description or player, and use the link to download. If there are no button in the frame, get the URL of the frame showing the video.', expected=True)
|
raise ExtractorError(
|
||||||
|
'Copy the link from the button below the video description/player '
|
||||||
|
'and use that link to download. If there is no button in the frame, '
|
||||||
|
'get the URL of the frame showing the video.', expected=True)
|
||||||
|
|
||||||
|
|
||||||
class SangiinIE(InfoExtractor):
|
class SangiinIE(InfoExtractor):
|
||||||
|
|
|
@ -22,7 +22,7 @@ class KalturaIE(InfoExtractor):
|
||||||
(?:
|
(?:
|
||||||
kaltura:(?P<partner_id>\w+):(?P<id>\w+)(?::(?P<player_type>\w+))?|
|
kaltura:(?P<partner_id>\w+):(?P<id>\w+)(?::(?P<player_type>\w+))?|
|
||||||
https?://
|
https?://
|
||||||
(:?(?:www|cdnapi(?:sec)?)\.)?kaltura\.com(?::\d+)?/
|
(?:(?:www|cdnapi(?:sec)?)\.)?kaltura\.com(?::\d+)?/
|
||||||
(?:
|
(?:
|
||||||
(?:
|
(?:
|
||||||
# flash player
|
# flash player
|
||||||
|
|
|
@ -148,7 +148,7 @@ def _real_extract(self, url):
|
||||||
|
|
||||||
class KickClipIE(KickBaseIE):
|
class KickClipIE(KickBaseIE):
|
||||||
IE_NAME = 'kick:clips'
|
IE_NAME = 'kick:clips'
|
||||||
_VALID_URL = r'https?://(?:www\.)?kick\.com/[\w-]+/?\?(?:[^#]+&)?clip=(?P<id>clip_[\w-]+)'
|
_VALID_URL = r'https?://(?:www\.)?kick\.com/[\w-]+(?:/clips/|/?\?(?:[^#]+&)?clip=)(?P<id>clip_[\w-]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://kick.com/mxddy?clip=clip_01GYXVB5Y8PWAPWCWMSBCFB05X',
|
'url': 'https://kick.com/mxddy?clip=clip_01GYXVB5Y8PWAPWCWMSBCFB05X',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
|
@ -189,6 +189,26 @@ class KickClipIE(KickBaseIE):
|
||||||
'age_limit': 0,
|
'age_limit': 0,
|
||||||
},
|
},
|
||||||
'params': {'skip_download': 'm3u8'},
|
'params': {'skip_download': 'm3u8'},
|
||||||
|
}, {
|
||||||
|
'url': 'https://kick.com/spreen/clips/clip_01J8RGZRKHXHXXKJEHGRM932A5',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'clip_01J8RGZRKHXHXXKJEHGRM932A5',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'KLJASLDJKLJKASDLJKDAS',
|
||||||
|
'channel': 'spreen',
|
||||||
|
'channel_id': '5312671',
|
||||||
|
'uploader': 'AnormalBarraBaja',
|
||||||
|
'uploader_id': '26518262',
|
||||||
|
'duration': 43.0,
|
||||||
|
'upload_date': '20240927',
|
||||||
|
'timestamp': 1727399987,
|
||||||
|
'thumbnail': 'https://clips.kick.com/clips/f2/clip_01J8RGZRKHXHXXKJEHGRM932A5/thumbnail.webp',
|
||||||
|
'view_count': int,
|
||||||
|
'like_count': int,
|
||||||
|
'categories': ['Minecraft'],
|
||||||
|
'age_limit': 0,
|
||||||
|
},
|
||||||
|
'params': {'skip_download': 'm3u8'},
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
|
|
|
@ -136,6 +136,7 @@ def _playlist_entries(self, url, display_id, claim_param, metadata):
|
||||||
|
|
||||||
class LBRYIE(LBRYBaseIE):
|
class LBRYIE(LBRYBaseIE):
|
||||||
IE_NAME = 'lbry'
|
IE_NAME = 'lbry'
|
||||||
|
IE_DESC = 'odysee.com'
|
||||||
_VALID_URL = LBRYBaseIE._BASE_URL_REGEX + rf'''
|
_VALID_URL = LBRYBaseIE._BASE_URL_REGEX + rf'''
|
||||||
(?:\$/(?:download|embed)/)?
|
(?:\$/(?:download|embed)/)?
|
||||||
(?P<id>
|
(?P<id>
|
||||||
|
@ -364,6 +365,7 @@ def _real_extract(self, url):
|
||||||
|
|
||||||
class LBRYChannelIE(LBRYBaseIE):
|
class LBRYChannelIE(LBRYBaseIE):
|
||||||
IE_NAME = 'lbry:channel'
|
IE_NAME = 'lbry:channel'
|
||||||
|
IE_DESC = 'odysee.com channels'
|
||||||
_VALID_URL = LBRYBaseIE._BASE_URL_REGEX + rf'(?P<id>@{LBRYBaseIE._OPT_CLAIM_ID})/?(?:[?&]|$)'
|
_VALID_URL = LBRYBaseIE._BASE_URL_REGEX + rf'(?P<id>@{LBRYBaseIE._OPT_CLAIM_ID})/?(?:[?&]|$)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://lbry.tv/@LBRYFoundation:0',
|
'url': 'https://lbry.tv/@LBRYFoundation:0',
|
||||||
|
@ -391,6 +393,7 @@ def _real_extract(self, url):
|
||||||
|
|
||||||
class LBRYPlaylistIE(LBRYBaseIE):
|
class LBRYPlaylistIE(LBRYBaseIE):
|
||||||
IE_NAME = 'lbry:playlist'
|
IE_NAME = 'lbry:playlist'
|
||||||
|
IE_DESC = 'odysee.com playlists'
|
||||||
_VALID_URL = LBRYBaseIE._BASE_URL_REGEX + r'\$/(?:play)?list/(?P<id>[0-9a-f-]+)'
|
_VALID_URL = LBRYBaseIE._BASE_URL_REGEX + r'\$/(?:play)?list/(?P<id>[0-9a-f-]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://odysee.com/$/playlist/ffef782f27486f0ac138bde8777f72ebdd0548c2',
|
'url': 'https://odysee.com/$/playlist/ffef782f27486f0ac138bde8777f72ebdd0548c2',
|
||||||
|
|
|
@ -92,9 +92,9 @@ class LoomIE(InfoExtractor):
|
||||||
},
|
},
|
||||||
'params': {'videopassword': 'seniorinfants2'},
|
'params': {'videopassword': 'seniorinfants2'},
|
||||||
}, {
|
}, {
|
||||||
# embed, transcoded-url endpoint sends empty JSON response
|
# embed, transcoded-url endpoint sends empty JSON response, split video and audio HLS formats
|
||||||
'url': 'https://www.loom.com/embed/ddcf1c1ad21f451ea7468b1e33917e4e',
|
'url': 'https://www.loom.com/embed/ddcf1c1ad21f451ea7468b1e33917e4e',
|
||||||
'md5': '8488817242a0db1cb2ad0ea522553cf6',
|
'md5': 'b321d261656848c184a94e3b93eae28d',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'ddcf1c1ad21f451ea7468b1e33917e4e',
|
'id': 'ddcf1c1ad21f451ea7468b1e33917e4e',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
|
@ -104,6 +104,7 @@ class LoomIE(InfoExtractor):
|
||||||
'timestamp': 1657216459,
|
'timestamp': 1657216459,
|
||||||
'duration': 181,
|
'duration': 181,
|
||||||
},
|
},
|
||||||
|
'params': {'format': 'bestvideo'}, # Test video-only fixup
|
||||||
'expected_warnings': ['Failed to parse JSON'],
|
'expected_warnings': ['Failed to parse JSON'],
|
||||||
}]
|
}]
|
||||||
_WEBPAGE_TESTS = [{
|
_WEBPAGE_TESTS = [{
|
||||||
|
@ -293,7 +294,11 @@ def get_formats(format_url, format_id, quality):
|
||||||
format_url = format_url.replace('-split.m3u8', '.m3u8')
|
format_url = format_url.replace('-split.m3u8', '.m3u8')
|
||||||
m3u8_formats = self._extract_m3u8_formats(
|
m3u8_formats = self._extract_m3u8_formats(
|
||||||
format_url, video_id, 'mp4', m3u8_id=f'hls-{format_id}', fatal=False, quality=quality)
|
format_url, video_id, 'mp4', m3u8_id=f'hls-{format_id}', fatal=False, quality=quality)
|
||||||
|
# Sometimes only split video/audio formats are available, need to fixup video-only formats
|
||||||
|
is_not_premerged = 'none' in traverse_obj(m3u8_formats, (..., 'vcodec'))
|
||||||
for fmt in m3u8_formats:
|
for fmt in m3u8_formats:
|
||||||
|
if is_not_premerged and fmt.get('vcodec') != 'none':
|
||||||
|
fmt['acodec'] = 'none'
|
||||||
yield {
|
yield {
|
||||||
**fmt,
|
**fmt,
|
||||||
'url': update_url(fmt['url'], query=query),
|
'url': update_url(fmt['url'], query=query),
|
||||||
|
|
|
@ -126,7 +126,7 @@ def _real_extract(self, url):
|
||||||
video_data = None
|
video_data = None
|
||||||
|
|
||||||
# fix meta_url if missing the host address
|
# fix meta_url if missing the host address
|
||||||
if re.match(r'^\/\+\/', meta_url):
|
if re.match(r'\/\+\/', meta_url):
|
||||||
meta_url = urljoin('https://my.mail.ru', meta_url)
|
meta_url = urljoin('https://my.mail.ru', meta_url)
|
||||||
|
|
||||||
if meta_url:
|
if meta_url:
|
||||||
|
|
|
@ -16,6 +16,15 @@ class MediaKlikkIE(InfoExtractor):
|
||||||
(?P<id>[^/#?_]+)'''
|
(?P<id>[^/#?_]+)'''
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
|
'url': 'https://mediaklikk.hu/filmajanlo/cikk/az-ajto/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '668177',
|
||||||
|
'title': 'Az ajtó',
|
||||||
|
'display_id': 'az-ajto',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'thumbnail': 'https://cdn.cms.mtv.hu/wp-content/uploads/sites/4/2016/01/vlcsnap-2023-07-31-14h18m52s111.jpg',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
# (old) mediaklikk. date in html.
|
# (old) mediaklikk. date in html.
|
||||||
'url': 'https://mediaklikk.hu/video/hazajaro-delnyugat-bacska-a-duna-menten-palankatol-doroszloig/',
|
'url': 'https://mediaklikk.hu/video/hazajaro-delnyugat-bacska-a-duna-menten-palankatol-doroszloig/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
|
@ -37,6 +46,7 @@ class MediaKlikkIE(InfoExtractor):
|
||||||
'upload_date': '20230903',
|
'upload_date': '20230903',
|
||||||
'thumbnail': 'https://mediaklikk.hu/wp-content/uploads/sites/4/2014/02/hazajarouj_JO.jpg',
|
'thumbnail': 'https://mediaklikk.hu/wp-content/uploads/sites/4/2014/02/hazajarouj_JO.jpg',
|
||||||
},
|
},
|
||||||
|
'skip': 'Webpage redirects to 404 page',
|
||||||
}, {
|
}, {
|
||||||
# (old) m4sport
|
# (old) m4sport
|
||||||
'url': 'https://m4sport.hu/video/2021/08/30/gyemant-liga-parizs/',
|
'url': 'https://m4sport.hu/video/2021/08/30/gyemant-liga-parizs/',
|
||||||
|
@ -59,6 +69,7 @@ class MediaKlikkIE(InfoExtractor):
|
||||||
'upload_date': '20230908',
|
'upload_date': '20230908',
|
||||||
'thumbnail': 'https://m4sport.hu/wp-content/uploads/sites/4/2023/09/vlcsnap-2023-09-08-22h43m18s691.jpg',
|
'thumbnail': 'https://m4sport.hu/wp-content/uploads/sites/4/2023/09/vlcsnap-2023-09-08-22h43m18s691.jpg',
|
||||||
},
|
},
|
||||||
|
'skip': 'Webpage redirects to 404 page',
|
||||||
}, {
|
}, {
|
||||||
# m4sport with *video/ url and no date
|
# m4sport with *video/ url and no date
|
||||||
'url': 'https://m4sport.hu/bl-video/real-madrid-chelsea-1-1/',
|
'url': 'https://m4sport.hu/bl-video/real-madrid-chelsea-1-1/',
|
||||||
|
@ -69,6 +80,7 @@ class MediaKlikkIE(InfoExtractor):
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'thumbnail': 'https://m4sport.hu/wp-content/uploads/sites/4/2021/04/Sequence-01.Still001-1024x576.png',
|
'thumbnail': 'https://m4sport.hu/wp-content/uploads/sites/4/2021/04/Sequence-01.Still001-1024x576.png',
|
||||||
},
|
},
|
||||||
|
'skip': 'Webpage redirects to 404 page',
|
||||||
}, {
|
}, {
|
||||||
# (old) hirado
|
# (old) hirado
|
||||||
'url': 'https://hirado.hu/videok/felteteleket-szabott-a-fovaros/',
|
'url': 'https://hirado.hu/videok/felteteleket-szabott-a-fovaros/',
|
||||||
|
@ -90,6 +102,7 @@ class MediaKlikkIE(InfoExtractor):
|
||||||
'upload_date': '20230911',
|
'upload_date': '20230911',
|
||||||
'thumbnail': 'https://hirado.hu/wp-content/uploads/sites/4/2023/09/vlcsnap-2023-09-11-09h16m09s882.jpg',
|
'thumbnail': 'https://hirado.hu/wp-content/uploads/sites/4/2023/09/vlcsnap-2023-09-11-09h16m09s882.jpg',
|
||||||
},
|
},
|
||||||
|
'skip': 'Webpage redirects to video list page',
|
||||||
}, {
|
}, {
|
||||||
# (old) petofilive
|
# (old) petofilive
|
||||||
'url': 'https://petofilive.hu/video/2021/06/07/tha-shudras-az-akusztikban/',
|
'url': 'https://petofilive.hu/video/2021/06/07/tha-shudras-az-akusztikban/',
|
||||||
|
@ -112,6 +125,7 @@ class MediaKlikkIE(InfoExtractor):
|
||||||
'upload_date': '20230909',
|
'upload_date': '20230909',
|
||||||
'thumbnail': 'https://petofilive.hu/wp-content/uploads/sites/4/2023/09/Clipboard11-2.jpg',
|
'thumbnail': 'https://petofilive.hu/wp-content/uploads/sites/4/2023/09/Clipboard11-2.jpg',
|
||||||
},
|
},
|
||||||
|
'skip': 'Webpage redirects to video list page',
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
|
@ -143,14 +157,14 @@ def _real_extract(self, url):
|
||||||
if not playlist_url:
|
if not playlist_url:
|
||||||
raise ExtractorError('Unable to extract playlist url')
|
raise ExtractorError('Unable to extract playlist url')
|
||||||
|
|
||||||
formats = self._extract_wowza_formats(
|
formats, subtitles = self._extract_m3u8_formats_and_subtitles(playlist_url, video_id)
|
||||||
playlist_url, video_id, skip_protocols=['f4m', 'smil', 'dash'])
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'title': title,
|
'title': title,
|
||||||
'display_id': display_id,
|
'display_id': display_id,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
|
'subtitles': subtitles,
|
||||||
'upload_date': upload_date,
|
'upload_date': upload_date,
|
||||||
'thumbnail': player_data.get('bgImage') or self._og_search_thumbnail(webpage),
|
'thumbnail': player_data.get('bgImage') or self._og_search_thumbnail(webpage),
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
|
|
||||||
class MGTVIE(InfoExtractor):
|
class MGTVIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:w(?:ww)?\.)?mgtv\.com/(v|b)/(?:[^/]+/)*(?P<id>\d+)\.html'
|
_VALID_URL = r'https?://(?:w(?:ww)?\.)?mgtv\.com/[bv]/(?:[^/]+/)*(?P<id>\d+)\.html'
|
||||||
IE_DESC = '芒果TV'
|
IE_DESC = '芒果TV'
|
||||||
IE_NAME = 'MangoTV'
|
IE_NAME = 'MangoTV'
|
||||||
|
|
||||||
|
|
|
@ -65,7 +65,7 @@ def _real_extract(self, url):
|
||||||
|
|
||||||
class OCWMITIE(InfoExtractor):
|
class OCWMITIE(InfoExtractor):
|
||||||
IE_NAME = 'ocw.mit.edu'
|
IE_NAME = 'ocw.mit.edu'
|
||||||
_VALID_URL = r'^https?://ocw\.mit\.edu/courses/(?P<topic>[a-z0-9\-]+)'
|
_VALID_URL = r'https?://ocw\.mit\.edu/courses/(?P<topic>[a-z0-9\-]+)'
|
||||||
_BASE_URL = 'http://ocw.mit.edu/'
|
_BASE_URL = 'http://ocw.mit.edu/'
|
||||||
|
|
||||||
_TESTS = [
|
_TESTS = [
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
from .telecinco import TelecincoIE
|
from .telecinco import TelecincoBaseIE
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
int_or_none,
|
int_or_none,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class MiTeleIE(TelecincoIE): # XXX: Do not subclass from concrete IE
|
class MiTeleIE(TelecincoBaseIE):
|
||||||
IE_DESC = 'mitele.es'
|
IE_DESC = 'mitele.es'
|
||||||
_VALID_URL = r'https?://(?:www\.)?mitele\.es/(?:[^/]+/)+(?P<id>[^/]+)/player'
|
_VALID_URL = r'https?://(?:www\.)?mitele\.es/(?:[^/]+/)+(?P<id>[^/]+)/player'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.mitele.es/programas-tv/diario-de/57b0dfb9c715da65618b4afa/player',
|
'url': 'http://www.mitele.es/programas-tv/diario-de/57b0dfb9c715da65618b4afa/player',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
|
@ -27,6 +26,7 @@ class MiTeleIE(TelecincoIE): # XXX: Do not subclass from concrete IE
|
||||||
'timestamp': 1471209401,
|
'timestamp': 1471209401,
|
||||||
'upload_date': '20160814',
|
'upload_date': '20160814',
|
||||||
},
|
},
|
||||||
|
'skip': 'HTTP Error 404 Not Found',
|
||||||
}, {
|
}, {
|
||||||
# no explicit title
|
# no explicit title
|
||||||
'url': 'http://www.mitele.es/programas-tv/cuarto-milenio/57b0de3dc915da14058b4876/player',
|
'url': 'http://www.mitele.es/programas-tv/cuarto-milenio/57b0de3dc915da14058b4876/player',
|
||||||
|
@ -49,6 +49,26 @@ class MiTeleIE(TelecincoIE): # XXX: Do not subclass from concrete IE
|
||||||
'params': {
|
'params': {
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
|
'skip': 'HTTP Error 404 Not Found',
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.mitele.es/programas-tv/horizonte/temporada-5/programa-171-40_013480051/player/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '7adbe22e-cd41-4787-afa4-36f3da7c2c6f',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Horizonte Temporada 5 Programa 171',
|
||||||
|
'description': 'md5:97f1fb712c5ac27e5693a8b3c5c0c6e3',
|
||||||
|
'episode': 'Las Zonas de Bajas Emisiones, a debate',
|
||||||
|
'episode_number': 171,
|
||||||
|
'season': 'Season 5',
|
||||||
|
'season_number': 5,
|
||||||
|
'series': 'Horizonte',
|
||||||
|
'duration': 7012,
|
||||||
|
'upload_date': '20240927',
|
||||||
|
'timestamp': 1727416450,
|
||||||
|
'thumbnail': 'https://album.mediaset.es/eimg/2024/09/27/horizonte-171_9f02.jpg',
|
||||||
|
'age_limit': 12,
|
||||||
|
},
|
||||||
|
'params': {'geo_bypass_country': 'ES'},
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://www.mitele.es/series-online/la-que-se-avecina/57aac5c1c915da951a8b45ed/player',
|
'url': 'http://www.mitele.es/series-online/la-que-se-avecina/57aac5c1c915da951a8b45ed/player',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
|
121
yt_dlp/extractor/mojevideo.py
Normal file
121
yt_dlp/extractor/mojevideo.py
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import js_to_json, remove_end, update_url_query
|
||||||
|
|
||||||
|
|
||||||
|
class MojevideoIE(InfoExtractor):
|
||||||
|
IE_DESC = 'mojevideo.sk'
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?mojevideo\.sk/video/(?P<id>\w+)/(?P<display_id>[\w()]+?)\.html'
|
||||||
|
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.mojevideo.sk/video/3d17c/chlapci_dobetonovali_sme_mame_hotovo.html',
|
||||||
|
'md5': '384a4628bd2bbd261c5206cf77c38c17',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '3d17c',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Chlapci dobetónovali sme, máme hotovo!',
|
||||||
|
'display_id': 'chlapci_dobetonovali_sme_mame_hotovo',
|
||||||
|
'description': 'md5:a0822126044050d304a9ef58c92ddb34',
|
||||||
|
'thumbnail': 'https://fs5.mojevideo.sk/imgfb/250236.jpg',
|
||||||
|
'duration': 21.0,
|
||||||
|
'upload_date': '20230919',
|
||||||
|
'timestamp': 1695129706,
|
||||||
|
'like_count': int,
|
||||||
|
'dislike_count': int,
|
||||||
|
'view_count': int,
|
||||||
|
'comment_count': int,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
# 720p
|
||||||
|
'url': 'https://www.mojevideo.sk/video/14677/den_blbec.html',
|
||||||
|
'md5': '517c3e111c53a67d10b429c1f344ba2f',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '14677',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Deň blbec?',
|
||||||
|
'display_id': 'den_blbec',
|
||||||
|
'description': 'I maličkosť vám môže zmeniť celý deň. Nikdy nezahadzujte žuvačky na zem!',
|
||||||
|
'thumbnail': 'https://fs5.mojevideo.sk/imgfb/83575.jpg',
|
||||||
|
'duration': 100.0,
|
||||||
|
'upload_date': '20120515',
|
||||||
|
'timestamp': 1337076481,
|
||||||
|
'like_count': int,
|
||||||
|
'dislike_count': int,
|
||||||
|
'view_count': int,
|
||||||
|
'comment_count': int,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
# 1080p
|
||||||
|
'url': 'https://www.mojevideo.sk/video/2feb2/band_maid_onset_(instrumental)_live_zepp_tokyo_(full_hd).html',
|
||||||
|
'md5': '64599a23d3ac31cf2fe069e4353d8162',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '2feb2',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'BAND-MAID - onset (Instrumental) Live - Zepp Tokyo (Full HD)',
|
||||||
|
'display_id': 'band_maid_onset_(instrumental)_live_zepp_tokyo_(full_hd)',
|
||||||
|
'description': 'Výborná inštrumentálna skladba od skupiny BAND-MAID.',
|
||||||
|
'thumbnail': 'https://fs5.mojevideo.sk/imgfb/196274.jpg',
|
||||||
|
'duration': 240.0,
|
||||||
|
'upload_date': '20190708',
|
||||||
|
'timestamp': 1562576592,
|
||||||
|
'like_count': int,
|
||||||
|
'dislike_count': int,
|
||||||
|
'view_count': int,
|
||||||
|
'comment_count': int,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
# 720p
|
||||||
|
'url': 'https://www.mojevideo.sk/video/358c8/dva_nissany_skyline_strielaju_v_londyne.html',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
# 720p
|
||||||
|
'url': 'https://www.mojevideo.sk/video/2455d/gopro_hero4_session_nova_sportova_vodotesna_kamera.html',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
# 1080p
|
||||||
|
'url': 'https://www.mojevideo.sk/video/352ee/amd_rx_6800_xt_vs_nvidia_rtx_3080_(test_v_9_hrach).html',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
# 1080p
|
||||||
|
'url': 'https://www.mojevideo.sk/video/2cbeb/trailer_z_avengers_infinity_war.html',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id, display_id = self._match_valid_url(url).groups()
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
|
video_id_dec = self._search_regex(
|
||||||
|
r'\bvId\s*=\s*(\d+)', webpage, 'video id', fatal=False) or str(int(video_id, 16))
|
||||||
|
video_exp = self._search_regex(r'\bvEx\s*=\s*["\'](\d+)', webpage, 'video expiry')
|
||||||
|
video_hashes = self._search_json(
|
||||||
|
r'\bvHash\s*=', webpage, 'video hashes', video_id,
|
||||||
|
contains_pattern=r'\[(?s:.+)\]', transform_source=js_to_json)
|
||||||
|
|
||||||
|
formats = []
|
||||||
|
for video_hash, (suffix, quality, format_note) in zip(video_hashes, [
|
||||||
|
('', 1, 'normálna kvalita'),
|
||||||
|
('_lq', 0, 'nízka kvalita'),
|
||||||
|
('_hd', 2, 'HD-720p'),
|
||||||
|
('_fhd', 3, 'FULL HD-1080p'),
|
||||||
|
('_2k', 4, '2K-1440p'),
|
||||||
|
]):
|
||||||
|
formats.append({
|
||||||
|
'format_id': f'mp4-{quality}',
|
||||||
|
'quality': quality,
|
||||||
|
'format_note': format_note,
|
||||||
|
'url': update_url_query(
|
||||||
|
f'https://cache01.mojevideo.sk/securevideos69/{video_id_dec}{suffix}.mp4', {
|
||||||
|
'md5': video_hash,
|
||||||
|
'expires': video_exp,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'display_id': display_id,
|
||||||
|
'formats': formats,
|
||||||
|
'title': (self._og_search_title(webpage, default=None)
|
||||||
|
or remove_end(self._html_extract_title(webpage, 'title'), ' - Mojevideo')),
|
||||||
|
'description': self._og_search_description(webpage),
|
||||||
|
**self._search_json_ld(webpage, video_id, default={}),
|
||||||
|
}
|
|
@ -371,7 +371,7 @@ def find_video(result):
|
||||||
# not all videos work via arc, e.g. nexx:741:1269984
|
# not all videos work via arc, e.g. nexx:741:1269984
|
||||||
if not video:
|
if not video:
|
||||||
# Reverse engineered from JS code (see getDeviceID function)
|
# Reverse engineered from JS code (see getDeviceID function)
|
||||||
device_id = f'{random.randint(1, 4)}:{int(time.time())}:{random.randint(1e4, 99999)}{random.randint(1, 9)}'
|
device_id = f'{random.randint(1, 4)}:{int(time.time())}:{random.randint(10000, 99999)}{random.randint(1, 9)}'
|
||||||
|
|
||||||
result = self._call_api(domain_id, 'session/init', video_id, data={
|
result = self._call_api(domain_id, 'session/init', video_id, data={
|
||||||
'nxp_devh': device_id,
|
'nxp_devh': device_id,
|
||||||
|
|
|
@ -420,7 +420,7 @@ def _yield_dms_formats(self, api_data, video_id):
|
||||||
'x-request-with': 'https://www.nicovideo.jp',
|
'x-request-with': 'https://www.nicovideo.jp',
|
||||||
})['data']['contentUrl']
|
})['data']['contentUrl']
|
||||||
# Getting all audio formats results in duplicate video formats which we filter out later
|
# Getting all audio formats results in duplicate video formats which we filter out later
|
||||||
dms_fmts = self._extract_m3u8_formats(dms_m3u8_url, video_id)
|
dms_fmts = self._extract_m3u8_formats(dms_m3u8_url, video_id, 'mp4')
|
||||||
|
|
||||||
# m3u8 extraction does not provide audio bitrates, so extract from the API data and fix
|
# m3u8 extraction does not provide audio bitrates, so extract from the API data and fix
|
||||||
for audio_fmt in traverse_obj(dms_fmts, lambda _, v: v['vcodec'] == 'none'):
|
for audio_fmt in traverse_obj(dms_fmts, lambda _, v: v['vcodec'] == 'none'):
|
||||||
|
@ -432,7 +432,6 @@ def _yield_dms_formats(self, api_data, video_id):
|
||||||
'asr': ('samplingRate', {int_or_none}),
|
'asr': ('samplingRate', {int_or_none}),
|
||||||
}), get_all=False),
|
}), get_all=False),
|
||||||
'acodec': 'aac',
|
'acodec': 'aac',
|
||||||
'ext': 'm4a',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Sort before removing dupes to keep the format dicts with the lowest tbr
|
# Sort before removing dupes to keep the format dicts with the lowest tbr
|
||||||
|
|
|
@ -43,14 +43,8 @@ def _real_extract(self, url):
|
||||||
def build_url(url_or_path):
|
def build_url(url_or_path):
|
||||||
return urljoin('https://adult.noodlemagazine.com', url_or_path)
|
return urljoin('https://adult.noodlemagazine.com', url_or_path)
|
||||||
|
|
||||||
headers = {'Referer': url}
|
playlist_info = self._search_json(
|
||||||
player_path = self._html_search_regex(
|
r'window\.playlist\s*=', webpage, video_id, 'playlist info')
|
||||||
r'<iframe[^>]+\bid="iplayer"[^>]+\bsrc="([^"]+)"', webpage, 'player path')
|
|
||||||
player_iframe = self._download_webpage(
|
|
||||||
build_url(player_path), video_id, 'Downloading iframe page', headers=headers)
|
|
||||||
playlist_url = self._search_regex(
|
|
||||||
r'window\.playlistUrl\s*=\s*["\']([^"\']+)["\']', player_iframe, 'playlist url')
|
|
||||||
playlist_info = self._download_json(build_url(playlist_url), video_id, headers=headers)
|
|
||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
for source in traverse_obj(playlist_info, ('sources', lambda _, v: v['file'])):
|
for source in traverse_obj(playlist_info, ('sources', lambda _, v: v['file'])):
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
|
|
||||||
|
|
||||||
class NZOnScreenIE(InfoExtractor):
|
class NZOnScreenIE(InfoExtractor):
|
||||||
_VALID_URL = r'^https?://www\.nzonscreen\.com/title/(?P<id>[^/?#]+)'
|
_VALID_URL = r'https?://www\.nzonscreen\.com/title/(?P<id>[^/?#]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.nzonscreen.com/title/shoop-shoop-diddy-wop-cumma-cumma-wang-dang-1982',
|
'url': 'https://www.nzonscreen.com/title/shoop-shoop-diddy-wop-cumma-cumma-wang-dang-1982',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import functools
|
||||||
import itertools
|
import itertools
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
|
@ -22,13 +23,19 @@
|
||||||
|
|
||||||
|
|
||||||
class PatreonBaseIE(InfoExtractor):
|
class PatreonBaseIE(InfoExtractor):
|
||||||
USER_AGENT = 'Patreon/7.6.28 (Android; Android 11; Scale/2.10)'
|
@functools.cached_property
|
||||||
|
def patreon_user_agent(self):
|
||||||
|
# Patreon mobile UA is needed to avoid triggering Cloudflare anti-bot protection.
|
||||||
|
# Newer UA yields higher res m3u8 formats for locked posts, but gives 401 if not logged-in
|
||||||
|
if self._get_cookies('https://www.patreon.com/').get('session_id'):
|
||||||
|
return 'Patreon/72.2.28 (Android; Android 14; Scale/2.10)'
|
||||||
|
return 'Patreon/7.6.28 (Android; Android 11; Scale/2.10)'
|
||||||
|
|
||||||
def _call_api(self, ep, item_id, query=None, headers=None, fatal=True, note=None):
|
def _call_api(self, ep, item_id, query=None, headers=None, fatal=True, note=None):
|
||||||
if headers is None:
|
if headers is None:
|
||||||
headers = {}
|
headers = {}
|
||||||
if 'User-Agent' not in headers:
|
if 'User-Agent' not in headers:
|
||||||
headers['User-Agent'] = self.USER_AGENT
|
headers['User-Agent'] = self.patreon_user_agent
|
||||||
if query:
|
if query:
|
||||||
query.update({'json-api-version': 1.0})
|
query.update({'json-api-version': 1.0})
|
||||||
|
|
||||||
|
@ -48,6 +55,7 @@ def _call_api(self, ep, item_id, query=None, headers=None, fatal=True, note=None
|
||||||
|
|
||||||
|
|
||||||
class PatreonIE(PatreonBaseIE):
|
class PatreonIE(PatreonBaseIE):
|
||||||
|
IE_NAME = 'patreon'
|
||||||
_VALID_URL = r'https?://(?:www\.)?patreon\.com/(?:creation\?hid=|posts/(?:[\w-]+-)?)(?P<id>\d+)'
|
_VALID_URL = r'https?://(?:www\.)?patreon\.com/(?:creation\?hid=|posts/(?:[\w-]+-)?)(?P<id>\d+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.patreon.com/creation?hid=743933',
|
'url': 'http://www.patreon.com/creation?hid=743933',
|
||||||
|
@ -111,6 +119,7 @@ class PatreonIE(PatreonBaseIE):
|
||||||
'comment_count': int,
|
'comment_count': int,
|
||||||
'channel_is_verified': True,
|
'channel_is_verified': True,
|
||||||
'chapters': 'count:4',
|
'chapters': 'count:4',
|
||||||
|
'timestamp': 1423689666,
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'noplaylist': True,
|
'noplaylist': True,
|
||||||
|
@ -221,6 +230,7 @@ class PatreonIE(PatreonBaseIE):
|
||||||
'thumbnail': r're:^https?://.+',
|
'thumbnail': r're:^https?://.+',
|
||||||
},
|
},
|
||||||
'params': {'skip_download': 'm3u8'},
|
'params': {'skip_download': 'm3u8'},
|
||||||
|
'expected_warnings': ['Failed to parse XML: not well-formed'],
|
||||||
}, {
|
}, {
|
||||||
# multiple attachments/embeds
|
# multiple attachments/embeds
|
||||||
'url': 'https://www.patreon.com/posts/holy-wars-solos-100601977',
|
'url': 'https://www.patreon.com/posts/holy-wars-solos-100601977',
|
||||||
|
@ -326,8 +336,13 @@ def _real_extract(self, url):
|
||||||
if embed_url and (urlh := self._request_webpage(
|
if embed_url and (urlh := self._request_webpage(
|
||||||
embed_url, video_id, 'Checking embed URL', headers=headers,
|
embed_url, video_id, 'Checking embed URL', headers=headers,
|
||||||
fatal=False, errnote=False, expected_status=403)):
|
fatal=False, errnote=False, expected_status=403)):
|
||||||
|
# Vimeo's Cloudflare anti-bot protection will return HTTP status 200 for 404, so we need
|
||||||
|
# to check for "Sorry, we couldn&rsquo;t find that page" in the meta description tag
|
||||||
|
meta_description = clean_html(self._html_search_meta(
|
||||||
|
'description', self._webpage_read_content(urlh, embed_url, video_id, fatal=False), default=None))
|
||||||
# Password-protected vids.io embeds return 403 errors w/o --video-password or session cookie
|
# Password-protected vids.io embeds return 403 errors w/o --video-password or session cookie
|
||||||
if urlh.status != 403 or VidsIoIE.suitable(embed_url):
|
if ((urlh.status != 403 and meta_description != 'Sorry, we couldn’t find that page')
|
||||||
|
or VidsIoIE.suitable(embed_url)):
|
||||||
entries.append(self.url_result(smuggle_url(embed_url, headers)))
|
entries.append(self.url_result(smuggle_url(embed_url, headers)))
|
||||||
|
|
||||||
post_file = traverse_obj(attributes, ('post_file', {dict}))
|
post_file = traverse_obj(attributes, ('post_file', {dict}))
|
||||||
|
@ -419,15 +434,19 @@ def _get_comments(self, post_id):
|
||||||
|
|
||||||
|
|
||||||
class PatreonCampaignIE(PatreonBaseIE):
|
class PatreonCampaignIE(PatreonBaseIE):
|
||||||
|
IE_NAME = 'patreon:campaign'
|
||||||
_VALID_URL = r'https?://(?:www\.)?patreon\.com/(?!rss)(?:(?:m|api/campaigns)/(?P<campaign_id>\d+)|(?P<vanity>[-\w]+))'
|
_VALID_URL = r'''(?x)
|
||||||
|
https?://(?:www\.)?patreon\.com/(?:
|
||||||
|
(?:m|api/campaigns)/(?P<campaign_id>\d+)|
|
||||||
|
(?P<vanity>(?!creation[?/]|posts/|rss[?/])[\w-]+)
|
||||||
|
)(?:/posts)?/?(?:$|[?#])'''
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.patreon.com/dissonancepod/',
|
'url': 'https://www.patreon.com/dissonancepod/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'title': 'Cognitive Dissonance Podcast',
|
'title': 'Cognitive Dissonance Podcast',
|
||||||
'channel_url': 'https://www.patreon.com/dissonancepod',
|
'channel_url': 'https://www.patreon.com/dissonancepod',
|
||||||
'id': '80642',
|
'id': '80642',
|
||||||
'description': 'md5:eb2fa8b83da7ab887adeac34da6b7af7',
|
'description': r're:(?s).*We produce a weekly news podcast focusing on stories that deal with skepticism and religion.*',
|
||||||
'channel_id': '80642',
|
'channel_id': '80642',
|
||||||
'channel': 'Cognitive Dissonance Podcast',
|
'channel': 'Cognitive Dissonance Podcast',
|
||||||
'age_limit': 0,
|
'age_limit': 0,
|
||||||
|
@ -445,7 +464,7 @@ class PatreonCampaignIE(PatreonBaseIE):
|
||||||
'id': '4767637',
|
'id': '4767637',
|
||||||
'channel_id': '4767637',
|
'channel_id': '4767637',
|
||||||
'channel_url': 'https://www.patreon.com/notjustbikes',
|
'channel_url': 'https://www.patreon.com/notjustbikes',
|
||||||
'description': 'md5:9f4b70051216c4d5c58afe580ffc8d0f',
|
'description': r're:(?s).*Not Just Bikes started as a way to explain why we chose to live in the Netherlands.*',
|
||||||
'age_limit': 0,
|
'age_limit': 0,
|
||||||
'channel': 'Not Just Bikes',
|
'channel': 'Not Just Bikes',
|
||||||
'uploader_url': 'https://www.patreon.com/notjustbikes',
|
'uploader_url': 'https://www.patreon.com/notjustbikes',
|
||||||
|
@ -462,7 +481,7 @@ class PatreonCampaignIE(PatreonBaseIE):
|
||||||
'id': '4243769',
|
'id': '4243769',
|
||||||
'channel_id': '4243769',
|
'channel_id': '4243769',
|
||||||
'channel_url': 'https://www.patreon.com/secondthought',
|
'channel_url': 'https://www.patreon.com/secondthought',
|
||||||
'description': 'md5:69c89a3aba43efdb76e85eb023e8de8b',
|
'description': r're:(?s).*Second Thought is an educational YouTube channel.*',
|
||||||
'age_limit': 0,
|
'age_limit': 0,
|
||||||
'channel': 'Second Thought',
|
'channel': 'Second Thought',
|
||||||
'uploader_url': 'https://www.patreon.com/secondthought',
|
'uploader_url': 'https://www.patreon.com/secondthought',
|
||||||
|
@ -482,10 +501,6 @@ class PatreonCampaignIE(PatreonBaseIE):
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def suitable(cls, url):
|
|
||||||
return False if PatreonIE.suitable(url) else super().suitable(url)
|
|
||||||
|
|
||||||
def _entries(self, campaign_id):
|
def _entries(self, campaign_id):
|
||||||
cursor = None
|
cursor = None
|
||||||
params = {
|
params = {
|
||||||
|
@ -512,7 +527,7 @@ def _real_extract(self, url):
|
||||||
|
|
||||||
campaign_id, vanity = self._match_valid_url(url).group('campaign_id', 'vanity')
|
campaign_id, vanity = self._match_valid_url(url).group('campaign_id', 'vanity')
|
||||||
if campaign_id is None:
|
if campaign_id is None:
|
||||||
webpage = self._download_webpage(url, vanity, headers={'User-Agent': self.USER_AGENT})
|
webpage = self._download_webpage(url, vanity, headers={'User-Agent': self.patreon_user_agent})
|
||||||
campaign_id = self._search_nextjs_data(
|
campaign_id = self._search_nextjs_data(
|
||||||
webpage, vanity)['props']['pageProps']['bootstrapEnvelope']['pageBootstrap']['campaign']['data']['id']
|
webpage, vanity)['props']['pageProps']['bootstrapEnvelope']['pageBootstrap']['campaign']['data']['id']
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
import functools
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..compat import functools
|
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
int_or_none,
|
int_or_none,
|
||||||
parse_duration,
|
parse_duration,
|
||||||
|
|
|
@ -628,8 +628,7 @@ def is_404(e):
|
||||||
page_entries = self._extract_entries(webpage, host)
|
page_entries = self._extract_entries(webpage, host)
|
||||||
if not page_entries:
|
if not page_entries:
|
||||||
break
|
break
|
||||||
for e in page_entries:
|
yield from page_entries
|
||||||
yield e
|
|
||||||
if not self._has_more(webpage):
|
if not self._has_more(webpage):
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
|
|
||||||
class RadioFranceIE(InfoExtractor):
|
class RadioFranceIE(InfoExtractor):
|
||||||
_VALID_URL = r'^https?://maison\.radiofrance\.fr/radiovisions/(?P<id>[^?#]+)'
|
_VALID_URL = r'https?://maison\.radiofrance\.fr/radiovisions/(?P<id>[^?#]+)'
|
||||||
IE_NAME = 'radiofrance'
|
IE_NAME = 'radiofrance'
|
||||||
|
|
||||||
_TEST = {
|
_TEST = {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import json
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
|
@ -17,7 +18,7 @@
|
||||||
|
|
||||||
class RedditIE(InfoExtractor):
|
class RedditIE(InfoExtractor):
|
||||||
_NETRC_MACHINE = 'reddit'
|
_NETRC_MACHINE = 'reddit'
|
||||||
_VALID_URL = r'https?://(?P<host>(?:\w+\.)?reddit(?:media)?\.com)/(?P<slug>(?:(?:r|user)/[^/]+/)?comments/(?P<id>[^/?#&]+))'
|
_VALID_URL = r'https?://(?:\w+\.)?reddit(?:media)?\.com/(?P<slug>(?:(?:r|user)/[^/]+/)?comments/(?P<id>[^/?#&]+))'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.reddit.com/r/videos/comments/6rrwyj/that_small_heart_attack/',
|
'url': 'https://www.reddit.com/r/videos/comments/6rrwyj/that_small_heart_attack/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
|
@ -251,15 +252,15 @@ def _get_subtitles(self, video_id):
|
||||||
return {'en': [{'url': caption_url}]}
|
return {'en': [{'url': caption_url}]}
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
host, slug, video_id = self._match_valid_url(url).group('host', 'slug', 'id')
|
slug, video_id = self._match_valid_url(url).group('slug', 'id')
|
||||||
|
|
||||||
|
try:
|
||||||
data = self._download_json(
|
data = self._download_json(
|
||||||
f'https://{host}/{slug}/.json', video_id, fatal=False, expected_status=403)
|
f'https://www.reddit.com/{slug}/.json', video_id, expected_status=403)
|
||||||
if not data:
|
except ExtractorError as e:
|
||||||
fallback_host = 'old.reddit.com' if host != 'old.reddit.com' else 'www.reddit.com'
|
if isinstance(e.cause, json.JSONDecodeError):
|
||||||
self.to_screen(f'{host} request failed, retrying with {fallback_host}')
|
self.raise_login_required('Account authentication is required')
|
||||||
data = self._download_json(
|
raise
|
||||||
f'https://{fallback_host}/{slug}/.json', video_id, expected_status=403)
|
|
||||||
|
|
||||||
if traverse_obj(data, 'error') == 403:
|
if traverse_obj(data, 'error') == 403:
|
||||||
reason = data.get('reason')
|
reason = data.get('reason')
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
|
|
||||||
class ReverbNationIE(InfoExtractor):
|
class ReverbNationIE(InfoExtractor):
|
||||||
_VALID_URL = r'^https?://(?:www\.)?reverbnation\.com/.*?/song/(?P<id>\d+).*?$'
|
_VALID_URL = r'https?://(?:www\.)?reverbnation\.com/.*?/song/(?P<id>\d+).*?$'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.reverbnation.com/alkilados/song/16965047-mona-lisa',
|
'url': 'http://www.reverbnation.com/alkilados/song/16965047-mona-lisa',
|
||||||
'md5': 'c0aaf339bcee189495fdf5a8c8ba8645',
|
'md5': 'c0aaf339bcee189495fdf5a8c8ba8645',
|
||||||
|
|
|
@ -8,14 +8,17 @@
|
||||||
UnsupportedError,
|
UnsupportedError,
|
||||||
clean_html,
|
clean_html,
|
||||||
determine_ext,
|
determine_ext,
|
||||||
|
extract_attributes,
|
||||||
format_field,
|
format_field,
|
||||||
get_element_by_class,
|
get_element_by_class,
|
||||||
|
get_elements_html_by_class,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
join_nonempty,
|
join_nonempty,
|
||||||
parse_count,
|
parse_count,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
traverse_obj,
|
traverse_obj,
|
||||||
unescapeHTML,
|
unescapeHTML,
|
||||||
|
urljoin,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -382,8 +385,10 @@ def entries(self, url, playlist_id):
|
||||||
if isinstance(e.cause, HTTPError) and e.cause.status == 404:
|
if isinstance(e.cause, HTTPError) and e.cause.status == 404:
|
||||||
break
|
break
|
||||||
raise
|
raise
|
||||||
for video_url in re.findall(r'class="[^>"]*videostream__link[^>]+href="([^"]+\.html)"', webpage):
|
for video_url in traverse_obj(
|
||||||
yield self.url_result('https://rumble.com' + video_url)
|
get_elements_html_by_class('videostream__link', webpage), (..., {extract_attributes}, 'href'),
|
||||||
|
):
|
||||||
|
yield self.url_result(urljoin('https://rumble.com', video_url))
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
url, playlist_id = self._match_valid_url(url).groups()
|
url, playlist_id = self._match_valid_url(url).groups()
|
||||||
|
|
76
yt_dlp/extractor/snapchat.py
Normal file
76
yt_dlp/extractor/snapchat.py
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import float_or_none, int_or_none, url_or_none
|
||||||
|
from ..utils.traversal import traverse_obj
|
||||||
|
|
||||||
|
|
||||||
|
class SnapchatSpotlightIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?snapchat\.com/spotlight/(?P<id>\w+)'
|
||||||
|
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.snapchat.com/spotlight/W7_EDlXWTBiXAEEniNoMPwAAYYWtidGhudGZpAX1TKn0JAX1TKnXJAAAAAA',
|
||||||
|
'md5': '46c580f63592d0cbb76e974d2f9f0fcc',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'W7_EDlXWTBiXAEEniNoMPwAAYYWtidGhudGZpAX1TKn0JAX1TKnXJAAAAAA',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Views 💕',
|
||||||
|
'description': '',
|
||||||
|
'thumbnail': r're:https://cf-st\.sc-cdn\.net/d/kKJHIR1QAznRKK9jgYYDq\.256\.IRZXSOY',
|
||||||
|
'duration': 4.665,
|
||||||
|
'timestamp': 1637777831.369,
|
||||||
|
'upload_date': '20211124',
|
||||||
|
'repost_count': int,
|
||||||
|
'uploader': 'shreypatel57',
|
||||||
|
'uploader_url': 'https://www.snapchat.com/add/shreypatel57',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.snapchat.com/spotlight/W7_EDlXWTBiXAEEniNoMPwAAYcnVjYWdwcGV1AZEaIYn5AZEaIYnrAAAAAQ',
|
||||||
|
'md5': '4cd9626458c1a0e3e6dbe72c544a9ec2',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'W7_EDlXWTBiXAEEniNoMPwAAYcnVjYWdwcGV1AZEaIYn5AZEaIYnrAAAAAQ',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Spotlight Snap',
|
||||||
|
'description': 'How he flirt her teacher🤭🤭🤩😍 #kdrama#cdrama #dramaclips #dramaspotlight',
|
||||||
|
'thumbnail': r're:https://cf-st\.sc-cdn\.net/i/ztfr6xFs0FOcFhwVczWfj\.256\.IRZXSOY',
|
||||||
|
'duration': 10.91,
|
||||||
|
'timestamp': 1722720291.307,
|
||||||
|
'upload_date': '20240803',
|
||||||
|
'view_count': int,
|
||||||
|
'repost_count': int,
|
||||||
|
'uploader': 'ganda0535',
|
||||||
|
'uploader_url': 'https://www.snapchat.com/add/ganda0535',
|
||||||
|
'tags': ['#dramaspotlight', '#dramaclips', '#cdrama', '#kdrama'],
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
page_props = self._search_nextjs_data(webpage, video_id)['props']['pageProps']
|
||||||
|
video_data = traverse_obj(page_props, (
|
||||||
|
'spotlightFeed', 'spotlightStories',
|
||||||
|
lambda _, v: v['story']['storyId']['value'] == video_id, 'metadata', any), None)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'ext': 'mp4',
|
||||||
|
**traverse_obj(video_data, ('videoMetadata', {
|
||||||
|
'title': ('name', {str}),
|
||||||
|
'description': ('description', {str}),
|
||||||
|
'timestamp': ('uploadDateMs', {lambda x: float_or_none(x, 1000)}),
|
||||||
|
'view_count': ('viewCount', {int_or_none}, {lambda x: None if x == -1 else x}),
|
||||||
|
'repost_count': ('shareCount', {int_or_none}),
|
||||||
|
'url': ('contentUrl', {url_or_none}),
|
||||||
|
'width': ('width', {int_or_none}),
|
||||||
|
'height': ('height', {int_or_none}),
|
||||||
|
'duration': ('durationMs', {lambda x: float_or_none(x, 1000)}),
|
||||||
|
'thumbnail': ('thumbnailUrl', {url_or_none}),
|
||||||
|
'uploader': ('creator', 'personCreator', 'username', {str}),
|
||||||
|
'uploader_url': ('creator', 'personCreator', 'url', {url_or_none}),
|
||||||
|
})),
|
||||||
|
**traverse_obj(video_data, {
|
||||||
|
'description': ('description', {str}),
|
||||||
|
'tags': ('hashtags', ..., {str}),
|
||||||
|
'view_count': ('engagementStats', 'viewCount', {int_or_none}, {lambda x: None if x == -1 else x}),
|
||||||
|
'repost_count': ('engagementStats', 'shareCount', {int_or_none}),
|
||||||
|
}),
|
||||||
|
}
|
|
@ -2,7 +2,13 @@
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import js_to_json, str_or_none, traverse_obj
|
from ..networking import HEADRequest
|
||||||
|
from ..utils import (
|
||||||
|
determine_ext,
|
||||||
|
js_to_json,
|
||||||
|
str_or_none,
|
||||||
|
)
|
||||||
|
from ..utils.traversal import traverse_obj
|
||||||
|
|
||||||
|
|
||||||
class SubstackIE(InfoExtractor):
|
class SubstackIE(InfoExtractor):
|
||||||
|
@ -43,6 +49,19 @@ class SubstackIE(InfoExtractor):
|
||||||
'uploader': "Andrew Zimmern's Spilled Milk ",
|
'uploader': "Andrew Zimmern's Spilled Milk ",
|
||||||
'uploader_id': '577659',
|
'uploader_id': '577659',
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
# Podcast that needs its file extension resolved to mp3
|
||||||
|
'url': 'https://persuasion1.substack.com/p/summers',
|
||||||
|
'md5': '1456a755d46084744facdfac9edf900f',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '141970405',
|
||||||
|
'ext': 'mp3',
|
||||||
|
'title': 'Larry Summers on What Went Wrong on Campus',
|
||||||
|
'description': 'Yascha Mounk and Larry Summers also discuss the promise and perils of artificial intelligence.',
|
||||||
|
'thumbnail': r're:https://substackcdn\.com/image/.+\.jpeg',
|
||||||
|
'uploader': 'Persuasion',
|
||||||
|
'uploader_id': '61579',
|
||||||
|
},
|
||||||
}]
|
}]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -89,7 +108,15 @@ def _real_extract(self, url):
|
||||||
post_type = webpage_info['post']['type']
|
post_type = webpage_info['post']['type']
|
||||||
formats, subtitles = [], {}
|
formats, subtitles = [], {}
|
||||||
if post_type == 'podcast':
|
if post_type == 'podcast':
|
||||||
formats, subtitles = [{'url': webpage_info['post']['podcast_url']}], {}
|
fmt = {'url': webpage_info['post']['podcast_url']}
|
||||||
|
if not determine_ext(fmt['url'], default_ext=None):
|
||||||
|
# The redirected format URL expires but the original URL doesn't,
|
||||||
|
# so we only want to extract the extension from this request
|
||||||
|
fmt['ext'] = determine_ext(self._request_webpage(
|
||||||
|
HEADRequest(fmt['url']), display_id,
|
||||||
|
'Resolving podcast file extension',
|
||||||
|
'Podcast URL is invalid').url)
|
||||||
|
formats.append(fmt)
|
||||||
elif post_type == 'video':
|
elif post_type == 'video':
|
||||||
formats, subtitles = self._extract_video_formats(webpage_info['post']['videoUpload']['id'], canonical_url)
|
formats, subtitles = self._extract_video_formats(webpage_info['post']['videoUpload']['id'], canonical_url)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -472,7 +472,7 @@ def _real_extract(self, url):
|
||||||
title = self._og_search_title(webpage)
|
title = self._og_search_title(webpage)
|
||||||
|
|
||||||
urql_state = self._search_json(
|
urql_state = self._search_json(
|
||||||
r'window\.svt\.nyh\.urqlState\s*=', webpage, 'json data', display_id)
|
r'window\.svt\.(?:nyh\.)?urqlState\s*=', webpage, 'json data', display_id)
|
||||||
|
|
||||||
data = traverse_obj(urql_state, (..., 'data', {str}, {json.loads}), get_all=False) or {}
|
data = traverse_obj(urql_state, (..., 'data', {str}, {json.loads}), get_all=False) or {}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
|
|
||||||
class Tele13IE(InfoExtractor):
|
class Tele13IE(InfoExtractor):
|
||||||
_VALID_URL = r'^https?://(?:www\.)?t13\.cl/videos(?:/[^/]+)+/(?P<id>[\w-]+)'
|
_VALID_URL = r'https?://(?:www\.)?t13\.cl/videos(?:/[^/]+)+/(?P<id>[\w-]+)'
|
||||||
_TESTS = [
|
_TESTS = [
|
||||||
{
|
{
|
||||||
'url': 'http://www.t13.cl/videos/actualidad/el-circulo-de-hierro-de-michelle-bachelet-en-su-regreso-a-la-moneda',
|
'url': 'http://www.t13.cl/videos/actualidad/el-circulo-de-hierro-de-michelle-bachelet-en-su-regreso-a-la-moneda',
|
||||||
|
|
|
@ -2,15 +2,69 @@
|
||||||
import re
|
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,
|
||||||
|
join_nonempty,
|
||||||
str_or_none,
|
str_or_none,
|
||||||
try_get,
|
traverse_obj,
|
||||||
|
update_url,
|
||||||
|
url_or_none,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TelecincoIE(InfoExtractor):
|
class TelecincoBaseIE(InfoExtractor):
|
||||||
|
def _parse_content(self, content, url):
|
||||||
|
video_id = content['dataMediaId']
|
||||||
|
config = self._download_json(
|
||||||
|
content['dataConfig'], video_id, 'Downloading config JSON')
|
||||||
|
services = config['services']
|
||||||
|
caronte = self._download_json(services['caronte'], video_id)
|
||||||
|
if traverse_obj(caronte, ('dls', 0, 'drm', {bool})):
|
||||||
|
self.report_drm(video_id)
|
||||||
|
|
||||||
|
stream = caronte['dls'][0]['stream']
|
||||||
|
headers = {
|
||||||
|
'Referer': url,
|
||||||
|
'Origin': re.match(r'https?://[^/]+', url).group(0),
|
||||||
|
}
|
||||||
|
geo_headers = {**headers, **self.geo_verification_headers()}
|
||||||
|
|
||||||
|
try:
|
||||||
|
cdn = self._download_json(
|
||||||
|
caronte['cerbero'], video_id, data=json.dumps({
|
||||||
|
'bbx': caronte['bbx'],
|
||||||
|
'gbx': self._download_json(services['gbx'], video_id)['gbx'],
|
||||||
|
}).encode(), headers={
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
**geo_headers,
|
||||||
|
})['tokens']['1']['cdn']
|
||||||
|
except ExtractorError as error:
|
||||||
|
if isinstance(error.cause, HTTPError) and error.cause.status == 403:
|
||||||
|
error_code = traverse_obj(
|
||||||
|
self._webpage_read_content(error.cause.response, caronte['cerbero'], video_id, fatal=False),
|
||||||
|
({json.loads}, 'code', {int}))
|
||||||
|
if error_code == 4038:
|
||||||
|
self.raise_geo_restricted(countries=['ES'])
|
||||||
|
raise
|
||||||
|
|
||||||
|
formats = self._extract_m3u8_formats(
|
||||||
|
update_url(stream, query=cdn), video_id, 'mp4', m3u8_id='hls', headers=geo_headers)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': traverse_obj(config, ('info', 'title', {str})),
|
||||||
|
'formats': formats,
|
||||||
|
'thumbnail': (traverse_obj(content, ('dataPoster', {url_or_none}))
|
||||||
|
or traverse_obj(config, 'poster', 'imageUrl', expected_type=url_or_none)),
|
||||||
|
'duration': traverse_obj(content, ('dataDuration', {int_or_none})),
|
||||||
|
'http_headers': headers,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TelecincoIE(TelecincoBaseIE):
|
||||||
IE_DESC = 'telecinco.es, cuatro.com and mediaset.es'
|
IE_DESC = 'telecinco.es, cuatro.com and mediaset.es'
|
||||||
_VALID_URL = r'https?://(?:www\.)?(?:telecinco\.es|cuatro\.com|mediaset\.es)/(?:[^/]+/)+(?P<id>.+?)\.html'
|
_VALID_URL = r'https?://(?:www\.)?(?:telecinco\.es|cuatro\.com|mediaset\.es)/(?:[^/]+/)+(?P<id>.+?)\.html'
|
||||||
|
|
||||||
|
@ -30,6 +84,7 @@ class TelecincoIE(InfoExtractor):
|
||||||
'duration': 662,
|
'duration': 662,
|
||||||
},
|
},
|
||||||
}],
|
}],
|
||||||
|
'skip': 'HTTP Error 410 Gone',
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://www.cuatro.com/deportes/futbol/barcelona/Leo_Messi-Champions-Roma_2_2052780128.html',
|
'url': 'http://www.cuatro.com/deportes/futbol/barcelona/Leo_Messi-Champions-Roma_2_2052780128.html',
|
||||||
'md5': 'c86fe0d99e3bdb46b7950d38bf6ef12a',
|
'md5': 'c86fe0d99e3bdb46b7950d38bf6ef12a',
|
||||||
|
@ -40,23 +95,24 @@ class TelecincoIE(InfoExtractor):
|
||||||
'description': 'md5:a62ecb5f1934fc787107d7b9a2262805',
|
'description': 'md5:a62ecb5f1934fc787107d7b9a2262805',
|
||||||
'duration': 79,
|
'duration': 79,
|
||||||
},
|
},
|
||||||
|
'skip': 'Redirects to main page',
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://www.mediaset.es/12meses/campanas/doylacara/conlatratanohaytrato/Ayudame-dar-cara-trata-trato_2_1986630220.html',
|
'url': 'http://www.mediaset.es/12meses/campanas/doylacara/conlatratanohaytrato/Ayudame-dar-cara-trata-trato_2_1986630220.html',
|
||||||
'md5': 'eddb50291df704ce23c74821b995bcac',
|
'md5': '5ce057f43f30b634fbaf0f18c71a140a',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'aywerkD2Sv1vGNqq9b85Q2',
|
'id': 'aywerkD2Sv1vGNqq9b85Q2',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': '#DOYLACARA. Con la trata no hay trato',
|
'title': '#DOYLACARA. Con la trata no hay trato',
|
||||||
'description': 'md5:2771356ff7bfad9179c5f5cd954f1477',
|
|
||||||
'duration': 50,
|
'duration': 50,
|
||||||
|
'thumbnail': 'https://album.mediaset.es/eimg/2017/11/02/1tlQLO5Q3mtKT24f3EaC24.jpg',
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
# video in opening's content
|
# video in opening's content
|
||||||
'url': 'https://www.telecinco.es/vivalavida/fiorella-sobrina-edmundo-arrocet-entrevista_18_2907195140.html',
|
'url': 'https://www.telecinco.es/vivalavida/fiorella-sobrina-edmundo-arrocet-entrevista_18_2907195140.html',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '2907195140',
|
'id': '1691427',
|
||||||
'title': 'La surrealista entrevista a la sobrina de Edmundo Arrocet: "No puedes venir aquí y tomarnos por tontos"',
|
'title': 'La surrealista entrevista a la sobrina de Edmundo Arrocet: "No puedes venir aquí y tomarnos por tontos"',
|
||||||
'description': 'md5:73f340a7320143d37ab895375b2bf13a',
|
'description': r're:Fiorella, la sobrina de Edmundo Arrocet, concedió .{727}',
|
||||||
},
|
},
|
||||||
'playlist': [{
|
'playlist': [{
|
||||||
'md5': 'adb28c37238b675dad0f042292f209a7',
|
'md5': 'adb28c37238b675dad0f042292f209a7',
|
||||||
|
@ -65,6 +121,7 @@ class TelecincoIE(InfoExtractor):
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'La surrealista entrevista a la sobrina de Edmundo Arrocet: "No puedes venir aquí y tomarnos por tontos"',
|
'title': 'La surrealista entrevista a la sobrina de Edmundo Arrocet: "No puedes venir aquí y tomarnos por tontos"',
|
||||||
'duration': 1015,
|
'duration': 1015,
|
||||||
|
'thumbnail': 'https://album.mediaset.es/eimg/2020/02/29/5opaC37lUhKlZ7FoDhiVC.jpg',
|
||||||
},
|
},
|
||||||
}],
|
}],
|
||||||
'params': {
|
'params': {
|
||||||
|
@ -81,66 +138,29 @@ class TelecincoIE(InfoExtractor):
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _parse_content(self, content, url):
|
|
||||||
video_id = content['dataMediaId']
|
|
||||||
config = self._download_json(
|
|
||||||
content['dataConfig'], video_id, 'Downloading config JSON')
|
|
||||||
title = config['info']['title']
|
|
||||||
services = config['services']
|
|
||||||
caronte = self._download_json(services['caronte'], video_id)
|
|
||||||
stream = caronte['dls'][0]['stream']
|
|
||||||
headers = self.geo_verification_headers()
|
|
||||||
headers.update({
|
|
||||||
'Content-Type': 'application/json;charset=UTF-8',
|
|
||||||
'Origin': re.match(r'https?://[^/]+', url).group(0),
|
|
||||||
})
|
|
||||||
cdn = self._download_json(
|
|
||||||
caronte['cerbero'], video_id, data=json.dumps({
|
|
||||||
'bbx': caronte['bbx'],
|
|
||||||
'gbx': self._download_json(services['gbx'], video_id)['gbx'],
|
|
||||||
}).encode(), headers=headers)['tokens']['1']['cdn']
|
|
||||||
formats = self._extract_m3u8_formats(
|
|
||||||
stream + '?' + cdn, video_id, 'mp4', 'm3u8_native', m3u8_id='hls')
|
|
||||||
|
|
||||||
return {
|
|
||||||
'id': video_id,
|
|
||||||
'title': title,
|
|
||||||
'formats': formats,
|
|
||||||
'thumbnail': content.get('dataPoster') or config.get('poster', {}).get('imageUrl'),
|
|
||||||
'duration': int_or_none(content.get('dataDuration')),
|
|
||||||
}
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
display_id = self._match_id(url)
|
display_id = self._match_id(url)
|
||||||
webpage = self._download_webpage(url, display_id)
|
webpage = self._download_webpage(url, display_id)
|
||||||
article = self._parse_json(self._search_regex(
|
article = self._search_json(
|
||||||
r'window\.\$REACTBASE_STATE\.article(?:_multisite)?\s*=\s*({.+})',
|
r'window\.\$REACTBASE_STATE\.article(?:_multisite)?\s*=',
|
||||||
webpage, 'article'), display_id)['article']
|
webpage, 'article', display_id)['article']
|
||||||
title = article.get('title')
|
description = traverse_obj(article, ('leadParagraph', {clean_html}, filter))
|
||||||
description = clean_html(article.get('leadParagraph')) or ''
|
|
||||||
if article.get('editorialType') != 'VID':
|
if article.get('editorialType') != 'VID':
|
||||||
entries = []
|
entries = []
|
||||||
body = [article.get('opening')]
|
|
||||||
body.extend(try_get(article, lambda x: x['body'], list) or [])
|
for p in traverse_obj(article, ((('opening', all), 'body'), lambda _, v: v['content'])):
|
||||||
for p in body:
|
content = p['content']
|
||||||
if not isinstance(p, dict):
|
|
||||||
continue
|
|
||||||
content = p.get('content')
|
|
||||||
if not content:
|
|
||||||
continue
|
|
||||||
type_ = p.get('type')
|
type_ = p.get('type')
|
||||||
if type_ == 'paragraph':
|
if type_ == 'paragraph' and isinstance(content, str):
|
||||||
content_str = str_or_none(content)
|
description = join_nonempty(description, content, delim='')
|
||||||
if content_str:
|
elif type_ == 'video' and isinstance(content, dict):
|
||||||
description += content_str
|
|
||||||
continue
|
|
||||||
if type_ == 'video' and isinstance(content, dict):
|
|
||||||
entries.append(self._parse_content(content, url))
|
entries.append(self._parse_content(content, url))
|
||||||
|
|
||||||
return self.playlist_result(
|
return self.playlist_result(
|
||||||
entries, str_or_none(article.get('id')), title, description)
|
entries, str_or_none(article.get('id')),
|
||||||
content = article['opening']['content']
|
traverse_obj(article, ('title', {str})), clean_html(description))
|
||||||
info = self._parse_content(content, url)
|
|
||||||
info.update({
|
info = self._parse_content(article['opening']['content'], url)
|
||||||
'description': description,
|
info['description'] = description
|
||||||
})
|
|
||||||
return info
|
return info
|
||||||
|
|
|
@ -542,16 +542,12 @@ def _extract_web_formats(self, aweme_detail):
|
||||||
**COMMON_FORMAT_INFO,
|
**COMMON_FORMAT_INFO,
|
||||||
'format_id': 'download',
|
'format_id': 'download',
|
||||||
'url': self._proto_relative_url(download_url),
|
'url': self._proto_relative_url(download_url),
|
||||||
|
'format_note': 'watermarked',
|
||||||
|
'preference': -2,
|
||||||
})
|
})
|
||||||
|
|
||||||
self._remove_duplicate_formats(formats)
|
self._remove_duplicate_formats(formats)
|
||||||
|
|
||||||
for f in traverse_obj(formats, lambda _, v: 'unwatermarked' not in v['url']):
|
|
||||||
f.update({
|
|
||||||
'format_note': join_nonempty(f.get('format_note'), 'watermarked', delim=', '),
|
|
||||||
'preference': f.get('preference') or -2,
|
|
||||||
})
|
|
||||||
|
|
||||||
# Is it a slideshow with only audio for download?
|
# Is it a slideshow with only audio for download?
|
||||||
if not formats and traverse_obj(aweme_detail, ('music', 'playUrl', {url_or_none})):
|
if not formats and traverse_obj(aweme_detail, ('music', 'playUrl', {url_or_none})):
|
||||||
audio_url = aweme_detail['music']['playUrl']
|
audio_url = aweme_detail['music']['playUrl']
|
||||||
|
@ -565,7 +561,8 @@ def _extract_web_formats(self, aweme_detail):
|
||||||
'vcodec': 'none',
|
'vcodec': 'none',
|
||||||
})
|
})
|
||||||
|
|
||||||
return formats
|
# Filter out broken formats, see https://github.com/yt-dlp/yt-dlp/issues/11034
|
||||||
|
return [f for f in formats if urllib.parse.urlparse(f['url']).hostname != 'www.tiktok.com']
|
||||||
|
|
||||||
def _parse_aweme_video_web(self, aweme_detail, webpage_url, video_id, extract_flat=False):
|
def _parse_aweme_video_web(self, aweme_detail, webpage_url, video_id, extract_flat=False):
|
||||||
author_info = traverse_obj(aweme_detail, (('authorInfo', 'author', None), {
|
author_info = traverse_obj(aweme_detail, (('authorInfo', 'author', None), {
|
||||||
|
|
|
@ -236,7 +236,7 @@ class TubeTuGrazSeriesIE(TubeTuGrazBaseIE):
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
'min_playlist_count': 4,
|
'playlist_mincount': 4,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
js_to_json,
|
js_to_json,
|
||||||
|
strip_or_none,
|
||||||
traverse_obj,
|
traverse_obj,
|
||||||
url_or_none,
|
url_or_none,
|
||||||
urlencode_postdata,
|
urlencode_postdata,
|
||||||
|
@ -132,12 +133,12 @@ def _real_extract(self, url):
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'title': title,
|
'title': strip_or_none(title),
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
'subtitles': subtitles,
|
'subtitles': subtitles,
|
||||||
'season_number': int_or_none(season_number),
|
'season_number': int_or_none(season_number),
|
||||||
'episode_number': int_or_none(episode_number),
|
'episode_number': int_or_none(episode_number),
|
||||||
'episode': episode_title,
|
'episode': strip_or_none(episode_title),
|
||||||
**traverse_obj(video_data, {
|
**traverse_obj(video_data, {
|
||||||
'description': ('description', {str}),
|
'description': ('description', {str}),
|
||||||
'duration': ('duration', {int_or_none}),
|
'duration': ('duration', {int_or_none}),
|
||||||
|
|
|
@ -6,11 +6,12 @@
|
||||||
str_or_none,
|
str_or_none,
|
||||||
strip_or_none,
|
strip_or_none,
|
||||||
traverse_obj,
|
traverse_obj,
|
||||||
|
update_url_query,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TVerIE(InfoExtractor):
|
class TVerIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?tver\.jp/(?:(?P<type>lp|corner|series|episodes?|feature|tokyo2020/video|olympic/paris2024/video)/)+(?P<id>[a-zA-Z0-9]+)'
|
_VALID_URL = r'https?://(?:www\.)?tver\.jp/(?:(?P<type>lp|corner|series|episodes?|feature)/)+(?P<id>[a-zA-Z0-9]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'skip': 'videos are only available for 7 days',
|
'skip': 'videos are only available for 7 days',
|
||||||
'url': 'https://tver.jp/episodes/ep83nf3w4p',
|
'url': 'https://tver.jp/episodes/ep83nf3w4p',
|
||||||
|
@ -21,80 +22,115 @@ class TVerIE(InfoExtractor):
|
||||||
'episode': '売り場席巻のチーズSP&財前直見×森泉親子の脱東京暮らし密着!',
|
'episode': '売り場席巻のチーズSP&財前直見×森泉親子の脱東京暮らし密着!',
|
||||||
'alt_title': '売り場席巻のチーズSP&財前直見×森泉親子の脱東京暮らし密着!',
|
'alt_title': '売り場席巻のチーズSP&財前直見×森泉親子の脱東京暮らし密着!',
|
||||||
'channel': 'テレビ朝日',
|
'channel': 'テレビ朝日',
|
||||||
|
'id': 'ep83nf3w4p',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'onair_label': '5月3日(火)放送分',
|
||||||
|
'ext_title': '家事ヤロウ!!! 売り場席巻のチーズSP&財前直見×森泉親子の脱東京暮らし密着! テレビ朝日 5月3日(火)放送分',
|
||||||
},
|
},
|
||||||
'add_ie': ['BrightcoveNew'],
|
'add_ie': ['BrightcoveNew'],
|
||||||
}, {
|
|
||||||
'url': 'https://tver.jp/olympic/paris2024/video/6359578055112/',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '6359578055112',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': '堀米雄斗 金メダルで五輪連覇!「みんなの応援が最後に乗れたカギ」',
|
|
||||||
'timestamp': 1722279928,
|
|
||||||
'upload_date': '20240729',
|
|
||||||
'tags': ['20240729', 'japanese', 'japanmedal', 'paris'],
|
|
||||||
'uploader_id': '4774017240001',
|
|
||||||
'thumbnail': r're:https?://[^/?#]+boltdns\.net/[^?#]+/1920x1080/match/image\.jpg',
|
|
||||||
'duration': 670.571,
|
|
||||||
},
|
|
||||||
'params': {'skip_download': 'm3u8'},
|
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://tver.jp/corner/f0103888',
|
'url': 'https://tver.jp/corner/f0103888',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://tver.jp/lp/f0033031',
|
'url': 'https://tver.jp/lp/f0033031',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://tver.jp/series/srtxft431v',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'srtxft431v',
|
||||||
|
'title': '名探偵コナン',
|
||||||
|
},
|
||||||
|
'playlist': [
|
||||||
|
{
|
||||||
|
'md5': '779ffd97493ed59b0a6277ea726b389e',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'ref:conan-1137-241005',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': '名探偵コナン #1137「行列店、味変の秘密」',
|
||||||
|
'uploader_id': '5330942432001',
|
||||||
|
'tags': [],
|
||||||
|
'channel': '読売テレビ',
|
||||||
|
'series': '名探偵コナン',
|
||||||
|
'description': 'md5:601fccc1d2430d942a2c8068c4b33eb5',
|
||||||
|
'episode': '#1137「行列店、味変の秘密」',
|
||||||
|
'duration': 1469.077,
|
||||||
|
'timestamp': 1728030405,
|
||||||
|
'upload_date': '20241004',
|
||||||
|
'alt_title': '名探偵コナン #1137「行列店、味変の秘密」 読売テレビ 10月5日(土)放送分',
|
||||||
|
'thumbnail': r're:https://.+\.jpg',
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
}, {
|
||||||
|
'url': 'https://tver.jp/series/sru35hwdd2',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'sru35hwdd2',
|
||||||
|
'title': '神回だけ見せます!',
|
||||||
|
},
|
||||||
|
'playlist_count': 11,
|
||||||
|
}, {
|
||||||
|
'url': 'https://tver.jp/series/srkq2shp9d',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/%s/default_default/index.html?videoId=%s'
|
BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/%s/default_default/index.html?videoId=%s'
|
||||||
_PLATFORM_UID = None
|
_HEADERS = {'x-tver-platform-type': 'web'}
|
||||||
_PLATFORM_TOKEN = None
|
_PLATFORM_QUERY = {}
|
||||||
|
|
||||||
def _real_initialize(self):
|
def _real_initialize(self):
|
||||||
create_response = self._download_json(
|
session_info = self._download_json(
|
||||||
'https://platform-api.tver.jp/v2/api/platform_users/browser/create', None,
|
'https://platform-api.tver.jp/v2/api/platform_users/browser/create',
|
||||||
note='Creating session', data=b'device_type=pc', headers={
|
None, 'Creating session', data=b'device_type=pc')
|
||||||
'Origin': 'https://s.tver.jp',
|
self._PLATFORM_QUERY = traverse_obj(session_info, ('result', {
|
||||||
'Referer': 'https://s.tver.jp/',
|
'platform_uid': 'platform_uid',
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'platform_token': 'platform_token',
|
||||||
|
}))
|
||||||
|
|
||||||
|
def _call_platform_api(self, path, video_id, note=None, fatal=True, query=None):
|
||||||
|
return self._download_json(
|
||||||
|
f'https://platform-api.tver.jp/service/api/{path}', video_id, note,
|
||||||
|
fatal=fatal, headers=self._HEADERS, query={
|
||||||
|
**self._PLATFORM_QUERY,
|
||||||
|
**(query or {}),
|
||||||
})
|
})
|
||||||
self._PLATFORM_UID = traverse_obj(create_response, ('result', 'platform_uid'))
|
|
||||||
self._PLATFORM_TOKEN = traverse_obj(create_response, ('result', 'platform_token'))
|
def _yield_episode_ids_for_series(self, series_id):
|
||||||
|
seasons_info = self._download_json(
|
||||||
|
f'https://service-api.tver.jp/api/v1/callSeriesSeasons/{series_id}',
|
||||||
|
series_id, 'Downloading seasons info', headers=self._HEADERS)
|
||||||
|
for season_id in traverse_obj(
|
||||||
|
seasons_info, ('result', 'contents', lambda _, v: v['type'] == 'season', 'content', 'id', {str})):
|
||||||
|
episodes_info = self._call_platform_api(
|
||||||
|
f'v1/callSeasonEpisodes/{season_id}', series_id, f'Downloading season {season_id} episodes info')
|
||||||
|
yield from traverse_obj(episodes_info, (
|
||||||
|
'result', 'contents', lambda _, v: v['type'] == 'episode', 'content', 'id', {str}))
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id, video_type = self._match_valid_url(url).group('id', 'type')
|
video_id, video_type = self._match_valid_url(url).group('id', 'type')
|
||||||
|
|
||||||
if video_type == 'olympic/paris2024/video':
|
if video_type == 'series':
|
||||||
# Player ID is taken from .content.brightcove.E200.pro.pc.account_id:
|
series_info = self._call_platform_api(
|
||||||
# https://tver.jp/olympic/paris2024/req/api/hook?q=https%3A%2F%2Folympic-assets.tver.jp%2Fweb-static%2Fjson%2Fconfig.json&d=
|
f'v2/callSeries/{video_id}', video_id, 'Downloading series info')
|
||||||
return self.url_result(smuggle_url(
|
return self.playlist_from_matches(
|
||||||
self.BRIGHTCOVE_URL_TEMPLATE % ('4774017240001', video_id),
|
self._yield_episode_ids_for_series(video_id), video_id,
|
||||||
{'geo_countries': ['JP']}), 'BrightcoveNew')
|
traverse_obj(series_info, ('result', 'content', 'content', 'title', {str})),
|
||||||
|
ie=TVerIE, getter=lambda x: f'https://tver.jp/episodes/{x}')
|
||||||
|
|
||||||
elif video_type not in {'series', 'episodes'}:
|
if video_type != 'episodes':
|
||||||
webpage = self._download_webpage(url, video_id, note='Resolving to new URL')
|
webpage = self._download_webpage(url, video_id, note='Resolving to new URL')
|
||||||
video_id = self._match_id(self._search_regex(
|
video_id = self._match_id(self._search_regex(
|
||||||
(r'canonical"\s*href="(https?://tver\.jp/[^"]+)"', r'&link=(https?://tver\.jp/[^?&]+)[?&]'),
|
(r'canonical"\s*href="(https?://tver\.jp/[^"]+)"', r'&link=(https?://tver\.jp/[^?&]+)[?&]'),
|
||||||
webpage, 'url regex'))
|
webpage, 'url regex'))
|
||||||
|
|
||||||
episode_info = self._download_json(
|
episode_info = self._call_platform_api(
|
||||||
f'https://platform-api.tver.jp/service/api/v1/callEpisode/{video_id}?require_data=mylist,later[epefy106ur],good[epefy106ur],resume[epefy106ur]',
|
f'v1/callEpisode/{video_id}', video_id, 'Downloading episode info', fatal=False, query={
|
||||||
video_id, fatal=False,
|
'require_data': 'mylist,later[epefy106ur],good[epefy106ur],resume[epefy106ur]',
|
||||||
query={
|
|
||||||
'platform_uid': self._PLATFORM_UID,
|
|
||||||
'platform_token': self._PLATFORM_TOKEN,
|
|
||||||
}, headers={
|
|
||||||
'x-tver-platform-type': 'web',
|
|
||||||
})
|
})
|
||||||
episode_content = traverse_obj(
|
episode_content = traverse_obj(
|
||||||
episode_info, ('result', 'episode', 'content')) or {}
|
episode_info, ('result', 'episode', 'content')) or {}
|
||||||
|
|
||||||
|
version = traverse_obj(episode_content, ('version', {str_or_none}), default='5')
|
||||||
video_info = self._download_json(
|
video_info = self._download_json(
|
||||||
f'https://statics.tver.jp/content/episode/{video_id}.json', video_id,
|
f'https://statics.tver.jp/content/episode/{video_id}.json', video_id, 'Downloading video info',
|
||||||
query={
|
query={'v': version}, headers={'Referer': 'https://tver.jp/'})
|
||||||
'v': str_or_none(episode_content.get('version')) or '5',
|
|
||||||
}, headers={
|
|
||||||
'Origin': 'https://tver.jp',
|
|
||||||
'Referer': 'https://tver.jp/',
|
|
||||||
})
|
|
||||||
p_id = video_info['video']['accountID']
|
p_id = video_info['video']['accountID']
|
||||||
r_id = traverse_obj(video_info, ('video', ('videoRefID', 'videoID')), get_all=False)
|
r_id = traverse_obj(video_info, ('video', ('videoRefID', 'videoID')), get_all=False)
|
||||||
if not r_id:
|
if not r_id:
|
||||||
|
@ -110,6 +146,23 @@ def _real_extract(self, url):
|
||||||
provider = str_or_none(episode_content.get('productionProviderName'))
|
provider = str_or_none(episode_content.get('productionProviderName'))
|
||||||
onair_label = str_or_none(episode_content.get('broadcastDateLabel'))
|
onair_label = str_or_none(episode_content.get('broadcastDateLabel'))
|
||||||
|
|
||||||
|
thumbnails = [
|
||||||
|
{
|
||||||
|
'id': quality,
|
||||||
|
'url': update_url_query(
|
||||||
|
f'https://statics.tver.jp/images/content/thumbnail/episode/{quality}/{video_id}.jpg',
|
||||||
|
{'v': version}),
|
||||||
|
'width': width,
|
||||||
|
'height': height,
|
||||||
|
}
|
||||||
|
for quality, width, height in [
|
||||||
|
('small', 480, 270),
|
||||||
|
('medium', 640, 360),
|
||||||
|
('large', 960, 540),
|
||||||
|
('xlarge', 1280, 720),
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'_type': 'url_transparent',
|
'_type': 'url_transparent',
|
||||||
'title': title,
|
'title': title,
|
||||||
|
@ -119,6 +172,7 @@ def _real_extract(self, url):
|
||||||
'alt_title': join_nonempty(title, provider, onair_label, delim=' '),
|
'alt_title': join_nonempty(title, provider, onair_label, delim=' '),
|
||||||
'channel': provider,
|
'channel': provider,
|
||||||
'description': str_or_none(video_info.get('description')),
|
'description': str_or_none(video_info.get('description')),
|
||||||
|
'thumbnails': thumbnails,
|
||||||
'url': smuggle_url(
|
'url': smuggle_url(
|
||||||
self.BRIGHTCOVE_URL_TEMPLATE % (p_id, r_id), {'geo_countries': ['JP']}),
|
self.BRIGHTCOVE_URL_TEMPLATE % (p_id, r_id), {'geo_countries': ['JP']}),
|
||||||
'ie_key': 'BrightcoveNew',
|
'ie_key': 'BrightcoveNew',
|
||||||
|
|
|
@ -270,7 +270,7 @@ def _real_extract(self, url):
|
||||||
|
|
||||||
|
|
||||||
class TwitCastingUserIE(InfoExtractor):
|
class TwitCastingUserIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:[^/?#]+\.)?twitcasting\.tv/(?P<id>[^/?#]+)/(:?show|archive)/?(?:[#?]|$)'
|
_VALID_URL = r'https?://(?:[^/?#]+\.)?twitcasting\.tv/(?P<id>[^/?#]+)/(?:show|archive)/?(?:[#?]|$)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://twitcasting.tv/natsuiromatsuri/archive/',
|
'url': 'https://twitcasting.tv/natsuiromatsuri/archive/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
|
|
|
@ -934,14 +934,13 @@ class TwitterIE(TwitterBaseIE):
|
||||||
'uploader_id': 'MoniqueCamarra',
|
'uploader_id': 'MoniqueCamarra',
|
||||||
'live_status': 'was_live',
|
'live_status': 'was_live',
|
||||||
'release_timestamp': 1658417414,
|
'release_timestamp': 1658417414,
|
||||||
'description': 'md5:acce559345fd49f129c20dbcda3f1201',
|
'description': r're:Twitter Space participated by Sergej Sumlenny.+',
|
||||||
'timestamp': 1658407771,
|
'timestamp': 1658407771,
|
||||||
'release_date': '20220721',
|
'release_date': '20220721',
|
||||||
'upload_date': '20220721',
|
'upload_date': '20220721',
|
||||||
},
|
},
|
||||||
'add_ie': ['TwitterSpaces'],
|
'add_ie': ['TwitterSpaces'],
|
||||||
'params': {'skip_download': 'm3u8'},
|
'params': {'skip_download': 'm3u8'},
|
||||||
'skip': 'Requires authentication',
|
|
||||||
}, {
|
}, {
|
||||||
# URL specifies video number but --yes-playlist
|
# URL specifies video number but --yes-playlist
|
||||||
'url': 'https://twitter.com/CTVJLaidlaw/status/1600649710662213632/video/1',
|
'url': 'https://twitter.com/CTVJLaidlaw/status/1600649710662213632/video/1',
|
||||||
|
@ -1856,8 +1855,6 @@ def _build_graphql_query(self, space_id):
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
space_id = self._match_id(url)
|
space_id = self._match_id(url)
|
||||||
if not self.is_logged_in:
|
|
||||||
self.raise_login_required('Twitter Spaces require authentication')
|
|
||||||
space_data = self._call_graphql_api('HPEisOmj1epUNLCWTYhUWw/AudioSpaceById', space_id)['audioSpace']
|
space_data = self._call_graphql_api('HPEisOmj1epUNLCWTYhUWw/AudioSpaceById', space_id)['audioSpace']
|
||||||
if not space_data:
|
if not space_data:
|
||||||
raise ExtractorError('Twitter Space not found', expected=True)
|
raise ExtractorError('Twitter Space not found', expected=True)
|
||||||
|
|
|
@ -73,7 +73,7 @@ def _get_stream_info(self, url, video_id, app_id_ver, extra_note=None):
|
||||||
def num_to_hex(n):
|
def num_to_hex(n):
|
||||||
return hex(n)[2:]
|
return hex(n)[2:]
|
||||||
|
|
||||||
rnd = random.randrange
|
rnd = lambda x: random.randrange(int(x))
|
||||||
|
|
||||||
if not extra_note:
|
if not extra_note:
|
||||||
extra_note = ''
|
extra_note = ''
|
||||||
|
|
|
@ -8,7 +8,8 @@
|
||||||
int_or_none,
|
int_or_none,
|
||||||
parse_duration,
|
parse_duration,
|
||||||
qualities,
|
qualities,
|
||||||
try_get,
|
remove_start,
|
||||||
|
strip_or_none,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -108,7 +109,7 @@ def _real_extract(self, url):
|
||||||
|
|
||||||
categories = metadata.get('categoryPath')
|
categories = metadata.get('categoryPath')
|
||||||
if not categories:
|
if not categories:
|
||||||
category = try_get(video, lambda x: x['category'].strip().removeprefix('category_'))
|
category = remove_start(strip_or_none(video.get('category')), 'category_')
|
||||||
categories = [category] if category else None
|
categories = [category] if category else None
|
||||||
tags = video.get('tags')
|
tags = video.get('tags')
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
parse_filesize,
|
parse_filesize,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
parse_qs,
|
parse_qs,
|
||||||
|
qualities,
|
||||||
smuggle_url,
|
smuggle_url,
|
||||||
str_or_none,
|
str_or_none,
|
||||||
traverse_obj,
|
traverse_obj,
|
||||||
|
@ -146,6 +147,8 @@ def _parse_config(self, config, video_id):
|
||||||
})
|
})
|
||||||
|
|
||||||
# TODO: fix handling of 308 status code returned for live archive manifest requests
|
# TODO: fix handling of 308 status code returned for live archive manifest requests
|
||||||
|
QUALITIES = ('low', 'medium', 'high')
|
||||||
|
quality = qualities(QUALITIES)
|
||||||
sep_pattern = r'/sep/video/'
|
sep_pattern = r'/sep/video/'
|
||||||
for files_type in ('hls', 'dash'):
|
for files_type in ('hls', 'dash'):
|
||||||
for cdn_name, cdn_data in (try_get(config_files, lambda x: x[files_type]['cdns']) or {}).items():
|
for cdn_name, cdn_data in (try_get(config_files, lambda x: x[files_type]['cdns']) or {}).items():
|
||||||
|
@ -166,6 +169,11 @@ def _parse_config(self, config, video_id):
|
||||||
m_url, video_id, 'mp4', live=is_live, m3u8_id=f_id,
|
m_url, video_id, 'mp4', live=is_live, m3u8_id=f_id,
|
||||||
note=f'Downloading {cdn_name} m3u8 information',
|
note=f'Downloading {cdn_name} m3u8 information',
|
||||||
fatal=False)
|
fatal=False)
|
||||||
|
# m3u8 doesn't give audio bitrates; need to prioritize based on GROUP-ID
|
||||||
|
# See: https://github.com/yt-dlp/yt-dlp/issues/10854
|
||||||
|
for f in fmts:
|
||||||
|
if mobj := re.search(rf'audio-({"|".join(QUALITIES)})', f['format_id']):
|
||||||
|
f['quality'] = quality(mobj.group(1))
|
||||||
formats.extend(fmts)
|
formats.extend(fmts)
|
||||||
self._merge_subtitles(subs, target=subtitles)
|
self._merge_subtitles(subs, target=subtitles)
|
||||||
elif files_type == 'dash':
|
elif files_type == 'dash':
|
||||||
|
|
|
@ -90,7 +90,7 @@ def _real_extract(self, url):
|
||||||
formats, subtitles = self._extract_m3u8_formats_and_subtitles(m3u8_url, video_id, 'mp4')
|
formats, subtitles = self._extract_m3u8_formats_and_subtitles(m3u8_url, video_id, 'mp4')
|
||||||
|
|
||||||
for key, value in video_data.items():
|
for key, value in video_data.items():
|
||||||
mobj = re.match(r'^subtitle_(?P<lang>[^_]+)_(?P<ext>(vtt|srt))', key)
|
mobj = re.match(r'subtitle_(?P<lang>[^_]+)_(?P<ext>(vtt|srt))', key)
|
||||||
if not mobj:
|
if not mobj:
|
||||||
continue
|
continue
|
||||||
subtitles.setdefault(mobj.group('lang'), []).append({
|
subtitles.setdefault(mobj.group('lang'), []).append({
|
||||||
|
|
|
@ -27,8 +27,9 @@
|
||||||
|
|
||||||
class WeverseBaseIE(InfoExtractor):
|
class WeverseBaseIE(InfoExtractor):
|
||||||
_NETRC_MACHINE = 'weverse'
|
_NETRC_MACHINE = 'weverse'
|
||||||
_ACCOUNT_API_BASE = 'https://accountapi.weverse.io/web/api/v2'
|
_ACCOUNT_API_BASE = 'https://accountapi.weverse.io/web/api'
|
||||||
_API_HEADERS = {
|
_API_HEADERS = {
|
||||||
|
'Accept': 'application/json',
|
||||||
'Referer': 'https://weverse.io/',
|
'Referer': 'https://weverse.io/',
|
||||||
'WEV-device-Id': str(uuid.uuid4()),
|
'WEV-device-Id': str(uuid.uuid4()),
|
||||||
}
|
}
|
||||||
|
@ -39,14 +40,14 @@ def _perform_login(self, username, password):
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
'x-acc-app-secret': '5419526f1c624b38b10787e5c10b2a7a',
|
'x-acc-app-secret': '5419526f1c624b38b10787e5c10b2a7a',
|
||||||
'x-acc-app-version': '2.2.6',
|
'x-acc-app-version': '3.3.6',
|
||||||
'x-acc-language': 'en',
|
'x-acc-language': 'en',
|
||||||
'x-acc-service-id': 'weverse',
|
'x-acc-service-id': 'weverse',
|
||||||
'x-acc-trace-id': str(uuid.uuid4()),
|
'x-acc-trace-id': str(uuid.uuid4()),
|
||||||
'x-clog-user-device-id': str(uuid.uuid4()),
|
'x-clog-user-device-id': str(uuid.uuid4()),
|
||||||
}
|
}
|
||||||
valid_username = traverse_obj(self._download_json(
|
valid_username = traverse_obj(self._download_json(
|
||||||
f'{self._ACCOUNT_API_BASE}/signup/email/status', None, note='Checking username',
|
f'{self._ACCOUNT_API_BASE}/v2/signup/email/status', None, note='Checking username',
|
||||||
query={'email': username}, headers=headers, expected_status=(400, 404)), 'hasPassword')
|
query={'email': username}, headers=headers, expected_status=(400, 404)), 'hasPassword')
|
||||||
if not valid_username:
|
if not valid_username:
|
||||||
raise ExtractorError('Invalid username provided', expected=True)
|
raise ExtractorError('Invalid username provided', expected=True)
|
||||||
|
@ -54,8 +55,9 @@ def _perform_login(self, username, password):
|
||||||
headers['content-type'] = 'application/json'
|
headers['content-type'] = 'application/json'
|
||||||
try:
|
try:
|
||||||
auth = self._download_json(
|
auth = self._download_json(
|
||||||
f'{self._ACCOUNT_API_BASE}/auth/token/by-credentials', None, data=json.dumps({
|
f'{self._ACCOUNT_API_BASE}/v3/auth/token/by-credentials', None, data=json.dumps({
|
||||||
'email': username,
|
'email': username,
|
||||||
|
'otpSessionId': 'BY_PASS',
|
||||||
'password': password,
|
'password': password,
|
||||||
}, separators=(',', ':')).encode(), headers=headers, note='Logging in')
|
}, separators=(',', ':')).encode(), headers=headers, note='Logging in')
|
||||||
except ExtractorError as e:
|
except ExtractorError as e:
|
||||||
|
@ -78,8 +80,10 @@ def _call_api(self, ep, video_id, data=None, note='Downloading API JSON'):
|
||||||
# From https://ssl.pstatic.net/static/wevweb/2_3_2_11101725/public/static/js/main.e206f7c1.js:
|
# From https://ssl.pstatic.net/static/wevweb/2_3_2_11101725/public/static/js/main.e206f7c1.js:
|
||||||
key = b'1b9cb6378d959b45714bec49971ade22e6e24e42'
|
key = b'1b9cb6378d959b45714bec49971ade22e6e24e42'
|
||||||
api_path = update_url_query(ep, {
|
api_path = update_url_query(ep, {
|
||||||
|
# 'gcc': 'US',
|
||||||
'appId': 'be4d79eb8fc7bd008ee82c8ec4ff6fd4',
|
'appId': 'be4d79eb8fc7bd008ee82c8ec4ff6fd4',
|
||||||
'language': 'en',
|
'language': 'en',
|
||||||
|
'os': 'WEB',
|
||||||
'platform': 'WEB',
|
'platform': 'WEB',
|
||||||
'wpf': 'pc',
|
'wpf': 'pc',
|
||||||
})
|
})
|
||||||
|
@ -152,7 +156,7 @@ def _parse_post_meta(self, metadata):
|
||||||
'description': ((('extension', 'mediaInfo', 'body'), 'body'), {str}),
|
'description': ((('extension', 'mediaInfo', 'body'), 'body'), {str}),
|
||||||
'uploader': ('author', 'profileName', {str}),
|
'uploader': ('author', 'profileName', {str}),
|
||||||
'uploader_id': ('author', 'memberId', {str}),
|
'uploader_id': ('author', 'memberId', {str}),
|
||||||
'creator': ('community', 'communityName', {str}),
|
'creators': ('community', 'communityName', {str}, all),
|
||||||
'channel_id': (('community', 'author'), 'communityId', {str_or_none}),
|
'channel_id': (('community', 'author'), 'communityId', {str_or_none}),
|
||||||
'duration': ('extension', 'video', 'playTime', {float_or_none}),
|
'duration': ('extension', 'video', 'playTime', {float_or_none}),
|
||||||
'timestamp': ('publishedAt', {lambda x: int_or_none(x, 1000)}),
|
'timestamp': ('publishedAt', {lambda x: int_or_none(x, 1000)}),
|
||||||
|
@ -196,7 +200,7 @@ class WeverseIE(WeverseBaseIE):
|
||||||
'channel': 'billlie',
|
'channel': 'billlie',
|
||||||
'channel_id': '72',
|
'channel_id': '72',
|
||||||
'channel_url': 'https://weverse.io/billlie',
|
'channel_url': 'https://weverse.io/billlie',
|
||||||
'creator': 'Billlie',
|
'creators': ['Billlie'],
|
||||||
'timestamp': 1666262062,
|
'timestamp': 1666262062,
|
||||||
'upload_date': '20221020',
|
'upload_date': '20221020',
|
||||||
'release_timestamp': 1666262058,
|
'release_timestamp': 1666262058,
|
||||||
|
@ -222,7 +226,7 @@ class WeverseIE(WeverseBaseIE):
|
||||||
'channel': 'lesserafim',
|
'channel': 'lesserafim',
|
||||||
'channel_id': '47',
|
'channel_id': '47',
|
||||||
'channel_url': 'https://weverse.io/lesserafim',
|
'channel_url': 'https://weverse.io/lesserafim',
|
||||||
'creator': 'LE SSERAFIM',
|
'creators': ['LE SSERAFIM'],
|
||||||
'timestamp': 1659353400,
|
'timestamp': 1659353400,
|
||||||
'upload_date': '20220801',
|
'upload_date': '20220801',
|
||||||
'release_timestamp': 1659353400,
|
'release_timestamp': 1659353400,
|
||||||
|
@ -286,7 +290,7 @@ def _real_extract(self, url):
|
||||||
|
|
||||||
elif live_status == 'is_live':
|
elif live_status == 'is_live':
|
||||||
video_info = self._call_api(
|
video_info = self._call_api(
|
||||||
f'/video/v1.0/lives/{api_video_id}/playInfo?preview.format=json&preview.version=v2',
|
f'/video/v1.2/lives/{api_video_id}/playInfo?preview.format=json&preview.version=v2',
|
||||||
video_id, note='Downloading live JSON')
|
video_id, note='Downloading live JSON')
|
||||||
playback = self._parse_json(video_info['lipPlayback'], video_id)
|
playback = self._parse_json(video_info['lipPlayback'], video_id)
|
||||||
m3u8_url = traverse_obj(playback, (
|
m3u8_url = traverse_obj(playback, (
|
||||||
|
@ -302,7 +306,7 @@ def _real_extract(self, url):
|
||||||
else:
|
else:
|
||||||
infra_video_id = post['extension']['video']['infraVideoId']
|
infra_video_id = post['extension']['video']['infraVideoId']
|
||||||
in_key = self._call_api(
|
in_key = self._call_api(
|
||||||
f'/video/v1.0/vod/{api_video_id}/inKey?preview=false', video_id,
|
f'/video/v1.1/vod/{api_video_id}/inKey?preview=false', video_id,
|
||||||
data=b'{}', note='Downloading VOD API key')['inKey']
|
data=b'{}', note='Downloading VOD API key')['inKey']
|
||||||
|
|
||||||
video_info = self._download_json(
|
video_info = self._download_json(
|
||||||
|
@ -347,7 +351,6 @@ class WeverseMediaIE(WeverseBaseIE):
|
||||||
_VALID_URL = r'https?://(?:www\.|m\.)?weverse\.io/(?P<artist>[^/?#]+)/media/(?P<id>[\d-]+)'
|
_VALID_URL = r'https?://(?:www\.|m\.)?weverse\.io/(?P<artist>[^/?#]+)/media/(?P<id>[\d-]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://weverse.io/billlie/media/4-116372884',
|
'url': 'https://weverse.io/billlie/media/4-116372884',
|
||||||
'md5': '8efc9cfd61b2f25209eb1a5326314d28',
|
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'e-C9wLSQs6o',
|
'id': 'e-C9wLSQs6o',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
|
@ -358,8 +361,9 @@ class WeverseMediaIE(WeverseBaseIE):
|
||||||
'channel_url': 'https://www.youtube.com/channel/UCyc9sUCxELTDK9vELO5Fzeg',
|
'channel_url': 'https://www.youtube.com/channel/UCyc9sUCxELTDK9vELO5Fzeg',
|
||||||
'uploader': 'Billlie',
|
'uploader': 'Billlie',
|
||||||
'uploader_id': '@Billlie',
|
'uploader_id': '@Billlie',
|
||||||
'uploader_url': 'http://www.youtube.com/@Billlie',
|
'uploader_url': 'https://www.youtube.com/@Billlie',
|
||||||
'upload_date': '20230403',
|
'upload_date': '20230403',
|
||||||
|
'timestamp': 1680533992,
|
||||||
'duration': 211,
|
'duration': 211,
|
||||||
'age_limit': 0,
|
'age_limit': 0,
|
||||||
'playable_in_embed': True,
|
'playable_in_embed': True,
|
||||||
|
@ -372,6 +376,8 @@ class WeverseMediaIE(WeverseBaseIE):
|
||||||
'thumbnail': 'https://i.ytimg.com/vi/e-C9wLSQs6o/maxresdefault.jpg',
|
'thumbnail': 'https://i.ytimg.com/vi/e-C9wLSQs6o/maxresdefault.jpg',
|
||||||
'categories': ['Entertainment'],
|
'categories': ['Entertainment'],
|
||||||
'tags': 'count:7',
|
'tags': 'count:7',
|
||||||
|
'channel_is_verified': True,
|
||||||
|
'heatmap': 'count:100',
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://weverse.io/billlie/media/3-102914520',
|
'url': 'https://weverse.io/billlie/media/3-102914520',
|
||||||
|
@ -386,7 +392,7 @@ class WeverseMediaIE(WeverseBaseIE):
|
||||||
'channel': 'billlie',
|
'channel': 'billlie',
|
||||||
'channel_id': '72',
|
'channel_id': '72',
|
||||||
'channel_url': 'https://weverse.io/billlie',
|
'channel_url': 'https://weverse.io/billlie',
|
||||||
'creator': 'Billlie',
|
'creators': ['Billlie'],
|
||||||
'timestamp': 1662174000,
|
'timestamp': 1662174000,
|
||||||
'upload_date': '20220903',
|
'upload_date': '20220903',
|
||||||
'release_timestamp': 1662174000,
|
'release_timestamp': 1662174000,
|
||||||
|
@ -432,7 +438,7 @@ class WeverseMomentIE(WeverseBaseIE):
|
||||||
'uploader_id': '66a07e164b56a696ee71c99315ffe27b',
|
'uploader_id': '66a07e164b56a696ee71c99315ffe27b',
|
||||||
'channel': 'secretnumber',
|
'channel': 'secretnumber',
|
||||||
'channel_id': '56',
|
'channel_id': '56',
|
||||||
'creator': 'SECRET NUMBER',
|
'creators': ['SECRET NUMBER'],
|
||||||
'duration': 10,
|
'duration': 10,
|
||||||
'upload_date': '20230405',
|
'upload_date': '20230405',
|
||||||
'timestamp': 1680653968,
|
'timestamp': 1680653968,
|
||||||
|
@ -441,7 +447,6 @@ class WeverseMomentIE(WeverseBaseIE):
|
||||||
'comment_count': int,
|
'comment_count': int,
|
||||||
'availability': 'needs_auth',
|
'availability': 'needs_auth',
|
||||||
},
|
},
|
||||||
'skip': 'Moment has expired',
|
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
|
@ -571,7 +576,7 @@ class WeverseLiveIE(WeverseBaseIE):
|
||||||
'channel': 'purplekiss',
|
'channel': 'purplekiss',
|
||||||
'channel_id': '35',
|
'channel_id': '35',
|
||||||
'channel_url': 'https://weverse.io/purplekiss',
|
'channel_url': 'https://weverse.io/purplekiss',
|
||||||
'creator': 'PURPLE KISS',
|
'creators': ['PURPLE KISS'],
|
||||||
'timestamp': 1680780892,
|
'timestamp': 1680780892,
|
||||||
'upload_date': '20230406',
|
'upload_date': '20230406',
|
||||||
'release_timestamp': 1680780883,
|
'release_timestamp': 1680780883,
|
||||||
|
@ -584,6 +589,31 @@ class WeverseLiveIE(WeverseBaseIE):
|
||||||
'live_status': 'is_live',
|
'live_status': 'is_live',
|
||||||
},
|
},
|
||||||
'skip': 'Livestream has ended',
|
'skip': 'Livestream has ended',
|
||||||
|
}, {
|
||||||
|
'url': 'https://weverse.io/lesserafim',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '4-181521628',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': r're:심심해서요',
|
||||||
|
'description': '',
|
||||||
|
'uploader': '채채🤎',
|
||||||
|
'uploader_id': 'd49b8b06f3cc1d92d655b25ab27ac2e7',
|
||||||
|
'channel': 'lesserafim',
|
||||||
|
'channel_id': '47',
|
||||||
|
'creators': ['LE SSERAFIM'],
|
||||||
|
'channel_url': 'https://weverse.io/lesserafim',
|
||||||
|
'timestamp': 1728570273,
|
||||||
|
'upload_date': '20241010',
|
||||||
|
'release_timestamp': 1728570264,
|
||||||
|
'release_date': '20241010',
|
||||||
|
'thumbnail': r're:https://phinf\.wevpstatic\.net/.+\.png',
|
||||||
|
'view_count': int,
|
||||||
|
'like_count': int,
|
||||||
|
'comment_count': int,
|
||||||
|
'availability': 'needs_auth',
|
||||||
|
'live_status': 'is_live',
|
||||||
|
},
|
||||||
|
'skip': 'Livestream has ended',
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://weverse.io/billlie/',
|
'url': 'https://weverse.io/billlie/',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
determine_ext,
|
determine_ext,
|
||||||
|
filter_dict,
|
||||||
float_or_none,
|
float_or_none,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
parse_qs,
|
parse_qs,
|
||||||
|
@ -25,16 +26,25 @@ class WistiaBaseIE(InfoExtractor):
|
||||||
|
|
||||||
def _download_embed_config(self, config_type, config_id, referer):
|
def _download_embed_config(self, config_type, config_id, referer):
|
||||||
base_url = self._EMBED_BASE_URL + f'{config_type}/{config_id}'
|
base_url = self._EMBED_BASE_URL + f'{config_type}/{config_id}'
|
||||||
|
video_password = self.get_param('videopassword')
|
||||||
embed_config = self._download_json(
|
embed_config = self._download_json(
|
||||||
base_url + '.json', config_id, headers={
|
base_url + '.json', config_id, headers={
|
||||||
'Referer': referer if referer.startswith('http') else base_url, # Some videos require this.
|
'Referer': referer if referer.startswith('http') else base_url, # Some videos require this.
|
||||||
})
|
}, query=filter_dict({'password': video_password}))
|
||||||
|
|
||||||
error = traverse_obj(embed_config, 'error')
|
error = traverse_obj(embed_config, 'error')
|
||||||
if error:
|
if error:
|
||||||
raise ExtractorError(
|
raise ExtractorError(
|
||||||
f'Error while getting the playlist: {error}', expected=True)
|
f'Error while getting the playlist: {error}', expected=True)
|
||||||
|
|
||||||
|
if traverse_obj(embed_config, (
|
||||||
|
'media', ('embed_options', 'embedOptions'), 'plugin',
|
||||||
|
'passwordProtectedVideo', 'on', any)) == 'true':
|
||||||
|
if video_password:
|
||||||
|
raise ExtractorError('Invalid video password', expected=True)
|
||||||
|
raise ExtractorError(
|
||||||
|
'This content is password-protected. Use the --video-password option', expected=True)
|
||||||
|
|
||||||
return embed_config
|
return embed_config
|
||||||
|
|
||||||
def _get_real_ext(self, url):
|
def _get_real_ext(self, url):
|
||||||
|
|
|
@ -21,7 +21,7 @@ class XimalayaBaseIE(InfoExtractor):
|
||||||
class XimalayaIE(XimalayaBaseIE):
|
class XimalayaIE(XimalayaBaseIE):
|
||||||
IE_NAME = 'ximalaya'
|
IE_NAME = 'ximalaya'
|
||||||
IE_DESC = '喜马拉雅FM'
|
IE_DESC = '喜马拉雅FM'
|
||||||
_VALID_URL = r'https?://(?:www\.|m\.)?ximalaya\.com/(:?(?P<uid>\d+)/)?sound/(?P<id>[0-9]+)'
|
_VALID_URL = r'https?://(?:www\.|m\.)?ximalaya\.com/(?:(?P<uid>\d+)/)?sound/(?P<id>[0-9]+)'
|
||||||
_TESTS = [
|
_TESTS = [
|
||||||
{
|
{
|
||||||
'url': 'http://www.ximalaya.com/sound/47740352/',
|
'url': 'http://www.ximalaya.com/sound/47740352/',
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
|
|
||||||
|
|
||||||
class YleAreenaIE(InfoExtractor):
|
class YleAreenaIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://areena\.yle\.fi/(?P<id>[\d-]+)'
|
_VALID_URL = r'https?://areena\.yle\.fi/(?P<podcast>podcastit/)?(?P<id>[\d-]+)'
|
||||||
_GEO_COUNTRIES = ['FI']
|
_GEO_COUNTRIES = ['FI']
|
||||||
_TESTS = [
|
_TESTS = [
|
||||||
{
|
{
|
||||||
|
@ -77,7 +77,7 @@ class YleAreenaIE(InfoExtractor):
|
||||||
]
|
]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id, is_podcast = self._match_valid_url(url).group('id', 'podcast')
|
||||||
info = self._search_json_ld(self._download_webpage(url, video_id), video_id, default={})
|
info = self._search_json_ld(self._download_webpage(url, video_id), video_id, default={})
|
||||||
video_data = self._download_json(
|
video_data = self._download_json(
|
||||||
f'https://player.api.yle.fi/v1/preview/{video_id}.json?app_id=player_static_prod&app_key=8930d72170e48303cf5f3867780d549b',
|
f'https://player.api.yle.fi/v1/preview/{video_id}.json?app_id=player_static_prod&app_key=8930d72170e48303cf5f3867780d549b',
|
||||||
|
@ -103,8 +103,11 @@ def _real_extract(self, url):
|
||||||
'name': sub.get('kind'),
|
'name': sub.get('kind'),
|
||||||
})
|
})
|
||||||
|
|
||||||
kaltura_id = traverse_obj(video_data, ('data', 'ongoing_ondemand', 'kaltura', 'id'), expected_type=str)
|
if is_podcast:
|
||||||
if kaltura_id:
|
info_dict = {
|
||||||
|
'url': video_data['data']['ongoing_ondemand']['media_url'],
|
||||||
|
}
|
||||||
|
elif kaltura_id := traverse_obj(video_data, ('data', 'ongoing_ondemand', 'kaltura', 'id', {str})):
|
||||||
info_dict = {
|
info_dict = {
|
||||||
'_type': 'url_transparent',
|
'_type': 'url_transparent',
|
||||||
'url': smuggle_url(f'kaltura:1955031:{kaltura_id}', {'source_url': url}),
|
'url': smuggle_url(f'kaltura:1955031:{kaltura_id}', {'source_url': url}),
|
||||||
|
@ -114,13 +117,11 @@ def _real_extract(self, url):
|
||||||
formats, subs = self._extract_m3u8_formats_and_subtitles(
|
formats, subs = self._extract_m3u8_formats_and_subtitles(
|
||||||
video_data['data']['ongoing_ondemand']['manifest_url'], video_id, 'mp4', m3u8_id='hls')
|
video_data['data']['ongoing_ondemand']['manifest_url'], video_id, 'mp4', m3u8_id='hls')
|
||||||
self._merge_subtitles(subs, target=subtitles)
|
self._merge_subtitles(subs, target=subtitles)
|
||||||
info_dict = {
|
info_dict = {'formats': formats}
|
||||||
'id': video_id,
|
|
||||||
'formats': formats,
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
**info_dict,
|
**info_dict,
|
||||||
|
'id': video_id,
|
||||||
'title': (traverse_obj(video_data, ('data', 'ongoing_ondemand', 'title', 'fin'), expected_type=str)
|
'title': (traverse_obj(video_data, ('data', 'ongoing_ondemand', 'title', 'fin'), expected_type=str)
|
||||||
or episode or info.get('title')),
|
or episode or info.get('title')),
|
||||||
'description': description,
|
'description': description,
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
from .common import InfoExtractor, SearchInfoExtractor
|
from .common import InfoExtractor, SearchInfoExtractor
|
||||||
from .openload import PhantomJSwrapper
|
from .openload import PhantomJSwrapper
|
||||||
from ..jsinterp import JSInterpreter
|
from ..jsinterp import JSInterpreter
|
||||||
from ..networking.exceptions import HTTPError, network_exceptions
|
from ..networking.exceptions import HTTPError, TransportError, network_exceptions
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
NO_DEFAULT,
|
NO_DEFAULT,
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
|
@ -55,6 +55,7 @@
|
||||||
str_or_none,
|
str_or_none,
|
||||||
str_to_int,
|
str_to_int,
|
||||||
strftime_or_none,
|
strftime_or_none,
|
||||||
|
time_seconds,
|
||||||
traverse_obj,
|
traverse_obj,
|
||||||
try_call,
|
try_call,
|
||||||
try_get,
|
try_get,
|
||||||
|
@ -114,6 +115,7 @@
|
||||||
},
|
},
|
||||||
'INNERTUBE_CONTEXT_CLIENT_NAME': 67,
|
'INNERTUBE_CONTEXT_CLIENT_NAME': 67,
|
||||||
},
|
},
|
||||||
|
# This client now requires sign-in for every video
|
||||||
'web_creator': {
|
'web_creator': {
|
||||||
'INNERTUBE_CONTEXT': {
|
'INNERTUBE_CONTEXT': {
|
||||||
'client': {
|
'client': {
|
||||||
|
@ -153,6 +155,7 @@
|
||||||
'REQUIRE_JS_PLAYER': False,
|
'REQUIRE_JS_PLAYER': False,
|
||||||
'REQUIRE_PO_TOKEN': True,
|
'REQUIRE_PO_TOKEN': True,
|
||||||
},
|
},
|
||||||
|
# This client now requires sign-in for every video
|
||||||
'android_creator': {
|
'android_creator': {
|
||||||
'INNERTUBE_CONTEXT': {
|
'INNERTUBE_CONTEXT': {
|
||||||
'client': {
|
'client': {
|
||||||
|
@ -200,21 +203,6 @@
|
||||||
'REQUIRE_JS_PLAYER': False,
|
'REQUIRE_JS_PLAYER': False,
|
||||||
'PLAYER_PARAMS': '2AMB',
|
'PLAYER_PARAMS': '2AMB',
|
||||||
},
|
},
|
||||||
# This client only has legacy formats and storyboards
|
|
||||||
'android_producer': {
|
|
||||||
'INNERTUBE_CONTEXT': {
|
|
||||||
'client': {
|
|
||||||
'clientName': 'ANDROID_PRODUCER',
|
|
||||||
'clientVersion': '0.111.1',
|
|
||||||
'androidSdkVersion': 30,
|
|
||||||
'userAgent': 'com.google.android.apps.youtube.producer/0.111.1 (Linux; U; Android 11) gzip',
|
|
||||||
'osName': 'Android',
|
|
||||||
'osVersion': '11',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'INNERTUBE_CONTEXT_CLIENT_NAME': 91,
|
|
||||||
'REQUIRE_JS_PLAYER': False,
|
|
||||||
},
|
|
||||||
# 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
|
||||||
'ios': {
|
'ios': {
|
||||||
|
@ -247,6 +235,7 @@
|
||||||
'INNERTUBE_CONTEXT_CLIENT_NAME': 26,
|
'INNERTUBE_CONTEXT_CLIENT_NAME': 26,
|
||||||
'REQUIRE_JS_PLAYER': False,
|
'REQUIRE_JS_PLAYER': False,
|
||||||
},
|
},
|
||||||
|
# This client now requires sign-in for every video
|
||||||
'ios_creator': {
|
'ios_creator': {
|
||||||
'INNERTUBE_CONTEXT': {
|
'INNERTUBE_CONTEXT': {
|
||||||
'client': {
|
'client': {
|
||||||
|
@ -282,8 +271,9 @@
|
||||||
},
|
},
|
||||||
'INNERTUBE_CONTEXT_CLIENT_NAME': 7,
|
'INNERTUBE_CONTEXT_CLIENT_NAME': 7,
|
||||||
},
|
},
|
||||||
# This client can access age restricted videos (unless the uploader has disabled the 'allow embedding' option)
|
# This client now requires sign-in for every video
|
||||||
# See: https://github.com/zerodytrash/YouTube-Internal-Clients
|
# It was previously an age-gate workaround for videos that were `playable_in_embed`
|
||||||
|
# It may still be useful if signed into an EU account that is not age-verified
|
||||||
'tv_embedded': {
|
'tv_embedded': {
|
||||||
'INNERTUBE_CONTEXT': {
|
'INNERTUBE_CONTEXT': {
|
||||||
'client': {
|
'client': {
|
||||||
|
@ -526,6 +516,8 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
|
||||||
_YT_HANDLE_RE = r'@[\w.-]{3,30}' # https://support.google.com/youtube/answer/11585688?hl=en
|
_YT_HANDLE_RE = r'@[\w.-]{3,30}' # https://support.google.com/youtube/answer/11585688?hl=en
|
||||||
_YT_CHANNEL_UCID_RE = r'UC[\w-]{22}'
|
_YT_CHANNEL_UCID_RE = r'UC[\w-]{22}'
|
||||||
|
|
||||||
|
_NETRC_MACHINE = 'youtube'
|
||||||
|
|
||||||
def ucid_or_none(self, ucid):
|
def ucid_or_none(self, ucid):
|
||||||
return self._search_regex(rf'^({self._YT_CHANNEL_UCID_RE})$', ucid, 'UC-id', default=None)
|
return self._search_regex(rf'^({self._YT_CHANNEL_UCID_RE})$', ucid, 'UC-id', default=None)
|
||||||
|
|
||||||
|
@ -584,9 +576,213 @@ def _real_initialize(self):
|
||||||
self._initialize_consent()
|
self._initialize_consent()
|
||||||
self._check_login_required()
|
self._check_login_required()
|
||||||
|
|
||||||
|
def _perform_login(self, username, password):
|
||||||
|
auth_type, _, user = (username or '').partition('+')
|
||||||
|
|
||||||
|
if auth_type != 'oauth':
|
||||||
|
raise ExtractorError(self._youtube_login_hint, expected=True)
|
||||||
|
|
||||||
|
self._initialize_oauth(user, password)
|
||||||
|
|
||||||
|
'''
|
||||||
|
OAuth 2.0 Device Authorization Grant flow, used by the YouTube TV client (youtube.com/tv).
|
||||||
|
|
||||||
|
For more information regarding OAuth 2.0 and the Device Authorization Grant flow in general, see:
|
||||||
|
- https://developers.google.com/identity/protocols/oauth2/limited-input-device
|
||||||
|
- https://accounts.google.com/.well-known/openid-configuration
|
||||||
|
- https://www.rfc-editor.org/rfc/rfc8628
|
||||||
|
- https://www.rfc-editor.org/rfc/rfc6749
|
||||||
|
|
||||||
|
Note: The official client appears to use a proxied version of the oauth2 endpoints on youtube.com/o/oauth2,
|
||||||
|
which applies some modifications to the response (such as returning errors as 200 OK).
|
||||||
|
Since the client works with the standard API, we will use that as it is well-documented.
|
||||||
|
'''
|
||||||
|
|
||||||
|
_OAUTH_PROFILE = None
|
||||||
|
_OAUTH_ACCESS_TOKEN_CACHE = {}
|
||||||
|
_OAUTH_DISPLAY_ID = 'oauth'
|
||||||
|
|
||||||
|
# YouTube TV (TVHTML5) client. You can find these at youtube.com/tv
|
||||||
|
_OAUTH_CLIENT_ID = '861556708454-d6dlm3lh05idd8npek18k6be8ba3oc68.apps.googleusercontent.com'
|
||||||
|
_OAUTH_CLIENT_SECRET = 'SboVhoG9s0rNafixCSGGKXAT'
|
||||||
|
_OAUTH_SCOPE = 'http://gdata.youtube.com https://www.googleapis.com/auth/youtube-paid-content'
|
||||||
|
|
||||||
|
# From https://accounts.google.com/.well-known/openid-configuration
|
||||||
|
# Technically, these should be fetched dynamically and not hard-coded.
|
||||||
|
# However, as these endpoints rarely change, we can risk saving an extra request for every invocation.
|
||||||
|
_OAUTH_DEVICE_AUTHORIZATION_ENDPOINT = 'https://oauth2.googleapis.com/device/code'
|
||||||
|
_OAUTH_TOKEN_ENDPOINT = 'https://oauth2.googleapis.com/token'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _oauth_cache_key(self):
|
||||||
|
return f'oauth_refresh_token_{self._OAUTH_PROFILE}'
|
||||||
|
|
||||||
|
def _read_oauth_error_response(self, response):
|
||||||
|
return traverse_obj(
|
||||||
|
self._webpage_read_content(response, self._OAUTH_TOKEN_ENDPOINT, self._OAUTH_DISPLAY_ID, fatal=False),
|
||||||
|
({json.loads}, 'error', {str}))
|
||||||
|
|
||||||
|
def _set_oauth_info(self, token_response):
|
||||||
|
YoutubeBaseInfoExtractor._OAUTH_ACCESS_TOKEN_CACHE.setdefault(self._OAUTH_PROFILE, {}).update({
|
||||||
|
'access_token': token_response['access_token'],
|
||||||
|
'token_type': token_response['token_type'],
|
||||||
|
'expiry': time_seconds(
|
||||||
|
seconds=traverse_obj(token_response, ('expires_in', {float_or_none}), default=300) - 10),
|
||||||
|
})
|
||||||
|
refresh_token = traverse_obj(token_response, ('refresh_token', {str}))
|
||||||
|
if refresh_token:
|
||||||
|
self.cache.store(self._NETRC_MACHINE, self._oauth_cache_key, refresh_token)
|
||||||
|
YoutubeBaseInfoExtractor._OAUTH_ACCESS_TOKEN_CACHE[self._OAUTH_PROFILE]['refresh_token'] = refresh_token
|
||||||
|
|
||||||
|
def _initialize_oauth(self, user, refresh_token):
|
||||||
|
self._OAUTH_PROFILE = user or 'default'
|
||||||
|
|
||||||
|
if self._OAUTH_PROFILE in YoutubeBaseInfoExtractor._OAUTH_ACCESS_TOKEN_CACHE:
|
||||||
|
self.write_debug(f'{self._OAUTH_DISPLAY_ID}: Using cached access token for profile "{self._OAUTH_PROFILE}"')
|
||||||
|
return
|
||||||
|
|
||||||
|
YoutubeBaseInfoExtractor._OAUTH_ACCESS_TOKEN_CACHE[self._OAUTH_PROFILE] = {}
|
||||||
|
|
||||||
|
if refresh_token:
|
||||||
|
refresh_token = refresh_token.strip('\'') or None
|
||||||
|
|
||||||
|
# Allow refresh token passed to initialize cache
|
||||||
|
if refresh_token:
|
||||||
|
self.cache.store(self._NETRC_MACHINE, self._oauth_cache_key, refresh_token)
|
||||||
|
|
||||||
|
refresh_token = refresh_token or self.cache.load(self._NETRC_MACHINE, self._oauth_cache_key)
|
||||||
|
if refresh_token:
|
||||||
|
YoutubeBaseInfoExtractor._OAUTH_ACCESS_TOKEN_CACHE[self._OAUTH_PROFILE]['refresh_token'] = refresh_token
|
||||||
|
try:
|
||||||
|
token_response = self._refresh_token(refresh_token)
|
||||||
|
except ExtractorError as e:
|
||||||
|
error_msg = str(e.orig_msg).replace('Failed to refresh access token: ', '')
|
||||||
|
self.report_warning(f'{self._OAUTH_DISPLAY_ID}: Failed to refresh access token: {error_msg}')
|
||||||
|
token_response = self._oauth_authorize
|
||||||
|
else:
|
||||||
|
token_response = self._oauth_authorize
|
||||||
|
|
||||||
|
self._set_oauth_info(token_response)
|
||||||
|
self.write_debug(f'{self._OAUTH_DISPLAY_ID}: Logged in using profile "{self._OAUTH_PROFILE}"')
|
||||||
|
|
||||||
|
def _refresh_token(self, refresh_token):
|
||||||
|
try:
|
||||||
|
token_response = self._download_json(
|
||||||
|
self._OAUTH_TOKEN_ENDPOINT,
|
||||||
|
video_id=self._OAUTH_DISPLAY_ID,
|
||||||
|
note='Refreshing access token',
|
||||||
|
data=json.dumps({
|
||||||
|
'client_id': self._OAUTH_CLIENT_ID,
|
||||||
|
'client_secret': self._OAUTH_CLIENT_SECRET,
|
||||||
|
'refresh_token': refresh_token,
|
||||||
|
'grant_type': 'refresh_token',
|
||||||
|
}).encode(),
|
||||||
|
headers={'Content-Type': 'application/json'})
|
||||||
|
except ExtractorError as e:
|
||||||
|
if isinstance(e.cause, HTTPError):
|
||||||
|
error = self._read_oauth_error_response(e.cause.response)
|
||||||
|
if error == 'invalid_grant':
|
||||||
|
# RFC6749 § 5.2
|
||||||
|
raise ExtractorError(
|
||||||
|
'Failed to refresh access token: Refresh token is invalid, revoked, or expired (invalid_grant)',
|
||||||
|
expected=True, video_id=self._OAUTH_DISPLAY_ID)
|
||||||
|
raise ExtractorError(
|
||||||
|
f'Failed to refresh access token: Authorization server returned error {error}',
|
||||||
|
video_id=self._OAUTH_DISPLAY_ID)
|
||||||
|
raise
|
||||||
|
return token_response
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _oauth_authorize(self):
|
||||||
|
code_response = self._download_json(
|
||||||
|
self._OAUTH_DEVICE_AUTHORIZATION_ENDPOINT,
|
||||||
|
video_id=self._OAUTH_DISPLAY_ID,
|
||||||
|
note='Initializing authorization flow',
|
||||||
|
data=json.dumps({
|
||||||
|
'client_id': self._OAUTH_CLIENT_ID,
|
||||||
|
'scope': self._OAUTH_SCOPE,
|
||||||
|
}).encode(),
|
||||||
|
headers={'Content-Type': 'application/json'})
|
||||||
|
|
||||||
|
verification_url = traverse_obj(code_response, ('verification_url', {str}))
|
||||||
|
user_code = traverse_obj(code_response, ('user_code', {str}))
|
||||||
|
if not verification_url or not user_code:
|
||||||
|
raise ExtractorError(
|
||||||
|
'Authorization server did not provide verification_url or user_code', video_id=self._OAUTH_DISPLAY_ID)
|
||||||
|
|
||||||
|
# note: The whitespace is intentional
|
||||||
|
self.to_screen(
|
||||||
|
f'{self._OAUTH_DISPLAY_ID}: To give yt-dlp access to your account, '
|
||||||
|
f'go to {verification_url} and enter code {user_code}')
|
||||||
|
|
||||||
|
# RFC8628 § 3.5: default poll interval is 5 seconds if not provided
|
||||||
|
poll_interval = traverse_obj(code_response, ('interval', {int}), default=5)
|
||||||
|
|
||||||
|
for retry in self.RetryManager():
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
token_response = self._download_json(
|
||||||
|
self._OAUTH_TOKEN_ENDPOINT,
|
||||||
|
video_id=self._OAUTH_DISPLAY_ID,
|
||||||
|
note=False,
|
||||||
|
errnote='Failed to request access token',
|
||||||
|
data=json.dumps({
|
||||||
|
'client_id': self._OAUTH_CLIENT_ID,
|
||||||
|
'client_secret': self._OAUTH_CLIENT_SECRET,
|
||||||
|
'device_code': code_response['device_code'],
|
||||||
|
'grant_type': 'urn:ietf:params:oauth:grant-type:device_code',
|
||||||
|
}).encode(),
|
||||||
|
headers={'Content-Type': 'application/json'})
|
||||||
|
except ExtractorError as e:
|
||||||
|
if isinstance(e.cause, TransportError):
|
||||||
|
retry.error = e
|
||||||
|
break
|
||||||
|
elif isinstance(e.cause, HTTPError):
|
||||||
|
error = self._read_oauth_error_response(e.cause.response)
|
||||||
|
if not error:
|
||||||
|
retry.error = e
|
||||||
|
break
|
||||||
|
|
||||||
|
if error == 'authorization_pending':
|
||||||
|
time.sleep(poll_interval)
|
||||||
|
continue
|
||||||
|
elif error == 'expired_token':
|
||||||
|
raise ExtractorError(
|
||||||
|
'Authorization timed out', expected=True, video_id=self._OAUTH_DISPLAY_ID)
|
||||||
|
elif error == 'access_denied':
|
||||||
|
raise ExtractorError(
|
||||||
|
'You denied access to an account', expected=True, video_id=self._OAUTH_DISPLAY_ID)
|
||||||
|
elif error == 'slow_down':
|
||||||
|
# RFC8628 § 3.5: add 5 seconds to the poll interval
|
||||||
|
poll_interval += 5
|
||||||
|
time.sleep(poll_interval)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
raise ExtractorError(
|
||||||
|
f'Authorization server returned an error when fetching access token: {error}',
|
||||||
|
video_id=self._OAUTH_DISPLAY_ID)
|
||||||
|
raise
|
||||||
|
|
||||||
|
return token_response
|
||||||
|
|
||||||
|
def _update_oauth(self):
|
||||||
|
token = YoutubeBaseInfoExtractor._OAUTH_ACCESS_TOKEN_CACHE.get(self._OAUTH_PROFILE)
|
||||||
|
if token is None or token['expiry'] > time.time():
|
||||||
|
return
|
||||||
|
|
||||||
|
self._set_oauth_info(self._refresh_token(token['refresh_token']))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _youtube_login_hint(self):
|
||||||
|
return ('Use --username=oauth[+PROFILE] --password="" to log in using oauth, '
|
||||||
|
f'or else u{self._login_hint(method="cookies")[1:]}. '
|
||||||
|
'See https://github.com/yt-dlp/yt-dlp/wiki/Extractors#logging-in-with-oauth for more on how to use oauth. '
|
||||||
|
'See https://github.com/yt-dlp/yt-dlp/wiki/Extractors#exporting-youtube-cookies for help with cookies')
|
||||||
|
|
||||||
def _check_login_required(self):
|
def _check_login_required(self):
|
||||||
if self._LOGIN_REQUIRED and not self._cookies_passed:
|
if self._LOGIN_REQUIRED and not self.is_authenticated:
|
||||||
self.raise_login_required('Login details are needed to download this content', method='cookies')
|
self.raise_login_required(
|
||||||
|
f'Login details are needed to download this content. {self._youtube_login_hint}', method=None)
|
||||||
|
|
||||||
_YT_INITIAL_DATA_RE = r'(?:window\s*\[\s*["\']ytInitialData["\']\s*\]|ytInitialData)\s*='
|
_YT_INITIAL_DATA_RE = r'(?:window\s*\[\s*["\']ytInitialData["\']\s*\]|ytInitialData)\s*='
|
||||||
_YT_INITIAL_PLAYER_RESPONSE_RE = r'ytInitialPlayerResponse\s*='
|
_YT_INITIAL_PLAYER_RESPONSE_RE = r'ytInitialPlayerResponse\s*='
|
||||||
|
@ -685,17 +881,6 @@ def _extract_session_index(*data):
|
||||||
if session_index is not None:
|
if session_index is not None:
|
||||||
return session_index
|
return session_index
|
||||||
|
|
||||||
# Deprecated?
|
|
||||||
def _extract_identity_token(self, ytcfg=None, webpage=None):
|
|
||||||
if ytcfg:
|
|
||||||
token = try_get(ytcfg, lambda x: x['ID_TOKEN'], str)
|
|
||||||
if token:
|
|
||||||
return token
|
|
||||||
if webpage:
|
|
||||||
return self._search_regex(
|
|
||||||
r'\bID_TOKEN["\']\s*:\s*["\'](.+?)["\']', webpage,
|
|
||||||
'identity token', default=None, fatal=False)
|
|
||||||
|
|
||||||
def _data_sync_id_to_delegated_session_id(self, data_sync_id):
|
def _data_sync_id_to_delegated_session_id(self, data_sync_id):
|
||||||
if not data_sync_id:
|
if not data_sync_id:
|
||||||
return
|
return
|
||||||
|
@ -742,7 +927,7 @@ def _extract_visitor_data(self, *args):
|
||||||
|
|
||||||
@functools.cached_property
|
@functools.cached_property
|
||||||
def is_authenticated(self):
|
def is_authenticated(self):
|
||||||
return bool(self._generate_sapisidhash_header())
|
return self._OAUTH_PROFILE or bool(self._generate_sapisidhash_header())
|
||||||
|
|
||||||
def extract_ytcfg(self, video_id, webpage):
|
def extract_ytcfg(self, video_id, webpage):
|
||||||
if not webpage:
|
if not webpage:
|
||||||
|
@ -752,21 +937,21 @@ def extract_ytcfg(self, video_id, webpage):
|
||||||
r'ytcfg\.set\s*\(\s*({.+?})\s*\)\s*;', webpage, 'ytcfg',
|
r'ytcfg\.set\s*\(\s*({.+?})\s*\)\s*;', webpage, 'ytcfg',
|
||||||
default='{}'), video_id, fatal=False) or {}
|
default='{}'), video_id, fatal=False) or {}
|
||||||
|
|
||||||
def generate_api_headers(
|
def _generate_oauth_headers(self):
|
||||||
self, *, ytcfg=None, account_syncid=None, session_index=None,
|
self._update_oauth()
|
||||||
visitor_data=None, identity_token=None, api_hostname=None, default_client='web'):
|
oauth_token = YoutubeBaseInfoExtractor._OAUTH_ACCESS_TOKEN_CACHE.get(self._OAUTH_PROFILE)
|
||||||
|
if not oauth_token:
|
||||||
|
return {}
|
||||||
|
|
||||||
origin = 'https://' + (self._select_api_hostname(api_hostname, default_client))
|
return {
|
||||||
headers = {
|
'Authorization': f'{oauth_token["token_type"]} {oauth_token["access_token"]}',
|
||||||
'X-YouTube-Client-Name': str(
|
|
||||||
self._ytcfg_get_safe(ytcfg, lambda x: x['INNERTUBE_CONTEXT_CLIENT_NAME'], default_client=default_client)),
|
|
||||||
'X-YouTube-Client-Version': self._extract_client_version(ytcfg, default_client),
|
|
||||||
'Origin': origin,
|
|
||||||
'X-Youtube-Identity-Token': identity_token or self._extract_identity_token(ytcfg),
|
|
||||||
'X-Goog-PageId': account_syncid or self._extract_account_syncid(ytcfg),
|
|
||||||
'X-Goog-Visitor-Id': visitor_data or self._extract_visitor_data(ytcfg),
|
|
||||||
'User-Agent': self._ytcfg_get_safe(ytcfg, lambda x: x['INNERTUBE_CONTEXT']['client']['userAgent'], default_client=default_client),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _generate_cookie_auth_headers(self, *, ytcfg=None, account_syncid=None, session_index=None, origin=None, **kwargs):
|
||||||
|
headers = {}
|
||||||
|
account_syncid = account_syncid or self._extract_account_syncid(ytcfg)
|
||||||
|
if account_syncid:
|
||||||
|
headers['X-Goog-PageId'] = account_syncid
|
||||||
if session_index is None:
|
if session_index is None:
|
||||||
session_index = self._extract_session_index(ytcfg)
|
session_index = self._extract_session_index(ytcfg)
|
||||||
if account_syncid or session_index is not None:
|
if account_syncid or session_index is not None:
|
||||||
|
@ -776,8 +961,29 @@ def generate_api_headers(
|
||||||
if auth is not None:
|
if auth is not None:
|
||||||
headers['Authorization'] = auth
|
headers['Authorization'] = auth
|
||||||
headers['X-Origin'] = origin
|
headers['X-Origin'] = origin
|
||||||
|
|
||||||
|
return headers
|
||||||
|
|
||||||
|
def generate_api_headers(
|
||||||
|
self, *, ytcfg=None, account_syncid=None, session_index=None,
|
||||||
|
visitor_data=None, api_hostname=None, default_client='web', **kwargs):
|
||||||
|
|
||||||
|
origin = 'https://' + (self._select_api_hostname(api_hostname, default_client))
|
||||||
|
headers = {
|
||||||
|
'X-YouTube-Client-Name': str(
|
||||||
|
self._ytcfg_get_safe(ytcfg, lambda x: x['INNERTUBE_CONTEXT_CLIENT_NAME'], default_client=default_client)),
|
||||||
|
'X-YouTube-Client-Version': self._extract_client_version(ytcfg, default_client),
|
||||||
|
'Origin': origin,
|
||||||
|
'X-Goog-Visitor-Id': visitor_data or self._extract_visitor_data(ytcfg),
|
||||||
|
'User-Agent': self._ytcfg_get_safe(ytcfg, lambda x: x['INNERTUBE_CONTEXT']['client']['userAgent'], default_client=default_client),
|
||||||
|
**self._generate_oauth_headers(),
|
||||||
|
**self._generate_cookie_auth_headers(ytcfg=ytcfg, account_syncid=account_syncid, session_index=session_index, origin=origin),
|
||||||
|
}
|
||||||
return filter_dict(headers)
|
return filter_dict(headers)
|
||||||
|
|
||||||
|
def _generate_webpage_headers(self):
|
||||||
|
return self._generate_oauth_headers()
|
||||||
|
|
||||||
def _download_ytcfg(self, client, video_id):
|
def _download_ytcfg(self, client, video_id):
|
||||||
url = {
|
url = {
|
||||||
'web': 'https://www.youtube.com',
|
'web': 'https://www.youtube.com',
|
||||||
|
@ -787,7 +993,8 @@ def _download_ytcfg(self, client, video_id):
|
||||||
if not url:
|
if not url:
|
||||||
return {}
|
return {}
|
||||||
webpage = self._download_webpage(
|
webpage = self._download_webpage(
|
||||||
url, video_id, fatal=False, note=f'Downloading {client.replace("_", " ").strip()} client config')
|
url, video_id, fatal=False, note=f'Downloading {client.replace("_", " ").strip()} client config',
|
||||||
|
headers=self._generate_webpage_headers())
|
||||||
return self.extract_ytcfg(video_id, webpage) or {}
|
return self.extract_ytcfg(video_id, webpage) or {}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -1357,7 +1564,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||||
'401': {'ext': 'mp4', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'av01.0.12M.08'},
|
'401': {'ext': 'mp4', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'av01.0.12M.08'},
|
||||||
}
|
}
|
||||||
_SUBTITLE_FORMATS = ('json3', 'srv1', 'srv2', 'srv3', 'ttml', 'vtt')
|
_SUBTITLE_FORMATS = ('json3', 'srv1', 'srv2', 'srv3', 'ttml', 'vtt')
|
||||||
_DEFAULT_CLIENTS = ('ios', 'web_creator')
|
_DEFAULT_CLIENTS = ('ios', 'mweb')
|
||||||
|
|
||||||
_GEO_BYPASS = False
|
_GEO_BYPASS = False
|
||||||
|
|
||||||
|
@ -1525,6 +1732,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||||
'heatmap': 'count:100',
|
'heatmap': 'count:100',
|
||||||
'timestamp': 1401991663,
|
'timestamp': 1401991663,
|
||||||
},
|
},
|
||||||
|
'skip': 'Age-restricted; requires authentication',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'note': 'Age-gate video with embed allowed in public site',
|
'note': 'Age-gate video with embed allowed in public site',
|
||||||
|
@ -1555,6 +1763,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||||
'comment_count': int,
|
'comment_count': int,
|
||||||
'channel_is_verified': True,
|
'channel_is_verified': True,
|
||||||
},
|
},
|
||||||
|
'skip': 'Age-restricted; requires authentication',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'note': 'Age-gate video embedable only with clientScreen=EMBED',
|
'note': 'Age-gate video embedable only with clientScreen=EMBED',
|
||||||
|
@ -1585,6 +1794,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||||
'uploader_id': '@ProjektMelody',
|
'uploader_id': '@ProjektMelody',
|
||||||
'timestamp': 1577508724,
|
'timestamp': 1577508724,
|
||||||
},
|
},
|
||||||
|
'skip': 'Age-restricted; requires authentication',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'note': 'Non-Agegated non-embeddable video',
|
'note': 'Non-Agegated non-embeddable video',
|
||||||
|
@ -2356,6 +2566,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||||
'channel_is_verified': True,
|
'channel_is_verified': True,
|
||||||
'timestamp': 1405513526,
|
'timestamp': 1405513526,
|
||||||
},
|
},
|
||||||
|
'skip': 'Age-restricted; requires authentication',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
# restricted location, https://github.com/ytdl-org/youtube-dl/issues/28685
|
# restricted location, https://github.com/ytdl-org/youtube-dl/issues/28685
|
||||||
|
@ -2726,6 +2937,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||||
'timestamp': 1577508724,
|
'timestamp': 1577508724,
|
||||||
},
|
},
|
||||||
'params': {'extractor_args': {'youtube': {'player_client': ['tv_embedded']}}, 'format': '251-drc'},
|
'params': {'extractor_args': {'youtube': {'player_client': ['tv_embedded']}}, 'format': '251-drc'},
|
||||||
|
'skip': 'Age-restricted; requires authentication',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'url': 'https://www.youtube.com/live/qVv6vCqciTM',
|
'url': 'https://www.youtube.com/live/qVv6vCqciTM',
|
||||||
|
@ -3047,7 +3259,8 @@ def _load_player(self, video_id, player_url, fatal=True):
|
||||||
code = self._download_webpage(
|
code = self._download_webpage(
|
||||||
player_url, video_id, fatal=fatal,
|
player_url, video_id, fatal=fatal,
|
||||||
note='Downloading player ' + player_id,
|
note='Downloading player ' + player_id,
|
||||||
errnote=f'Download of {player_url} failed')
|
errnote=f'Download of {player_url} failed',
|
||||||
|
headers=self._generate_webpage_headers())
|
||||||
if code:
|
if code:
|
||||||
self._code_cache[player_id] = code
|
self._code_cache[player_id] = code
|
||||||
return self._code_cache.get(player_id)
|
return self._code_cache.get(player_id)
|
||||||
|
@ -3330,7 +3543,8 @@ def _mark_watched(self, video_id, player_responses):
|
||||||
|
|
||||||
self._download_webpage(
|
self._download_webpage(
|
||||||
url, video_id, f'Marking {label}watched',
|
url, video_id, f'Marking {label}watched',
|
||||||
'Unable to mark watched', fatal=False)
|
'Unable to mark watched', fatal=False,
|
||||||
|
headers=self._generate_webpage_headers())
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _extract_from_webpage(cls, url, webpage):
|
def _extract_from_webpage(cls, url, webpage):
|
||||||
|
@ -3953,26 +4167,15 @@ def append_client(*client_names):
|
||||||
else:
|
else:
|
||||||
prs.append(pr)
|
prs.append(pr)
|
||||||
|
|
||||||
# tv_embedded can work around age-gate and age-verification IF the video is embeddable
|
|
||||||
if self._is_agegated(pr) and variant != 'tv_embedded':
|
|
||||||
append_client(f'tv_embedded.{base_client}')
|
|
||||||
|
|
||||||
# Unauthenticated users will only get tv_embedded client formats if age-gated
|
|
||||||
if self._is_agegated(pr) and not self.is_authenticated:
|
|
||||||
self.to_screen(
|
|
||||||
f'{video_id}: This video is age-restricted; some formats may be missing '
|
|
||||||
f'without authentication. {self._login_hint()}', only_once=True)
|
|
||||||
|
|
||||||
# EU countries require age-verification for accounts to access age-restricted videos
|
# EU countries require age-verification for accounts to access age-restricted videos
|
||||||
# If account is not age-verified, _is_agegated() will be truthy for non-embedded clients
|
# If account is not age-verified, _is_agegated() will be truthy for non-embedded clients
|
||||||
# If embedding is disabled for the video, _is_unplayable() will be truthy for tv_embedded
|
if self.is_authenticated and self._is_agegated(pr):
|
||||||
embedding_is_disabled = variant == 'tv_embedded' and self._is_unplayable(pr)
|
|
||||||
if self.is_authenticated and (self._is_agegated(pr) or embedding_is_disabled):
|
|
||||||
self.to_screen(
|
self.to_screen(
|
||||||
f'{video_id}: This video is age-restricted and YouTube is requiring '
|
f'{video_id}: This video is age-restricted and YouTube is requiring '
|
||||||
'account age-verification; some formats may be missing', only_once=True)
|
'account age-verification; some formats may be missing', only_once=True)
|
||||||
# web_creator and mediaconnect can work around the age-verification requirement
|
# web_creator and mediaconnect can work around the age-verification requirement
|
||||||
# _producer, _testsuite, & _vr variants can also work around age-verification
|
# _testsuite & _vr variants can also work around age-verification
|
||||||
|
# tv_embedded may(?) still work around age-verification if the video is embeddable
|
||||||
append_client('web_creator', 'mediaconnect')
|
append_client('web_creator', 'mediaconnect')
|
||||||
|
|
||||||
prs.extend(deprioritized_prs)
|
prs.extend(deprioritized_prs)
|
||||||
|
@ -4110,7 +4313,7 @@ def build_fragments(f):
|
||||||
self.report_warning(
|
self.report_warning(
|
||||||
f'{video_id}: Some formats are possibly damaged. They will be deprioritized', only_once=True)
|
f'{video_id}: Some formats are possibly damaged. They will be deprioritized', only_once=True)
|
||||||
|
|
||||||
client_name = fmt.get(STREAMING_DATA_CLIENT_NAME)
|
client_name = fmt[STREAMING_DATA_CLIENT_NAME]
|
||||||
po_token = fmt.get(STREAMING_DATA_PO_TOKEN)
|
po_token = fmt.get(STREAMING_DATA_PO_TOKEN)
|
||||||
|
|
||||||
if po_token:
|
if po_token:
|
||||||
|
@ -4135,7 +4338,7 @@ def build_fragments(f):
|
||||||
try_get(fmt, lambda x: x['projectionType'].replace('RECTANGULAR', '').lower()),
|
try_get(fmt, lambda x: x['projectionType'].replace('RECTANGULAR', '').lower()),
|
||||||
try_get(fmt, lambda x: x['spatialAudioType'].replace('SPATIAL_AUDIO_TYPE_', '').lower()),
|
try_get(fmt, lambda x: x['spatialAudioType'].replace('SPATIAL_AUDIO_TYPE_', '').lower()),
|
||||||
is_damaged and 'DAMAGED', is_broken and 'BROKEN',
|
is_damaged and 'DAMAGED', is_broken and 'BROKEN',
|
||||||
(self.get_param('verbose') or all_formats) and client_name,
|
(self.get_param('verbose') or all_formats) and short_client_name(client_name),
|
||||||
delim=', '),
|
delim=', '),
|
||||||
# Format 22 is likely to be damaged. See https://github.com/yt-dlp/yt-dlp/issues/3372
|
# Format 22 is likely to be damaged. See https://github.com/yt-dlp/yt-dlp/issues/3372
|
||||||
'source_preference': (-5 if itag == '22' else -1) + (100 if 'Premium' in name else 0),
|
'source_preference': (-5 if itag == '22' else -1) + (100 if 'Premium' in name else 0),
|
||||||
|
@ -4234,7 +4437,8 @@ def process_manifest_format(f, proto, client_name, itag, po_token):
|
||||||
if f['quality'] == -1 and f.get('height'):
|
if f['quality'] == -1 and f.get('height'):
|
||||||
f['quality'] = q(res_qualities[min(res_qualities, key=lambda x: abs(x - f['height']))])
|
f['quality'] = q(res_qualities[min(res_qualities, key=lambda x: abs(x - f['height']))])
|
||||||
if self.get_param('verbose') or all_formats:
|
if self.get_param('verbose') or all_formats:
|
||||||
f['format_note'] = join_nonempty(f.get('format_note'), client_name, delim=', ')
|
f['format_note'] = join_nonempty(
|
||||||
|
f.get('format_note'), short_client_name(client_name), delim=', ')
|
||||||
if f.get('fps') and f['fps'] <= 1:
|
if f.get('fps') and f['fps'] <= 1:
|
||||||
del f['fps']
|
del f['fps']
|
||||||
|
|
||||||
|
@ -4245,7 +4449,7 @@ def process_manifest_format(f, proto, client_name, itag, po_token):
|
||||||
|
|
||||||
subtitles = {}
|
subtitles = {}
|
||||||
for sd in streaming_data:
|
for sd in streaming_data:
|
||||||
client_name = sd.get(STREAMING_DATA_CLIENT_NAME)
|
client_name = sd[STREAMING_DATA_CLIENT_NAME]
|
||||||
po_token = sd.get(STREAMING_DATA_PO_TOKEN)
|
po_token = sd.get(STREAMING_DATA_PO_TOKEN)
|
||||||
hls_manifest_url = 'hls' not in skip_manifests and sd.get('hlsManifestUrl')
|
hls_manifest_url = 'hls' not in skip_manifests and sd.get('hlsManifestUrl')
|
||||||
if hls_manifest_url:
|
if hls_manifest_url:
|
||||||
|
@ -4321,7 +4525,7 @@ def _download_player_responses(self, url, smuggled_data, video_id, webpage_url):
|
||||||
if pp:
|
if pp:
|
||||||
query['pp'] = pp
|
query['pp'] = pp
|
||||||
webpage = self._download_webpage(
|
webpage = self._download_webpage(
|
||||||
webpage_url, video_id, fatal=False, query=query)
|
webpage_url, video_id, fatal=False, query=query, headers=self._generate_webpage_headers())
|
||||||
|
|
||||||
master_ytcfg = self.extract_ytcfg(video_id, webpage) or self._get_default_ytcfg()
|
master_ytcfg = self.extract_ytcfg(video_id, webpage) or self._get_default_ytcfg()
|
||||||
|
|
||||||
|
@ -4700,11 +4904,12 @@ def process_language(container, base_url, lang_code, sub_name, query):
|
||||||
headers=self.generate_api_headers(ytcfg=master_ytcfg),
|
headers=self.generate_api_headers(ytcfg=master_ytcfg),
|
||||||
note='Downloading initial data API JSON')
|
note='Downloading initial data API JSON')
|
||||||
|
|
||||||
|
COMMENTS_SECTION_IDS = ('comment-item-section', 'engagement-panel-comments-section')
|
||||||
info['comment_count'] = traverse_obj(initial_data, (
|
info['comment_count'] = traverse_obj(initial_data, (
|
||||||
'contents', 'twoColumnWatchNextResults', 'results', 'results', 'contents', ..., 'itemSectionRenderer',
|
'contents', 'twoColumnWatchNextResults', 'results', 'results', 'contents', ..., 'itemSectionRenderer',
|
||||||
'contents', ..., 'commentsEntryPointHeaderRenderer', 'commentCount',
|
'contents', ..., 'commentsEntryPointHeaderRenderer', 'commentCount',
|
||||||
), (
|
), (
|
||||||
'engagementPanels', lambda _, v: v['engagementPanelSectionListRenderer']['panelIdentifier'] == 'comment-item-section',
|
'engagementPanels', lambda _, v: v['engagementPanelSectionListRenderer']['panelIdentifier'] in COMMENTS_SECTION_IDS,
|
||||||
'engagementPanelSectionListRenderer', 'header', 'engagementPanelTitleHeaderRenderer', 'contextualInfo',
|
'engagementPanelSectionListRenderer', 'header', 'engagementPanelTitleHeaderRenderer', 'contextualInfo',
|
||||||
), expected_type=self._get_count, get_all=False)
|
), expected_type=self._get_count, get_all=False)
|
||||||
|
|
||||||
|
@ -5608,7 +5813,7 @@ def _extract_webpage(self, url, item_id, fatal=True):
|
||||||
webpage, data = None, None
|
webpage, data = None, None
|
||||||
for retry in self.RetryManager(fatal=fatal):
|
for retry in self.RetryManager(fatal=fatal):
|
||||||
try:
|
try:
|
||||||
webpage = self._download_webpage(url, item_id, note='Downloading webpage')
|
webpage = self._download_webpage(url, item_id, note='Downloading webpage', headers=self._generate_webpage_headers())
|
||||||
data = self.extract_yt_initial_data(item_id, webpage or '', fatal=fatal) or {}
|
data = self.extract_yt_initial_data(item_id, webpage or '', fatal=fatal) or {}
|
||||||
except ExtractorError as e:
|
except ExtractorError as e:
|
||||||
if isinstance(e.cause, network_exceptions):
|
if isinstance(e.cause, network_exceptions):
|
||||||
|
@ -6982,7 +7187,7 @@ def _real_extract(self, url, smuggled_data):
|
||||||
raise ExtractorError('Unable to recognize tab page')
|
raise ExtractorError('Unable to recognize tab page')
|
||||||
|
|
||||||
|
|
||||||
class YoutubePlaylistIE(InfoExtractor):
|
class YoutubePlaylistIE(YoutubeBaseInfoExtractor):
|
||||||
IE_DESC = 'YouTube playlists'
|
IE_DESC = 'YouTube playlists'
|
||||||
_VALID_URL = r'''(?x)(?:
|
_VALID_URL = r'''(?x)(?:
|
||||||
(?:https?://)?
|
(?:https?://)?
|
||||||
|
@ -7096,7 +7301,7 @@ def _real_extract(self, url):
|
||||||
return self.url_result(url, ie=YoutubeTabIE.ie_key(), video_id=playlist_id)
|
return self.url_result(url, ie=YoutubeTabIE.ie_key(), video_id=playlist_id)
|
||||||
|
|
||||||
|
|
||||||
class YoutubeYtBeIE(InfoExtractor):
|
class YoutubeYtBeIE(YoutubeBaseInfoExtractor):
|
||||||
IE_DESC = 'youtu.be'
|
IE_DESC = 'youtu.be'
|
||||||
_VALID_URL = rf'https?://youtu\.be/(?P<id>[0-9A-Za-z_-]{{11}})/*?.*?\blist=(?P<playlist_id>{YoutubeBaseInfoExtractor._PLAYLIST_ID_RE})'
|
_VALID_URL = rf'https?://youtu\.be/(?P<id>[0-9A-Za-z_-]{{11}})/*?.*?\blist=(?P<playlist_id>{YoutubeBaseInfoExtractor._PLAYLIST_ID_RE})'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
|
@ -7147,7 +7352,7 @@ def _real_extract(self, url):
|
||||||
}), ie=YoutubeTabIE.ie_key(), video_id=playlist_id)
|
}), ie=YoutubeTabIE.ie_key(), video_id=playlist_id)
|
||||||
|
|
||||||
|
|
||||||
class YoutubeLivestreamEmbedIE(InfoExtractor):
|
class YoutubeLivestreamEmbedIE(YoutubeBaseInfoExtractor):
|
||||||
IE_DESC = 'YouTube livestream embeds'
|
IE_DESC = 'YouTube livestream embeds'
|
||||||
_VALID_URL = r'https?://(?:\w+\.)?youtube\.com/embed/live_stream/?\?(?:[^#]+&)?channel=(?P<id>[^&#]+)'
|
_VALID_URL = r'https?://(?:\w+\.)?youtube\.com/embed/live_stream/?\?(?:[^#]+&)?channel=(?P<id>[^&#]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
|
@ -7162,7 +7367,7 @@ def _real_extract(self, url):
|
||||||
ie=YoutubeTabIE.ie_key(), video_id=channel_id)
|
ie=YoutubeTabIE.ie_key(), video_id=channel_id)
|
||||||
|
|
||||||
|
|
||||||
class YoutubeYtUserIE(InfoExtractor):
|
class YoutubeYtUserIE(YoutubeBaseInfoExtractor):
|
||||||
IE_DESC = 'YouTube user videos; "ytuser:" prefix'
|
IE_DESC = 'YouTube user videos; "ytuser:" prefix'
|
||||||
IE_NAME = 'youtube:user'
|
IE_NAME = 'youtube:user'
|
||||||
_VALID_URL = r'ytuser:(?P<id>.+)'
|
_VALID_URL = r'ytuser:(?P<id>.+)'
|
||||||
|
@ -7449,7 +7654,7 @@ def _real_extract(self, url):
|
||||||
return self.playlist_result(self._search_results(query, params, default_client='web_music'), title, title)
|
return self.playlist_result(self._search_results(query, params, default_client='web_music'), title, title)
|
||||||
|
|
||||||
|
|
||||||
class YoutubeFeedsInfoExtractor(InfoExtractor):
|
class YoutubeFeedsInfoExtractor(YoutubeBaseInfoExtractor):
|
||||||
"""
|
"""
|
||||||
Base class for feed extractors
|
Base class for feed extractors
|
||||||
Subclasses must re-define the _FEED_NAME property.
|
Subclasses must re-define the _FEED_NAME property.
|
||||||
|
@ -7457,9 +7662,6 @@ class YoutubeFeedsInfoExtractor(InfoExtractor):
|
||||||
_LOGIN_REQUIRED = True
|
_LOGIN_REQUIRED = True
|
||||||
_FEED_NAME = 'feeds'
|
_FEED_NAME = 'feeds'
|
||||||
|
|
||||||
def _real_initialize(self):
|
|
||||||
YoutubeBaseInfoExtractor._check_login_required(self)
|
|
||||||
|
|
||||||
@classproperty
|
@classproperty
|
||||||
def IE_NAME(cls):
|
def IE_NAME(cls):
|
||||||
return f'youtube:{cls._FEED_NAME}'
|
return f'youtube:{cls._FEED_NAME}'
|
||||||
|
@ -7469,7 +7671,7 @@ def _real_extract(self, url):
|
||||||
f'https://www.youtube.com/feed/{self._FEED_NAME}', ie=YoutubeTabIE.ie_key())
|
f'https://www.youtube.com/feed/{self._FEED_NAME}', ie=YoutubeTabIE.ie_key())
|
||||||
|
|
||||||
|
|
||||||
class YoutubeWatchLaterIE(InfoExtractor):
|
class YoutubeWatchLaterIE(YoutubeBaseInfoExtractor):
|
||||||
IE_NAME = 'youtube:watchlater'
|
IE_NAME = 'youtube:watchlater'
|
||||||
IE_DESC = 'Youtube watch later list; ":ytwatchlater" keyword (requires cookies)'
|
IE_DESC = 'Youtube watch later list; ":ytwatchlater" keyword (requires cookies)'
|
||||||
_VALID_URL = r':ytwatchlater'
|
_VALID_URL = r':ytwatchlater'
|
||||||
|
@ -7523,7 +7725,7 @@ class YoutubeHistoryIE(YoutubeFeedsInfoExtractor):
|
||||||
}]
|
}]
|
||||||
|
|
||||||
|
|
||||||
class YoutubeShortsAudioPivotIE(InfoExtractor):
|
class YoutubeShortsAudioPivotIE(YoutubeBaseInfoExtractor):
|
||||||
IE_DESC = 'YouTube Shorts audio pivot (Shorts using audio of a given video)'
|
IE_DESC = 'YouTube Shorts audio pivot (Shorts using audio of a given video)'
|
||||||
IE_NAME = 'youtube:shorts:pivot:audio'
|
IE_NAME = 'youtube:shorts:pivot:audio'
|
||||||
_VALID_URL = r'https?://(?:www\.)?youtube\.com/source/(?P<id>[\w-]{11})/shorts'
|
_VALID_URL = r'https?://(?:www\.)?youtube\.com/source/(?P<id>[\w-]{11})/shorts'
|
||||||
|
@ -7547,7 +7749,7 @@ def _real_extract(self, url):
|
||||||
ie=YoutubeTabIE)
|
ie=YoutubeTabIE)
|
||||||
|
|
||||||
|
|
||||||
class YoutubeTruncatedURLIE(InfoExtractor):
|
class YoutubeTruncatedURLIE(YoutubeBaseInfoExtractor):
|
||||||
IE_NAME = 'youtube:truncated_url'
|
IE_NAME = 'youtube:truncated_url'
|
||||||
IE_DESC = False # Do not list
|
IE_DESC = False # Do not list
|
||||||
_VALID_URL = r'''(?x)
|
_VALID_URL = r'''(?x)
|
||||||
|
@ -7590,9 +7792,9 @@ def _real_extract(self, url):
|
||||||
raise ExtractorError(
|
raise ExtractorError(
|
||||||
'Did you forget to quote the URL? Remember that & is a meta '
|
'Did you forget to quote the URL? Remember that & is a meta '
|
||||||
'character in most shells, so you want to put the URL in quotes, '
|
'character in most shells, so you want to put the URL in quotes, '
|
||||||
'like youtube-dl '
|
'like yt-dlp '
|
||||||
'"https://www.youtube.com/watch?feature=foo&v=BaW_jenozKc" '
|
'"https://www.youtube.com/watch?feature=foo&v=BaW_jenozKc" '
|
||||||
' or simply youtube-dl BaW_jenozKc .',
|
' or simply yt-dlp BaW_jenozKc .',
|
||||||
expected=True)
|
expected=True)
|
||||||
|
|
||||||
|
|
||||||
|
@ -7654,6 +7856,8 @@ def _real_extract(self, url):
|
||||||
'id': clip_id,
|
'id': clip_id,
|
||||||
'section_start': int(clip_data['startTimeMs']) / 1000,
|
'section_start': int(clip_data['startTimeMs']) / 1000,
|
||||||
'section_end': int(clip_data['endTimeMs']) / 1000,
|
'section_end': int(clip_data['endTimeMs']) / 1000,
|
||||||
|
'_format_sort_fields': ( # https protocol is prioritized for ffmpeg compatibility
|
||||||
|
'proto:https', 'quality', 'res', 'fps', 'hdr:12', 'source', 'vcodec:vp9.2', 'channels', 'acodec', 'lang'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -7704,7 +7908,7 @@ def _real_extract(self, url):
|
||||||
return self.url_result(redirect_url)
|
return self.url_result(redirect_url)
|
||||||
|
|
||||||
|
|
||||||
class YoutubeTruncatedIDIE(InfoExtractor):
|
class YoutubeTruncatedIDIE(YoutubeBaseInfoExtractor):
|
||||||
IE_NAME = 'youtube:truncated_id'
|
IE_NAME = 'youtube:truncated_id'
|
||||||
IE_DESC = False # Do not list
|
IE_DESC = False # Do not list
|
||||||
_VALID_URL = r'https?://(?:www\.)?youtube\.com/watch\?v=(?P<id>[0-9A-Za-z_-]{1,10})$'
|
_VALID_URL = r'https?://(?:www\.)?youtube\.com/watch\?v=(?P<id>[0-9A-Za-z_-]{1,10})$'
|
||||||
|
|
|
@ -31,9 +31,9 @@
|
||||||
|
|
||||||
curl_cffi_version = tuple(map(int, re.split(r'[^\d]+', curl_cffi.__version__)[:3]))
|
curl_cffi_version = tuple(map(int, re.split(r'[^\d]+', curl_cffi.__version__)[:3]))
|
||||||
|
|
||||||
if curl_cffi_version != (0, 5, 10) and not ((0, 7, 0) <= curl_cffi_version < (0, 8, 0)):
|
if curl_cffi_version != (0, 5, 10) and not ((0, 7, 0) <= curl_cffi_version < (0, 7, 2)):
|
||||||
curl_cffi._yt_dlp__version = f'{curl_cffi.__version__} (unsupported)'
|
curl_cffi._yt_dlp__version = f'{curl_cffi.__version__} (unsupported)'
|
||||||
raise ImportError('Only curl_cffi versions 0.5.10, 0.7.X are supported')
|
raise ImportError('Only curl_cffi versions 0.5.10, 0.7.0 and 0.7.1 are supported')
|
||||||
|
|
||||||
import curl_cffi.requests
|
import curl_cffi.requests
|
||||||
from curl_cffi.const import CurlECode, CurlOpt
|
from curl_cffi.const import CurlECode, CurlOpt
|
||||||
|
|
|
@ -33,8 +33,8 @@
|
||||||
import websockets.version
|
import websockets.version
|
||||||
|
|
||||||
websockets_version = tuple(map(int_or_none, websockets.version.version.split('.')))
|
websockets_version = tuple(map(int_or_none, websockets.version.version.split('.')))
|
||||||
if websockets_version < (12, 0):
|
if websockets_version < (13, 0):
|
||||||
raise ImportError('Only websockets>=12.0 is supported')
|
raise ImportError('Only websockets>=13.0 is supported')
|
||||||
|
|
||||||
import websockets.sync.client
|
import websockets.sync.client
|
||||||
from websockets.uri import parse_uri
|
from websockets.uri import parse_uri
|
||||||
|
|
|
@ -408,6 +408,14 @@ def _alias_callback(option, opt_str, value, parser, opts, nargs):
|
||||||
help=(
|
help=(
|
||||||
'Location of the main configuration file; either the path to the config or its containing directory '
|
'Location of the main configuration file; either the path to the config or its containing directory '
|
||||||
'("-" for stdin). Can be used multiple times and inside other configuration files'))
|
'("-" for stdin). Can be used multiple times and inside other configuration files'))
|
||||||
|
general.add_option(
|
||||||
|
'--plugin-dirs',
|
||||||
|
dest='plugin_dirs', metavar='PATH', action='append',
|
||||||
|
help=(
|
||||||
|
'Path to an additional directory to search for plugins. '
|
||||||
|
'This option can be used multiple times to add multiple directories. '
|
||||||
|
'Note that this currently only works for extractor plugins; '
|
||||||
|
'postprocessor plugins can only be loaded from the default plugin directories'))
|
||||||
general.add_option(
|
general.add_option(
|
||||||
'--flat-playlist',
|
'--flat-playlist',
|
||||||
action='store_const', dest='extract_flat', const='in_playlist', default=False,
|
action='store_const', dest='extract_flat', const='in_playlist', default=False,
|
||||||
|
@ -623,13 +631,13 @@ def _alias_callback(option, opt_str, value, parser, opts, nargs):
|
||||||
metavar='DATE', dest='datebefore', default=None,
|
metavar='DATE', dest='datebefore', default=None,
|
||||||
help=(
|
help=(
|
||||||
'Download only videos uploaded on or before this date. '
|
'Download only videos uploaded on or before this date. '
|
||||||
'The date formats accepted is the same as --date'))
|
'The date formats accepted are the same as --date'))
|
||||||
selection.add_option(
|
selection.add_option(
|
||||||
'--dateafter',
|
'--dateafter',
|
||||||
metavar='DATE', dest='dateafter', default=None,
|
metavar='DATE', dest='dateafter', default=None,
|
||||||
help=(
|
help=(
|
||||||
'Download only videos uploaded on or after this date. '
|
'Download only videos uploaded on or after this date. '
|
||||||
'The date formats accepted is the same as --date'))
|
'The date formats accepted are the same as --date'))
|
||||||
selection.add_option(
|
selection.add_option(
|
||||||
'--min-views',
|
'--min-views',
|
||||||
metavar='COUNT', dest='min_views', default=None, type=int,
|
metavar='COUNT', dest='min_views', default=None, type=int,
|
||||||
|
@ -647,16 +655,16 @@ def _alias_callback(option, opt_str, value, parser, opts, nargs):
|
||||||
'You can also simply specify a field to match if the field is present, '
|
'You can also simply specify a field to match if the field is present, '
|
||||||
'use "!field" to check if the field is not present, and "&" to check multiple conditions. '
|
'use "!field" to check if the field is not present, and "&" to check multiple conditions. '
|
||||||
'Use a "\\" to escape "&" or quotes if needed. If used multiple times, '
|
'Use a "\\" to escape "&" or quotes if needed. If used multiple times, '
|
||||||
'the filter matches if at least one of the conditions is met. E.g. --match-filter '
|
'the filter matches if at least one of the conditions is met. E.g. --match-filters '
|
||||||
'!is_live --match-filter "like_count>?100 & description~=\'(?i)\\bcats \\& dogs\\b\'" '
|
'!is_live --match-filters "like_count>?100 & description~=\'(?i)\\bcats \\& dogs\\b\'" '
|
||||||
'matches only videos that are not live OR those that have a like count more than 100 '
|
'matches only videos that are not live OR those that have a like count more than 100 '
|
||||||
'(or the like field is not available) and also has a description '
|
'(or the like field is not available) and also has a description '
|
||||||
'that contains the phrase "cats & dogs" (caseless). '
|
'that contains the phrase "cats & dogs" (caseless). '
|
||||||
'Use "--match-filter -" to interactively ask whether to download each video'))
|
'Use "--match-filters -" to interactively ask whether to download each video'))
|
||||||
selection.add_option(
|
selection.add_option(
|
||||||
'--no-match-filters',
|
'--no-match-filters',
|
||||||
dest='match_filter', action='store_const', const=None,
|
dest='match_filter', action='store_const', const=None,
|
||||||
help='Do not use any --match-filter (default)')
|
help='Do not use any --match-filters (default)')
|
||||||
selection.add_option(
|
selection.add_option(
|
||||||
'--break-match-filters',
|
'--break-match-filters',
|
||||||
metavar='FILTER', dest='breaking_match_filter', action='append',
|
metavar='FILTER', dest='breaking_match_filter', action='append',
|
||||||
|
@ -704,7 +712,7 @@ def _alias_callback(option, opt_str, value, parser, opts, nargs):
|
||||||
selection.add_option(
|
selection.add_option(
|
||||||
'--break-per-input',
|
'--break-per-input',
|
||||||
action='store_true', dest='break_per_url', default=False,
|
action='store_true', dest='break_per_url', default=False,
|
||||||
help='Alters --max-downloads, --break-on-existing, --break-match-filter, and autonumber to reset per input URL')
|
help='Alters --max-downloads, --break-on-existing, --break-match-filters, and autonumber to reset per input URL')
|
||||||
selection.add_option(
|
selection.add_option(
|
||||||
'--no-break-per-input',
|
'--no-break-per-input',
|
||||||
action='store_false', dest='break_per_url',
|
action='store_false', dest='break_per_url',
|
||||||
|
@ -825,7 +833,7 @@ def _alias_callback(option, opt_str, value, parser, opts, nargs):
|
||||||
'--prefer-free-formats',
|
'--prefer-free-formats',
|
||||||
action='store_true', dest='prefer_free_formats', default=False,
|
action='store_true', dest='prefer_free_formats', default=False,
|
||||||
help=(
|
help=(
|
||||||
'Prefer video formats with free containers over non-free ones of same quality. '
|
'Prefer video formats with free containers over non-free ones of the same quality. '
|
||||||
'Use with "-S ext" to strictly prefer free containers irrespective of quality'))
|
'Use with "-S ext" to strictly prefer free containers irrespective of quality'))
|
||||||
video_format.add_option(
|
video_format.add_option(
|
||||||
'--no-prefer-free-formats',
|
'--no-prefer-free-formats',
|
||||||
|
@ -899,13 +907,14 @@ def _alias_callback(option, opt_str, value, parser, opts, nargs):
|
||||||
subtitles.add_option(
|
subtitles.add_option(
|
||||||
'--sub-format',
|
'--sub-format',
|
||||||
action='store', dest='subtitlesformat', metavar='FORMAT', default='best',
|
action='store', dest='subtitlesformat', metavar='FORMAT', default='best',
|
||||||
help='Subtitle format; accepts formats preference, e.g. "srt" or "ass/srt/best"')
|
help='Subtitle format; accepts formats preference separated by "/", e.g. "srt" or "ass/srt/best"')
|
||||||
subtitles.add_option(
|
subtitles.add_option(
|
||||||
'--sub-langs', '--srt-langs',
|
'--sub-langs', '--srt-langs',
|
||||||
action='callback', dest='subtitleslangs', metavar='LANGS', type='str',
|
action='callback', dest='subtitleslangs', metavar='LANGS', type='str',
|
||||||
default=[], callback=_list_from_options_callback,
|
default=[], callback=_list_from_options_callback,
|
||||||
help=(
|
help=(
|
||||||
'Languages of the subtitles to download (can be regex) or "all" separated by commas, e.g. --sub-langs "en.*,ja". '
|
'Languages of the subtitles to download (can be regex) or "all" separated by commas, e.g. --sub-langs "en.*,ja" '
|
||||||
|
'(where "en.*" is a regex pattern that matches "en" followed by 0 or more of any character). '
|
||||||
'You can prefix the language code with a "-" to exclude it from the requested languages, e.g. --sub-langs all,-live_chat. '
|
'You can prefix the language code with a "-" to exclude it from the requested languages, e.g. --sub-langs all,-live_chat. '
|
||||||
'Use --list-subs for a list of available language tags'))
|
'Use --list-subs for a list of available language tags'))
|
||||||
|
|
||||||
|
@ -1174,7 +1183,7 @@ def _alias_callback(option, opt_str, value, parser, opts, nargs):
|
||||||
'--print-to-file',
|
'--print-to-file',
|
||||||
metavar='[WHEN:]TEMPLATE FILE', dest='print_to_file', nargs=2, **when_prefix('video'),
|
metavar='[WHEN:]TEMPLATE FILE', dest='print_to_file', nargs=2, **when_prefix('video'),
|
||||||
help=(
|
help=(
|
||||||
'Append given template to the file. The values of WHEN and TEMPLATE are same as that of --print. '
|
'Append given template to the file. The values of WHEN and TEMPLATE are the same as that of --print. '
|
||||||
'FILE uses the same syntax as the output template. This option can be used multiple times'))
|
'FILE uses the same syntax as the output template. This option can be used multiple times'))
|
||||||
verbosity.add_option(
|
verbosity.add_option(
|
||||||
'-g', '--get-url',
|
'-g', '--get-url',
|
||||||
|
@ -1218,7 +1227,7 @@ def _alias_callback(option, opt_str, value, parser, opts, nargs):
|
||||||
'-J', '--dump-single-json',
|
'-J', '--dump-single-json',
|
||||||
action='store_true', dest='dump_single_json', default=False,
|
action='store_true', dest='dump_single_json', default=False,
|
||||||
help=(
|
help=(
|
||||||
'Quiet, but print JSON information for each url or infojson passed. Simulate unless --no-simulate is used. '
|
'Quiet, but print JSON information for each URL or infojson passed. Simulate unless --no-simulate is used. '
|
||||||
'If the URL refers to a playlist, the whole playlist information is dumped in a single line'))
|
'If the URL refers to a playlist, the whole playlist information is dumped in a single line'))
|
||||||
verbosity.add_option(
|
verbosity.add_option(
|
||||||
'--print-json',
|
'--print-json',
|
||||||
|
@ -1562,7 +1571,7 @@ def _alias_callback(option, opt_str, value, parser, opts, nargs):
|
||||||
help=(
|
help=(
|
||||||
'Remux the video into another container if necessary '
|
'Remux the video into another container if necessary '
|
||||||
f'(currently supported: {", ".join(FFmpegVideoRemuxerPP.SUPPORTED_EXTS)}). '
|
f'(currently supported: {", ".join(FFmpegVideoRemuxerPP.SUPPORTED_EXTS)}). '
|
||||||
'If target container does not support the video/audio codec, remuxing will fail. You can specify multiple rules; '
|
'If the target container does not support the video/audio codec, remuxing will fail. You can specify multiple rules; '
|
||||||
'e.g. "aac>m4a/mov>mp4/mkv" will remux aac to m4a, mov to mp4 and anything else to mkv'))
|
'e.g. "aac>m4a/mov>mp4/mkv" will remux aac to m4a, mov to mp4 and anything else to mkv'))
|
||||||
postproc.add_option(
|
postproc.add_option(
|
||||||
'--recode-video',
|
'--recode-video',
|
||||||
|
@ -1668,7 +1677,7 @@ def _alias_callback(option, opt_str, value, parser, opts, nargs):
|
||||||
postproc.add_option(
|
postproc.add_option(
|
||||||
'--xattrs', '--xattr',
|
'--xattrs', '--xattr',
|
||||||
action='store_true', dest='xattrs', default=False,
|
action='store_true', dest='xattrs', default=False,
|
||||||
help='Write metadata to the video file\'s xattrs (using dublin core and xdg standards)')
|
help='Write metadata to the video file\'s xattrs (using Dublin Core and XDG standards)')
|
||||||
postproc.add_option(
|
postproc.add_option(
|
||||||
'--concat-playlist',
|
'--concat-playlist',
|
||||||
metavar='POLICY', dest='concat_playlist', default='multi_video',
|
metavar='POLICY', dest='concat_playlist', default='multi_video',
|
||||||
|
@ -1676,7 +1685,7 @@ def _alias_callback(option, opt_str, value, parser, opts, nargs):
|
||||||
help=(
|
help=(
|
||||||
'Concatenate videos in a playlist. One of "never", "always", or '
|
'Concatenate videos in a playlist. One of "never", "always", or '
|
||||||
'"multi_video" (default; only when the videos form a single show). '
|
'"multi_video" (default; only when the videos form a single show). '
|
||||||
'All the video files must have same codecs and number of streams to be concatable. '
|
'All the video files must have the same codecs and number of streams to be concatenable. '
|
||||||
'The "pl_video:" prefix can be used with "--paths" and "--output" to '
|
'The "pl_video:" prefix can be used with "--paths" and "--output" to '
|
||||||
'set the output filename for the concatenated files. See "OUTPUT TEMPLATE" for details'))
|
'set the output filename for the concatenated files. See "OUTPUT TEMPLATE" for details'))
|
||||||
postproc.add_option(
|
postproc.add_option(
|
||||||
|
@ -1686,8 +1695,8 @@ def _alias_callback(option, opt_str, value, parser, opts, nargs):
|
||||||
help=(
|
help=(
|
||||||
'Automatically correct known faults of the file. '
|
'Automatically correct known faults of the file. '
|
||||||
'One of never (do nothing), warn (only emit a warning), '
|
'One of never (do nothing), warn (only emit a warning), '
|
||||||
'detect_or_warn (the default; fix file if we can, warn otherwise), '
|
'detect_or_warn (the default; fix the file if we can, warn otherwise), '
|
||||||
'force (try fixing even if file already exists)'))
|
'force (try fixing even if the file already exists)'))
|
||||||
postproc.add_option(
|
postproc.add_option(
|
||||||
'--prefer-avconv', '--no-prefer-ffmpeg',
|
'--prefer-avconv', '--no-prefer-ffmpeg',
|
||||||
action='store_false', dest='prefer_ffmpeg',
|
action='store_false', dest='prefer_ffmpeg',
|
||||||
|
@ -1706,7 +1715,7 @@ def _alias_callback(option, opt_str, value, parser, opts, nargs):
|
||||||
help=(
|
help=(
|
||||||
'Execute a command, optionally prefixed with when to execute it, separated by a ":". '
|
'Execute a command, optionally prefixed with when to execute it, separated by a ":". '
|
||||||
'Supported values of "WHEN" are the same as that of --use-postprocessor (default: after_move). '
|
'Supported values of "WHEN" are the same as that of --use-postprocessor (default: after_move). '
|
||||||
'Same syntax as the output template can be used to pass any field as arguments to the command. '
|
'The same syntax as the output template can be used to pass any field as arguments to the command. '
|
||||||
'If no fields are passed, %(filepath,_filename|)q is appended to the end of the command. '
|
'If no fields are passed, %(filepath,_filename|)q is appended to the end of the command. '
|
||||||
'This option can be used multiple times'))
|
'This option can be used multiple times'))
|
||||||
postproc.add_option(
|
postproc.add_option(
|
||||||
|
@ -1725,15 +1734,17 @@ def _alias_callback(option, opt_str, value, parser, opts, nargs):
|
||||||
'--convert-subs', '--convert-sub', '--convert-subtitles',
|
'--convert-subs', '--convert-sub', '--convert-subtitles',
|
||||||
metavar='FORMAT', dest='convertsubtitles', default=None,
|
metavar='FORMAT', dest='convertsubtitles', default=None,
|
||||||
help=(
|
help=(
|
||||||
'Convert the subtitles to another format (currently supported: {}) '
|
'Convert the subtitles to another format '
|
||||||
'(Alias: --convert-subtitles)'.format(', '.join(sorted(FFmpegSubtitlesConvertorPP.SUPPORTED_EXTS)))))
|
f'(currently supported: {", ".join(sorted(FFmpegSubtitlesConvertorPP.SUPPORTED_EXTS))}). '
|
||||||
|
'Use "--convert-subs none" to disable conversion (default) (Alias: --convert-subtitles)'))
|
||||||
postproc.add_option(
|
postproc.add_option(
|
||||||
'--convert-thumbnails',
|
'--convert-thumbnails',
|
||||||
metavar='FORMAT', dest='convertthumbnails', default=None,
|
metavar='FORMAT', dest='convertthumbnails', default=None,
|
||||||
help=(
|
help=(
|
||||||
'Convert the thumbnails to another format '
|
'Convert the thumbnails to another format '
|
||||||
f'(currently supported: {", ".join(sorted(FFmpegThumbnailsConvertorPP.SUPPORTED_EXTS))}). '
|
f'(currently supported: {", ".join(sorted(FFmpegThumbnailsConvertorPP.SUPPORTED_EXTS))}). '
|
||||||
'You can specify multiple rules using similar syntax as --remux-video'))
|
'You can specify multiple rules using similar syntax as "--remux-video". '
|
||||||
|
'Use "--convert-thumbnails none" to disable conversion (default)'))
|
||||||
postproc.add_option(
|
postproc.add_option(
|
||||||
'--split-chapters', '--split-tracks',
|
'--split-chapters', '--split-tracks',
|
||||||
dest='split_chapters', action='store_true', default=False,
|
dest='split_chapters', action='store_true', default=False,
|
||||||
|
@ -1775,14 +1786,14 @@ def _alias_callback(option, opt_str, value, parser, opts, nargs):
|
||||||
'delim': None,
|
'delim': None,
|
||||||
'process': lambda val: dict(_postprocessor_opts_parser(*val.split(':', 1))),
|
'process': lambda val: dict(_postprocessor_opts_parser(*val.split(':', 1))),
|
||||||
}, help=(
|
}, help=(
|
||||||
'The (case sensitive) name of plugin postprocessors to be enabled, '
|
'The (case-sensitive) name of plugin postprocessors to be enabled, '
|
||||||
'and (optionally) arguments to be passed to it, separated by a colon ":". '
|
'and (optionally) arguments to be passed to it, separated by a colon ":". '
|
||||||
'ARGS are a semicolon ";" delimited list of NAME=VALUE. '
|
'ARGS are a semicolon ";" delimited list of NAME=VALUE. '
|
||||||
'The "when" argument determines when the postprocessor is invoked. '
|
'The "when" argument determines when the postprocessor is invoked. '
|
||||||
'It can be one of "pre_process" (after video extraction), "after_filter" (after video passes filter), '
|
'It can be one of "pre_process" (after video extraction), "after_filter" (after video passes filter), '
|
||||||
'"video" (after --format; before --print/--output), "before_dl" (before each video download), '
|
'"video" (after --format; before --print/--output), "before_dl" (before each video download), '
|
||||||
'"post_process" (after each video download; default), '
|
'"post_process" (after each video download; default), '
|
||||||
'"after_move" (after moving video file to its final locations), '
|
'"after_move" (after moving the video file to its final location), '
|
||||||
'"after_video" (after downloading and processing all formats of a video), '
|
'"after_video" (after downloading and processing all formats of a video), '
|
||||||
'or "playlist" (at end of playlist). '
|
'or "playlist" (at end of playlist). '
|
||||||
'This option can be used multiple times to add different postprocessors'))
|
'This option can be used multiple times to add different postprocessors'))
|
||||||
|
@ -1799,7 +1810,7 @@ def _alias_callback(option, opt_str, value, parser, opts, nargs):
|
||||||
}, help=(
|
}, help=(
|
||||||
'SponsorBlock categories to create chapters for, separated by commas. '
|
'SponsorBlock categories to create chapters for, separated by commas. '
|
||||||
f'Available categories are {", ".join(SponsorBlockPP.CATEGORIES.keys())}, all and default (=all). '
|
f'Available categories are {", ".join(SponsorBlockPP.CATEGORIES.keys())}, all and default (=all). '
|
||||||
'You can prefix the category with a "-" to exclude it. See [1] for description of the categories. '
|
'You can prefix the category with a "-" to exclude it. See [1] for descriptions of the categories. '
|
||||||
'E.g. --sponsorblock-mark all,-preview [1] https://wiki.sponsor.ajay.app/w/Segment_Categories'))
|
'E.g. --sponsorblock-mark all,-preview [1] https://wiki.sponsor.ajay.app/w/Segment_Categories'))
|
||||||
sponsorblock.add_option(
|
sponsorblock.add_option(
|
||||||
'--sponsorblock-remove', metavar='CATS',
|
'--sponsorblock-remove', metavar='CATS',
|
||||||
|
@ -1885,7 +1896,7 @@ def _alias_callback(option, opt_str, value, parser, opts, nargs):
|
||||||
extractor.add_option(
|
extractor.add_option(
|
||||||
'--no-hls-split-discontinuity',
|
'--no-hls-split-discontinuity',
|
||||||
dest='hls_split_discontinuity', action='store_false',
|
dest='hls_split_discontinuity', action='store_false',
|
||||||
help='Do not split HLS playlists to different formats at discontinuities such as ad breaks (default)')
|
help='Do not split HLS playlists into different formats at discontinuities such as ad breaks (default)')
|
||||||
_extractor_arg_parser = lambda key, vals='': (key.strip().lower().replace('-', '_'), [
|
_extractor_arg_parser = lambda key, vals='': (key.strip().lower().replace('-', '_'), [
|
||||||
val.replace(r'\,', ',').strip() for val in re.split(r'(?<!\\),', vals)])
|
val.replace(r'\,', ',').strip() for val in re.split(r'(?<!\\),', vals)])
|
||||||
extractor.add_option(
|
extractor.add_option(
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue