mirror of
https://github.com/yt-dlp/yt-dlp
synced 2024-12-26 21:59:08 +01:00
Merged master
This commit is contained in:
commit
94e2a94b75
282 changed files with 11558 additions and 4664 deletions
23
.github/ISSUE_TEMPLATE/1_broken_site.yml
vendored
23
.github/ISSUE_TEMPLATE/1_broken_site.yml
vendored
|
@ -63,17 +63,26 @@ body:
|
||||||
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>
|
||||||
render: shell
|
render: shell
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
> [!CAUTION]
|
||||||
|
> ### GitHub is experiencing a high volume of malicious spam comments.
|
||||||
|
> ### If you receive any replies asking you download a file, do NOT follow the download links!
|
||||||
|
>
|
||||||
|
> Note that this issue may be temporarily locked as an anti-spam measure after it is opened.
|
||||||
|
|
|
@ -75,17 +75,26 @@ body:
|
||||||
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>
|
||||||
render: shell
|
render: shell
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
> [!CAUTION]
|
||||||
|
> ### GitHub is experiencing a high volume of malicious spam comments.
|
||||||
|
> ### If you receive any replies asking you download a file, do NOT follow the download links!
|
||||||
|
>
|
||||||
|
> Note that this issue may be temporarily locked as an anti-spam measure after it is opened.
|
||||||
|
|
|
@ -71,17 +71,26 @@ body:
|
||||||
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>
|
||||||
render: shell
|
render: shell
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
> [!CAUTION]
|
||||||
|
> ### GitHub is experiencing a high volume of malicious spam comments.
|
||||||
|
> ### If you receive any replies asking you download a file, do NOT follow the download links!
|
||||||
|
>
|
||||||
|
> Note that this issue may be temporarily locked as an anti-spam measure after it is opened.
|
||||||
|
|
23
.github/ISSUE_TEMPLATE/4_bug_report.yml
vendored
23
.github/ISSUE_TEMPLATE/4_bug_report.yml
vendored
|
@ -56,17 +56,26 @@ body:
|
||||||
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>
|
||||||
render: shell
|
render: shell
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
> [!CAUTION]
|
||||||
|
> ### GitHub is experiencing a high volume of malicious spam comments.
|
||||||
|
> ### If you receive any replies asking you download a file, do NOT follow the download links!
|
||||||
|
>
|
||||||
|
> Note that this issue may be temporarily locked as an anti-spam measure after it is opened.
|
||||||
|
|
23
.github/ISSUE_TEMPLATE/5_feature_request.yml
vendored
23
.github/ISSUE_TEMPLATE/5_feature_request.yml
vendored
|
@ -52,15 +52,24 @@ body:
|
||||||
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>
|
||||||
render: shell
|
render: shell
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
> [!CAUTION]
|
||||||
|
> ### GitHub is experiencing a high volume of malicious spam comments.
|
||||||
|
> ### If you receive any replies asking you download a file, do NOT follow the download links!
|
||||||
|
>
|
||||||
|
> Note that this issue may be temporarily locked as an anti-spam measure after it is opened.
|
||||||
|
|
23
.github/ISSUE_TEMPLATE/6_question.yml
vendored
23
.github/ISSUE_TEMPLATE/6_question.yml
vendored
|
@ -58,15 +58,24 @@ body:
|
||||||
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>
|
||||||
render: shell
|
render: shell
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
> [!CAUTION]
|
||||||
|
> ### GitHub is experiencing a high volume of malicious spam comments.
|
||||||
|
> ### If you receive any replies asking you download a file, do NOT follow the download links!
|
||||||
|
>
|
||||||
|
> Note that this issue may be temporarily locked as an anti-spam measure after it is opened.
|
||||||
|
|
121
.github/workflows/build.yml
vendored
121
.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 }}"
|
||||||
|
@ -237,27 +239,43 @@ jobs:
|
||||||
macos:
|
macos:
|
||||||
needs: process
|
needs: process
|
||||||
if: inputs.macos
|
if: inputs.macos
|
||||||
runs-on: macos-12
|
permissions:
|
||||||
|
contents: read
|
||||||
|
actions: write # For cleaning up cache
|
||||||
|
runs-on: macos-13
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
# NB: Building universal2 does not work with python from actions/setup-python
|
# NB: Building universal2 does not work with python from actions/setup-python
|
||||||
|
|
||||||
|
- name: Restore cached requirements
|
||||||
|
id: restore-cache
|
||||||
|
uses: actions/cache/restore@v4
|
||||||
|
env:
|
||||||
|
SEGMENT_DOWNLOAD_TIMEOUT_MINS: 1
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/yt-dlp-build-venv
|
||||||
|
key: cache-reqs-${{ github.job }}
|
||||||
|
|
||||||
- name: Install Requirements
|
- name: Install Requirements
|
||||||
run: |
|
run: |
|
||||||
brew install coreutils
|
brew install coreutils
|
||||||
python3 devscripts/install_deps.py --user -o --include build
|
python3 -m venv ~/yt-dlp-build-venv
|
||||||
|
source ~/yt-dlp-build-venv/bin/activate
|
||||||
|
python3 devscripts/install_deps.py -o --include build
|
||||||
python3 devscripts/install_deps.py --print --include pyinstaller > requirements.txt
|
python3 devscripts/install_deps.py --print --include pyinstaller > requirements.txt
|
||||||
# We need to ignore wheels otherwise we break universal2 builds
|
# We need to ignore wheels otherwise we break universal2 builds
|
||||||
python3 -m pip install -U --user --no-binary :all: -r requirements.txt
|
python3 -m pip install -U --no-binary :all: -r requirements.txt
|
||||||
# We need to fuse our own universal2 wheels for curl_cffi
|
# We need to fuse our own universal2 wheels for curl_cffi
|
||||||
python3 -m pip install -U --user delocate
|
python3 -m pip install -U 'delocate==0.11.0'
|
||||||
mkdir curl_cffi_whls curl_cffi_universal2
|
mkdir curl_cffi_whls curl_cffi_universal2
|
||||||
python3 devscripts/install_deps.py --print -o --include curl-cffi > requirements.txt
|
python3 devscripts/install_deps.py --print -o --include curl-cffi > requirements.txt
|
||||||
for platform in "macosx_11_0_arm64" "macosx_11_0_x86_64"; do
|
for platform in "macosx_11_0_arm64" "macosx_11_0_x86_64"; do
|
||||||
python3 -m pip download \
|
python3 -m pip download \
|
||||||
--only-binary=:all: \
|
--only-binary=:all: \
|
||||||
--platform "${platform}" \
|
--platform "${platform}" \
|
||||||
--pre -d curl_cffi_whls \
|
-d curl_cffi_whls \
|
||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
done
|
done
|
||||||
( # Overwrite x86_64-only libs with fat/universal2 libs or else Pyinstaller will do the opposite
|
( # Overwrite x86_64-only libs with fat/universal2 libs or else Pyinstaller will do the opposite
|
||||||
|
@ -274,9 +292,10 @@ jobs:
|
||||||
)
|
)
|
||||||
python3 -m delocate.cmd.delocate_fuse curl_cffi_whls/curl_cffi*.whl -w curl_cffi_universal2
|
python3 -m delocate.cmd.delocate_fuse curl_cffi_whls/curl_cffi*.whl -w curl_cffi_universal2
|
||||||
python3 -m delocate.cmd.delocate_fuse curl_cffi_whls/cffi*.whl -w curl_cffi_universal2
|
python3 -m delocate.cmd.delocate_fuse curl_cffi_whls/cffi*.whl -w curl_cffi_universal2
|
||||||
cd curl_cffi_universal2
|
for wheel in curl_cffi_universal2/*cffi*.whl; do
|
||||||
for wheel in ./*cffi*.whl; do mv -n -- "${wheel}" "${wheel/x86_64/universal2}"; done
|
mv -n -- "${wheel}" "${wheel/x86_64/universal2}"
|
||||||
python3 -m pip install -U --user ./*cffi*.whl
|
done
|
||||||
|
python3 -m pip install --force-reinstall -U curl_cffi_universal2/*cffi*.whl
|
||||||
|
|
||||||
- name: Prepare
|
- name: Prepare
|
||||||
run: |
|
run: |
|
||||||
|
@ -284,6 +303,7 @@ jobs:
|
||||||
python3 devscripts/make_lazy_extractors.py
|
python3 devscripts/make_lazy_extractors.py
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
|
source ~/yt-dlp-build-venv/bin/activate
|
||||||
python3 -m bundle.pyinstaller --target-architecture universal2 --onedir
|
python3 -m bundle.pyinstaller --target-architecture universal2 --onedir
|
||||||
(cd ./dist/yt-dlp_macos && zip -r ../yt-dlp_macos.zip .)
|
(cd ./dist/yt-dlp_macos && zip -r ../yt-dlp_macos.zip .)
|
||||||
python3 -m bundle.pyinstaller --target-architecture universal2
|
python3 -m bundle.pyinstaller --target-architecture universal2
|
||||||
|
@ -307,10 +327,28 @@ jobs:
|
||||||
dist/yt-dlp_macos.zip
|
dist/yt-dlp_macos.zip
|
||||||
compression-level: 0
|
compression-level: 0
|
||||||
|
|
||||||
|
- name: Cleanup cache
|
||||||
|
if: steps.restore-cache.outputs.cache-hit == 'true'
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
cache_key: cache-reqs-${{ github.job }}
|
||||||
|
repository: ${{ github.repository }}
|
||||||
|
branch: ${{ github.ref }}
|
||||||
|
run: |
|
||||||
|
gh extension install actions/gh-actions-cache
|
||||||
|
gh actions-cache delete "${cache_key}" -R "${repository}" -B "${branch}" --confirm
|
||||||
|
|
||||||
|
- name: Cache requirements
|
||||||
|
uses: actions/cache/save@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/yt-dlp-build-venv
|
||||||
|
key: cache-reqs-${{ github.job }}
|
||||||
|
|
||||||
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
|
||||||
|
@ -367,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: |
|
||||||
|
@ -383,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
|
||||||
|
@ -414,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
|
||||||
|
|
||||||
|
@ -427,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: |
|
||||||
|
@ -489,6 +516,10 @@ jobs:
|
||||||
# make sure SHA sums are also printed to stdout
|
# make sure SHA sums are also printed to stdout
|
||||||
sha256sum -- * | tee ../SHA2-256SUMS
|
sha256sum -- * | tee ../SHA2-256SUMS
|
||||||
sha512sum -- * | tee ../SHA2-512SUMS
|
sha512sum -- * | tee ../SHA2-512SUMS
|
||||||
|
# also print as permanent annotations to the summary page
|
||||||
|
while read -r shasum; do
|
||||||
|
echo "::notice title=${shasum##* }::sha256: ${shasum% *}"
|
||||||
|
done < ../SHA2-256SUMS
|
||||||
|
|
||||||
- name: Make Update spec
|
- name: Make Update spec
|
||||||
run: |
|
run: |
|
||||||
|
@ -497,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
|
||||||
|
|
15
.github/workflows/core.yml
vendored
15
.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 }}
|
||||||
|
@ -55,7 +59,8 @@ jobs:
|
||||||
- name: Install test requirements
|
- name: Install test requirements
|
||||||
run: python3 ./devscripts/install_deps.py --include test --include curl-cffi
|
run: python3 ./devscripts/install_deps.py --include test --include curl-cffi
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
|
timeout-minutes: 15
|
||||||
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 }}
|
||||||
|
|
21
.github/workflows/issue-lockdown.yml
vendored
Normal file
21
.github/workflows/issue-lockdown.yml
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
name: Issue Lockdown
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lockdown:
|
||||||
|
name: Issue Lockdown
|
||||||
|
if: vars.ISSUE_LOCKDOWN
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: "Lock new issue"
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||||
|
REPOSITORY: ${{ github.repository }}
|
||||||
|
run: |
|
||||||
|
gh issue lock "${ISSUE_NUMBER}" -R "${REPOSITORY}"
|
11
.github/workflows/quick-test.yml
vendored
11
.github/workflows/quick-test.yml
vendored
|
@ -10,16 +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
|
||||||
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')"
|
||||||
|
@ -28,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
|
||||||
|
|
3
.github/workflows/release-master.yml
vendored
3
.github/workflows/release-master.yml
vendored
|
@ -24,6 +24,7 @@ jobs:
|
||||||
source: master
|
source: master
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
packages: write
|
packages: write # For package cache
|
||||||
|
actions: write # For cleaning up cache
|
||||||
id-token: write # mandatory for trusted publishing
|
id-token: write # mandatory for trusted publishing
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
3
.github/workflows/release-nightly.yml
vendored
3
.github/workflows/release-nightly.yml
vendored
|
@ -37,6 +37,7 @@ jobs:
|
||||||
source: nightly
|
source: nightly
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
packages: write
|
packages: write # For package cache
|
||||||
|
actions: write # For cleaning up cache
|
||||||
id-token: write # mandatory for trusted publishing
|
id-token: write # mandatory for trusted publishing
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
8
.github/workflows/release.yml
vendored
8
.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
|
||||||
|
@ -228,7 +228,8 @@ jobs:
|
||||||
origin: ${{ needs.prepare.outputs.target_repo }}
|
origin: ${{ needs.prepare.outputs.target_repo }}
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write # For package cache
|
packages: write # For package cache
|
||||||
|
actions: write # For cleaning up cache
|
||||||
secrets:
|
secrets:
|
||||||
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }}
|
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }}
|
||||||
|
|
||||||
|
@ -281,6 +282,7 @@ jobs:
|
||||||
uses: pypa/gh-action-pypi-publish@release/v1
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
with:
|
with:
|
||||||
verbose: true
|
verbose: true
|
||||||
|
attestations: false # Currently doesn't work w/ reusable workflows (breaks nightly)
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
needs: [prepare, build]
|
needs: [prepare, build]
|
||||||
|
@ -324,7 +326,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
|
||||||
|
|
17
.github/workflows/sanitize-comment.yml
vendored
Normal file
17
.github/workflows/sanitize-comment.yml
vendored
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
name: Sanitize comment
|
||||||
|
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types: [created, edited]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sanitize-comment:
|
||||||
|
name: Sanitize comment
|
||||||
|
if: vars.SANITIZE_COMMENT && !github.event.issue.pull_request
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Sanitize comment
|
||||||
|
uses: yt-dlp/sanitize-comment@v1
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -51,7 +51,6 @@ cookies
|
||||||
*.srt
|
*.srt
|
||||||
*.ssa
|
*.ssa
|
||||||
*.swf
|
*.swf
|
||||||
*.swp
|
|
||||||
*.tt
|
*.tt
|
||||||
*.ttml
|
*.ttml
|
||||||
*.url
|
*.url
|
||||||
|
@ -119,6 +118,7 @@ yt-dlp.zip
|
||||||
.vscode
|
.vscode
|
||||||
*.sublime-*
|
*.sublime-*
|
||||||
*.code-workspace
|
*.code-workspace
|
||||||
|
*.swp
|
||||||
|
|
||||||
# Lazy extractors
|
# Lazy extractors
|
||||||
*/extractor/lazy_extractors.py
|
*/extractor/lazy_extractors.py
|
||||||
|
|
|
@ -37,14 +37,18 @@ Bugs and suggestions should be reported at: [yt-dlp/yt-dlp/issues](https://githu
|
||||||
**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 @@ After you have ensured this site is distributing its content legally, you can fo
|
||||||
# * 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 @@ After you have ensured this site is distributing its content legally, you can fo
|
||||||
|
|
||||||
You can use `hatch fmt` to automatically fix problems. Rules that the linter/formatter enforces should not be disabled with `# noqa` unless a maintainer requests it. The only exception allowed is for old/printf-style string formatting in GraphQL query templates (use `# noqa: UP031`).
|
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 @@ Extractors are very fragile by nature since they depend on the layout of the sou
|
||||||
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.
|
||||||
|
|
||||||
|
|
64
CONTRIBUTORS
64
CONTRIBUTORS
|
@ -631,3 +631,67 @@ voidful
|
||||||
vtexier
|
vtexier
|
||||||
WyohKnott
|
WyohKnott
|
||||||
trueauracoral
|
trueauracoral
|
||||||
|
ASertacAkkaya
|
||||||
|
axpauls
|
||||||
|
chilinux
|
||||||
|
hafeoz
|
||||||
|
JSubelj
|
||||||
|
jucor
|
||||||
|
megumintyan
|
||||||
|
mgedmin
|
||||||
|
Niluge-KiWi
|
||||||
|
peisenwang
|
||||||
|
TheZ3ro
|
||||||
|
tippfehlr
|
||||||
|
varunchopra
|
||||||
|
DrakoCpp
|
||||||
|
PatrykMis
|
||||||
|
DinhHuy2010
|
||||||
|
exterrestris
|
||||||
|
harbhim
|
||||||
|
LeSuisse
|
||||||
|
DunnesH
|
||||||
|
iancmy
|
||||||
|
mokrueger
|
||||||
|
luvyana
|
||||||
|
szantnerb
|
||||||
|
hugepower
|
||||||
|
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
|
||||||
|
avagordon01
|
||||||
|
CounterPillow
|
||||||
|
JoseAngelB
|
||||||
|
KBelmin
|
||||||
|
kesor
|
||||||
|
MellowKyler
|
||||||
|
Wesley107772
|
||||||
|
|
446
Changelog.md
446
Changelog.md
|
@ -4,6 +4,452 @@
|
||||||
# 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.11.04
|
||||||
|
|
||||||
|
#### Important changes
|
||||||
|
- **Beginning with 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)
|
||||||
|
- **The minimum *required* Python version has been raised to 3.9**
|
||||||
|
Python 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)
|
||||||
|
|
||||||
|
#### Core changes
|
||||||
|
- [Allow thumbnails with `.jpe` extension](https://github.com/yt-dlp/yt-dlp/commit/5bc5fb2835ea59bdf326bd12176d74d2c7348a95) ([#11408](https://github.com/yt-dlp/yt-dlp/issues/11408)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [Expand paths in `--plugin-dirs`](https://github.com/yt-dlp/yt-dlp/commit/914af9a0cf51c9a3f74aa88d952bee8334c67511) ([#11334](https://github.com/yt-dlp/yt-dlp/issues/11334)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [Fix `--netrc` empty string parsing for Python <=3.10](https://github.com/yt-dlp/yt-dlp/commit/88402b714ec124633933737bc156b172a3dec3d6) ([#11414](https://github.com/yt-dlp/yt-dlp/issues/11414)) by [bashonly](https://github.com/bashonly), [Grub4K](https://github.com/Grub4K)
|
||||||
|
- [Populate format sorting fields before dependent fields](https://github.com/yt-dlp/yt-dlp/commit/5c880ef42e9c2b2fc412f6d69dad37d34fb75a62) ([#11353](https://github.com/yt-dlp/yt-dlp/issues/11353)) by [Grub4K](https://github.com/Grub4K)
|
||||||
|
- [Prioritize AV1](https://github.com/yt-dlp/yt-dlp/commit/3945677a75e94a1fecc085432d791e1c21220cd3) ([#11153](https://github.com/yt-dlp/yt-dlp/issues/11153)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- [Remove Python 3.8 support](https://github.com/yt-dlp/yt-dlp/commit/d784464399b600ba9516bbcec6286f11d68974dd) ([#11321](https://github.com/yt-dlp/yt-dlp/issues/11321)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **aes**: [Fix GCM pad length calculation](https://github.com/yt-dlp/yt-dlp/commit/beae2db127d3b5017cbcf685da9de7a9ef496541) ([#11438](https://github.com/yt-dlp/yt-dlp/issues/11438)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- **cookies**: [Support chrome table version 24](https://github.com/yt-dlp/yt-dlp/commit/4613096f2e6eab9dcbac0e98b6cec760bbc99375) ([#11425](https://github.com/yt-dlp/yt-dlp/issues/11425)) by [kesor](https://github.com/kesor), [seproDev](https://github.com/seproDev)
|
||||||
|
- **utils**
|
||||||
|
- [Allow partial application for more functions](https://github.com/yt-dlp/yt-dlp/commit/b6dc2c49e8793c6dfa21275e61caf49ec1148b81) ([#11391](https://github.com/yt-dlp/yt-dlp/issues/11391)) by [bashonly](https://github.com/bashonly), [Grub4K](https://github.com/Grub4K) (With fixes in [422195e](https://github.com/yt-dlp/yt-dlp/commit/422195ec70a00b0d2002b238cacbae7790c57fdf) by [Grub4K](https://github.com/Grub4K))
|
||||||
|
- [Fix `find_element` by class](https://github.com/yt-dlp/yt-dlp/commit/f93c16395cea1fe9ffc3c594d3e019c3b214544c) ([#11402](https://github.com/yt-dlp/yt-dlp/issues/11402)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [Fix and improve `find_element` and `find_elements`](https://github.com/yt-dlp/yt-dlp/commit/b103aca24d35b72b405c340357dc01a0ed534281) ([#11443](https://github.com/yt-dlp/yt-dlp/issues/11443)) by [bashonly](https://github.com/bashonly), [Grub4K](https://github.com/Grub4K)
|
||||||
|
|
||||||
|
#### Extractor changes
|
||||||
|
- [Resolve `language` to ISO639-2 for ISM formats](https://github.com/yt-dlp/yt-dlp/commit/21cdcf03a237a0c4979c941d5a5385cae44c7906) ([#11359](https://github.com/yt-dlp/yt-dlp/issues/11359)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **ardmediathek**: [Extract chapters](https://github.com/yt-dlp/yt-dlp/commit/59f8dd8239c31f00b708da53b39b1e2e9409b6e6) ([#11442](https://github.com/yt-dlp/yt-dlp/issues/11442)) by [iw0nderhow](https://github.com/iw0nderhow)
|
||||||
|
- **bfmtv**: [Fix extractors](https://github.com/yt-dlp/yt-dlp/commit/754940e9a558565d6bd3c0c529802569b1d0ae4e) ([#11444](https://github.com/yt-dlp/yt-dlp/issues/11444)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- **bluesky**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/5c7a5aaab27e9c3cb367b663a6136ca58866e547) ([#11055](https://github.com/yt-dlp/yt-dlp/issues/11055)) by [MellowKyler](https://github.com/MellowKyler), [seproDev](https://github.com/seproDev)
|
||||||
|
- **ccma**: [Support new 3cat.cat domain](https://github.com/yt-dlp/yt-dlp/commit/330335386d4f7603d92d6796798375336005275e) ([#11222](https://github.com/yt-dlp/yt-dlp/issues/11222)) by [JoseAngelB](https://github.com/JoseAngelB)
|
||||||
|
- **chzzk**: video: [Fix extraction](https://github.com/yt-dlp/yt-dlp/commit/9c6534da81e485b2325b3489ee4128943e6d3e4b) ([#11228](https://github.com/yt-dlp/yt-dlp/issues/11228)) by [hui1601](https://github.com/hui1601)
|
||||||
|
- **cnn**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/9acf79c91a8c6c55ca972747c6858e784e2da351) ([#10185](https://github.com/yt-dlp/yt-dlp/issues/10185)) by [kylegustavo](https://github.com/kylegustavo), [seproDev](https://github.com/seproDev)
|
||||||
|
- **dailymotion**
|
||||||
|
- [Improve embed extraction](https://github.com/yt-dlp/yt-dlp/commit/a403dcf9be20b49cbb3017328f4aaa352fb6d685) ([#10843](https://github.com/yt-dlp/yt-dlp/issues/10843)) by [bashonly](https://github.com/bashonly), [pzhlkj6612](https://github.com/pzhlkj6612)
|
||||||
|
- [Support shortened URLs](https://github.com/yt-dlp/yt-dlp/commit/d1358231371f20fa23020fa9176be3b56119873e) ([#11374](https://github.com/yt-dlp/yt-dlp/issues/11374)) by [bashonly](https://github.com/bashonly), [seproDev](https://github.com/seproDev)
|
||||||
|
- **facebook**: [Fix formats extraction](https://github.com/yt-dlp/yt-dlp/commit/ec9b25043f399de6a591d8370d32bf0e66c117f2) ([#11343](https://github.com/yt-dlp/yt-dlp/issues/11343)) by [kclauhk](https://github.com/kclauhk)
|
||||||
|
- **generic**: [Do not impersonate by default](https://github.com/yt-dlp/yt-dlp/commit/c29f5a7fae93a08f3cfbb6127b2faa75145b06a0) ([#11336](https://github.com/yt-dlp/yt-dlp/issues/11336)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **nfl**: [Fix extractors](https://github.com/yt-dlp/yt-dlp/commit/838f4385de8300a4dd4e7ffbbf0e5b7b85fb52c2) ([#11409](https://github.com/yt-dlp/yt-dlp/issues/11409)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **niconicouser**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/6abef74232c0fc695cd803c18ae446cacb129389) ([#11324](https://github.com/yt-dlp/yt-dlp/issues/11324)) by [Wesley107772](https://github.com/Wesley107772)
|
||||||
|
- **soundcloud**: [Extract artists](https://github.com/yt-dlp/yt-dlp/commit/f101e5d34c97c608156ad5396714c2a2edca966a) ([#11377](https://github.com/yt-dlp/yt-dlp/issues/11377)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- **tumblr**: [Support more URLs](https://github.com/yt-dlp/yt-dlp/commit/b03267bf0675eeb8df5baf1daac7cf67840c91a5) ([#6057](https://github.com/yt-dlp/yt-dlp/issues/6057)) by [selfisekai](https://github.com/selfisekai), [seproDev](https://github.com/seproDev)
|
||||||
|
- **twitter**: [Remove cookies migration workaround](https://github.com/yt-dlp/yt-dlp/commit/76802f461332d444e596437c42374fa237fa5174) ([#11392](https://github.com/yt-dlp/yt-dlp/issues/11392)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **vimeo**: [Fix API retries](https://github.com/yt-dlp/yt-dlp/commit/57212a5f97ce367590aaa5c3e9a135eead8f81f7) ([#11351](https://github.com/yt-dlp/yt-dlp/issues/11351)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **yle_areena**: [Support live events](https://github.com/yt-dlp/yt-dlp/commit/a6783a3b9905e547f6c1d4df9d7c7999feda8afa) ([#11358](https://github.com/yt-dlp/yt-dlp/issues/11358)) by [bashonly](https://github.com/bashonly), [CounterPillow](https://github.com/CounterPillow)
|
||||||
|
- **youtube**: [Adjust OAuth refresh token handling](https://github.com/yt-dlp/yt-dlp/commit/d569a8845254d90ce13ad74ae76695e8d6441068) ([#11414](https://github.com/yt-dlp/yt-dlp/issues/11414)) by [bashonly](https://github.com/bashonly)
|
||||||
|
|
||||||
|
#### Misc. changes
|
||||||
|
- **build**
|
||||||
|
- [Disable attestations for trusted publishing](https://github.com/yt-dlp/yt-dlp/commit/428ffb75aa3534b275cf54de42693a4d261519da) ([#11418](https://github.com/yt-dlp/yt-dlp/issues/11418)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [Move optional dependencies to the `default` group](https://github.com/yt-dlp/yt-dlp/commit/87884f15580910e4e0fe0e1db73508debc657471) ([#11255](https://github.com/yt-dlp/yt-dlp/issues/11255)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [Use Ubuntu 20.04 and Python 3.9 for Linux ARM builds](https://github.com/yt-dlp/yt-dlp/commit/dd2e24446954246a2ec4d4a7e95531f52a14b351) ([#8638](https://github.com/yt-dlp/yt-dlp/issues/8638)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **cleanup**
|
||||||
|
- Miscellaneous
|
||||||
|
- [ea9e35d](https://github.com/yt-dlp/yt-dlp/commit/ea9e35d85fba5eab341cdcaf1eaed69b57f7e465) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [c998238](https://github.com/yt-dlp/yt-dlp/commit/c998238c2e76c62d1d29962c6e8ebe916cc7913b) by [bashonly](https://github.com/bashonly), [KBelmin](https://github.com/KBelmin)
|
||||||
|
- [197d0b0](https://github.com/yt-dlp/yt-dlp/commit/197d0b03b6a3c8fe4fa5ace630eeffec629bf72c) by [avagordon01](https://github.com/avagordon01), [bashonly](https://github.com/bashonly), [grqz](https://github.com/grqz), [Grub4K](https://github.com/Grub4K), [seproDev](https://github.com/seproDev)
|
||||||
|
- **devscripts**: `make_changelog`: [Parse full commit message for fixes](https://github.com/yt-dlp/yt-dlp/commit/0a3991edae0e10f2ea41ece9fdea5e48f789f1de) ([#11366](https://github.com/yt-dlp/yt-dlp/issues/11366)) by [bashonly](https://github.com/bashonly), [Grub4K](https://github.com/Grub4K)
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
|
#### Core changes
|
||||||
|
- **jsinterp**: [Improve `slice` implementation](https://github.com/yt-dlp/yt-dlp/commit/bb8bf1db993f59752d20b73b861bd55e40cf0e31) ([#10664](https://github.com/yt-dlp/yt-dlp/issues/10664)) by [seproDev](https://github.com/seproDev)
|
||||||
|
|
||||||
|
#### Extractor changes
|
||||||
|
- **discoveryplusitaly**: [Support sport and olympics URLs](https://github.com/yt-dlp/yt-dlp/commit/e7d73bc4531ee3f91a46b15e218dcc1fbeb6226c) ([#10655](https://github.com/yt-dlp/yt-dlp/issues/10655)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **gem.cbc.ca**: live: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/fc5eecfa31c9571b6031cc3968aaa0394be55d7a) ([#10565](https://github.com/yt-dlp/yt-dlp/issues/10565)) by [bashonly](https://github.com/bashonly), [scribblemaniac](https://github.com/scribblemaniac)
|
||||||
|
- **niconico**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/4d9231208332d4c32364b8cd814bff8b20232cae) ([#10677](https://github.com/yt-dlp/yt-dlp/issues/10677)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **olympics**: [Fix extraction](https://github.com/yt-dlp/yt-dlp/commit/919540a9644e55deb78cdd6751757ec8fdaf76f4) ([#10625](https://github.com/yt-dlp/yt-dlp/issues/10625)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **youku**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/0088c6de23d832b117061a33e984dc452d992e9c) ([#10626](https://github.com/yt-dlp/yt-dlp/issues/10626)) by [hugepower](https://github.com/hugepower)
|
||||||
|
- **youtube**
|
||||||
|
- [Change default player clients to `ios,web_creator`](https://github.com/yt-dlp/yt-dlp/commit/406f4c2e47502fffc1b0c210b4ee6487c89a44cb) ([#10674](https://github.com/yt-dlp/yt-dlp/issues/10674)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [Fix `n` function name extraction for player `b12cc44b`](https://github.com/yt-dlp/yt-dlp/commit/c86891eb9434b4d7eec426d38c0c625b5e13cb2f) ([#10668](https://github.com/yt-dlp/yt-dlp/issues/10668)) by [seproDev](https://github.com/seproDev)
|
||||||
|
|
||||||
|
### 2024.08.01
|
||||||
|
|
||||||
|
#### Core changes
|
||||||
|
- **utils**: `unified_timestamp`: [Recognize Sunday](https://github.com/yt-dlp/yt-dlp/commit/6daf2c27c0464fba98337be30de0b66d520d0db1) ([#10589](https://github.com/yt-dlp/yt-dlp/issues/10589)) by [bashonly](https://github.com/bashonly)
|
||||||
|
|
||||||
|
#### Extractor changes
|
||||||
|
- **abematv**: [Fix availability extraction](https://github.com/yt-dlp/yt-dlp/commit/ef36d517f9b05785d61abca7691d9ab7d63cc75c) ([#10569](https://github.com/yt-dlp/yt-dlp/issues/10569)) by [middlingphys](https://github.com/middlingphys)
|
||||||
|
- **cbc.ca**: player: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/94a1c5e642e468cebeb51f74c6c220434cb47d96) ([#10302](https://github.com/yt-dlp/yt-dlp/issues/10302)) by [bashonly](https://github.com/bashonly), [trainman261](https://github.com/trainman261)
|
||||||
|
- **discoveryplus**: [Support olympics URLs](https://github.com/yt-dlp/yt-dlp/commit/0b7728618417e1aa382722a4d29b916b594d4459) ([#10566](https://github.com/yt-dlp/yt-dlp/issues/10566)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **kick**: clips: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/bb3936ae2b3ce96d0b53f9e17cad1082058f032b) ([#10572](https://github.com/yt-dlp/yt-dlp/issues/10572)) by [luvyana](https://github.com/luvyana)
|
||||||
|
- **learningonscreen**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/fe15d3178e242803ae7a934b90137f13598eba2e) ([#10590](https://github.com/yt-dlp/yt-dlp/issues/10590)) by [bashonly](https://github.com/bashonly), [Grub4K](https://github.com/Grub4K)
|
||||||
|
- **mediaklikk**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/7e3e4779ad13e4511c9ba3869879e53f0267bd7a) ([#10605](https://github.com/yt-dlp/yt-dlp/issues/10605)) by [szantnerb](https://github.com/szantnerb)
|
||||||
|
- **mlbtv**: [Fix makeup game extraction](https://github.com/yt-dlp/yt-dlp/commit/4b69e1b53ea21e631cd5dd68ff531e2f1671ec17) ([#10607](https://github.com/yt-dlp/yt-dlp/issues/10607)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **olympics**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/2f1ddfe12a2c174bc777264c5c8ffe7ca0922d94) ([#10604](https://github.com/yt-dlp/yt-dlp/issues/10604)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **tva**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/28d485714fef88937c82635438afba5db81f9089) ([#10567](https://github.com/yt-dlp/yt-dlp/issues/10567)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **tver**: [Support olympic URLs](https://github.com/yt-dlp/yt-dlp/commit/5260696b1cba77161828941fdb38f09f14ac6c60) ([#10600](https://github.com/yt-dlp/yt-dlp/issues/10600)) by [vvto33](https://github.com/vvto33)
|
||||||
|
- **vimeo**: review: [Fix password-protected video extraction](https://github.com/yt-dlp/yt-dlp/commit/2b6df93a243bdfb9d6bb5c1e18020625cd02d465) ([#10598](https://github.com/yt-dlp/yt-dlp/issues/10598)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **youtube**
|
||||||
|
- [Change default player clients to `ios,tv`](https://github.com/yt-dlp/yt-dlp/commit/efb42763dec23ccf6a2e3bac3afbfefce8efd012) ([#10457](https://github.com/yt-dlp/yt-dlp/issues/10457)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- [Fix `n` function name extraction for player `20dfca59`](https://github.com/yt-dlp/yt-dlp/commit/011b4a04db2a636c3ef0a0ad4e2d3ae482c9fd76) ([#10611](https://github.com/yt-dlp/yt-dlp/issues/10611)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [Fix age-verification workaround](https://github.com/yt-dlp/yt-dlp/commit/d19fcb934269465fd707e68a87f735ec6983e93d) ([#10610](https://github.com/yt-dlp/yt-dlp/issues/10610)) by [bashonly](https://github.com/bashonly), [Grub4K](https://github.com/Grub4K)
|
||||||
|
- [Player client maintenance](https://github.com/yt-dlp/yt-dlp/commit/0e539617a41913c7da1edd74fb6543c10ad727b3) ([#10573](https://github.com/yt-dlp/yt-dlp/issues/10573)) by [bashonly](https://github.com/bashonly)
|
||||||
|
|
||||||
|
#### Misc. changes
|
||||||
|
- **cleanup**: Miscellaneous: [ffd7781](https://github.com/yt-dlp/yt-dlp/commit/ffd7781d6588926f820b44a34b9e6e3068fb9f97) by [bashonly](https://github.com/bashonly)
|
||||||
|
|
||||||
|
### 2024.07.25
|
||||||
|
|
||||||
|
#### Extractor changes
|
||||||
|
- **abematv**: [Adapt key retrieval to request handler framework](https://github.com/yt-dlp/yt-dlp/commit/a3bab4752a2b3d56e5a59b4e0411bb8f695c010b) ([#10491](https://github.com/yt-dlp/yt-dlp/issues/10491)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **facebook**: [Fix extraction](https://github.com/yt-dlp/yt-dlp/commit/1a34a802f44a1dab8f642c79c3cc810e21541d3b) ([#10531](https://github.com/yt-dlp/yt-dlp/issues/10531)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **mlbtv**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/f0993391e6052ec8f7aacc286609564f226943b9) ([#10515](https://github.com/yt-dlp/yt-dlp/issues/10515)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **tiktok**: [Fix and deprioritize JSON subtitles](https://github.com/yt-dlp/yt-dlp/commit/2f97779f335ac069ecccd9c7bf81abf4a83cfe7a) ([#10516](https://github.com/yt-dlp/yt-dlp/issues/10516)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **vimeo**: [Fix chapters extraction](https://github.com/yt-dlp/yt-dlp/commit/a0a1bc3d8d8e3bb9a48a06e835815a0460e90e77) ([#10544](https://github.com/yt-dlp/yt-dlp/issues/10544)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **youtube**: [Fix `n` function name extraction for player `3400486c`](https://github.com/yt-dlp/yt-dlp/commit/713b4cd18f00556771af8cfdd9cea6cc1a09e948) ([#10542](https://github.com/yt-dlp/yt-dlp/issues/10542)) by [bashonly](https://github.com/bashonly)
|
||||||
|
|
||||||
|
#### Misc. changes
|
||||||
|
- **build**: [Pin `setuptools` version](https://github.com/yt-dlp/yt-dlp/commit/e046db8a116b1c320d4785daadd48ea0b22a3987) ([#10493](https://github.com/yt-dlp/yt-dlp/issues/10493)) by [bashonly](https://github.com/bashonly)
|
||||||
|
|
||||||
|
### 2024.07.16
|
||||||
|
|
||||||
|
#### Core changes
|
||||||
|
- [Fix `noprogress` if `test=True` with `--quiet` and `--verbose`](https://github.com/yt-dlp/yt-dlp/commit/66ce3d76d87af3f81cc9dfec4be4704016cb1cdb) ([#10454](https://github.com/yt-dlp/yt-dlp/issues/10454)) by [Grub4K](https://github.com/Grub4K)
|
||||||
|
- [Support `auto-tty` and `no_color-tty` for `--color`](https://github.com/yt-dlp/yt-dlp/commit/d9cbced493cae2008508d94a2db5dd98be7c01fc) ([#10453](https://github.com/yt-dlp/yt-dlp/issues/10453)) by [Grub4K](https://github.com/Grub4K)
|
||||||
|
- **update**: [Fix network error handling](https://github.com/yt-dlp/yt-dlp/commit/ed1b9ed93dd90d2cc960c0d8eaa9d919db224203) ([#10486](https://github.com/yt-dlp/yt-dlp/issues/10486)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **utils**: `parse_codecs`: [Fix parsing of mixed case codec strings](https://github.com/yt-dlp/yt-dlp/commit/cc0070f6496e501d77352bad475fb02d6a86846a) by [bashonly](https://github.com/bashonly)
|
||||||
|
|
||||||
|
#### Extractor changes
|
||||||
|
- **adn**: [Adjust for .com domain change](https://github.com/yt-dlp/yt-dlp/commit/959b7a379b8e5da059d110a63339c964b6265736) ([#10399](https://github.com/yt-dlp/yt-dlp/issues/10399)) by [infanf](https://github.com/infanf)
|
||||||
|
- **afreecatv**: [Fix login and use `legacy_ssl`](https://github.com/yt-dlp/yt-dlp/commit/4cd41469243624d90b7a2009b95cbe0609343efe) ([#10440](https://github.com/yt-dlp/yt-dlp/issues/10440)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **box**: [Support enterprise URLs](https://github.com/yt-dlp/yt-dlp/commit/705f5b84dec75cc7af97f42fd1530e8062735970) ([#10419](https://github.com/yt-dlp/yt-dlp/issues/10419)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- **digitalconcerthall**: [Extract HEVC and FLAC formats](https://github.com/yt-dlp/yt-dlp/commit/e62fa6b0e0186f8c5666c2c5ab64cf191abdafc1) ([#10470](https://github.com/yt-dlp/yt-dlp/issues/10470)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **dplay**: [Fix extractors](https://github.com/yt-dlp/yt-dlp/commit/39e6c4cb44b9292e89ac0afec3cd0afc2ae8775f) ([#10471](https://github.com/yt-dlp/yt-dlp/issues/10471)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **epidemicsound**: [Support sound effects URLs](https://github.com/yt-dlp/yt-dlp/commit/8531d2b03bac9cc746f2ee8098aaf8f115505f5b) ([#10436](https://github.com/yt-dlp/yt-dlp/issues/10436)) by [iancmy](https://github.com/iancmy)
|
||||||
|
- **generic**: [Fix direct video link extensions](https://github.com/yt-dlp/yt-dlp/commit/b9afb99e7c34d0eb15ddc6689cd7d20eebfda68e) ([#10468](https://github.com/yt-dlp/yt-dlp/issues/10468)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **picarto**: [Fix extractors](https://github.com/yt-dlp/yt-dlp/commit/bacd18b7df08b4995644fd12cee1f8c8e8636bc7) ([#10414](https://github.com/yt-dlp/yt-dlp/issues/10414)) by [Frankgoji](https://github.com/Frankgoji)
|
||||||
|
- **soundcloud**: permalink, user: [Extract tracks only](https://github.com/yt-dlp/yt-dlp/commit/22870b81bad97dfa6307a7add44753b2dffc76a9) ([#10463](https://github.com/yt-dlp/yt-dlp/issues/10463)) by [DunnesH](https://github.com/DunnesH)
|
||||||
|
- **tiktok**: live: [Fix room ID extraction](https://github.com/yt-dlp/yt-dlp/commit/d2189d3d36987ebeac426fd70a60a5fe86325a2b) ([#10408](https://github.com/yt-dlp/yt-dlp/issues/10408)) by [mokrueger](https://github.com/mokrueger)
|
||||||
|
- **tv5monde**: [Support browser impersonation](https://github.com/yt-dlp/yt-dlp/commit/9b95a6765a5f6325af99c4aca961587f0c426e8c) ([#10417](https://github.com/yt-dlp/yt-dlp/issues/10417)) by [bashonly](https://github.com/bashonly) (With fixes in [cc1a309](https://github.com/yt-dlp/yt-dlp/commit/cc1a3098c00995c6aebc2a16bd1050a66bad64db))
|
||||||
|
- **youtube**
|
||||||
|
- [Avoid poToken experiment player responses](https://github.com/yt-dlp/yt-dlp/commit/8b8b442cb005a8d85315f301615f83fb736b967a) ([#10456](https://github.com/yt-dlp/yt-dlp/issues/10456)) by [seproDev](https://github.com/seproDev) (With fixes in [16da8ef](https://github.com/yt-dlp/yt-dlp/commit/16da8ef9937ff76632dfef02e5062c5ba99c8ea2))
|
||||||
|
- [Invalidate nsig cache from < 2024.07.09](https://github.com/yt-dlp/yt-dlp/commit/04e17ba20a139f1b3e30ec4bafa3fba26888f0b3) ([#10401](https://github.com/yt-dlp/yt-dlp/issues/10401)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [Reduce android client priority](https://github.com/yt-dlp/yt-dlp/commit/b85eef0a615a01304f88a3847309c667e09a20df) ([#10467](https://github.com/yt-dlp/yt-dlp/issues/10467)) by [seproDev](https://github.com/seproDev)
|
||||||
|
|
||||||
|
#### Networking changes
|
||||||
|
- [Add `legacy_ssl` request extension](https://github.com/yt-dlp/yt-dlp/commit/150ecc45d9cacc919550c13b04fd998ac5103a6b) ([#10448](https://github.com/yt-dlp/yt-dlp/issues/10448)) by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
- **Request Handler**: curl_cffi: [Support `curl_cffi` 0.7.X](https://github.com/yt-dlp/yt-dlp/commit/42bfca00a6b460fc053514cdd7ac6f5b5daddf0c) by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
|
||||||
|
#### Misc. changes
|
||||||
|
- **build**
|
||||||
|
- [Include `curl_cffi` in `yt-dlp_linux`](https://github.com/yt-dlp/yt-dlp/commit/4521f30d1479315cd5c3bf4abdad19391952df98) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [Pin `curl-cffi` to 0.5.10 for Windows](https://github.com/yt-dlp/yt-dlp/commit/ac30941ae682f71eab010877c9a977736a61d3cf) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **cleanup**: Miscellaneous: [89a161e](https://github.com/yt-dlp/yt-dlp/commit/89a161e8c62569a662deda1c948664152efcb6b4) by [bashonly](https://github.com/bashonly)
|
||||||
|
|
||||||
|
### 2024.07.09
|
||||||
|
|
||||||
|
#### Core changes
|
||||||
|
- [Do not alter default format selection when simulated](https://github.com/yt-dlp/yt-dlp/commit/0b570f2a90ce2363ba06089217514d644e7be2e0) ([#9862](https://github.com/yt-dlp/yt-dlp/issues/9862)) by [seproDev](https://github.com/seproDev)
|
||||||
|
|
||||||
|
#### Extractor changes
|
||||||
|
- **youtube**: [Remove broken `n` function extraction fallback](https://github.com/yt-dlp/yt-dlp/commit/7ead7332af69422cee931aec3faa277288e9e212) ([#10396](https://github.com/yt-dlp/yt-dlp/issues/10396)) by [pukkandan](https://github.com/pukkandan), [seproDev](https://github.com/seproDev)
|
||||||
|
|
||||||
|
### 2024.07.08
|
||||||
|
|
||||||
|
#### Core changes
|
||||||
|
- **jsinterp**: [Implement `Function.prototype` resolving for `call` and `apply`](https://github.com/yt-dlp/yt-dlp/commit/6c056ea7aeb03660281653a9668547f2548f194f) ([#10392](https://github.com/yt-dlp/yt-dlp/issues/10392)) by [Grub4K](https://github.com/Grub4K)
|
||||||
|
|
||||||
|
#### Extractor changes
|
||||||
|
- **soundcloud**: [Fix rate-limit handling](https://github.com/yt-dlp/yt-dlp/commit/4b50b292cc98534fb8c7cdf0ae5cb85862f7ebfc) ([#10389](https://github.com/yt-dlp/yt-dlp/issues/10389)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **youtube**: [Fix JS `n` function name extraction](https://github.com/yt-dlp/yt-dlp/commit/297b0a379282a15c80d82d51f3757c961db2dae1) ([#10390](https://github.com/yt-dlp/yt-dlp/issues/10390)) by [bashonly](https://github.com/bashonly), [seproDev](https://github.com/seproDev)
|
||||||
|
|
||||||
|
### 2024.07.07
|
||||||
|
|
||||||
|
#### Important changes
|
||||||
|
- Security: [[ie/douyutv] Do not use dangerous javascript source/URL](https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-3v33-3wmw-3785)
|
||||||
|
- A dependency on potentially malicious third-party JavaScript code has been removed from the Douyu extractors
|
||||||
|
|
||||||
|
#### Core changes
|
||||||
|
- [Address gaps in allowed extensions](https://github.com/yt-dlp/yt-dlp/commit/2469119490d7e0397ebbf5c5ae327316f955eef2) ([#10362](https://github.com/yt-dlp/yt-dlp/issues/10362)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [Fix `--ignore-no-formats-error`](https://github.com/yt-dlp/yt-dlp/commit/cc767e9490056efaaa11c186b0d032e4b4969180) ([#10345](https://github.com/yt-dlp/yt-dlp/issues/10345)) by [Grub4K](https://github.com/Grub4K)
|
||||||
|
|
||||||
|
#### Extractor changes
|
||||||
|
- **abematv**: [Extract availability](https://github.com/yt-dlp/yt-dlp/commit/2a1a1b8e67e864289ac7ba5d05ec63dbb19a639f) ([#10348](https://github.com/yt-dlp/yt-dlp/issues/10348)) by [middlingphys](https://github.com/middlingphys)
|
||||||
|
- **chzzk**: [Extract with API v3](https://github.com/yt-dlp/yt-dlp/commit/4862a29854d4044120e3f97b52199711ad04bee1) ([#10363](https://github.com/yt-dlp/yt-dlp/issues/10363)) by [hui1601](https://github.com/hui1601)
|
||||||
|
- **douyutv**: [Do not use dangerous javascript source/URL](https://github.com/yt-dlp/yt-dlp/commit/6075a029dba70a89675ae1250e7cdfd91f0eba41) ([#10347](https://github.com/yt-dlp/yt-dlp/issues/10347)) by [LeSuisse](https://github.com/LeSuisse)
|
||||||
|
- **jiosaavn**: playlist: [Support featured playlists](https://github.com/yt-dlp/yt-dlp/commit/f0f867f008a1728f5f6ac1224b9e014b5d27f817) ([#10382](https://github.com/yt-dlp/yt-dlp/issues/10382)) by [harbhim](https://github.com/harbhim)
|
||||||
|
- **vidyard**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/00766ece0c5c7a80781a4ff677198c5fb69d9dc0) ([#10155](https://github.com/yt-dlp/yt-dlp/issues/10155)) by [exterrestris](https://github.com/exterrestris)
|
||||||
|
- **vimeo**: [Fix password-protected video extraction](https://github.com/yt-dlp/yt-dlp/commit/c1c9bb4adb42d0d93a2fb5d93a7de0a87b6ba884) ([#10341](https://github.com/yt-dlp/yt-dlp/issues/10341)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **vtv**: [Add extractors](https://github.com/yt-dlp/yt-dlp/commit/987a1f94c24275f2b0cd82e719956687415dd732) ([#10173](https://github.com/yt-dlp/yt-dlp/issues/10173)) by [DinhHuy2010](https://github.com/DinhHuy2010)
|
||||||
|
- **yle_areena**
|
||||||
|
- [Fix metadata extraction](https://github.com/yt-dlp/yt-dlp/commit/4cdc976bd861b5835601ae402bef543eacd88f3d) ([#10380](https://github.com/yt-dlp/yt-dlp/issues/10380)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- [Fix subtitle extraction](https://github.com/yt-dlp/yt-dlp/commit/0d174e8bed32081eb38ef7f5d1a1282ae154f517) ([#10379](https://github.com/yt-dlp/yt-dlp/issues/10379)) by [Grub4K](https://github.com/Grub4K)
|
||||||
|
|
||||||
|
#### Misc. changes
|
||||||
|
- **cleanup**: Miscellaneous: [b337d29](https://github.com/yt-dlp/yt-dlp/commit/b337d2989ce0614651d363383f6f743d977248ef) by [bashonly](https://github.com/bashonly)
|
||||||
|
|
||||||
|
### 2024.07.02
|
||||||
|
|
||||||
|
#### Core changes
|
||||||
|
- [Fix `--compat-opt allow-unsafe-ext`](https://github.com/yt-dlp/yt-dlp/commit/773bbb181506856ffda95496ab60c1c9603f1f71) ([#10336](https://github.com/yt-dlp/yt-dlp/issues/10336)) by [bashonly](https://github.com/bashonly), [rdamas](https://github.com/rdamas)
|
||||||
|
|
||||||
|
#### Extractor changes
|
||||||
|
- **banbye**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/7509791385ba88cb7ec0ab17e826681f4af4b66e) ([#10332](https://github.com/yt-dlp/yt-dlp/issues/10332)) by [PatrykMis](https://github.com/PatrykMis), [seproDev](https://github.com/seproDev)
|
||||||
|
- **murrtube**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/6403530e2dfe259a87afe444708c4f3024cc45b8) ([#9249](https://github.com/yt-dlp/yt-dlp/issues/9249)) by [DrakoCpp](https://github.com/DrakoCpp)
|
||||||
|
- **zaiko**: [Support JWT video URLs](https://github.com/yt-dlp/yt-dlp/commit/7799e518956387bb3c1064c9beae26eab8d5044a) ([#10130](https://github.com/yt-dlp/yt-dlp/issues/10130)) by [pzhlkj6612](https://github.com/pzhlkj6612)
|
||||||
|
|
||||||
|
#### Postprocessor changes
|
||||||
|
- **embedthumbnail**: [Fix embedding with mutagen](https://github.com/yt-dlp/yt-dlp/commit/d502f4c6d95b74896f40070d07229997f0850f31) ([#10337](https://github.com/yt-dlp/yt-dlp/issues/10337)) by [bashonly](https://github.com/bashonly)
|
||||||
|
|
||||||
|
#### Misc. changes
|
||||||
|
- **cleanup**: Miscellaneous: [93d33cb](https://github.com/yt-dlp/yt-dlp/commit/93d33cb29af9e2e84369ac43589d50ce8e0160ef) by [bashonly](https://github.com/bashonly)
|
||||||
|
|
||||||
|
### 2024.07.01
|
||||||
|
|
||||||
|
#### Important changes
|
||||||
|
- Security: [[CVE-2024-38519](https://nvd.nist.gov/vuln/detail/CVE-2024-38519)] [Properly sanitize file-extension to prevent file system modification and RCE](https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-79w7-vh3h-8g4j)
|
||||||
|
- Unsafe extensions are now blocked from being downloaded
|
||||||
|
|
||||||
|
#### Core changes
|
||||||
|
- [Add `playlist_channel` and `playlist_channel_id` fields](https://github.com/yt-dlp/yt-dlp/commit/55e3e6fd21e741ec5ae3d8624de5e5ea345810eb) ([#10266](https://github.com/yt-dlp/yt-dlp/issues/10266)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [Disallow unsafe extensions (CVE-2024-38519)](https://github.com/yt-dlp/yt-dlp/commit/5ce582448ececb8d9c30c8c31f58330090ced03a) by [Grub4K](https://github.com/Grub4K)
|
||||||
|
- **cookies**: [Fix `--cookies-from-browser` DE detection on Linux](https://github.com/yt-dlp/yt-dlp/commit/a8520244b8642880e4d35925e9e49eff94d548de) ([#10237](https://github.com/yt-dlp/yt-dlp/issues/10237)) by [peisenwang](https://github.com/peisenwang)
|
||||||
|
|
||||||
|
#### Extractor changes
|
||||||
|
- **afreecatv**
|
||||||
|
- [Support browser impersonation](https://github.com/yt-dlp/yt-dlp/commit/e8352ad6599de7b5371dc39a1a1edc7890aaedb4) ([#10174](https://github.com/yt-dlp/yt-dlp/issues/10174)) by [hui1601](https://github.com/hui1601)
|
||||||
|
- catchstory: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/054a3ba7d1293f9fbe21800d62d1e5ddcbded238) ([#10235](https://github.com/yt-dlp/yt-dlp/issues/10235)) by [hui1601](https://github.com/hui1601)
|
||||||
|
- **bilibili**: [Support legacy formats](https://github.com/yt-dlp/yt-dlp/commit/1d6ab17d0752ee9cf19e3e63c7dec7b600d3f228) ([#9117](https://github.com/yt-dlp/yt-dlp/issues/9117)) by [c-basalt](https://github.com/c-basalt), [GD-Slime](https://github.com/GD-Slime)
|
||||||
|
- **bitchute**: [Fix extractors](https://github.com/yt-dlp/yt-dlp/commit/5b1a2aa978d0074cee278e7659f32f52ecc4ab53) ([#10301](https://github.com/yt-dlp/yt-dlp/issues/10301)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- **brightcove**: [Upgrade requests to HTTPS](https://github.com/yt-dlp/yt-dlp/commit/90c3721a322756bb7f4ca10ceb73744500bee37e) ([#10202](https://github.com/yt-dlp/yt-dlp/issues/10202)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **cloudflarestream**: [Fix `_VALID_URL` and embed extraction](https://github.com/yt-dlp/yt-dlp/commit/7aa322c02cec54eb77154a89da7e400194f0bd03) ([#10215](https://github.com/yt-dlp/yt-dlp/issues/10215)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **cloudycdn**: [Fix formats extraction](https://github.com/yt-dlp/yt-dlp/commit/b758877afa225747fba81c8a580e27583a231734) ([#10271](https://github.com/yt-dlp/yt-dlp/issues/10271)) by [Caesim404](https://github.com/Caesim404)
|
||||||
|
- **digitalconcerthall**: [Rework extractor](https://github.com/yt-dlp/yt-dlp/commit/2a4f2e82dbeeb0c9130883c83dac689d5260c871) ([#10152](https://github.com/yt-dlp/yt-dlp/issues/10152)) by [seproDev](https://github.com/seproDev), [tippfehlr](https://github.com/tippfehlr)
|
||||||
|
- **facebook**: reel: [Fix extraction](https://github.com/yt-dlp/yt-dlp/commit/8ca1d57ed08d00efa117820a5a82f763b20e2d1d) ([#10232](https://github.com/yt-dlp/yt-dlp/issues/10232)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **francetv**
|
||||||
|
- [Detect and raise errors for DRM](https://github.com/yt-dlp/yt-dlp/commit/3690c2f59827c79a1bbe388a7c1ae75db7477db2) ([#10165](https://github.com/yt-dlp/yt-dlp/issues/10165)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/081708d6074dfbb907e25af61ba530bba0d4b31d) ([#10177](https://github.com/yt-dlp/yt-dlp/issues/10177)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **generic**: [Add `key_query` extractor-arg](https://github.com/yt-dlp/yt-dlp/commit/5dbac313ae4e3e8521dfe2e1a6a048a98ff4b4fe) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **graspop**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/1d369b4096d79233e0ac2c93762746a64d7a69c8) ([#10268](https://github.com/yt-dlp/yt-dlp/issues/10268)) by [Niluge-KiWi](https://github.com/Niluge-KiWi)
|
||||||
|
- **jiocinema**: series: [Fix extraction](https://github.com/yt-dlp/yt-dlp/commit/61714f46956f61612032bba857aed7ad1387eccd) ([#10139](https://github.com/yt-dlp/yt-dlp/issues/10139)) by [varunchopra](https://github.com/varunchopra)
|
||||||
|
- **khanacademy**: [Fix extractors](https://github.com/yt-dlp/yt-dlp/commit/4093eb1fcc29a0e2aea9adfcba479787d9ae0c0c) ([#9136](https://github.com/yt-dlp/yt-dlp/issues/9136)) by [c-basalt](https://github.com/c-basalt)
|
||||||
|
- **laracasts**: [Add extractors](https://github.com/yt-dlp/yt-dlp/commit/b8da8a98f897599095d4ef1644b8c5fd39921118) ([#10055](https://github.com/yt-dlp/yt-dlp/issues/10055)) by [ASertacAkkaya](https://github.com/ASertacAkkaya), [seproDev](https://github.com/seproDev)
|
||||||
|
- **matchtv**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/f3411af12e209bc5624e1ac31271b8aabe2d3c90) ([#10190](https://github.com/yt-dlp/yt-dlp/issues/10190)) by [megumintyan](https://github.com/megumintyan)
|
||||||
|
- **mediasite**: [Fix extraction](https://github.com/yt-dlp/yt-dlp/commit/0953209a857c51648aee89d205c086b0e1dd3864) ([#10273](https://github.com/yt-dlp/yt-dlp/issues/10273)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **microsoftembed**: [Add extractors for dev materials](https://github.com/yt-dlp/yt-dlp/commit/9200bc70c94546b2191bb6fbfc9cea98a919cc56) ([#9177](https://github.com/yt-dlp/yt-dlp/issues/9177)) by [c-basalt](https://github.com/c-basalt)
|
||||||
|
- **mlbtv**: [Fix extraction](https://github.com/yt-dlp/yt-dlp/commit/61edf57f8f13f6dfd81154174e647eb5fdd26089) ([#10296](https://github.com/yt-dlp/yt-dlp/issues/10296)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **neteasemusic**: [Extract more formats from new API](https://github.com/yt-dlp/yt-dlp/commit/7a03f88c40b80d3cf54f68edd9d4bdd6aa527570) ([#10258](https://github.com/yt-dlp/yt-dlp/issues/10258)) by [hafeoz](https://github.com/hafeoz)
|
||||||
|
- **nhkradiru**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/b8e2a5e0e1030076f833917906e19bb6c7b318f6) ([#10106](https://github.com/yt-dlp/yt-dlp/issues/10106)) by [garret1317](https://github.com/garret1317)
|
||||||
|
- **nuum**: [Fix formats extraction](https://github.com/yt-dlp/yt-dlp/commit/aefede25561a06cba398d4f593eee2fbe942693b) ([#10316](https://github.com/yt-dlp/yt-dlp/issues/10316)) by [DmitryScaletta](https://github.com/DmitryScaletta)
|
||||||
|
- **orf**
|
||||||
|
- on
|
||||||
|
- [Add `prefer_segments_playlist` extractor-arg](https://github.com/yt-dlp/yt-dlp/commit/e6a22834df1776ec4e486526f6df2bf53cb7e06f) ([#10314](https://github.com/yt-dlp/yt-dlp/issues/10314)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- [Support segmented episodes](https://github.com/yt-dlp/yt-dlp/commit/8b46ad4d8b8ee8c5472af0cde863baa89ca3f425) ([#10053](https://github.com/yt-dlp/yt-dlp/issues/10053)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- **patreoncampaign**: [Fix `campaign_id` extraction](https://github.com/yt-dlp/yt-dlp/commit/2e5a47da400b645aadbda6afd1156bd89c744f48) ([#10070](https://github.com/yt-dlp/yt-dlp/issues/10070)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **podbayfm**: [Fix extraction](https://github.com/yt-dlp/yt-dlp/commit/d4b52ce3fcb8d9578ed12365648eaba8718c603e) ([#10195](https://github.com/yt-dlp/yt-dlp/issues/10195)) by [bashonly](https://github.com/bashonly), [seproDev](https://github.com/seproDev)
|
||||||
|
- **pokergo**: [Make metadata extraction non-fatal](https://github.com/yt-dlp/yt-dlp/commit/36e8dd832579b5375a0f6626af4268b86b4eb21a) ([#10319](https://github.com/yt-dlp/yt-dlp/issues/10319)) by [axpauls](https://github.com/axpauls)
|
||||||
|
- **qqmusic**: [Fix extractors](https://github.com/yt-dlp/yt-dlp/commit/4f5d7be3c5590bb257d8ff521572aee9839ab754) ([#9768](https://github.com/yt-dlp/yt-dlp/issues/9768)) by [c-basalt](https://github.com/c-basalt)
|
||||||
|
- **rtvslo.si**: show: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/92a1c4abaeeba9a69d611c57b73555cb1a1f00ad) ([#8418](https://github.com/yt-dlp/yt-dlp/issues/8418)) by [JSubelj](https://github.com/JSubelj), [seproDev](https://github.com/seproDev)
|
||||||
|
- **soundcloud**: [Fix `download` format extraction](https://github.com/yt-dlp/yt-dlp/commit/e53e56b73543799638fa6abb0c78f8b091aa84e1) ([#10125](https://github.com/yt-dlp/yt-dlp/issues/10125)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **sproutvideo**: [Add extractors](https://github.com/yt-dlp/yt-dlp/commit/d6c2c2bc84f1434255be5c73baeb17d893d2c0d4) ([#10098](https://github.com/yt-dlp/yt-dlp/issues/10098)) by [bashonly](https://github.com/bashonly), [TheZ3ro](https://github.com/TheZ3ro)
|
||||||
|
- **tiktok**
|
||||||
|
- [Detect and raise when login is required](https://github.com/yt-dlp/yt-dlp/commit/ea88129784fcbb6987161df9ba05909325d8e2e9) ([#10124](https://github.com/yt-dlp/yt-dlp/issues/10124)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [Fix API extraction](https://github.com/yt-dlp/yt-dlp/commit/96472d72f29550c25c5dcedcde02c38c192b0011) ([#10216](https://github.com/yt-dlp/yt-dlp/issues/10216)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **tubitv**
|
||||||
|
- [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/bef9a9e5361fd7a72e21d0f1a8c8afb70d89e8c5) ([#9975](https://github.com/yt-dlp/yt-dlp/issues/9975)) by [chilinux](https://github.com/chilinux)
|
||||||
|
- series: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/d7d861811c15585a4f7ec9d5ae68d2ac28de28a0) ([#10116](https://github.com/yt-dlp/yt-dlp/issues/10116)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **vimeo**: [Support browser impersonation](https://github.com/yt-dlp/yt-dlp/commit/d4b99a233314bf31f9c842035ea9884673d5313a) ([#10327](https://github.com/yt-dlp/yt-dlp/issues/10327)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **youtube**
|
||||||
|
- [Extract all formats from multi-language m3u8s](https://github.com/yt-dlp/yt-dlp/commit/9bd85019931927a99b0fe0dc58ac51acca9fbe72) ([#9875](https://github.com/yt-dlp/yt-dlp/issues/9875)) by [bashonly](https://github.com/bashonly), [clienthax](https://github.com/clienthax)
|
||||||
|
- [Skip formats if nsig decoding fails](https://github.com/yt-dlp/yt-dlp/commit/800ec085ccf98420584d8bb38c20a2c079669b09) ([#10223](https://github.com/yt-dlp/yt-dlp/issues/10223)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [Suppress "Unavailable videos are hidden" warning](https://github.com/yt-dlp/yt-dlp/commit/24f3097ea9a470a984d0454dc013cafa2325f5f8) ([#10159](https://github.com/yt-dlp/yt-dlp/issues/10159)) by [mgedmin](https://github.com/mgedmin)
|
||||||
|
- tab: [Fix channel metadata extraction](https://github.com/yt-dlp/yt-dlp/commit/a0d9967f6822fc279e86bce33464194985148727) ([#10071](https://github.com/yt-dlp/yt-dlp/issues/10071)) by [bashonly](https://github.com/bashonly), [shoxie007](https://github.com/shoxie007)
|
||||||
|
|
||||||
|
#### Downloader changes
|
||||||
|
- **hls**: [Apply `extra_param_to_key_url` from info dict](https://github.com/yt-dlp/yt-dlp/commit/ca8885edd93bdf8912af6c22ee335b6222cb9ba9) by [bashonly](https://github.com/bashonly)
|
||||||
|
|
||||||
|
#### Postprocessor changes
|
||||||
|
- **embedthumbnail**: [Fix postprocessor](https://github.com/yt-dlp/yt-dlp/commit/f2a4ea1794718e4dc0148bc172cb877f1080903b) ([#10248](https://github.com/yt-dlp/yt-dlp/issues/10248)) by [Grub4K](https://github.com/Grub4K)
|
||||||
|
|
||||||
|
#### Networking changes
|
||||||
|
- **Request Handler**: requests: [Bump minimum `requests` version to 2.32.2](https://github.com/yt-dlp/yt-dlp/commit/db50f19d76c6870a5a13d0cab9287d684fd7449a) ([#10079](https://github.com/yt-dlp/yt-dlp/issues/10079)) by [bashonly](https://github.com/bashonly)
|
||||||
|
|
||||||
|
#### Misc. changes
|
||||||
|
- **build**
|
||||||
|
- [Bump Pyinstaller to `>=6.7.0` for all builds](https://github.com/yt-dlp/yt-dlp/commit/5fdd13006a1c5d78642c8d3c4c7df0448273c2ae) ([#10069](https://github.com/yt-dlp/yt-dlp/issues/10069)) by [bashonly](https://github.com/bashonly), [seproDev](https://github.com/seproDev)
|
||||||
|
- [Cache dependencies for `macos` job](https://github.com/yt-dlp/yt-dlp/commit/46c1b7cfec1d0e6155083ca7e6948674c64ecb97) ([#10088](https://github.com/yt-dlp/yt-dlp/issues/10088)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [Use `macos-12` image for `yt-dlp_macos`](https://github.com/yt-dlp/yt-dlp/commit/03334d639d5282cd4107edb32c623ba400262fc4) ([#10063](https://github.com/yt-dlp/yt-dlp/issues/10063)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **cleanup**
|
||||||
|
- [Add more ruff rules](https://github.com/yt-dlp/yt-dlp/commit/add96eb9f84cfffe85682bf2fb85135746994ee8) ([#10149](https://github.com/yt-dlp/yt-dlp/issues/10149)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- [Bump ruff to 0.5.x](https://github.com/yt-dlp/yt-dlp/commit/7814c50948a2b9a4c746441ecbc509ae563d5d1f) ([#10282](https://github.com/yt-dlp/yt-dlp/issues/10282)) by [seproDev](https://github.com/seproDev)
|
||||||
|
- Miscellaneous: [6aaf96a](https://github.com/yt-dlp/yt-dlp/commit/6aaf96a3d6e7d0d426e97e11a2fcf52fda00e733) by [bashonly](https://github.com/bashonly), [c-basalt](https://github.com/c-basalt), [jucor](https://github.com/jucor), [seproDev](https://github.com/seproDev)
|
||||||
|
- **test**: download: [Raise on network errors](https://github.com/yt-dlp/yt-dlp/commit/54a63e80af82791d2f0985bd0176bb182963fd5f) ([#10283](https://github.com/yt-dlp/yt-dlp/issues/10283)) by [bashonly](https://github.com/bashonly), [seproDev](https://github.com/seproDev)
|
||||||
|
|
||||||
### 2024.05.27
|
### 2024.05.27
|
||||||
|
|
||||||
#### Extractor changes
|
#### Extractor changes
|
||||||
|
|
|
@ -61,3 +61,10 @@ You can also find lists of all [contributors of yt-dlp](CONTRIBUTORS) and [autho
|
||||||
* Reworked internals like `traverse_obj`, various core refactors and bugs fixes
|
* Reworked internals like `traverse_obj`, various core refactors and bugs fixes
|
||||||
* Implemented proper progress reporting for parallel downloads
|
* Implemented proper progress reporting for parallel downloads
|
||||||
* Improved/fixed/added Bundestag, crunchyroll, pr0gramm, Twitter, WrestleUniverse etc
|
* Improved/fixed/added Bundestag, crunchyroll, pr0gramm, Twitter, WrestleUniverse etc
|
||||||
|
|
||||||
|
|
||||||
|
## [sepro](https://github.com/seproDev)
|
||||||
|
|
||||||
|
* UX improvements: Warn when ffmpeg is missing, warn when double-clicking exe
|
||||||
|
* Code cleanup: Remove dead extractors, mark extractors as broken, enable/apply ruff rules
|
||||||
|
* Improved/fixed/added ArdMediathek, DRTV, Floatplane, MagentaMusik, Naver, Nebula, OnDemandKorea, Vbox7 etc
|
||||||
|
|
2
Makefile
2
Makefile
|
@ -21,7 +21,7 @@ clean-test:
|
||||||
rm -rf test/testdata/sigs/player-*.js tmp/ *.annotations.xml *.aria2 *.description *.dump *.frag \
|
rm -rf test/testdata/sigs/player-*.js tmp/ *.annotations.xml *.aria2 *.description *.dump *.frag \
|
||||||
*.frag.aria2 *.frag.urls *.info.json *.live_chat.json *.meta *.part* *.tmp *.temp *.unknown_video *.ytdl \
|
*.frag.aria2 *.frag.urls *.info.json *.live_chat.json *.meta *.part* *.tmp *.temp *.unknown_video *.ytdl \
|
||||||
*.3gp *.ape *.ass *.avi *.desktop *.f4v *.flac *.flv *.gif *.jpeg *.jpg *.lrc *.m4a *.m4v *.mhtml *.mkv *.mov *.mp3 *.mp4 \
|
*.3gp *.ape *.ass *.avi *.desktop *.f4v *.flac *.flv *.gif *.jpeg *.jpg *.lrc *.m4a *.m4v *.mhtml *.mkv *.mov *.mp3 *.mp4 \
|
||||||
*.mpg *.mpga *.oga *.ogg *.opus *.png *.sbv *.srt *.ssa *.swf *.swp *.tt *.ttml *.url *.vtt *.wav *.webloc *.webm *.webp
|
*.mpg *.mpga *.oga *.ogg *.opus *.png *.sbv *.srt *.ssa *.swf *.tt *.ttml *.url *.vtt *.wav *.webloc *.webm *.webp
|
||||||
clean-dist:
|
clean-dist:
|
||||||
rm -rf yt-dlp.1.temp.md yt-dlp.1 README.txt MANIFEST build/ dist/ .coverage cover/ yt-dlp.tar.gz completions/ \
|
rm -rf yt-dlp.1.temp.md yt-dlp.1 README.txt MANIFEST build/ dist/ .coverage cover/ yt-dlp.tar.gz completions/ \
|
||||||
yt_dlp/extractor/lazy_extractors.py *.spec CONTRIBUTING.md.tmp yt-dlp yt-dlp.exe yt_dlp.egg-info/ AUTHORS
|
yt_dlp/extractor/lazy_extractors.py *.spec CONTRIBUTING.md.tmp yt-dlp yt-dlp.exe yt_dlp.egg-info/ AUTHORS
|
||||||
|
|
277
README.md
277
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 @@ yt-dlp is a feature-rich command-line audio/video downloader with support for [t
|
||||||
[![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 @@ You can install yt-dlp using [the binaries](#release-files), [pip](https://pypi.
|
||||||
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
|
||||||
|
@ -141,7 +140,7 @@ You can use `yt-dlp -U` to update if you are using the [release binaries](#relea
|
||||||
|
|
||||||
If you [installed with pip](https://github.com/yt-dlp/yt-dlp/wiki/Installation#with-pip), simply re-run the same command that was used to install the program
|
If you [installed with pip](https://github.com/yt-dlp/yt-dlp/wiki/Installation#with-pip), simply re-run the same command that was used to install the program
|
||||||
|
|
||||||
For other third-party package managers, see [the wiki](https://github.com/yt-dlp/yt-dlp/wiki/Installation#third-party-package-managers) or refer their documentation
|
For other third-party package managers, see [the wiki](https://github.com/yt-dlp/yt-dlp/wiki/Installation#third-party-package-managers) or refer to their documentation
|
||||||
|
|
||||||
<a id="update-channels"></a>
|
<a id="update-channels"></a>
|
||||||
|
|
||||||
|
@ -173,18 +172,18 @@ python3 -m pip install -U --pre "yt-dlp[default]"
|
||||||
```
|
```
|
||||||
|
|
||||||
## 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
|
||||||
|
|
||||||
### Strongly recommended
|
### Strongly recommended
|
||||||
|
|
||||||
* [**ffmpeg** and **ffprobe**](https://www.ffmpeg.org) - Required for [merging separate video and audio files](#format-selection) as well as for various [post-processing](#post-processing-options) tasks. License [depends on the build](https://www.ffmpeg.org/legal.html)
|
* [**ffmpeg** and **ffprobe**](https://www.ffmpeg.org) - Required for [merging separate video and audio files](#format-selection), as well as for various [post-processing](#post-processing-options) tasks. License [depends on the build](https://www.ffmpeg.org/legal.html)
|
||||||
|
|
||||||
There are bugs in ffmpeg that cause various issues when used alongside yt-dlp. Since ffmpeg is such an important dependency, we provide [custom builds](https://github.com/yt-dlp/FFmpeg-Builds#ffmpeg-static-auto-builds) with patches for some of these issues at [yt-dlp/FFmpeg-Builds](https://github.com/yt-dlp/FFmpeg-Builds). See [the readme](https://github.com/yt-dlp/FFmpeg-Builds#patches-applied) for details on the specific issues solved by these builds
|
There are bugs in ffmpeg that cause various issues when used alongside yt-dlp. Since ffmpeg is such an important dependency, we provide [custom builds](https://github.com/yt-dlp/FFmpeg-Builds#ffmpeg-static-auto-builds) with patches for some of these issues at [yt-dlp/FFmpeg-Builds](https://github.com/yt-dlp/FFmpeg-Builds). See [the readme](https://github.com/yt-dlp/FFmpeg-Builds#patches-applied) for details on the specific issues solved by these builds
|
||||||
|
|
||||||
|
@ -200,9 +199,9 @@ While all the other dependencies are optional, `ffmpeg` and `ffprobe` are highly
|
||||||
|
|
||||||
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 only included in `yt-dlp.exe` and `yt-dlp_macos` builds
|
* Currently included in `yt-dlp.exe`, `yt-dlp_linux` and `yt-dlp_macos` builds
|
||||||
|
|
||||||
|
|
||||||
### Metadata
|
### Metadata
|
||||||
|
@ -254,31 +253,19 @@ On some systems, you may need to use `py` or `python` instead of `python3`.
|
||||||
**Important**: Running `pyinstaller` directly **instead of** using `python -m bundle.pyinstaller` is **not** officially supported. This may or may not work correctly.
|
**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 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 @@ If you fork the project on GitHub, you can run your fork's [build workflow](.git
|
||||||
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
|
||||||
|
@ -368,7 +362,9 @@ If you fork the project on GitHub, you can run your fork's [build workflow](.git
|
||||||
stderr) to apply the setting to. Can be one
|
stderr) to apply the setting to. Can be one
|
||||||
of "always", "auto" (default), "never", or
|
of "always", "auto" (default), "never", or
|
||||||
"no_color" (use non color terminal
|
"no_color" (use non color terminal
|
||||||
sequences). Can be used multiple times
|
sequences). Use "auto-tty" or "no_color-tty"
|
||||||
|
to decide based on terminal support only.
|
||||||
|
Can be used multiple times
|
||||||
--compat-options OPTS Options that can help keep compatibility
|
--compat-options OPTS Options that can help keep compatibility
|
||||||
with youtube-dl or youtube-dlc
|
with youtube-dl or youtube-dlc
|
||||||
configurations by reverting some of the
|
configurations by reverting some of the
|
||||||
|
@ -442,10 +438,10 @@ If you fork the project on GitHub, you can run your fork's [build workflow](.git
|
||||||
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
|
||||||
|
@ -456,18 +452,18 @@ If you fork the project on GitHub, you can run your fork's [build workflow](.git
|
||||||
is not present, and "&" to check multiple
|
is not present, and "&" to check multiple
|
||||||
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 atleast one of the
|
the filter matches if at least one of the
|
||||||
conditions are 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)
|
||||||
|
@ -483,12 +479,13 @@ If you fork the project on GitHub, you can run your fork's [build workflow](.git
|
||||||
--no-download-archive Do not use archive file (default)
|
--no-download-archive Do not use archive file (default)
|
||||||
--max-downloads NUMBER Abort after downloading NUMBER files
|
--max-downloads NUMBER Abort after downloading NUMBER files
|
||||||
--break-on-existing Stop the download process when encountering
|
--break-on-existing Stop the download process when encountering
|
||||||
a file that is in the archive
|
a file that is in the archive supplied with
|
||||||
|
the --download-archive option
|
||||||
--no-break-on-existing Do not stop the download process when
|
--no-break-on-existing Do not stop the download process when
|
||||||
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
|
||||||
|
@ -674,7 +671,7 @@ If you fork the project on GitHub, you can run your fork's [build workflow](.git
|
||||||
PROFILE to load cookies from, and the
|
PROFILE to load cookies from, and the
|
||||||
CONTAINER name (if Firefox) ("none" for no
|
CONTAINER name (if Firefox) ("none" for no
|
||||||
container) can be given with their
|
container) can be given with their
|
||||||
respective seperators. By default, all
|
respective separators. By default, all
|
||||||
containers of the most recently accessed
|
containers of the most recently accessed
|
||||||
profile are used. Currently supported
|
profile are used. Currently supported
|
||||||
keyrings are: basictext, gnomekeyring,
|
keyrings are: basictext, gnomekeyring,
|
||||||
|
@ -730,16 +727,16 @@ If you fork the project on GitHub, you can run your fork's [build workflow](.git
|
||||||
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
|
||||||
|
@ -814,9 +811,9 @@ If you fork the project on GitHub, you can run your fork's [build workflow](.git
|
||||||
--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
|
||||||
|
@ -841,15 +838,17 @@ If you fork the project on GitHub, you can run your fork's [build workflow](.git
|
||||||
(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
|
||||||
|
@ -897,9 +896,9 @@ If you fork the project on GitHub, you can run your fork's [build workflow](.git
|
||||||
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
|
||||||
|
@ -967,29 +966,29 @@ If you fork the project on GitHub, you can run your fork's [build workflow](.git
|
||||||
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
|
||||||
|
@ -997,12 +996,16 @@ If you fork the project on GitHub, you can run your fork's [build workflow](.git
|
||||||
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
|
||||||
|
@ -1023,7 +1026,7 @@ If you fork the project on GitHub, you can run your fork's [build workflow](.git
|
||||||
--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
|
||||||
|
@ -1036,8 +1039,8 @@ If you fork the project on GitHub, you can run your fork's [build workflow](.git
|
||||||
--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 it's 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
|
||||||
|
@ -1055,7 +1058,7 @@ Make chapter entries for, or remove various segments (sponsor,
|
||||||
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
|
||||||
|
@ -1087,7 +1090,7 @@ Make chapter entries for, or remove various segments (sponsor,
|
||||||
(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.
|
||||||
|
@ -1097,7 +1100,7 @@ Make chapter entries for, or remove various segments (sponsor,
|
||||||
|
|
||||||
# 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`
|
||||||
|
@ -1125,7 +1128,7 @@ You can configure yt-dlp by placing any supported command line option to a confi
|
||||||
* `/etc/yt-dlp/config`
|
* `/etc/yt-dlp/config`
|
||||||
* `/etc/yt-dlp/config.txt`
|
* `/etc/yt-dlp/config.txt`
|
||||||
|
|
||||||
E.g. with the following configuration file yt-dlp will always extract the audio, not copy the mtime, use a proxy and save all videos under `YouTube` directory in your home directory:
|
E.g. with the following configuration file, yt-dlp will always extract the audio, not copy the mtime, use a proxy and save all videos under `YouTube` directory in your home directory:
|
||||||
```
|
```
|
||||||
# Lines starting with # are comments
|
# Lines starting with # are comments
|
||||||
|
|
||||||
|
@ -1142,7 +1145,7 @@ E.g. with the following configuration file yt-dlp will always extract the audio,
|
||||||
-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.
|
||||||
|
|
||||||
|
@ -1154,12 +1157,12 @@ If you want your file to be decoded differently, add `# coding: ENCODING` to the
|
||||||
|
|
||||||
### Authentication with netrc
|
### Authentication with netrc
|
||||||
|
|
||||||
You may also want to configure automatic credentials storage for extractors that support authentication (by providing login and password with `--username` and `--password`) in order not to pass credentials as command line arguments on every yt-dlp execution and prevent tracking plain text passwords in the shell command history. You can achieve this using a [`.netrc` file](https://stackoverflow.com/tags/.netrc/info) on a per-extractor basis. For that you will need to create a `.netrc` file in `--netrc-location` and restrict permissions to read/write by only you:
|
You may also want to configure automatic credentials storage for extractors that support authentication (by providing login and password with `--username` and `--password`) in order not to pass credentials as command line arguments on every yt-dlp execution and prevent tracking plain text passwords in the shell command history. You can achieve this using a [`.netrc` file](https://stackoverflow.com/tags/.netrc/info) on a per-extractor basis. For that, you will need to create a `.netrc` file in `--netrc-location` and restrict permissions to read/write by only you:
|
||||||
```
|
```
|
||||||
touch ${HOME}/.netrc
|
touch ${HOME}/.netrc
|
||||||
chmod a-rwx,u+rw ${HOME}/.netrc
|
chmod a-rwx,u+rw ${HOME}/.netrc
|
||||||
```
|
```
|
||||||
After that you can add credentials for an extractor in the following format, where *extractor* is the name of the extractor in lowercase:
|
After that, you can add credentials for an extractor in the following format, where *extractor* is the name of the extractor in lowercase:
|
||||||
```
|
```
|
||||||
machine <extractor> login <username> password <password>
|
machine <extractor> login <username> password <password>
|
||||||
```
|
```
|
||||||
|
@ -1176,13 +1179,13 @@ As an alternative to using the `.netrc` file, which has the disadvantage of keep
|
||||||
|
|
||||||
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`
|
||||||
|
@ -1201,7 +1204,7 @@ It may however also contain special sequences that will be replaced when downloa
|
||||||
|
|
||||||
The field names themselves (the part inside the parenthesis) can also have some special formatting:
|
The field names themselves (the part inside the parenthesis) can also have some special formatting:
|
||||||
|
|
||||||
1. **Object traversal**: The dictionaries and lists available in metadata can be traversed by using a dot `.` separator; e.g. `%(tags.0)s`, `%(subtitles.en.-1.ext)s`. You can do Python slicing with colon `:`; E.g. `%(id.3:7:-1)s`, `%(formats.:.format_id)s`. Curly braces `{}` can be used to build dictionaries with only specific keys; e.g. `%(formats.:.{format_id,height})#j`. An empty field name `%()s` refers to the entire infodict; e.g. `%(.{id,title})s`. Note that all the fields that become available using this method are not listed below. Use `-j` to see such fields
|
1. **Object traversal**: The dictionaries and lists available in metadata can be traversed by using a dot `.` separator; e.g. `%(tags.0)s`, `%(subtitles.en.-1.ext)s`. You can do Python slicing with colon `:`; E.g. `%(id.3:7)s`, `%(id.6:2:-1)s`, `%(formats.:.format_id)s`. Curly braces `{}` can be used to build dictionaries with only specific keys; e.g. `%(formats.:.{format_id,height})#j`. An empty field name `%()s` refers to the entire infodict; e.g. `%(.{id,title})s`. Note that all the fields that become available using this method are not listed below. Use `-j` to see such fields
|
||||||
|
|
||||||
1. **Arithmetic**: Simple arithmetic can be done on numeric fields using `+`, `-` and `*`. E.g. `%(playlist_index+10)03d`, `%(n_entries+1-playlist_index)d`
|
1. **Arithmetic**: Simple arithmetic can be done on numeric fields using `+`, `-` and `*`. E.g. `%(playlist_index+10)03d`, `%(n_entries+1-playlist_index)d`
|
||||||
|
|
||||||
|
@ -1222,7 +1225,7 @@ To summarize, the general syntax for a field is:
|
||||||
%(name[.keys][addition][>strf][,alternate][&replacement][|default])[flags][width][.precision][length]type
|
%(name[.keys][addition][>strf][,alternate][&replacement][|default])[flags][width][.precision][length]type
|
||||||
```
|
```
|
||||||
|
|
||||||
Additionally, you can set different output templates for the various metadata files separately from the general output template by specifying the type of file followed by the template separated by a colon `:`. The different file types supported are `subtitle`, `thumbnail`, `description`, `annotation` (deprecated), `infojson`, `link`, `pl_thumbnail`, `pl_description`, `pl_infojson`, `chapter`, `pl_video`. E.g. `-o "%(title)s.%(ext)s" -o "thumbnail:%(title)s\%(title)s.%(ext)s"` will put the thumbnails in a folder with the same name as the video. If any of the templates is empty, that type of file will not be written. E.g. `--write-thumbnail -o "thumbnail:"` will write thumbnails only for playlists and not for video.
|
Additionally, you can set different output templates for the various metadata files separately from the general output template by specifying the type of file followed by the template separated by a colon `:`. The different file types supported are `subtitle`, `thumbnail`, `description`, `annotation` (deprecated), `infojson`, `link`, `pl_thumbnail`, `pl_description`, `pl_infojson`, `chapter`, `pl_video`. E.g. `-o "%(title)s.%(ext)s" -o "thumbnail:%(title)s\%(title)s.%(ext)s"` will put the thumbnails in a folder with the same name as the video. If any of the templates is empty, that type of file will not be written. E.g. `--write-thumbnail -o "thumbnail:"` will write thumbnails only for playlists and not for video.
|
||||||
|
|
||||||
<a id="outtmpl-postprocess-note"></a>
|
<a id="outtmpl-postprocess-note"></a>
|
||||||
|
|
||||||
|
@ -1263,7 +1266,7 @@ The available fields are:
|
||||||
- `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)
|
||||||
|
@ -1282,16 +1285,18 @@ The available fields are:
|
||||||
- `n_entries` (numeric): Total number of extracted items in the playlist
|
- `n_entries` (numeric): Total number of extracted items in the playlist
|
||||||
- `playlist_id` (string): Identifier of the playlist that contains the video
|
- `playlist_id` (string): Identifier of the playlist that contains the video
|
||||||
- `playlist_title` (string): Name of the playlist that contains the video
|
- `playlist_title` (string): Name of the playlist that contains the video
|
||||||
- `playlist` (string): `playlist_id` or `playlist_title`
|
- `playlist` (string): `playlist_title` if available or else `playlist_id`
|
||||||
- `playlist_count` (numeric): Total number of items in the playlist. May not be known if entire playlist is not extracted
|
- `playlist_count` (numeric): Total number of items in the playlist. May not be known if entire playlist is not extracted
|
||||||
- `playlist_index` (numeric): Index of the video in the playlist padded with leading zeros according the final index
|
- `playlist_index` (numeric): Index of the video in the playlist padded with leading zeros according the final index
|
||||||
- `playlist_autonumber` (numeric): Position of the video in the playlist download queue padded with leading zeros according to the total length of the playlist
|
- `playlist_autonumber` (numeric): Position of the video in the playlist download queue padded with leading zeros according to the total length of the playlist
|
||||||
- `playlist_uploader` (string): Full name of the playlist uploader
|
- `playlist_uploader` (string): Full name of the playlist uploader
|
||||||
- `playlist_uploader_id` (string): Nickname or id of the playlist uploader
|
- `playlist_uploader_id` (string): Nickname or id of the playlist uploader
|
||||||
- `webpage_url` (string): A URL to the video webpage which if given to yt-dlp should allow to get the same result again
|
- `playlist_channel` (string): Display name of the channel that uploaded the playlist
|
||||||
|
- `playlist_channel_id` (string): Identifier of the channel that uploaded the playlist
|
||||||
|
- `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
|
||||||
|
@ -1304,10 +1309,10 @@ Available for the video that belongs to some logical chapter or section:
|
||||||
- `chapter_number` (numeric): Number of the chapter the video belongs to
|
- `chapter_number` (numeric): Number of the chapter the video belongs to
|
||||||
- `chapter_id` (string): Id of the chapter the video belongs to
|
- `chapter_id` (string): Id of the chapter the video belongs to
|
||||||
|
|
||||||
Available for the video that is an episode of some series or programme:
|
Available for the video that is an episode of some series or program:
|
||||||
|
|
||||||
- `series` (string): Title of the series or programme the video episode belongs to
|
- `series` (string): Title of the series or program the video episode belongs to
|
||||||
- `series_id` (string): Id of the series or programme the video episode belongs to
|
- `series_id` (string): Id of the series or program the video episode belongs to
|
||||||
- `season` (string): Title of the season the video episode belongs to
|
- `season` (string): Title of the season the video episode belongs to
|
||||||
- `season_number` (numeric): Number of the season the video episode belongs to
|
- `season_number` (numeric): Number of the season the video episode belongs to
|
||||||
- `season_id` (string): Id of the season the video episode belongs to
|
- `season_id` (string): Id of the season the video episode belongs to
|
||||||
|
@ -1364,11 +1369,11 @@ Available only in `--sponsorblock-chapter-title`:
|
||||||
|
|
||||||
Each aforementioned sequence when referenced in an output template will be replaced by the actual value corresponding to the sequence name. E.g. for `-o %(title)s-%(id)s.%(ext)s` and an mp4 video with title `yt-dlp test video` and id `BaW_jenozKc`, this will result in a `yt-dlp test video-BaW_jenozKc.mp4` file created in the current directory.
|
Each aforementioned sequence when referenced in an output template will be replaced by the actual value corresponding to the sequence name. E.g. for `-o %(title)s-%(id)s.%(ext)s` and an mp4 video with title `yt-dlp test video` and id `BaW_jenozKc`, this will result in a `yt-dlp test video-BaW_jenozKc.mp4` file created in the current directory.
|
||||||
|
|
||||||
**Note**: Some of the sequences are not guaranteed to be present since they depend on the metadata obtained by a particular extractor. Such sequences will be replaced with placeholder value provided with `--output-na-placeholder` (`NA` by default).
|
**Note**: Some of the sequences are not guaranteed to be present, since they depend on the metadata obtained by a particular extractor. Such sequences will be replaced with placeholder value provided with `--output-na-placeholder` (`NA` by default).
|
||||||
|
|
||||||
**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.
|
||||||
|
|
||||||
|
@ -1410,7 +1415,7 @@ $ yt-dlp -P "C:/MyVideos" -o "%(series)s/%(season_number)s - %(season)s/%(episod
|
||||||
|
|
||||||
# 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
|
||||||
|
@ -1442,7 +1447,7 @@ You can also use special names to select particular edge case formats:
|
||||||
|
|
||||||
- `all`: Select **all formats** separately
|
- `all`: Select **all formats** separately
|
||||||
- `mergeall`: Select and **merge all formats** (Must be used with `--audio-multistreams`, `--video-multistreams` or both)
|
- `mergeall`: Select and **merge all formats** (Must be used with `--audio-multistreams`, `--video-multistreams` or both)
|
||||||
- `b*`, `best*`: Select the best quality format that **contains either** a video or an audio or both (ie; `vcodec!=none or acodec!=none`)
|
- `b*`, `best*`: Select the best quality format that **contains either** a video or an audio or both (i.e.; `vcodec!=none or acodec!=none`)
|
||||||
- `b`, `best`: Select the best quality format that **contains both** video and audio. Equivalent to `best*[vcodec!=none][acodec!=none]`
|
- `b`, `best`: Select the best quality format that **contains both** video and audio. Equivalent to `best*[vcodec!=none][acodec!=none]`
|
||||||
- `bv`, `bestvideo`: Select the best quality **video-only** format. Equivalent to `best*[acodec=none]`
|
- `bv`, `bestvideo`: Select the best quality **video-only** format. Equivalent to `best*[acodec=none]`
|
||||||
- `bv*`, `bestvideo*`: Select the best quality format that **contains video**. It may also contain audio. Equivalent to `best*[vcodec!=none]`
|
- `bv*`, `bestvideo*`: Select the best quality format that **contains video**. It may also contain audio. Equivalent to `best*[vcodec!=none]`
|
||||||
|
@ -1455,7 +1460,7 @@ You can also use special names to select particular edge case formats:
|
||||||
- `wa`, `worstaudio`: Select the worst quality audio-only format. Equivalent to `worst*[vcodec=none]`
|
- `wa`, `worstaudio`: Select the worst quality audio-only format. Equivalent to `worst*[vcodec=none]`
|
||||||
- `wa*`, `worstaudio*`: Select the worst quality format that contains audio. It may also contain video. Equivalent to `worst*[acodec!=none]`
|
- `wa*`, `worstaudio*`: Select the worst quality format that contains audio. It may also contain video. Equivalent to `worst*[acodec!=none]`
|
||||||
|
|
||||||
For example, to download the worst quality video-only format you can use `-f worstvideo`. It is however recommended not to use `worst` and related options. When your format selector is `worst`, the format which is worst in all respects is selected. Most of the time, what you actually want is the video with the smallest filesize instead. So it is generally better to use `-S +size` or more rigorously, `-S +size,+br,+res,+fps` instead of `-f worst`. See [Sorting Formats](#sorting-formats) for more details.
|
For example, to download the worst quality video-only format you can use `-f worstvideo`. It is, however, recommended not to use `worst` and related options. When your format selector is `worst`, the format which is worst in all respects is selected. Most of the time, what you actually want is the video with the smallest filesize instead. So it is generally better to use `-S +size` or more rigorously, `-S +size,+br,+res,+fps` instead of `-f worst`. See [Sorting Formats](#sorting-formats) for more details.
|
||||||
|
|
||||||
You can select the n'th best format of a type by using `best<type>.<n>`. For example, `best.2` will select the 2nd best combined format. Similarly, `bv*.3` will select the 3rd best format that contains a video stream.
|
You can select the n'th best format of a type by using `best<type>.<n>`. For example, `best.2` will select the 2nd best combined format. Similarly, `bv*.3` will select the 3rd best format that contains a video stream.
|
||||||
|
|
||||||
|
@ -1505,7 +1510,7 @@ Also filtering work for comparisons `=` (equals), `^=` (starts with), `$=` (ends
|
||||||
|
|
||||||
Any string comparison may be prefixed with negation `!` in order to produce an opposite comparison, e.g. `!*=` (does not contain). The comparand of a string comparison needs to be quoted with either double or single quotes if it contains spaces or special characters other than `._-`.
|
Any string comparison may be prefixed with negation `!` in order to produce an opposite comparison, e.g. `!*=` (does not contain). The comparand of a string comparison needs to be quoted with either double or single quotes if it contains spaces or special characters other than `._-`.
|
||||||
|
|
||||||
**Note**: None of the aforementioned meta fields are guaranteed to be present since this solely depends on the metadata obtained by particular extractor, i.e. the metadata offered by the website. Any other field made available by the extractor can also be used for filtering.
|
**Note**: None of the aforementioned meta fields are guaranteed to be present since this solely depends on the metadata obtained by the particular extractor, i.e. the metadata offered by the website. Any other field made available by the extractor can also be used for filtering.
|
||||||
|
|
||||||
Formats for which the value is not known are excluded unless you put a question mark (`?`) after the operator. You can combine format filters, so `-f "bv[height<=?720][tbr>500]"` selects up to 720p videos (or videos where the height is not known) with a bitrate of at least 500 kbps. You can also use the filters with `all` to download all formats that satisfy the filter, e.g. `-f "all[vcodec=none]"` selects all audio-only formats.
|
Formats for which the value is not known are excluded unless you put a question mark (`?`) after the operator. You can combine format filters, so `-f "bv[height<=?720][tbr>500]"` selects up to 720p videos (or videos where the height is not known) with a bitrate of at least 500 kbps. You can also use the filters with `all` to download all formats that satisfy the filter, e.g. `-f "all[vcodec=none]"` selects all audio-only formats.
|
||||||
|
|
||||||
|
@ -1549,9 +1554,9 @@ The available fields are:
|
||||||
|
|
||||||
All fields, unless specified otherwise, are sorted in descending order. To reverse this, prefix the field with a `+`. E.g. `+res` prefers format with the smallest resolution. Additionally, you can suffix a preferred value for the fields, separated by a `:`. E.g. `res:720` prefers larger videos, but no larger than 720p and the smallest video if there are no videos less than 720p. For `codec` and `ext`, you can provide two preferred values, the first for video and the second for audio. E.g. `+codec:avc:m4a` (equivalent to `+vcodec:avc,+acodec:m4a`) sets the video codec preference to `h264` > `h265` > `vp9` > `vp9.2` > `av01` > `vp8` > `h263` > `theora` and audio codec preference to `mp4a` > `aac` > `vorbis` > `opus` > `mp3` > `ac3` > `dts`. You can also make the sorting prefer the nearest values to the provided by using `~` as the delimiter. E.g. `filesize~1G` prefers the format with filesize closest to 1 GiB.
|
All fields, unless specified otherwise, are sorted in descending order. To reverse this, prefix the field with a `+`. E.g. `+res` prefers format with the smallest resolution. Additionally, you can suffix a preferred value for the fields, separated by a `:`. E.g. `res:720` prefers larger videos, but no larger than 720p and the smallest video if there are no videos less than 720p. For `codec` and `ext`, you can provide two preferred values, the first for video and the second for audio. E.g. `+codec:avc:m4a` (equivalent to `+vcodec:avc,+acodec:m4a`) sets the video codec preference to `h264` > `h265` > `vp9` > `vp9.2` > `av01` > `vp8` > `h263` > `theora` and audio codec preference to `mp4a` > `aac` > `vorbis` > `opus` > `mp3` > `ac3` > `dts`. You can also make the sorting prefer the nearest values to the provided by using `~` as the delimiter. E.g. `filesize~1G` prefers the format with filesize closest to 1 GiB.
|
||||||
|
|
||||||
The fields `hasvid` and `ie_pref` are always given highest priority in sorting, irrespective of the user-defined order. This behaviour can be changed by using `--format-sort-force`. Apart from these, the default order used is: `lang,quality,res,fps,hdr:12,vcodec:vp9.2,channels,acodec,size,br,asr,proto,ext,hasaud,source,id`. The extractors may override this default order, but they cannot override the user-provided order.
|
The fields `hasvid` and `ie_pref` are always given highest priority in sorting, irrespective of the user-defined order. This behavior can be changed by using `--format-sort-force`. Apart from these, the default order used is: `lang,quality,res,fps,hdr:12,vcodec,channels,acodec,size,br,asr,proto,ext,hasaud,source,id`. The extractors may override this default order, but they cannot override the user-provided order.
|
||||||
|
|
||||||
Note that the default has `vcodec:vp9.2`; i.e. `av1` is not preferred. Similarly, the default for hdr is `hdr:12`; i.e. dolby vision is not preferred. These choices are made since DV and AV1 formats are not yet fully compatible with most devices. This may be changed in the future as more devices become capable of smoothly playing back these formats.
|
Note that the default for hdr is `hdr:12`; i.e. Dolby Vision is not preferred. This choice was made since DV formats are not yet fully compatible with most devices. This may be changed in the future.
|
||||||
|
|
||||||
If your format selector is `worst`, the last item is selected after sorting. This means it will select the format that is worst in all respects. Most of the time, what you actually want is the video with the smallest filesize instead. So it is generally better to use `-f best -S +size,+br,+res,+fps`.
|
If your format selector is `worst`, the last item is selected after sorting. This means it will select the format that is worst in all respects. Most of the time, what you actually want is the video with the smallest filesize instead. So it is generally better to use `-f best -S +size,+br,+res,+fps`.
|
||||||
|
|
||||||
|
@ -1628,11 +1633,11 @@ $ yt-dlp -S "res:480"
|
||||||
# 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"
|
||||||
|
|
||||||
|
|
||||||
|
@ -1688,7 +1693,7 @@ The metadata obtained by the extractors can be modified by using `--parse-metada
|
||||||
|
|
||||||
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:
|
||||||
|
|
||||||
|
@ -1754,7 +1759,7 @@ $ yt-dlp --replace-in-metadata "title,uploader" "[ _]" "-"
|
||||||
|
|
||||||
# EXTRACTOR ARGUMENTS
|
# EXTRACTOR ARGUMENTS
|
||||||
|
|
||||||
Some extractors accept additional arguments which can be passed using `--extractor-args KEY:ARGS`. `ARGS` is a `;` (semicolon) separated string of `ARG=VAL1,VAL2`. E.g. `--extractor-args "youtube:player-client=android_embedded,web;formats=incomplete" --extractor-args "funimation:version=uncut"`
|
Some extractors accept additional arguments which can be passed using `--extractor-args KEY:ARGS`. `ARGS` is a `;` (semicolon) separated string of `ARG=VAL1,VAL2`. E.g. `--extractor-args "youtube:player-client=mediaconnect,web;formats=incomplete" --extractor-args "funimation:version=uncut"`
|
||||||
|
|
||||||
Note: In CLI, `ARG` can use `-` instead of `_`; e.g. `youtube:player-client"` becomes `youtube:player_client"`
|
Note: In CLI, `ARG` can use `-` instead of `_`; e.g. `youtube:player-client"` becomes `youtube:player_client"`
|
||||||
|
|
||||||
|
@ -1763,7 +1768,7 @@ The following extractors use this feature:
|
||||||
#### youtube
|
#### youtube
|
||||||
* `lang`: Prefer translated metadata (`title`, `description` etc) of this language code (case-sensitive). By default, the video primary language metadata is preferred, with a fallback to `en` translated. See [youtube.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`, `_embedded`, `_embedscreen`, `_creator` (e.g. `web_embedded`); and `mweb`, `mweb_embedscreen` and `tv_embedded` (agegate bypass) with no variants. By default, `ios,web` is used, but `tv_embedded` and `creator` variants are added as required for age-gated videos. Similarly, the music variants are added for `music.youtube.com` urls. The `android` clients will always 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)
|
||||||
|
@ -1771,18 +1776,23 @@ The following extractors use this feature:
|
||||||
* E.g. `all,all,1000,10` will get a maximum of 1000 replies total, with up to 10 replies per thread. `1000,all,100` will get a maximum of 1000 comments, with a maximum of 100 replies total
|
* E.g. `all,all,1000,10` will get a maximum of 1000 replies total, with up to 10 replies per thread. `1000,all,100` will get a maximum of 1000 comments, with a maximum of 100 replies total
|
||||||
* `formats`: Change the types of formats to return. `dashy` (convert HTTP to DASH), `duplicate` (identical content but different URLs or protocol; includes `dashy`), `incomplete` (cannot be downloaded completely - live dash and post-live m3u8)
|
* `formats`: Change the types of formats to return. `dashy` (convert HTTP to DASH), `duplicate` (identical content but different URLs or protocol; includes `dashy`), `incomplete` (cannot be downloaded completely - live dash and post-live m3u8)
|
||||||
* `innertube_host`: Innertube API host to use for all API requests; e.g. `studio.youtube.com`, `youtubei.googleapis.com`. Note that cookies exported from one subdomain will not work on others
|
* `innertube_host`: Innertube API host to use for all API requests; e.g. `studio.youtube.com`, `youtubei.googleapis.com`. Note that cookies exported from one subdomain will not work on others
|
||||||
* `innertube_key`: Innertube API key to use for all API requests
|
* `innertube_key`: Innertube API key to use for all API requests. By default, no API key is used
|
||||||
* `raise_incomplete_data`: `Incomplete Data Received` raises an error instead of reporting a warning
|
* `raise_incomplete_data`: `Incomplete Data Received` raises an error instead of reporting a warning
|
||||||
|
* `data_sync_id`: Overrides the account Data Sync ID used in Innertube API requests. This may be needed if you are using an account with `youtube:player_skip=webpage,configs` or `youtubetab:skip=webpage`
|
||||||
|
* `visitor_data`: Overrides the Visitor Data used in Innertube API requests. This should be used with `player_skip=webpage,configs` and without cookies. Note: this may have adverse effects if used improperly. If a session from a browser is wanted, you should pass cookies instead (which contain the Visitor ID)
|
||||||
|
* `po_token`: Proof of Origin (PO) Token(s) to use for requesting video playback. Comma seperated list of PO Tokens in the format `CLIENT+PO_TOKEN`, e.g. `youtube:po_token=web+XXX,android+YYY`
|
||||||
|
|
||||||
#### youtubetab (YouTube playlists, channels, feeds, etc.)
|
#### youtubetab (YouTube playlists, channels, feeds, etc.)
|
||||||
* `skip`: One or more of `webpage` (skip initial webpage download), `authcheck` (allow the download of playlists requiring authentication when no initial webpage is downloaded. This may cause unwanted behavior, see [#1122](https://github.com/yt-dlp/yt-dlp/pull/1122) for more details)
|
* `skip`: One or more of `webpage` (skip initial webpage download), `authcheck` (allow the download of playlists requiring authentication when no initial webpage is downloaded. This may cause unwanted behavior, see [#1122](https://github.com/yt-dlp/yt-dlp/pull/1122) for more details)
|
||||||
* `approximate_date`: Extract approximate `upload_date` and `timestamp` in flat-playlist. This may cause date-based filters to be slightly off
|
* `approximate_date`: Extract approximate `upload_date` and `timestamp` in flat-playlist. This may cause date-based filters to be slightly off
|
||||||
|
|
||||||
#### generic
|
#### generic
|
||||||
* `fragment_query`: Passthrough any query in mpd/m3u8 manifest URLs to their fragments if no value is provided, or else apply the query string given as `fragment_query=VALUE`. Does not apply to ffmpeg
|
* `fragment_query`: Passthrough any query in mpd/m3u8 manifest URLs to their fragments if no value is provided, or else apply the query string given as `fragment_query=VALUE`. Note that if the stream has an HLS AES-128 key, then the query parameters will be passed to the key URI as well, unless the `key_query` extractor-arg is passed, or unless an external key URI is provided via the `hls_key` extractor-arg. Does not apply to ffmpeg
|
||||||
* `variant_query`: Passthrough the master m3u8 URL query to its variant playlist URLs if no value is provided, or else apply the query string given as `variant_query=VALUE`
|
* `variant_query`: Passthrough the master m3u8 URL query to its variant playlist URLs if no value is provided, or else apply the query string given as `variant_query=VALUE`
|
||||||
|
* `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`
|
||||||
|
@ -1848,7 +1858,16 @@ The following extractors use this feature:
|
||||||
* `cdn`: One or more CDN IDs to use with the API call for stream URLs, e.g. `gcp_cdn`, `gs_cdn_pc_app`, `gs_cdn_mobile_web`, `gs_cdn_pc_web`
|
* `cdn`: One or more CDN IDs to use with the API call for stream URLs, e.g. `gcp_cdn`, `gs_cdn_pc_app`, `gs_cdn_mobile_web`, `gs_cdn_pc_web`
|
||||||
|
|
||||||
#### soundcloud
|
#### soundcloud
|
||||||
* `formats`: Formats to request from the API. Requested values should be in the format of `{protocol}_{extension}` (omitting the bitrate), e.g. `hls_opus,http_aac`. The `*` character functions as a wildcard, e.g. `*_mp3`, and can passed by itself to request all formats. Known protocols include `http`, `hls` and `hls-aes`; known extensions include `aac`, `opus` and `mp3`. Original `download` formats are always extracted. Default is `http_aac,hls_aac,http_opus,hls_opus,http_mp3,hls_mp3`
|
* `formats`: Formats to request from the API. Requested values should be in the format of `{protocol}_{extension}` (omitting the bitrate), e.g. `hls_opus,http_aac`. The `*` character functions as a wildcard, e.g. `*_mp3`, and can be passed by itself to request all formats. Known protocols include `http`, `hls` and `hls-aes`; known extensions include `aac`, `opus` and `mp3`. Original `download` formats are always extracted. Default is `http_aac,hls_aac,http_opus,hls_opus,http_mp3,hls_mp3`
|
||||||
|
|
||||||
|
#### orfon (orf:on)
|
||||||
|
* `prefer_segments_playlist`: Prefer a playlist of program segments instead of a single complete video when available. If individual segments are desired, use `--concat-playlist never --extractor-args "orfon:prefer_segments_playlist"`
|
||||||
|
|
||||||
|
#### bilibili
|
||||||
|
* `prefer_multi_flv`: Prefer extracting flv formats over mp4 for older videos that still provide legacy formats
|
||||||
|
|
||||||
|
#### digitalconcerthall
|
||||||
|
* `prefer_combined_hls`: Prefer extracting combined/pre-merged video and audio HLS formats. This will exclude 4K/HEVC video and lossless/FLAC audio formats, which are only available as split video/audio HLS formats
|
||||||
|
|
||||||
**Note**: These options may be changed/removed in the future without concern for backward compatibility
|
**Note**: These options may be changed/removed in the future without concern for backward compatibility
|
||||||
|
|
||||||
|
@ -1861,7 +1880,7 @@ Note that **all** plugins are imported even if not invoked, and that **there are
|
||||||
|
|
||||||
Plugins can be of `<type>`s `extractor` or `postprocessor`.
|
Plugins can be of `<type>`s `extractor` or `postprocessor`.
|
||||||
- Extractor plugins do not need to be enabled from the CLI and are automatically invoked when the input URL is suitable for it.
|
- Extractor plugins do not need to be enabled from the CLI and are automatically invoked when the input URL is suitable for it.
|
||||||
- Extractor plugins take priority over builtin extractors.
|
- Extractor plugins take priority over built-in extractors.
|
||||||
- Postprocessor plugins can be invoked using `--use-postprocessor NAME`.
|
- Postprocessor plugins can be invoked using `--use-postprocessor NAME`.
|
||||||
|
|
||||||
|
|
||||||
|
@ -1876,6 +1895,7 @@ In other words, the file structure on the disk looks something like:
|
||||||
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)
|
||||||
|
|
||||||
|
@ -1903,7 +1923,7 @@ Plugins can be installed using various methods and locations.
|
||||||
* 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.
|
||||||
|
@ -1916,7 +1936,7 @@ Run yt-dlp with `--verbose` to check if the plugin has been loaded.
|
||||||
|
|
||||||
See the [yt-dlp-sample-plugins](https://github.com/yt-dlp/yt-dlp-sample-plugins) repo for a template plugin package and the [Plugin Development](https://github.com/yt-dlp/yt-dlp/wiki/Plugin-Development) section of the wiki for a plugin development guide.
|
See the [yt-dlp-sample-plugins](https://github.com/yt-dlp/yt-dlp-sample-plugins) repo for a template plugin package and the [Plugin Development](https://github.com/yt-dlp/yt-dlp/wiki/Plugin-Development) section of the wiki for a plugin development guide.
|
||||||
|
|
||||||
All public classes with a name ending in `IE`/`PP` are imported from each file for extractors and postprocessors repectively. This respects underscore prefix (e.g. `_MyBasePluginIE` is private) and `__all__`. Modules can similarly be excluded by prefixing the module name with an underscore (e.g. `_myplugin.py`).
|
All public classes with a name ending in `IE`/`PP` are imported from each file for extractors and postprocessors respectively. This respects underscore prefix (e.g. `_MyBasePluginIE` is private) and `__all__`. Modules can similarly be excluded by prefixing the module name with an underscore (e.g. `_myplugin.py`).
|
||||||
|
|
||||||
To replace an existing extractor with a subclass of one, set the `plugin_name` class keyword argument (e.g. `class MyPluginIE(ABuiltInIE, plugin_name='myplugin')` will replace `ABuiltInIE` with `MyPluginIE`). Since the extractor replaces the parent, you should exclude the subclass extractor from being imported separately by making it private using one of the methods described above.
|
To replace an existing extractor with a subclass of one, set the `plugin_name` class keyword argument (e.g. `class MyPluginIE(ABuiltInIE, plugin_name='myplugin')` will replace `ABuiltInIE` with `MyPluginIE`). Since the extractor replaces the parent, you should exclude the subclass extractor from being imported separately by making it private using one of the methods described above.
|
||||||
|
|
||||||
|
@ -1928,7 +1948,7 @@ See the [Developer Instructions](https://github.com/yt-dlp/yt-dlp/blob/master/CO
|
||||||
|
|
||||||
yt-dlp makes the best effort to be a good command-line program, and thus should be callable from any programming language.
|
yt-dlp makes the best effort to be a good command-line program, and thus should be callable from any programming language.
|
||||||
|
|
||||||
Your program should avoid parsing the normal stdout since they may change in future versions. Instead they should use options such as `-J`, `--print`, `--progress-template`, `--exec` etc to create console output that you can reliably reproduce and parse.
|
Your program should avoid parsing the normal stdout since they may change in future versions. Instead, they should use options such as `-J`, `--print`, `--progress-template`, `--exec` etc to create console output that you can reliably reproduce and parse.
|
||||||
|
|
||||||
From a Python program, you can embed yt-dlp in a more powerful fashion, like this:
|
From a Python program, you can embed yt-dlp in a more powerful fashion, like this:
|
||||||
|
|
||||||
|
@ -2137,9 +2157,9 @@ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||||
* **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]`
|
||||||
|
|
||||||
|
@ -2163,9 +2183,9 @@ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||||
|
|
||||||
* **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
|
||||||
|
|
||||||
|
@ -2181,12 +2201,12 @@ Features marked with a **\*** have been back-ported to youtube-dl
|
||||||
|
|
||||||
Some of yt-dlp's default options are different from that of youtube-dl and youtube-dlc:
|
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
|
||||||
* The default [output template](#output-template) is `%(title)s [%(id)s].%(ext)s`. There is no real reason for this change. This was changed before yt-dlp was ever made public and now there are no plans to change it back to `%(title)s-%(id)s.%(ext)s`. Instead, you may use `--compat-options filename`
|
* The default [output template](#output-template) is `%(title)s [%(id)s].%(ext)s`. There is no real reason for this change. This was changed before yt-dlp was ever made public and now there are no plans to change it back to `%(title)s-%(id)s.%(ext)s`. Instead, you may use `--compat-options filename`
|
||||||
* The default [format sorting](#sorting-formats) is different from youtube-dl and prefers higher resolution and better codecs rather than higher bitrates. You can use the `--format-sort` option to change this to any order you prefer, or use `--compat-options format-sort` to use youtube-dl's sorting order
|
* The default [format sorting](#sorting-formats) is different from youtube-dl and prefers higher resolution and better codecs rather than higher bitrates. You can use the `--format-sort` option to change this to any order you prefer, or use `--compat-options format-sort` to use youtube-dl's sorting order. Older versions of yt-dlp preferred VP9 due to its broader compatibility; you can use `--compat-options prefer-vp9-sort` to revert to that format sorting preference. These two compat options cannot be used together
|
||||||
* The default format selector is `bv*+ba/b`. This means that if a combined video + audio format that is better than the best video-only format is found, the former will be preferred. Use `-f bv+ba/b` or `--compat-options format-spec` to revert this
|
* The default format selector is `bv*+ba/b`. This means that if a combined video + audio format that is better than the best video-only format is found, the former will be preferred. Use `-f bv+ba/b` or `--compat-options format-spec` to revert this
|
||||||
* Unlike youtube-dlc, yt-dlp does not allow merging multiple audio/video streams into one file by default (since this conflicts with the use of `-f bv*+ba`). If needed, this feature must be enabled using `--audio-multistreams` and `--video-multistreams`. You can also use `--compat-options multistreams` to enable both
|
* Unlike youtube-dlc, yt-dlp does not allow merging multiple audio/video streams into one file by default (since this conflicts with the use of `-f bv*+ba`). If needed, this feature must be enabled using `--audio-multistreams` and `--video-multistreams`. You can also use `--compat-options multistreams` to enable both
|
||||||
* `--no-abort-on-error` is enabled by default. Use `--abort-on-error` or `--compat-options abort-on-error` to abort on errors instead
|
* `--no-abort-on-error` is enabled by default. Use `--abort-on-error` or `--compat-options abort-on-error` to abort on errors instead
|
||||||
|
@ -2206,19 +2226,28 @@ Some of yt-dlp's default options are different from that of youtube-dl and youtu
|
||||||
* `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.
|
||||||
|
* Passing `--simulate` (or calling `extract_info` with `download=False`) no longer alters the default format selection. See [#9843](https://github.com/yt-dlp/yt-dlp/issues/9843) for details.
|
||||||
|
|
||||||
For ease of use, a few more compat options are available:
|
For ease of use, a few more compat options are available:
|
||||||
|
|
||||||
* `--compat-options all`: Use all compat options (Do NOT use)
|
* `--compat-options all`: Use all compat options (**Do NOT use this!**)
|
||||||
* `--compat-options youtube-dl`: Same as `--compat-options all,-multistreams,-playlist-match-filter,-manifest-filesize-approx`
|
* `--compat-options youtube-dl`: Same as `--compat-options all,-multistreams,-playlist-match-filter,-manifest-filesize-approx,-allow-unsafe-ext,-prefer-vp9-sort`
|
||||||
* `--compat-options youtube-dlc`: Same as `--compat-options all,-no-live-chat,-no-youtube-channel-redirect,-playlist-match-filter,-manifest-filesize-approx`
|
* `--compat-options youtube-dlc`: Same as `--compat-options all,-no-live-chat,-no-youtube-channel-redirect,-playlist-match-filter,-manifest-filesize-approx,-allow-unsafe-ext,-prefer-vp9-sort`
|
||||||
* `--compat-options 2021`: Same as `--compat-options 2022,no-certifi,filename-sanitization,no-youtube-prefer-utc-upload-date`
|
* `--compat-options 2021`: Same as `--compat-options 2022,no-certifi,filename-sanitization,no-youtube-prefer-utc-upload-date`
|
||||||
* `--compat-options 2022`: Same as `--compat-options 2023,playlist-match-filter,no-external-downloader-progress,prefer-legacy-http-handler,manifest-filesize-approx`
|
* `--compat-options 2022`: Same as `--compat-options 2023,playlist-match-filter,no-external-downloader-progress,prefer-legacy-http-handler,manifest-filesize-approx`
|
||||||
* `--compat-options 2023`: Currently does nothing. Use this to enable all future compat options
|
* `--compat-options 2023`: Same as `--compat-options prefer-vp9-sort`. Use this to enable all future compat options
|
||||||
|
|
||||||
|
The following compat options restore vulnerable behavior from before security patches:
|
||||||
|
|
||||||
|
* `--compat-options allow-unsafe-ext`: Allow files with any extension (including unsafe ones) to be downloaded ([GHSA-79w7-vh3h-8g4j](<https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-79w7-vh3h-8g4j>))
|
||||||
|
|
||||||
|
> :warning: Only use if a valid file download is rejected because its extension is detected as uncommon
|
||||||
|
>
|
||||||
|
> **This option can enable remote code execution! Consider [opening an issue](<https://github.com/yt-dlp/yt-dlp/issues/new/choose>) instead!**
|
||||||
|
|
||||||
### Deprecated options
|
### Deprecated options
|
||||||
|
|
||||||
|
@ -2243,13 +2272,13 @@ While these options are redundant, they are still expected to be used due to the
|
||||||
--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
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
source ~/.local/share/pipx/venvs/pyinstaller/bin/activate
|
source ~/.local/share/pipx/venvs/pyinstaller/bin/activate
|
||||||
python -m devscripts.install_deps --include secretstorage
|
python -m devscripts.install_deps --include secretstorage --include curl-cffi
|
||||||
python -m devscripts.make_lazy_extractors
|
python -m devscripts.make_lazy_extractors
|
||||||
python devscripts/update-version.py -c "${channel}" -r "${origin}" "${version}"
|
python devscripts/update-version.py -c "${channel}" -r "${origin}" "${version}"
|
||||||
python -m bundle.pyinstaller
|
python -m bundle.pyinstaller
|
||||||
|
|
|
@ -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()
|
|
|
@ -169,5 +169,70 @@
|
||||||
"when": "5c019f6328ad40d66561eac3c4de0b3cd070d0f6",
|
"when": "5c019f6328ad40d66561eac3c4de0b3cd070d0f6",
|
||||||
"short": "[cleanup] Misc (#9765)",
|
"short": "[cleanup] Misc (#9765)",
|
||||||
"authors": ["bashonly", "Grub4K", "seproDev"]
|
"authors": ["bashonly", "Grub4K", "seproDev"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "change",
|
||||||
|
"when": "e6a22834df1776ec4e486526f6df2bf53cb7e06f",
|
||||||
|
"short": "[ie/orf:on] Add `prefer_segments_playlist` extractor-arg (#10314)",
|
||||||
|
"authors": ["seproDev"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "add",
|
||||||
|
"when": "6aaf96a3d6e7d0d426e97e11a2fcf52fda00e733",
|
||||||
|
"short": "[priority] Security: [[CVE-2024-38519](https://nvd.nist.gov/vuln/detail/CVE-2024-38519)] [Properly sanitize file-extension to prevent file system modification and RCE](https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-79w7-vh3h-8g4j)\n - Unsafe extensions are now blocked from being downloaded"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "add",
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "change",
|
||||||
|
"when": "914af9a0cf51c9a3f74aa88d952bee8334c67511",
|
||||||
|
"short": "Expand paths in `--plugin-dirs` (#11334)",
|
||||||
|
"authors": ["bashonly"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "change",
|
||||||
|
"when": "c29f5a7fae93a08f3cfbb6127b2faa75145b06a0",
|
||||||
|
"short": "[ie/generic] Do not impersonate by default (#11336)",
|
||||||
|
"authors": ["bashonly"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "change",
|
||||||
|
"when": "57212a5f97ce367590aaa5c3e9a135eead8f81f7",
|
||||||
|
"short": "[ie/vimeo] Fix API retries (#11351)",
|
||||||
|
"authors": ["bashonly"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
2
devscripts/cli_to_api.py
Normal file → Executable file
2
devscripts/cli_to_api.py
Normal file → Executable file
|
@ -1,3 +1,5 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
# Allow direct execution
|
# Allow direct execution
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
|
@ -71,14 +71,13 @@ class CommitGroup(enum.Enum):
|
||||||
def get(cls, value: str) -> tuple[CommitGroup | None, str | None]:
|
def get(cls, value: str) -> tuple[CommitGroup | None, str | None]:
|
||||||
group, _, subgroup = (group.strip().lower() for group in value.partition('/'))
|
group, _, subgroup = (group.strip().lower() for group in value.partition('/'))
|
||||||
|
|
||||||
result = cls.group_lookup().get(group)
|
if result := cls.group_lookup().get(group):
|
||||||
if not result:
|
return result, subgroup or None
|
||||||
if subgroup:
|
|
||||||
return None, value
|
|
||||||
subgroup = group
|
|
||||||
result = cls.subgroup_lookup().get(subgroup)
|
|
||||||
|
|
||||||
return result, subgroup or None
|
if subgroup:
|
||||||
|
return None, value
|
||||||
|
|
||||||
|
return cls.subgroup_lookup().get(group), group or None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -136,8 +135,7 @@ class Changelog:
|
||||||
first = False
|
first = False
|
||||||
yield '\n<details><summary><h3>Changelog</h3></summary>\n'
|
yield '\n<details><summary><h3>Changelog</h3></summary>\n'
|
||||||
|
|
||||||
group = groups[item]
|
if group := groups[item]:
|
||||||
if group:
|
|
||||||
yield self.format_module(item.value, group)
|
yield self.format_module(item.value, group)
|
||||||
|
|
||||||
if self._collapsible:
|
if self._collapsible:
|
||||||
|
@ -253,7 +251,7 @@ class CommitRange:
|
||||||
''', re.VERBOSE | re.DOTALL)
|
''', re.VERBOSE | re.DOTALL)
|
||||||
EXTRACTOR_INDICATOR_RE = re.compile(r'(?:Fix|Add)\s+Extractors?', re.IGNORECASE)
|
EXTRACTOR_INDICATOR_RE = re.compile(r'(?:Fix|Add)\s+Extractors?', re.IGNORECASE)
|
||||||
REVERT_RE = re.compile(r'(?:\[[^\]]+\]\s+)?(?i:Revert)\s+([\da-f]{40})')
|
REVERT_RE = re.compile(r'(?:\[[^\]]+\]\s+)?(?i:Revert)\s+([\da-f]{40})')
|
||||||
FIXES_RE = re.compile(r'(?i:Fix(?:es)?(?:\s+bugs?)?(?:\s+in|\s+for)?|Revert|Improve)\s+([\da-f]{40})')
|
FIXES_RE = re.compile(r'(?i:(?:bug\s*)?fix(?:es)?(?:\s+bugs?)?(?:\s+in|\s+for)?|Improve)\s+([\da-f]{40})')
|
||||||
UPSTREAM_MERGE_RE = re.compile(r'Update to ytdl-commit-([\da-f]+)')
|
UPSTREAM_MERGE_RE = re.compile(r'Update to ytdl-commit-([\da-f]+)')
|
||||||
|
|
||||||
def __init__(self, start, end, default_author=None):
|
def __init__(self, start, end, default_author=None):
|
||||||
|
@ -287,11 +285,16 @@ class CommitRange:
|
||||||
short = next(lines)
|
short = next(lines)
|
||||||
skip = short.startswith('Release ') or short == '[version] update'
|
skip = short.startswith('Release ') or short == '[version] update'
|
||||||
|
|
||||||
|
fix_commitish = None
|
||||||
|
if match := self.FIXES_RE.search(short):
|
||||||
|
fix_commitish = match.group(1)
|
||||||
|
|
||||||
authors = [default_author] if default_author else []
|
authors = [default_author] if default_author else []
|
||||||
for line in iter(lambda: next(lines), self.COMMIT_SEPARATOR):
|
for line in iter(lambda: next(lines), self.COMMIT_SEPARATOR):
|
||||||
match = self.AUTHOR_INDICATOR_RE.match(line)
|
if match := self.AUTHOR_INDICATOR_RE.match(line):
|
||||||
if match:
|
|
||||||
authors = sorted(map(str.strip, line[match.end():].split(',')), key=str.casefold)
|
authors = sorted(map(str.strip, line[match.end():].split(',')), key=str.casefold)
|
||||||
|
if not fix_commitish and (match := self.FIXES_RE.fullmatch(line)):
|
||||||
|
fix_commitish = match.group(1)
|
||||||
|
|
||||||
commit = Commit(commit_hash, short, authors)
|
commit = Commit(commit_hash, short, authors)
|
||||||
if skip and (self._start or not i):
|
if skip and (self._start or not i):
|
||||||
|
@ -301,21 +304,17 @@ class CommitRange:
|
||||||
logger.debug(f'Reached Release commit, breaking: {commit}')
|
logger.debug(f'Reached Release commit, breaking: {commit}')
|
||||||
break
|
break
|
||||||
|
|
||||||
revert_match = self.REVERT_RE.fullmatch(commit.short)
|
if match := self.REVERT_RE.fullmatch(commit.short):
|
||||||
if revert_match:
|
reverts[match.group(1)] = commit
|
||||||
reverts[revert_match.group(1)] = commit
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
fix_match = self.FIXES_RE.search(commit.short)
|
if fix_commitish:
|
||||||
if fix_match:
|
fixes[fix_commitish].append(commit)
|
||||||
commitish = fix_match.group(1)
|
|
||||||
fixes[commitish].append(commit)
|
|
||||||
|
|
||||||
commits[commit.hash] = commit
|
commits[commit.hash] = commit
|
||||||
|
|
||||||
for commitish, revert_commit in reverts.items():
|
for commitish, revert_commit in reverts.items():
|
||||||
reverted = commits.pop(commitish, None)
|
if reverted := commits.pop(commitish, None):
|
||||||
if reverted:
|
|
||||||
logger.debug(f'{commitish} fully reverted {reverted}')
|
logger.debug(f'{commitish} fully reverted {reverted}')
|
||||||
else:
|
else:
|
||||||
commits[revert_commit.hash] = revert_commit
|
commits[revert_commit.hash] = revert_commit
|
||||||
|
@ -461,8 +460,7 @@ def create_changelog(args):
|
||||||
|
|
||||||
logger.info(f'Loaded {len(commits)} commits')
|
logger.info(f'Loaded {len(commits)} commits')
|
||||||
|
|
||||||
new_contributors = get_new_contributors(args.contributors_path, commits)
|
if new_contributors := get_new_contributors(args.contributors_path, commits):
|
||||||
if new_contributors:
|
|
||||||
if args.contributors:
|
if args.contributors:
|
||||||
write_file(args.contributors_path, '\n'.join(new_contributors) + '\n', mode='a')
|
write_file(args.contributors_path, '\n'.join(new_contributors) + '\n', mode='a')
|
||||||
logger.info(f'New contributors: {", ".join(new_contributors)}')
|
logger.info(f'New contributors: {", ".join(new_contributors)}')
|
||||||
|
|
|
@ -32,20 +32,29 @@ VERBOSE_TMPL = '''
|
||||||
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>
|
||||||
render: shell
|
render: shell
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
> [!CAUTION]
|
||||||
|
> ### GitHub is experiencing a high volume of malicious spam comments.
|
||||||
|
> ### If you receive any replies asking you download a file, do NOT follow the download links!
|
||||||
|
>
|
||||||
|
> Note that this issue may be temporarily locked as an anti-spam measure after it is opened.
|
||||||
'''.strip()
|
'''.strip()
|
||||||
|
|
||||||
NO_SKIP = '''
|
NO_SKIP = '''
|
||||||
|
|
|
@ -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 @@ MODULE_TEMPLATE = read_file('devscripts/lazy_load_template.py')
|
||||||
|
|
||||||
|
|
||||||
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 @@ fix_test_name = functools.partial(re.compile(r'IE(_all|_\d+)?$').sub, r'\1')
|
||||||
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:
|
||||||
|
|
|
@ -9,10 +9,11 @@ maintainers = [
|
||||||
{name = "Grub4K", email = "contact@grub4k.xyz"},
|
{name = "Grub4K", email = "contact@grub4k.xyz"},
|
||||||
{name = "bashonly", email = "bashonly@protonmail.com"},
|
{name = "bashonly", email = "bashonly@protonmail.com"},
|
||||||
{name = "coletdjnz", email = "coletdjnz@protonmail.com"},
|
{name = "coletdjnz", email = "coletdjnz@protonmail.com"},
|
||||||
|
{name = "sepro", email = "sepro@sepr0.com"},
|
||||||
]
|
]
|
||||||
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",
|
||||||
|
@ -28,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",
|
||||||
|
@ -40,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",
|
||||||
|
@ -48,12 +52,12 @@ dependencies = [
|
||||||
"pycryptodomex",
|
"pycryptodomex",
|
||||||
"requests>=2.32.2,<3",
|
"requests>=2.32.2,<3",
|
||||||
"urllib3>=1.26.17,<3",
|
"urllib3>=1.26.17,<3",
|
||||||
"websockets>=12.0",
|
"websockets>=13.0",
|
||||||
|
]
|
||||||
|
curl-cffi = [
|
||||||
|
"curl-cffi==0.5.10; os_name=='nt' and implementation_name=='cpython'",
|
||||||
|
"curl-cffi>=0.5.10,!=0.6.*,<0.7.2; os_name!='nt' and implementation_name=='cpython'",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
|
||||||
default = []
|
|
||||||
curl-cffi = ["curl-cffi==0.5.10; implementation_name=='cpython'"]
|
|
||||||
secretstorage = [
|
secretstorage = [
|
||||||
"cffi",
|
"cffi",
|
||||||
"secretstorage",
|
"secretstorage",
|
||||||
|
@ -62,7 +66,7 @@ build = [
|
||||||
"build",
|
"build",
|
||||||
"hatchling",
|
"hatchling",
|
||||||
"pip",
|
"pip",
|
||||||
"setuptools",
|
"setuptools>=71.0.2", # 71.0.0 broke pyinstaller
|
||||||
"wheel",
|
"wheel",
|
||||||
]
|
]
|
||||||
dev = [
|
dev = [
|
||||||
|
@ -72,16 +76,14 @@ dev = [
|
||||||
]
|
]
|
||||||
static-analysis = [
|
static-analysis = [
|
||||||
"autopep8~=2.0",
|
"autopep8~=2.0",
|
||||||
"ruff~=0.4.4",
|
"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]
|
||||||
|
@ -158,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",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -168,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",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -211,6 +210,7 @@ ignore = [
|
||||||
"TD002", # missing-todo-author
|
"TD002", # missing-todo-author
|
||||||
"TD003", # missing-todo-link
|
"TD003", # missing-todo-link
|
||||||
"PLE0604", # invalid-all-object (false positives)
|
"PLE0604", # invalid-all-object (false positives)
|
||||||
|
"PLE0643", # potential-index-error (false positives)
|
||||||
"PLW0603", # global-statement
|
"PLW0603", # global-statement
|
||||||
"PLW1510", # subprocess-run-without-check
|
"PLW1510", # subprocess-run-without-check
|
||||||
"PLW2901", # redefined-loop-name
|
"PLW2901", # redefined-loop-name
|
||||||
|
@ -298,7 +298,7 @@ banned-from = [
|
||||||
"string",
|
"string",
|
||||||
"sys",
|
"sys",
|
||||||
"time",
|
"time",
|
||||||
"urllib",
|
"urllib.parse",
|
||||||
"uuid",
|
"uuid",
|
||||||
"xml",
|
"xml",
|
||||||
]
|
]
|
||||||
|
|
|
@ -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,9 +45,6 @@
|
||||||
- **aenetworks:collection**
|
- **aenetworks:collection**
|
||||||
- **aenetworks:show**
|
- **aenetworks:show**
|
||||||
- **AeonCo**
|
- **AeonCo**
|
||||||
- **afreecatv**: [*afreecatv*](## "netrc machine") afreecatv.com
|
|
||||||
- **afreecatv:live**: [*afreecatv*](## "netrc machine") afreecatv.com livestreams
|
|
||||||
- **afreecatv:user**
|
|
||||||
- **AirTV**
|
- **AirTV**
|
||||||
- **AitubeKZVideo**
|
- **AitubeKZVideo**
|
||||||
- **AliExpressLive**
|
- **AliExpressLive**
|
||||||
|
@ -142,6 +139,7 @@
|
||||||
- **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**
|
||||||
|
@ -192,6 +190,7 @@
|
||||||
- **blerp**
|
- **blerp**
|
||||||
- **blogger.com**
|
- **blogger.com**
|
||||||
- **Bloomberg**
|
- **Bloomberg**
|
||||||
|
- **Bluesky**
|
||||||
- **BokeCC**
|
- **BokeCC**
|
||||||
- **BongaCams**
|
- **BongaCams**
|
||||||
- **Boosty**
|
- **Boosty**
|
||||||
|
@ -249,9 +248,10 @@
|
||||||
- **cbsnews:livevideo**: CBS News Live Videos
|
- **cbsnews:livevideo**: CBS News Live Videos
|
||||||
- **cbssports**: (**Currently broken**)
|
- **cbssports**: (**Currently broken**)
|
||||||
- **cbssports:embed**: (**Currently broken**)
|
- **cbssports:embed**: (**Currently broken**)
|
||||||
- **CCMA**
|
- **CCMA**: 3Cat, TV3 and Catalunya Ràdio
|
||||||
- **CCTV**: 央视网
|
- **CCTV**: 央视网
|
||||||
- **CDA**: [*cdapl*](## "netrc machine")
|
- **CDA**: [*cdapl*](## "netrc machine")
|
||||||
|
- **CDAFolder**
|
||||||
- **Cellebrite**
|
- **Cellebrite**
|
||||||
- **CeskaTelevize**
|
- **CeskaTelevize**
|
||||||
- **CGTN**
|
- **CGTN**
|
||||||
|
@ -281,8 +281,6 @@
|
||||||
- **cmt.com**: (**Currently broken**)
|
- **cmt.com**: (**Currently broken**)
|
||||||
- **CNBCVideo**
|
- **CNBCVideo**
|
||||||
- **CNN**
|
- **CNN**
|
||||||
- **CNNArticle**
|
|
||||||
- **CNNBlogs**
|
|
||||||
- **CNNIndonesia**
|
- **CNNIndonesia**
|
||||||
- **ComedyCentral**
|
- **ComedyCentral**
|
||||||
- **ComedyCentralTV**
|
- **ComedyCentralTV**
|
||||||
|
@ -353,7 +351,6 @@
|
||||||
- **DigitallySpeaking**
|
- **DigitallySpeaking**
|
||||||
- **Digiteka**
|
- **Digiteka**
|
||||||
- **DiscogsReleasePlaylist**
|
- **DiscogsReleasePlaylist**
|
||||||
- **Discovery**
|
|
||||||
- **DiscoveryLife**
|
- **DiscoveryLife**
|
||||||
- **DiscoveryNetworksDe**
|
- **DiscoveryNetworksDe**
|
||||||
- **DiscoveryPlus**
|
- **DiscoveryPlus**
|
||||||
|
@ -362,7 +359,6 @@
|
||||||
- **DiscoveryPlusItaly**
|
- **DiscoveryPlusItaly**
|
||||||
- **DiscoveryPlusItalyShow**
|
- **DiscoveryPlusItalyShow**
|
||||||
- **Disney**
|
- **Disney**
|
||||||
- **DIYNetwork**
|
|
||||||
- **dlf**
|
- **dlf**
|
||||||
- **dlf:corpus**: DLF Multi-feed Archives
|
- **dlf:corpus**: DLF Multi-feed Archives
|
||||||
- **dlive:stream**
|
- **dlive:stream**
|
||||||
|
@ -506,6 +502,7 @@
|
||||||
- **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**
|
||||||
|
@ -515,7 +512,6 @@
|
||||||
- **GlattvisionTVLive**: [*glattvisiontv*](## "netrc machine")
|
- **GlattvisionTVLive**: [*glattvisiontv*](## "netrc machine")
|
||||||
- **GlattvisionTVRecordings**: [*glattvisiontv*](## "netrc machine")
|
- **GlattvisionTVRecordings**: [*glattvisiontv*](## "netrc machine")
|
||||||
- **Glide**: Glide mobile video messages (glide.me)
|
- **Glide**: Glide mobile video messages (glide.me)
|
||||||
- **GlobalCyclingNetworkPlus**
|
|
||||||
- **GlobalPlayerAudio**
|
- **GlobalPlayerAudio**
|
||||||
- **GlobalPlayerAudioEpisode**
|
- **GlobalPlayerAudioEpisode**
|
||||||
- **GlobalPlayerLive**
|
- **GlobalPlayerLive**
|
||||||
|
@ -542,6 +538,7 @@
|
||||||
- **Goshgay**
|
- **Goshgay**
|
||||||
- **GoToStage**
|
- **GoToStage**
|
||||||
- **GPUTechConf**
|
- **GPUTechConf**
|
||||||
|
- **Graspop**
|
||||||
- **Gronkh**
|
- **Gronkh**
|
||||||
- **gronkh:feed**
|
- **gronkh:feed**
|
||||||
- **gronkh:vods**
|
- **gronkh:vods**
|
||||||
|
@ -581,6 +578,7 @@
|
||||||
- **HungamaAlbumPlaylist**
|
- **HungamaAlbumPlaylist**
|
||||||
- **HungamaSong**
|
- **HungamaSong**
|
||||||
- **huya:live**: huya.com
|
- **huya:live**: huya.com
|
||||||
|
- **huya:video**: 虎牙视频
|
||||||
- **Hypem**
|
- **Hypem**
|
||||||
- **Hytale**
|
- **Hytale**
|
||||||
- **Icareus**
|
- **Icareus**
|
||||||
|
@ -656,10 +654,12 @@
|
||||||
- **Ketnet**
|
- **Ketnet**
|
||||||
- **khanacademy**
|
- **khanacademy**
|
||||||
- **khanacademy:unit**
|
- **khanacademy:unit**
|
||||||
- **Kick**
|
- **kick:clips**
|
||||||
|
- **kick:live**
|
||||||
|
- **kick:vod**
|
||||||
- **Kicker**
|
- **Kicker**
|
||||||
- **KickStarter**
|
- **KickStarter**
|
||||||
- **KickVOD**
|
- **Kika**: KiKA.de
|
||||||
- **kinja:embed**
|
- **kinja:embed**
|
||||||
- **KinoPoisk**
|
- **KinoPoisk**
|
||||||
- **Kommunetv**
|
- **Kommunetv**
|
||||||
|
@ -678,17 +678,20 @@
|
||||||
- **la7.it**
|
- **la7.it**
|
||||||
- **la7.it:pod:episode**
|
- **la7.it:pod:episode**
|
||||||
- **la7.it:podcast**
|
- **la7.it:podcast**
|
||||||
|
- **laracasts**
|
||||||
|
- **laracasts:series**
|
||||||
- **LastFM**
|
- **LastFM**
|
||||||
- **LastFMPlaylist**
|
- **LastFMPlaylist**
|
||||||
- **LastFMUser**
|
- **LastFMUser**
|
||||||
- **LaXarxaMes**: [*laxarxames*](## "netrc machine")
|
- **LaXarxaMes**: [*laxarxames*](## "netrc machine")
|
||||||
- **lbry**
|
- **lbry**: odysee.com
|
||||||
- **lbry:channel**
|
- **lbry:channel**: odysee.com channels
|
||||||
- **lbry:playlist**
|
- **lbry:playlist**: odysee.com playlists
|
||||||
- **LCI**
|
- **LCI**
|
||||||
- **Lcp**
|
- **Lcp**
|
||||||
- **LcpPlay**
|
- **LcpPlay**
|
||||||
- **Le**: 乐视网
|
- **Le**: 乐视网
|
||||||
|
- **LearningOnScreen**
|
||||||
- **Lecture2Go**: (**Currently broken**)
|
- **Lecture2Go**: (**Currently broken**)
|
||||||
- **Lecturio**: [*lecturio*](## "netrc machine")
|
- **Lecturio**: [*lecturio*](## "netrc machine")
|
||||||
- **LecturioCourse**: [*lecturio*](## "netrc machine")
|
- **LecturioCourse**: [*lecturio*](## "netrc machine")
|
||||||
|
@ -719,7 +722,6 @@
|
||||||
- **livestream:original**
|
- **livestream:original**
|
||||||
- **Livestreamfails**
|
- **Livestreamfails**
|
||||||
- **Lnk**
|
- **Lnk**
|
||||||
- **LnkGo**
|
|
||||||
- **loc**: Library of Congress
|
- **loc**: Library of Congress
|
||||||
- **loom**
|
- **loom**
|
||||||
- **loom:folder**
|
- **loom:folder**
|
||||||
|
@ -753,7 +755,7 @@
|
||||||
- **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**
|
||||||
|
@ -775,7 +777,12 @@
|
||||||
- **MelonVOD**
|
- **MelonVOD**
|
||||||
- **Metacritic**
|
- **Metacritic**
|
||||||
- **mewatch**
|
- **mewatch**
|
||||||
|
- **MicrosoftBuild**
|
||||||
- **MicrosoftEmbed**
|
- **MicrosoftEmbed**
|
||||||
|
- **MicrosoftLearnEpisode**
|
||||||
|
- **MicrosoftLearnPlaylist**
|
||||||
|
- **MicrosoftLearnSession**
|
||||||
|
- **MicrosoftMedius**
|
||||||
- **microsoftstream**: Microsoft Stream
|
- **microsoftstream**: Microsoft Stream
|
||||||
- **mildom**: Record ongoing live by specific user in Mildom
|
- **mildom**: Record ongoing live by specific user in Mildom
|
||||||
- **mildom:clip**: Clip in Mildom
|
- **mildom:clip**: Clip in Mildom
|
||||||
|
@ -803,6 +810,7 @@
|
||||||
- **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**
|
||||||
|
@ -811,8 +819,6 @@
|
||||||
- **MotherlessGroup**
|
- **MotherlessGroup**
|
||||||
- **MotherlessUploader**
|
- **MotherlessUploader**
|
||||||
- **Motorsport**: motorsport.com (**Currently broken**)
|
- **Motorsport**: motorsport.com (**Currently broken**)
|
||||||
- **MotorTrend**
|
|
||||||
- **MotorTrendOnDemand**
|
|
||||||
- **MovieFap**
|
- **MovieFap**
|
||||||
- **Moviepilot**
|
- **Moviepilot**
|
||||||
- **MoviewPlay**
|
- **MoviewPlay**
|
||||||
|
@ -830,7 +836,7 @@
|
||||||
- **MTVUutisetArticle**: (**Currently broken**)
|
- **MTVUutisetArticle**: (**Currently broken**)
|
||||||
- **MuenchenTV**: münchen.tv (**Currently broken**)
|
- **MuenchenTV**: münchen.tv (**Currently broken**)
|
||||||
- **MujRozhlas**
|
- **MujRozhlas**
|
||||||
- **Murrtube**: (**Currently broken**)
|
- **Murrtube**
|
||||||
- **MurrtubeUser**: Murrtube user profile (**Currently broken**)
|
- **MurrtubeUser**: Murrtube user profile (**Currently broken**)
|
||||||
- **MuseAI**
|
- **MuseAI**
|
||||||
- **MuseScore**
|
- **MuseScore**
|
||||||
|
@ -838,8 +844,6 @@
|
||||||
- **MusicdexArtist**
|
- **MusicdexArtist**
|
||||||
- **MusicdexPlaylist**
|
- **MusicdexPlaylist**
|
||||||
- **MusicdexSong**
|
- **MusicdexSong**
|
||||||
- **mva**: Microsoft Virtual Academy videos
|
|
||||||
- **mva:course**: Microsoft Virtual Academy courses
|
|
||||||
- **Mx3**
|
- **Mx3**
|
||||||
- **Mx3Neo**
|
- **Mx3Neo**
|
||||||
- **Mx3Volksmusik**
|
- **Mx3Volksmusik**
|
||||||
|
@ -1038,8 +1042,8 @@
|
||||||
- **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**
|
||||||
|
@ -1131,13 +1135,13 @@
|
||||||
- **QingTing**
|
- **QingTing**
|
||||||
- **qqmusic**: QQ音乐
|
- **qqmusic**: QQ音乐
|
||||||
- **qqmusic:album**: QQ音乐 - 专辑
|
- **qqmusic:album**: QQ音乐 - 专辑
|
||||||
|
- **qqmusic:mv**: QQ音乐 - MV
|
||||||
- **qqmusic:playlist**: QQ音乐 - 歌单
|
- **qqmusic:playlist**: QQ音乐 - 歌单
|
||||||
- **qqmusic:singer**: QQ音乐 - 歌手
|
- **qqmusic:singer**: QQ音乐 - 歌手
|
||||||
- **qqmusic:toplist**: QQ音乐 - 排行榜
|
- **qqmusic:toplist**: QQ音乐 - 排行榜
|
||||||
- **QuantumTV**: [*quantumtv*](## "netrc machine")
|
- **QuantumTV**: [*quantumtv*](## "netrc machine")
|
||||||
- **QuantumTVLive**: [*quantumtv*](## "netrc machine")
|
- **QuantumTVLive**: [*quantumtv*](## "netrc machine")
|
||||||
- **QuantumTVRecordings**: [*quantumtv*](## "netrc machine")
|
- **QuantumTVRecordings**: [*quantumtv*](## "netrc machine")
|
||||||
- **Qub**
|
|
||||||
- **R7**: (**Currently broken**)
|
- **R7**: (**Currently broken**)
|
||||||
- **R7Article**: (**Currently broken**)
|
- **R7Article**: (**Currently broken**)
|
||||||
- **Radiko**
|
- **Radiko**
|
||||||
|
@ -1237,6 +1241,7 @@
|
||||||
- **rtve.es:television**
|
- **rtve.es:television**
|
||||||
- **RTVS**
|
- **RTVS**
|
||||||
- **rtvslo.si**
|
- **rtvslo.si**
|
||||||
|
- **rtvslo.si:show**
|
||||||
- **RudoVideo**
|
- **RudoVideo**
|
||||||
- **Rule34Video**
|
- **Rule34Video**
|
||||||
- **Rumble**
|
- **Rumble**
|
||||||
|
@ -1280,12 +1285,14 @@
|
||||||
- **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**
|
||||||
|
@ -1322,11 +1329,16 @@
|
||||||
- **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")
|
||||||
|
@ -1360,6 +1372,7 @@
|
||||||
- **SpreakerShowPage**
|
- **SpreakerShowPage**
|
||||||
- **SpringboardPlatform**
|
- **SpringboardPlatform**
|
||||||
- **Sprout**
|
- **Sprout**
|
||||||
|
- **SproutVideo**
|
||||||
- **sr:mediathek**: Saarländischer Rundfunk (**Currently broken**)
|
- **sr:mediathek**: Saarländischer Rundfunk (**Currently broken**)
|
||||||
- **SRGSSR**
|
- **SRGSSR**
|
||||||
- **SRGSSRPlay**: srf.ch, rts.ch, rsi.ch, rtr.ch and swissinfo.ch play sites
|
- **SRGSSRPlay**: srf.ch, rts.ch, rsi.ch, rtr.ch and swissinfo.ch play sites
|
||||||
|
@ -1432,7 +1445,7 @@
|
||||||
- **TeleQuebecSquat**
|
- **TeleQuebecSquat**
|
||||||
- **TeleQuebecVideo**
|
- **TeleQuebecVideo**
|
||||||
- **TeleTask**: (**Currently broken**)
|
- **TeleTask**: (**Currently broken**)
|
||||||
- **Telewebion**
|
- **Telewebion**: (**Currently broken**)
|
||||||
- **Tempo**
|
- **Tempo**
|
||||||
- **TennisTV**: [*tennistv*](## "netrc machine")
|
- **TennisTV**: [*tennistv*](## "netrc machine")
|
||||||
- **TenPlay**: [*10play*](## "netrc machine")
|
- **TenPlay**: [*10play*](## "netrc machine")
|
||||||
|
@ -1494,8 +1507,8 @@
|
||||||
- **Tube8**: (**Currently broken**)
|
- **Tube8**: (**Currently broken**)
|
||||||
- **TubeTuGraz**: [*tubetugraz*](## "netrc machine") tube.tugraz.at
|
- **TubeTuGraz**: [*tubetugraz*](## "netrc machine") tube.tugraz.at
|
||||||
- **TubeTuGrazSeries**: [*tubetugraz*](## "netrc machine")
|
- **TubeTuGrazSeries**: [*tubetugraz*](## "netrc machine")
|
||||||
- **TubiTv**: [*tubitv*](## "netrc machine")
|
- **tubitv**: [*tubitv*](## "netrc machine")
|
||||||
- **TubiTvShow**
|
- **tubitv:series**
|
||||||
- **Tumblr**: [*tumblr*](## "netrc machine")
|
- **Tumblr**: [*tumblr*](## "netrc machine")
|
||||||
- **TuneInPodcast**
|
- **TuneInPodcast**
|
||||||
- **TuneInPodcastEpisode**
|
- **TuneInPodcastEpisode**
|
||||||
|
@ -1512,9 +1525,9 @@
|
||||||
- **tv5unis**
|
- **tv5unis**
|
||||||
- **tv5unis:video**
|
- **tv5unis:video**
|
||||||
- **tv8.it**
|
- **tv8.it**
|
||||||
- **TVA**
|
|
||||||
- **TVANouvelles**
|
- **TVANouvelles**
|
||||||
- **TVANouvellesArticle**
|
- **TVANouvellesArticle**
|
||||||
|
- **tvaplus**: TVA+
|
||||||
- **TVC**
|
- **TVC**
|
||||||
- **TVCArticle**
|
- **TVCArticle**
|
||||||
- **TVer**
|
- **TVer**
|
||||||
|
@ -1602,11 +1615,14 @@
|
||||||
- **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")
|
||||||
- **VidLii**
|
- **VidLii**
|
||||||
- **Vidly**
|
- **Vidly**
|
||||||
|
- **vids.io**
|
||||||
|
- **Vidyard**
|
||||||
- **viewlift**
|
- **viewlift**
|
||||||
- **viewlift:embed**
|
- **viewlift:embed**
|
||||||
- **Viidea**
|
- **Viidea**
|
||||||
|
@ -1654,6 +1670,8 @@
|
||||||
- **VRT**: VRT NWS, Flanders News, Flandern Info and Sporza
|
- **VRT**: VRT NWS, Flanders News, Flandern Info and Sporza
|
||||||
- **VrtNU**: [*vrtnu*](## "netrc machine") VRT MAX
|
- **VrtNU**: [*vrtnu*](## "netrc machine") VRT MAX
|
||||||
- **VTM**: (**Currently broken**)
|
- **VTM**: (**Currently broken**)
|
||||||
|
- **VTV**
|
||||||
|
- **VTVGo**
|
||||||
- **VTXTV**: [*vtxtv*](## "netrc machine")
|
- **VTXTV**: [*vtxtv*](## "netrc machine")
|
||||||
- **VTXTVLive**: [*vtxtv*](## "netrc machine")
|
- **VTXTVLive**: [*vtxtv*](## "netrc machine")
|
||||||
- **VTXTVRecordings**: [*vtxtv*](## "netrc machine")
|
- **VTXTVRecordings**: [*vtxtv*](## "netrc machine")
|
||||||
|
@ -1726,7 +1744,7 @@
|
||||||
- **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**
|
||||||
|
@ -1760,24 +1778,24 @@
|
||||||
- **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**
|
||||||
|
|
|
@ -53,6 +53,18 @@ class TestInfoExtractor(unittest.TestCase):
|
||||||
def test_ie_key(self):
|
def test_ie_key(self):
|
||||||
self.assertEqual(get_info_extractor(YoutubeIE.ie_key()), YoutubeIE)
|
self.assertEqual(get_info_extractor(YoutubeIE.ie_key()), YoutubeIE)
|
||||||
|
|
||||||
|
def test_get_netrc_login_info(self):
|
||||||
|
for params in [
|
||||||
|
{'usenetrc': True, 'netrc_location': './test/testdata/netrc/netrc'},
|
||||||
|
{'netrc_cmd': f'{sys.executable} ./test/testdata/netrc/print_netrc.py'},
|
||||||
|
]:
|
||||||
|
ie = DummyIE(FakeYDL(params))
|
||||||
|
self.assertEqual(ie._get_netrc_login_info(netrc_machine='normal_use'), ('user', 'pass'))
|
||||||
|
self.assertEqual(ie._get_netrc_login_info(netrc_machine='empty_user'), ('', 'pass'))
|
||||||
|
self.assertEqual(ie._get_netrc_login_info(netrc_machine='empty_pass'), ('user', ''))
|
||||||
|
self.assertEqual(ie._get_netrc_login_info(netrc_machine='both_empty'), ('', ''))
|
||||||
|
self.assertEqual(ie._get_netrc_login_info(netrc_machine='nonexistent'), (None, None))
|
||||||
|
|
||||||
def test_html_search_regex(self):
|
def test_html_search_regex(self):
|
||||||
html = '<p id="foo">Watch this <a href="http://www.youtube.com/watch?v=BaW_jenozKc">video</a></p>'
|
html = '<p id="foo">Watch this <a href="http://www.youtube.com/watch?v=BaW_jenozKc">video</a></p>'
|
||||||
search = lambda re, *args: self.ie._html_search_regex(re, html, *args)
|
search = lambda re, *args: self.ie._html_search_regex(re, html, *args)
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import unittest
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
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__))))
|
||||||
|
|
||||||
|
@ -235,6 +236,35 @@ class TestFormatSelection(unittest.TestCase):
|
||||||
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},
|
||||||
|
@ -520,7 +550,33 @@ class TestFormatSelection(unittest.TestCase):
|
||||||
ydl.process_ie_result(info_dict)
|
ydl.process_ie_result(info_dict)
|
||||||
self.assertEqual(ydl.downloaded_info_dicts, [])
|
self.assertEqual(ydl.downloaded_info_dicts, [])
|
||||||
|
|
||||||
def test_default_format_spec(self):
|
@patch('yt_dlp.postprocessor.ffmpeg.FFmpegMergerPP.available', False)
|
||||||
|
def test_default_format_spec_without_ffmpeg(self):
|
||||||
|
ydl = YDL({})
|
||||||
|
self.assertEqual(ydl._default_format_spec({}), 'best/bestvideo+bestaudio')
|
||||||
|
|
||||||
|
ydl = YDL({'simulate': True})
|
||||||
|
self.assertEqual(ydl._default_format_spec({}), 'best/bestvideo+bestaudio')
|
||||||
|
|
||||||
|
ydl = YDL({})
|
||||||
|
self.assertEqual(ydl._default_format_spec({'is_live': True}), 'best/bestvideo+bestaudio')
|
||||||
|
|
||||||
|
ydl = YDL({'simulate': True})
|
||||||
|
self.assertEqual(ydl._default_format_spec({'is_live': True}), 'best/bestvideo+bestaudio')
|
||||||
|
|
||||||
|
ydl = YDL({'outtmpl': '-'})
|
||||||
|
self.assertEqual(ydl._default_format_spec({}), 'best/bestvideo+bestaudio')
|
||||||
|
|
||||||
|
ydl = YDL({})
|
||||||
|
self.assertEqual(ydl._default_format_spec({}), 'best/bestvideo+bestaudio')
|
||||||
|
self.assertEqual(ydl._default_format_spec({'is_live': True}), 'best/bestvideo+bestaudio')
|
||||||
|
|
||||||
|
@patch('yt_dlp.postprocessor.ffmpeg.FFmpegMergerPP.available', True)
|
||||||
|
@patch('yt_dlp.postprocessor.ffmpeg.FFmpegMergerPP.can_merge', lambda _: True)
|
||||||
|
def test_default_format_spec_with_ffmpeg(self):
|
||||||
|
ydl = YDL({})
|
||||||
|
self.assertEqual(ydl._default_format_spec({}), 'bestvideo*+bestaudio/best')
|
||||||
|
|
||||||
ydl = YDL({'simulate': True})
|
ydl = YDL({'simulate': True})
|
||||||
self.assertEqual(ydl._default_format_spec({}), 'bestvideo*+bestaudio/best')
|
self.assertEqual(ydl._default_format_spec({}), 'bestvideo*+bestaudio/best')
|
||||||
|
|
||||||
|
@ -528,13 +584,13 @@ class TestFormatSelection(unittest.TestCase):
|
||||||
self.assertEqual(ydl._default_format_spec({'is_live': True}), 'best/bestvideo+bestaudio')
|
self.assertEqual(ydl._default_format_spec({'is_live': True}), 'best/bestvideo+bestaudio')
|
||||||
|
|
||||||
ydl = YDL({'simulate': True})
|
ydl = YDL({'simulate': True})
|
||||||
self.assertEqual(ydl._default_format_spec({'is_live': True}), 'bestvideo*+bestaudio/best')
|
self.assertEqual(ydl._default_format_spec({'is_live': True}), 'best/bestvideo+bestaudio')
|
||||||
|
|
||||||
ydl = YDL({'outtmpl': '-'})
|
ydl = YDL({'outtmpl': '-'})
|
||||||
self.assertEqual(ydl._default_format_spec({}), 'best/bestvideo+bestaudio')
|
self.assertEqual(ydl._default_format_spec({}), 'best/bestvideo+bestaudio')
|
||||||
|
|
||||||
ydl = YDL({})
|
ydl = YDL({})
|
||||||
self.assertEqual(ydl._default_format_spec({}, download=False), 'bestvideo*+bestaudio/best')
|
self.assertEqual(ydl._default_format_spec({}), 'bestvideo*+bestaudio/best')
|
||||||
self.assertEqual(ydl._default_format_spec({'is_live': True}), 'best/bestvideo+bestaudio')
|
self.assertEqual(ydl._default_format_spec({'is_live': True}), 'best/bestvideo+bestaudio')
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -83,6 +83,18 @@ class TestAES(unittest.TestCase):
|
||||||
data, intlist_to_bytes(self.key), authentication_tag, intlist_to_bytes(self.iv[:12]))
|
data, intlist_to_bytes(self.key), authentication_tag, intlist_to_bytes(self.iv[:12]))
|
||||||
self.assertEqual(decrypted.rstrip(b'\x08'), self.secret_msg)
|
self.assertEqual(decrypted.rstrip(b'\x08'), self.secret_msg)
|
||||||
|
|
||||||
|
def test_gcm_aligned_decrypt(self):
|
||||||
|
data = b'\x159Y\xcf5eud\x90\x9c\x85&]\x14\x1d\x0f'
|
||||||
|
authentication_tag = b'\x08\xb1\x9d!&\x98\xd0\xeaRq\x90\xe6;\xb5]\xd8'
|
||||||
|
|
||||||
|
decrypted = intlist_to_bytes(aes_gcm_decrypt_and_verify(
|
||||||
|
list(data), self.key, list(authentication_tag), self.iv[:12]))
|
||||||
|
self.assertEqual(decrypted.rstrip(b'\x08'), self.secret_msg[:16])
|
||||||
|
if Cryptodome.AES:
|
||||||
|
decrypted = aes_gcm_decrypt_and_verify_bytes(
|
||||||
|
data, bytes(self.key), authentication_tag, bytes(self.iv[:12]))
|
||||||
|
self.assertEqual(decrypted.rstrip(b'\x08'), self.secret_msg[:16])
|
||||||
|
|
||||||
def test_decrypt_text(self):
|
def test_decrypt_text(self):
|
||||||
password = intlist_to_bytes(self.key).decode()
|
password = intlist_to_bytes(self.key).decode()
|
||||||
encrypted = base64.b64encode(
|
encrypted = base64.b64encode(
|
||||||
|
|
|
@ -67,6 +67,7 @@ class TestCookies(unittest.TestCase):
|
||||||
({'XDG_CURRENT_DESKTOP': 'GNOME'}, _LinuxDesktopEnvironment.GNOME),
|
({'XDG_CURRENT_DESKTOP': 'GNOME'}, _LinuxDesktopEnvironment.GNOME),
|
||||||
({'XDG_CURRENT_DESKTOP': 'GNOME:GNOME-Classic'}, _LinuxDesktopEnvironment.GNOME),
|
({'XDG_CURRENT_DESKTOP': 'GNOME:GNOME-Classic'}, _LinuxDesktopEnvironment.GNOME),
|
||||||
({'XDG_CURRENT_DESKTOP': 'GNOME : GNOME-Classic'}, _LinuxDesktopEnvironment.GNOME),
|
({'XDG_CURRENT_DESKTOP': 'GNOME : GNOME-Classic'}, _LinuxDesktopEnvironment.GNOME),
|
||||||
|
({'XDG_CURRENT_DESKTOP': 'ubuntu:GNOME'}, _LinuxDesktopEnvironment.GNOME),
|
||||||
|
|
||||||
({'XDG_CURRENT_DESKTOP': 'Unity', 'DESKTOP_SESSION': 'gnome-fallback'}, _LinuxDesktopEnvironment.GNOME),
|
({'XDG_CURRENT_DESKTOP': 'Unity', 'DESKTOP_SESSION': 'gnome-fallback'}, _LinuxDesktopEnvironment.GNOME),
|
||||||
({'XDG_CURRENT_DESKTOP': 'KDE', 'KDE_SESSION_VERSION': '5'}, _LinuxDesktopEnvironment.KDE5),
|
({'XDG_CURRENT_DESKTOP': 'KDE', 'KDE_SESSION_VERSION': '5'}, _LinuxDesktopEnvironment.KDE5),
|
||||||
|
@ -104,6 +105,13 @@ class TestCookies(unittest.TestCase):
|
||||||
decryptor = LinuxChromeCookieDecryptor('Chrome', Logger())
|
decryptor = LinuxChromeCookieDecryptor('Chrome', Logger())
|
||||||
self.assertEqual(decryptor.decrypt(encrypted_value), value)
|
self.assertEqual(decryptor.decrypt(encrypted_value), value)
|
||||||
|
|
||||||
|
def test_chrome_cookie_decryptor_linux_v10_meta24(self):
|
||||||
|
with MonkeyPatch(cookies, {'_get_linux_keyring_password': lambda *args, **kwargs: b''}):
|
||||||
|
encrypted_value = b'v10\x1f\xe4\x0e[\x83\x0c\xcc*kPi \xce\x8d\x1d\xbb\x80\r\x11\t\xbb\x9e^Hy\x94\xf4\x963\x9f\x82\xba\xfe\xa1\xed\xb9\xf1)\x00710\x92\xc8/<\x96B'
|
||||||
|
value = 'DE'
|
||||||
|
decryptor = LinuxChromeCookieDecryptor('Chrome', Logger(), meta_version=24)
|
||||||
|
self.assertEqual(decryptor.decrypt(encrypted_value), value)
|
||||||
|
|
||||||
def test_chrome_cookie_decryptor_windows_v10(self):
|
def test_chrome_cookie_decryptor_windows_v10(self):
|
||||||
with MonkeyPatch(cookies, {
|
with MonkeyPatch(cookies, {
|
||||||
'_get_windows_v10_key': lambda *args, **kwargs: b'Y\xef\xad\xad\xeerp\xf0Y\xe6\x9b\x12\xc2<z\x16]\n\xbb\xb8\xcb\xd7\x9bA\xc3\x14e\x99{\xd6\xf4&',
|
'_get_windows_v10_key': lambda *args, **kwargs: b'Y\xef\xad\xad\xeerp\xf0Y\xe6\x9b\x12\xc2<z\x16]\n\xbb\xb8\xcb\xd7\x9bA\xc3\x14e\x99{\xd6\xf4&',
|
||||||
|
@ -113,6 +121,15 @@ class TestCookies(unittest.TestCase):
|
||||||
decryptor = WindowsChromeCookieDecryptor('', Logger())
|
decryptor = WindowsChromeCookieDecryptor('', Logger())
|
||||||
self.assertEqual(decryptor.decrypt(encrypted_value), value)
|
self.assertEqual(decryptor.decrypt(encrypted_value), value)
|
||||||
|
|
||||||
|
def test_chrome_cookie_decryptor_windows_v10_meta24(self):
|
||||||
|
with MonkeyPatch(cookies, {
|
||||||
|
'_get_windows_v10_key': lambda *args, **kwargs: b'\xea\x8b\x02\xc3\xc6\xc5\x99\xc3\xa3[ j\xfa\xf6\xfcU\xac\x13u\xdc\x0c\x0e\xf1\x03\x90\xb6\xdf\xbb\x8fL\xb1\xb2',
|
||||||
|
}):
|
||||||
|
encrypted_value = b'v10dN\xe1\xacy\x84^\xe1I\xact\x03r\xfb\xe2\xce{^\x0e<(\xb0y\xeb\x01\xfb@"\x9e\x8c\xa53~\xdb*\x8f\xac\x8b\xe3\xfd3\x06\xe5\x93\x19OyOG\xb2\xfb\x1d$\xc0\xda\x13j\x9e\xfe\xc5\xa3\xa8\xfe\xd9'
|
||||||
|
value = '1234'
|
||||||
|
decryptor = WindowsChromeCookieDecryptor('', Logger(), meta_version=24)
|
||||||
|
self.assertEqual(decryptor.decrypt(encrypted_value), value)
|
||||||
|
|
||||||
def test_chrome_cookie_decryptor_mac_v10(self):
|
def test_chrome_cookie_decryptor_mac_v10(self):
|
||||||
with MonkeyPatch(cookies, {'_get_mac_keyring_password': lambda *args, **kwargs: b'6eIDUdtKAacvlHwBVwvg/Q=='}):
|
with MonkeyPatch(cookies, {'_get_mac_keyring_password': lambda *args, **kwargs: b'6eIDUdtKAacvlHwBVwvg/Q=='}):
|
||||||
encrypted_value = b'v10\xb3\xbe\xad\xa1[\x9fC\xa1\x98\xe0\x9a\x01\xd9\xcf\xbfc'
|
encrypted_value = b'v10\xb3\xbe\xad\xa1[\x9fC\xa1\x98\xe0\x9a\x01\xd9\xcf\xbfc'
|
||||||
|
|
|
@ -20,7 +20,6 @@ from test.helper import (
|
||||||
gettestcases,
|
gettestcases,
|
||||||
getwebpagetestcases,
|
getwebpagetestcases,
|
||||||
is_download_test,
|
is_download_test,
|
||||||
report_warning,
|
|
||||||
try_rm,
|
try_rm,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -178,8 +177,7 @@ def generator(test_case, tname):
|
||||||
raise
|
raise
|
||||||
|
|
||||||
if try_num == RETRIES:
|
if try_num == RETRIES:
|
||||||
report_warning(f'{tname} failed due to network errors, skipping...')
|
raise
|
||||||
return
|
|
||||||
|
|
||||||
print(f'Retrying: {try_num} failed tries\n\n##########\n\n')
|
print(f'Retrying: {try_num} failed tries\n\n##########\n\n')
|
||||||
|
|
||||||
|
|
|
@ -92,6 +92,7 @@ class TestJSInterpreter(unittest.TestCase):
|
||||||
self._test('function f(){return 0 && 1 || 2;}', 2)
|
self._test('function f(){return 0 && 1 || 2;}', 2)
|
||||||
self._test('function f(){return 0 ?? 42;}', 0)
|
self._test('function f(){return 0 ?? 42;}', 0)
|
||||||
self._test('function f(){return "life, the universe and everything" < 42;}', False)
|
self._test('function f(){return "life, the universe and everything" < 42;}', False)
|
||||||
|
self._test('function f(){return 0 - 7 * - 6;}', 42)
|
||||||
|
|
||||||
def test_array_access(self):
|
def test_array_access(self):
|
||||||
self._test('function f(){var x = [1,2,3]; x[0] = 4; x[0] = 5; x[2.0] = 7; return x;}', [5, 2, 7])
|
self._test('function f(){var x = [1,2,3]; x[0] = 4; x[0] = 5; x[2.0] = 7; return x;}', [5, 2, 7])
|
||||||
|
@ -375,6 +376,61 @@ class TestJSInterpreter(unittest.TestCase):
|
||||||
jsi = JSInterpreter('''function f(p,a,c,k,e,d){while(c--)if(k[c])p=p.replace(new RegExp('\\b'+c.toString(a)+'\\b','g'),k[c]);return p}''')
|
jsi = JSInterpreter('''function f(p,a,c,k,e,d){while(c--)if(k[c])p=p.replace(new RegExp('\\b'+c.toString(a)+'\\b','g'),k[c]);return p}''')
|
||||||
self.assertEqual(jsi.call_function('f', '''h 7=g("1j");7.7h({7g:[{33:"w://7f-7e-7d-7c.v.7b/7a/79/78/77/76.74?t=73&s=2s&e=72&f=2t&71=70.0.0.1&6z=6y&6x=6w"}],6v:"w://32.v.u/6u.31",16:"r%",15:"r%",6t:"6s",6r:"",6q:"l",6p:"l",6o:"6n",6m:\'6l\',6k:"6j",9:[{33:"/2u?b=6i&n=50&6h=w://32.v.u/6g.31",6f:"6e"}],1y:{6d:1,6c:\'#6b\',6a:\'#69\',68:"67",66:30,65:r,},"64":{63:"%62 2m%m%61%5z%5y%5x.u%5w%5v%5u.2y%22 2k%m%1o%22 5t%m%1o%22 5s%m%1o%22 2j%m%5r%22 16%m%5q%22 15%m%5p%22 5o%2z%5n%5m%2z",5l:"w://v.u/d/1k/5k.2y",5j:[]},\'5i\':{"5h":"5g"},5f:"5e",5d:"w://v.u",5c:{},5b:l,1x:[0.25,0.50,0.75,1,1.25,1.5,2]});h 1m,1n,5a;h 59=0,58=0;h 7=g("1j");h 2x=0,57=0,56=0;$.55({54:{\'53-52\':\'2i-51\'}});7.j(\'4z\',6(x){c(5>0&&x.1l>=5&&1n!=1){1n=1;$(\'q.4y\').4x(\'4w\')}});7.j(\'13\',6(x){2x=x.1l});7.j(\'2g\',6(x){2w(x)});7.j(\'4v\',6(){$(\'q.2v\').4u()});6 2w(x){$(\'q.2v\').4t();c(1m)19;1m=1;17=0;c(4s.4r===l){17=1}$.4q(\'/2u?b=4p&2l=1k&4o=2t-4n-4m-2s-4l&4k=&4j=&4i=&17=\'+17,6(2r){$(\'#4h\').4g(2r)});$(\'.3-8-4f-4e:4d("4c")\').2h(6(e){2q();g().4b(0);g().4a(l)});6 2q(){h $14=$("<q />").2p({1l:"49",16:"r%",15:"r%",48:0,2n:0,2o:47,46:"45(10%, 10%, 10%, 0.4)","44-43":"42"});$("<41 />").2p({16:"60%",15:"60%",2o:40,"3z-2n":"3y"}).3x({\'2m\':\'/?b=3w&2l=1k\',\'2k\':\'0\',\'2j\':\'2i\'}).2f($14);$14.2h(6(){$(3v).3u();g().2g()});$14.2f($(\'#1j\'))}g().13(0);}6 3t(){h 9=7.1b(2e);2d.2c(9);c(9.n>1){1r(i=0;i<9.n;i++){c(9[i].1a==2e){2d.2c(\'!!=\'+i);7.1p(i)}}}}7.j(\'3s\',6(){g().1h("/2a/3r.29","3q 10 28",6(){g().13(g().27()+10)},"2b");$("q[26=2b]").23().21(\'.3-20-1z\');g().1h("/2a/3p.29","3o 10 28",6(){h 12=g().27()-10;c(12<0)12=0;g().13(12)},"24");$("q[26=24]").23().21(\'.3-20-1z\');});6 1i(){}7.j(\'3n\',6(){1i()});7.j(\'3m\',6(){1i()});7.j("k",6(y){h 9=7.1b();c(9.n<2)19;$(\'.3-8-3l-3k\').3j(6(){$(\'#3-8-a-k\').1e(\'3-8-a-z\');$(\'.3-a-k\').p(\'o-1f\',\'11\')});7.1h("/3i/3h.3g","3f 3e",6(){$(\'.3-1w\').3d(\'3-8-1v\');$(\'.3-8-1y, .3-8-1x\').p(\'o-1g\',\'11\');c($(\'.3-1w\').3c(\'3-8-1v\')){$(\'.3-a-k\').p(\'o-1g\',\'l\');$(\'.3-a-k\').p(\'o-1f\',\'l\');$(\'.3-8-a\').1e(\'3-8-a-z\');$(\'.3-8-a:1u\').3b(\'3-8-a-z\')}3a{$(\'.3-a-k\').p(\'o-1g\',\'11\');$(\'.3-a-k\').p(\'o-1f\',\'11\');$(\'.3-8-a:1u\').1e(\'3-8-a-z\')}},"39");7.j("38",6(y){1d.37(\'1c\',y.9[y.36].1a)});c(1d.1t(\'1c\')){35("1s(1d.1t(\'1c\'));",34)}});h 18;6 1s(1q){h 9=7.1b();c(9.n>1){1r(i=0;i<9.n;i++){c(9[i].1a==1q){c(i==18){19}18=i;7.1p(i)}}}}',36,270,'|||jw|||function|player|settings|tracks|submenu||if||||jwplayer|var||on|audioTracks|true|3D|length|aria|attr|div|100|||sx|filemoon|https||event|active||false|tt|seek|dd|height|width|adb|current_audio|return|name|getAudioTracks|default_audio|localStorage|removeClass|expanded|checked|addButton|callMeMaybe|vplayer|0fxcyc2ajhp1|position|vvplay|vvad|220|setCurrentAudioTrack|audio_name|for|audio_set|getItem|last|open|controls|playbackRates|captions|rewind|icon|insertAfter||detach|ff00||button|getPosition|sec|png|player8|ff11|log|console|track_name|appendTo|play|click|no|scrolling|frameborder|file_code|src|top|zIndex|css|showCCform|data|1662367683|383371|dl|video_ad|doPlay|prevt|mp4|3E||jpg|thumbs|file|300|setTimeout|currentTrack|setItem|audioTrackChanged|dualSound|else|addClass|hasClass|toggleClass|Track|Audio|svg|dualy|images|mousedown|buttons|topbar|playAttemptFailed|beforePlay|Rewind|fr|Forward|ff|ready|set_audio_track|remove|this|upload_srt|prop|50px|margin|1000001|iframe|center|align|text|rgba|background|1000000|left|absolute|pause|setCurrentCaptions|Upload|contains|item|content|html|fviews|referer|prem|embed|3e57249ef633e0d03bf76ceb8d8a4b65|216|83|hash|view|get|TokenZir|window|hide|show|complete|slow|fadeIn|video_ad_fadein|time||cache|Cache|Content|headers|ajaxSetup|v2done|tott|vastdone2|vastdone1|vvbefore|playbackRateControls|cast|aboutlink|FileMoon|abouttext|UHD|1870|qualityLabels|sites|GNOME_POWER|link|2Fiframe|3C|allowfullscreen|22360|22640|22no|marginheight|marginwidth|2FGNOME_POWER|2F0fxcyc2ajhp1|2Fe|2Ffilemoon|2F|3A||22https|3Ciframe|code|sharing|fontOpacity|backgroundOpacity|Tahoma|fontFamily|303030|backgroundColor|FFFFFF|color|userFontScale|thumbnails|kind|0fxcyc2ajhp10000|url|get_slides|start|startparam|none|preload|html5|primary|hlshtml|androidhls|duration|uniform|stretching|0fxcyc2ajhp1_xt|image|2048|sp|6871|asn|127|srv|43200|_g3XlBcu2lmD9oDexD2NLWSmah2Nu3XcDrl93m9PwXY|m3u8||master|0fxcyc2ajhp1_x|00076|01|hls2|to|s01|delivery|storage|moon|sources|setup'''.split('|')))
|
self.assertEqual(jsi.call_function('f', '''h 7=g("1j");7.7h({7g:[{33:"w://7f-7e-7d-7c.v.7b/7a/79/78/77/76.74?t=73&s=2s&e=72&f=2t&71=70.0.0.1&6z=6y&6x=6w"}],6v:"w://32.v.u/6u.31",16:"r%",15:"r%",6t:"6s",6r:"",6q:"l",6p:"l",6o:"6n",6m:\'6l\',6k:"6j",9:[{33:"/2u?b=6i&n=50&6h=w://32.v.u/6g.31",6f:"6e"}],1y:{6d:1,6c:\'#6b\',6a:\'#69\',68:"67",66:30,65:r,},"64":{63:"%62 2m%m%61%5z%5y%5x.u%5w%5v%5u.2y%22 2k%m%1o%22 5t%m%1o%22 5s%m%1o%22 2j%m%5r%22 16%m%5q%22 15%m%5p%22 5o%2z%5n%5m%2z",5l:"w://v.u/d/1k/5k.2y",5j:[]},\'5i\':{"5h":"5g"},5f:"5e",5d:"w://v.u",5c:{},5b:l,1x:[0.25,0.50,0.75,1,1.25,1.5,2]});h 1m,1n,5a;h 59=0,58=0;h 7=g("1j");h 2x=0,57=0,56=0;$.55({54:{\'53-52\':\'2i-51\'}});7.j(\'4z\',6(x){c(5>0&&x.1l>=5&&1n!=1){1n=1;$(\'q.4y\').4x(\'4w\')}});7.j(\'13\',6(x){2x=x.1l});7.j(\'2g\',6(x){2w(x)});7.j(\'4v\',6(){$(\'q.2v\').4u()});6 2w(x){$(\'q.2v\').4t();c(1m)19;1m=1;17=0;c(4s.4r===l){17=1}$.4q(\'/2u?b=4p&2l=1k&4o=2t-4n-4m-2s-4l&4k=&4j=&4i=&17=\'+17,6(2r){$(\'#4h\').4g(2r)});$(\'.3-8-4f-4e:4d("4c")\').2h(6(e){2q();g().4b(0);g().4a(l)});6 2q(){h $14=$("<q />").2p({1l:"49",16:"r%",15:"r%",48:0,2n:0,2o:47,46:"45(10%, 10%, 10%, 0.4)","44-43":"42"});$("<41 />").2p({16:"60%",15:"60%",2o:40,"3z-2n":"3y"}).3x({\'2m\':\'/?b=3w&2l=1k\',\'2k\':\'0\',\'2j\':\'2i\'}).2f($14);$14.2h(6(){$(3v).3u();g().2g()});$14.2f($(\'#1j\'))}g().13(0);}6 3t(){h 9=7.1b(2e);2d.2c(9);c(9.n>1){1r(i=0;i<9.n;i++){c(9[i].1a==2e){2d.2c(\'!!=\'+i);7.1p(i)}}}}7.j(\'3s\',6(){g().1h("/2a/3r.29","3q 10 28",6(){g().13(g().27()+10)},"2b");$("q[26=2b]").23().21(\'.3-20-1z\');g().1h("/2a/3p.29","3o 10 28",6(){h 12=g().27()-10;c(12<0)12=0;g().13(12)},"24");$("q[26=24]").23().21(\'.3-20-1z\');});6 1i(){}7.j(\'3n\',6(){1i()});7.j(\'3m\',6(){1i()});7.j("k",6(y){h 9=7.1b();c(9.n<2)19;$(\'.3-8-3l-3k\').3j(6(){$(\'#3-8-a-k\').1e(\'3-8-a-z\');$(\'.3-a-k\').p(\'o-1f\',\'11\')});7.1h("/3i/3h.3g","3f 3e",6(){$(\'.3-1w\').3d(\'3-8-1v\');$(\'.3-8-1y, .3-8-1x\').p(\'o-1g\',\'11\');c($(\'.3-1w\').3c(\'3-8-1v\')){$(\'.3-a-k\').p(\'o-1g\',\'l\');$(\'.3-a-k\').p(\'o-1f\',\'l\');$(\'.3-8-a\').1e(\'3-8-a-z\');$(\'.3-8-a:1u\').3b(\'3-8-a-z\')}3a{$(\'.3-a-k\').p(\'o-1g\',\'11\');$(\'.3-a-k\').p(\'o-1f\',\'11\');$(\'.3-8-a:1u\').1e(\'3-8-a-z\')}},"39");7.j("38",6(y){1d.37(\'1c\',y.9[y.36].1a)});c(1d.1t(\'1c\')){35("1s(1d.1t(\'1c\'));",34)}});h 18;6 1s(1q){h 9=7.1b();c(9.n>1){1r(i=0;i<9.n;i++){c(9[i].1a==1q){c(i==18){19}18=i;7.1p(i)}}}}',36,270,'|||jw|||function|player|settings|tracks|submenu||if||||jwplayer|var||on|audioTracks|true|3D|length|aria|attr|div|100|||sx|filemoon|https||event|active||false|tt|seek|dd|height|width|adb|current_audio|return|name|getAudioTracks|default_audio|localStorage|removeClass|expanded|checked|addButton|callMeMaybe|vplayer|0fxcyc2ajhp1|position|vvplay|vvad|220|setCurrentAudioTrack|audio_name|for|audio_set|getItem|last|open|controls|playbackRates|captions|rewind|icon|insertAfter||detach|ff00||button|getPosition|sec|png|player8|ff11|log|console|track_name|appendTo|play|click|no|scrolling|frameborder|file_code|src|top|zIndex|css|showCCform|data|1662367683|383371|dl|video_ad|doPlay|prevt|mp4|3E||jpg|thumbs|file|300|setTimeout|currentTrack|setItem|audioTrackChanged|dualSound|else|addClass|hasClass|toggleClass|Track|Audio|svg|dualy|images|mousedown|buttons|topbar|playAttemptFailed|beforePlay|Rewind|fr|Forward|ff|ready|set_audio_track|remove|this|upload_srt|prop|50px|margin|1000001|iframe|center|align|text|rgba|background|1000000|left|absolute|pause|setCurrentCaptions|Upload|contains|item|content|html|fviews|referer|prem|embed|3e57249ef633e0d03bf76ceb8d8a4b65|216|83|hash|view|get|TokenZir|window|hide|show|complete|slow|fadeIn|video_ad_fadein|time||cache|Cache|Content|headers|ajaxSetup|v2done|tott|vastdone2|vastdone1|vvbefore|playbackRateControls|cast|aboutlink|FileMoon|abouttext|UHD|1870|qualityLabels|sites|GNOME_POWER|link|2Fiframe|3C|allowfullscreen|22360|22640|22no|marginheight|marginwidth|2FGNOME_POWER|2F0fxcyc2ajhp1|2Fe|2Ffilemoon|2F|3A||22https|3Ciframe|code|sharing|fontOpacity|backgroundOpacity|Tahoma|fontFamily|303030|backgroundColor|FFFFFF|color|userFontScale|thumbnails|kind|0fxcyc2ajhp10000|url|get_slides|start|startparam|none|preload|html5|primary|hlshtml|androidhls|duration|uniform|stretching|0fxcyc2ajhp1_xt|image|2048|sp|6871|asn|127|srv|43200|_g3XlBcu2lmD9oDexD2NLWSmah2Nu3XcDrl93m9PwXY|m3u8||master|0fxcyc2ajhp1_x|00076|01|hls2|to|s01|delivery|storage|moon|sources|setup'''.split('|')))
|
||||||
|
|
||||||
|
def test_join(self):
|
||||||
|
test_input = list('test')
|
||||||
|
tests = [
|
||||||
|
'function f(a, b){return a.join(b)}',
|
||||||
|
'function f(a, b){return Array.prototype.join.call(a, b)}',
|
||||||
|
'function f(a, b){return Array.prototype.join.apply(a, [b])}',
|
||||||
|
]
|
||||||
|
for test in tests:
|
||||||
|
jsi = JSInterpreter(test)
|
||||||
|
self._test(jsi, 'test', args=[test_input, ''])
|
||||||
|
self._test(jsi, 't-e-s-t', args=[test_input, '-'])
|
||||||
|
self._test(jsi, '', args=[[], '-'])
|
||||||
|
|
||||||
|
def test_split(self):
|
||||||
|
test_result = list('test')
|
||||||
|
tests = [
|
||||||
|
'function f(a, b){return a.split(b)}',
|
||||||
|
'function f(a, b){return String.prototype.split.call(a, b)}',
|
||||||
|
'function f(a, b){return String.prototype.split.apply(a, [b])}',
|
||||||
|
]
|
||||||
|
for test in tests:
|
||||||
|
jsi = JSInterpreter(test)
|
||||||
|
self._test(jsi, test_result, args=['test', ''])
|
||||||
|
self._test(jsi, test_result, args=['t-e-s-t', '-'])
|
||||||
|
self._test(jsi, [''], args=['', '-'])
|
||||||
|
self._test(jsi, [], args=['', ''])
|
||||||
|
|
||||||
|
def test_slice(self):
|
||||||
|
self._test('function f(){return [0, 1, 2, 3, 4, 5, 6, 7, 8].slice()}', [0, 1, 2, 3, 4, 5, 6, 7, 8])
|
||||||
|
self._test('function f(){return [0, 1, 2, 3, 4, 5, 6, 7, 8].slice(0)}', [0, 1, 2, 3, 4, 5, 6, 7, 8])
|
||||||
|
self._test('function f(){return [0, 1, 2, 3, 4, 5, 6, 7, 8].slice(5)}', [5, 6, 7, 8])
|
||||||
|
self._test('function f(){return [0, 1, 2, 3, 4, 5, 6, 7, 8].slice(99)}', [])
|
||||||
|
self._test('function f(){return [0, 1, 2, 3, 4, 5, 6, 7, 8].slice(-2)}', [7, 8])
|
||||||
|
self._test('function f(){return [0, 1, 2, 3, 4, 5, 6, 7, 8].slice(-99)}', [0, 1, 2, 3, 4, 5, 6, 7, 8])
|
||||||
|
self._test('function f(){return [0, 1, 2, 3, 4, 5, 6, 7, 8].slice(0, 0)}', [])
|
||||||
|
self._test('function f(){return [0, 1, 2, 3, 4, 5, 6, 7, 8].slice(1, 0)}', [])
|
||||||
|
self._test('function f(){return [0, 1, 2, 3, 4, 5, 6, 7, 8].slice(0, 1)}', [0])
|
||||||
|
self._test('function f(){return [0, 1, 2, 3, 4, 5, 6, 7, 8].slice(3, 6)}', [3, 4, 5])
|
||||||
|
self._test('function f(){return [0, 1, 2, 3, 4, 5, 6, 7, 8].slice(1, -1)}', [1, 2, 3, 4, 5, 6, 7])
|
||||||
|
self._test('function f(){return [0, 1, 2, 3, 4, 5, 6, 7, 8].slice(-1, 1)}', [])
|
||||||
|
self._test('function f(){return [0, 1, 2, 3, 4, 5, 6, 7, 8].slice(-3, -1)}', [6, 7])
|
||||||
|
self._test('function f(){return "012345678".slice()}', '012345678')
|
||||||
|
self._test('function f(){return "012345678".slice(0)}', '012345678')
|
||||||
|
self._test('function f(){return "012345678".slice(5)}', '5678')
|
||||||
|
self._test('function f(){return "012345678".slice(99)}', '')
|
||||||
|
self._test('function f(){return "012345678".slice(-2)}', '78')
|
||||||
|
self._test('function f(){return "012345678".slice(-99)}', '012345678')
|
||||||
|
self._test('function f(){return "012345678".slice(0, 0)}', '')
|
||||||
|
self._test('function f(){return "012345678".slice(1, 0)}', '')
|
||||||
|
self._test('function f(){return "012345678".slice(0, 1)}', '0')
|
||||||
|
self._test('function f(){return "012345678".slice(3, 6)}', '345')
|
||||||
|
self._test('function f(){return "012345678".slice(1, -1)}', '1234567')
|
||||||
|
self._test('function f(){return "012345678".slice(-1, 1)}', '')
|
||||||
|
self._test('function f(){return "012345678".slice(-3, -1)}', '67')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
@ -265,6 +265,11 @@ class HTTPTestRequestHandler(http.server.BaseHTTPRequestHandler):
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(payload)
|
self.wfile.write(payload)
|
||||||
self.finish()
|
self.finish()
|
||||||
|
elif self.path == '/get_cookie':
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Set-Cookie', 'test=ytdlp; path=/')
|
||||||
|
self.end_headers()
|
||||||
|
self.finish()
|
||||||
else:
|
else:
|
||||||
self._status(404)
|
self._status(404)
|
||||||
|
|
||||||
|
@ -338,6 +343,52 @@ class TestHTTPRequestHandler(TestRequestHandlerBase):
|
||||||
validate_and_send(rh, Request(f'https://127.0.0.1:{https_port}/headers'))
|
validate_and_send(rh, Request(f'https://127.0.0.1:{https_port}/headers'))
|
||||||
assert not issubclass(exc_info.type, CertificateVerifyError)
|
assert not issubclass(exc_info.type, CertificateVerifyError)
|
||||||
|
|
||||||
|
@pytest.mark.skip_handler('CurlCFFI', 'legacy_ssl ignored by CurlCFFI')
|
||||||
|
def test_legacy_ssl_extension(self, handler):
|
||||||
|
# HTTPS server with old ciphers
|
||||||
|
# XXX: is there a better way to test this than to create a new server?
|
||||||
|
https_httpd = http.server.ThreadingHTTPServer(
|
||||||
|
('127.0.0.1', 0), HTTPTestRequestHandler)
|
||||||
|
sslctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||||
|
sslctx.maximum_version = ssl.TLSVersion.TLSv1_2
|
||||||
|
sslctx.set_ciphers('SHA1:AESCCM:aDSS:eNULL:aNULL')
|
||||||
|
sslctx.load_cert_chain(os.path.join(TEST_DIR, 'testcert.pem'), None)
|
||||||
|
https_httpd.socket = sslctx.wrap_socket(https_httpd.socket, server_side=True)
|
||||||
|
https_port = http_server_port(https_httpd)
|
||||||
|
https_server_thread = threading.Thread(target=https_httpd.serve_forever)
|
||||||
|
https_server_thread.daemon = True
|
||||||
|
https_server_thread.start()
|
||||||
|
|
||||||
|
with handler(verify=False) as rh:
|
||||||
|
res = validate_and_send(rh, Request(f'https://127.0.0.1:{https_port}/headers', extensions={'legacy_ssl': True}))
|
||||||
|
assert res.status == 200
|
||||||
|
res.close()
|
||||||
|
|
||||||
|
# Ensure only applies to request extension
|
||||||
|
with pytest.raises(SSLError):
|
||||||
|
validate_and_send(rh, Request(f'https://127.0.0.1:{https_port}/headers'))
|
||||||
|
|
||||||
|
@pytest.mark.skip_handler('CurlCFFI', 'legacy_ssl ignored by CurlCFFI')
|
||||||
|
def test_legacy_ssl_support(self, handler):
|
||||||
|
# HTTPS server with old ciphers
|
||||||
|
# XXX: is there a better way to test this than to create a new server?
|
||||||
|
https_httpd = http.server.ThreadingHTTPServer(
|
||||||
|
('127.0.0.1', 0), HTTPTestRequestHandler)
|
||||||
|
sslctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||||
|
sslctx.maximum_version = ssl.TLSVersion.TLSv1_2
|
||||||
|
sslctx.set_ciphers('SHA1:AESCCM:aDSS:eNULL:aNULL')
|
||||||
|
sslctx.load_cert_chain(os.path.join(TEST_DIR, 'testcert.pem'), None)
|
||||||
|
https_httpd.socket = sslctx.wrap_socket(https_httpd.socket, server_side=True)
|
||||||
|
https_port = http_server_port(https_httpd)
|
||||||
|
https_server_thread = threading.Thread(target=https_httpd.serve_forever)
|
||||||
|
https_server_thread.daemon = True
|
||||||
|
https_server_thread.start()
|
||||||
|
|
||||||
|
with handler(verify=False, legacy_ssl_support=True) as rh:
|
||||||
|
res = validate_and_send(rh, Request(f'https://127.0.0.1:{https_port}/headers'))
|
||||||
|
assert res.status == 200
|
||||||
|
res.close()
|
||||||
|
|
||||||
def test_percent_encode(self, handler):
|
def test_percent_encode(self, handler):
|
||||||
with handler() as rh:
|
with handler() as rh:
|
||||||
# Unicode characters should be encoded with uppercase percent-encoding
|
# Unicode characters should be encoded with uppercase percent-encoding
|
||||||
|
@ -490,6 +541,24 @@ class TestHTTPRequestHandler(TestRequestHandlerBase):
|
||||||
rh, Request(f'http://127.0.0.1:{self.http_port}/headers', extensions={'cookiejar': cookiejar})).read()
|
rh, Request(f'http://127.0.0.1:{self.http_port}/headers', extensions={'cookiejar': cookiejar})).read()
|
||||||
assert b'cookie: test=ytdlp' in data.lower()
|
assert b'cookie: test=ytdlp' in data.lower()
|
||||||
|
|
||||||
|
def test_cookie_sync_only_cookiejar(self, handler):
|
||||||
|
# Ensure that cookies are ONLY being handled by the cookiejar
|
||||||
|
with handler() as rh:
|
||||||
|
validate_and_send(rh, Request(f'http://127.0.0.1:{self.http_port}/get_cookie', extensions={'cookiejar': YoutubeDLCookieJar()}))
|
||||||
|
data = validate_and_send(rh, Request(f'http://127.0.0.1:{self.http_port}/headers', extensions={'cookiejar': YoutubeDLCookieJar()})).read()
|
||||||
|
assert b'cookie: test=ytdlp' not in data.lower()
|
||||||
|
|
||||||
|
def test_cookie_sync_delete_cookie(self, handler):
|
||||||
|
# Ensure that cookies are ONLY being handled by the cookiejar
|
||||||
|
cookiejar = YoutubeDLCookieJar()
|
||||||
|
with handler(cookiejar=cookiejar) as rh:
|
||||||
|
validate_and_send(rh, Request(f'http://127.0.0.1:{self.http_port}/get_cookie'))
|
||||||
|
data = validate_and_send(rh, Request(f'http://127.0.0.1:{self.http_port}/headers')).read()
|
||||||
|
assert b'cookie: test=ytdlp' in data.lower()
|
||||||
|
cookiejar.clear_session_cookies()
|
||||||
|
data = validate_and_send(rh, Request(f'http://127.0.0.1:{self.http_port}/headers')).read()
|
||||||
|
assert b'cookie: test=ytdlp' not in data.lower()
|
||||||
|
|
||||||
def test_headers(self, handler):
|
def test_headers(self, handler):
|
||||||
|
|
||||||
with handler(headers=HTTPHeaderDict({'test1': 'test', 'test2': 'test2'})) as rh:
|
with handler(headers=HTTPHeaderDict({'test1': 'test', 'test2': 'test2'})) as rh:
|
||||||
|
@ -753,6 +822,24 @@ class TestRequestHandlerMisc:
|
||||||
rh.close()
|
rh.close()
|
||||||
assert len(logging_handlers) == before_count
|
assert len(logging_handlers) == before_count
|
||||||
|
|
||||||
|
def test_wrap_request_errors(self):
|
||||||
|
class TestRequestHandler(RequestHandler):
|
||||||
|
def _validate(self, request):
|
||||||
|
if request.headers.get('x-fail'):
|
||||||
|
raise UnsupportedRequest('test error')
|
||||||
|
|
||||||
|
def _send(self, request: Request):
|
||||||
|
raise RequestError('test error')
|
||||||
|
|
||||||
|
with TestRequestHandler(logger=FakeLogger()) as rh:
|
||||||
|
with pytest.raises(UnsupportedRequest, match='test error') as exc_info:
|
||||||
|
rh.validate(Request('http://example.com', headers={'x-fail': '1'}))
|
||||||
|
assert exc_info.value.handler is rh
|
||||||
|
|
||||||
|
with pytest.raises(RequestError, match='test error') as exc_info:
|
||||||
|
rh.send(Request('http://example.com'))
|
||||||
|
assert exc_info.value.handler is rh
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('handler', ['Urllib'], indirect=True)
|
@pytest.mark.parametrize('handler', ['Urllib'], indirect=True)
|
||||||
class TestUrllibRequestHandler(TestRequestHandlerBase):
|
class TestUrllibRequestHandler(TestRequestHandlerBase):
|
||||||
|
@ -914,7 +1001,6 @@ class TestRequestsRequestHandler(TestRequestHandlerBase):
|
||||||
class TestCurlCFFIRequestHandler(TestRequestHandlerBase):
|
class TestCurlCFFIRequestHandler(TestRequestHandlerBase):
|
||||||
|
|
||||||
@pytest.mark.parametrize('params,extensions', [
|
@pytest.mark.parametrize('params,extensions', [
|
||||||
({}, {'impersonate': ImpersonateTarget('chrome')}),
|
|
||||||
({'impersonate': ImpersonateTarget('chrome', '110')}, {}),
|
({'impersonate': ImpersonateTarget('chrome', '110')}, {}),
|
||||||
({'impersonate': ImpersonateTarget('chrome', '99')}, {'impersonate': ImpersonateTarget('chrome', '110')}),
|
({'impersonate': ImpersonateTarget('chrome', '99')}, {'impersonate': ImpersonateTarget('chrome', '110')}),
|
||||||
])
|
])
|
||||||
|
@ -1200,6 +1286,9 @@ class TestRequestHandlerValidation:
|
||||||
({'timeout': 1}, False),
|
({'timeout': 1}, False),
|
||||||
({'timeout': 'notatimeout'}, AssertionError),
|
({'timeout': 'notatimeout'}, AssertionError),
|
||||||
({'unsupported': 'value'}, UnsupportedRequest),
|
({'unsupported': 'value'}, UnsupportedRequest),
|
||||||
|
({'legacy_ssl': False}, False),
|
||||||
|
({'legacy_ssl': True}, False),
|
||||||
|
({'legacy_ssl': 'notabool'}, AssertionError),
|
||||||
]),
|
]),
|
||||||
('Requests', 'http', [
|
('Requests', 'http', [
|
||||||
({'cookiejar': 'notacookiejar'}, AssertionError),
|
({'cookiejar': 'notacookiejar'}, AssertionError),
|
||||||
|
@ -1207,6 +1296,9 @@ class TestRequestHandlerValidation:
|
||||||
({'timeout': 1}, False),
|
({'timeout': 1}, False),
|
||||||
({'timeout': 'notatimeout'}, AssertionError),
|
({'timeout': 'notatimeout'}, AssertionError),
|
||||||
({'unsupported': 'value'}, UnsupportedRequest),
|
({'unsupported': 'value'}, UnsupportedRequest),
|
||||||
|
({'legacy_ssl': False}, False),
|
||||||
|
({'legacy_ssl': True}, False),
|
||||||
|
({'legacy_ssl': 'notabool'}, AssertionError),
|
||||||
]),
|
]),
|
||||||
('CurlCFFI', 'http', [
|
('CurlCFFI', 'http', [
|
||||||
({'cookiejar': 'notacookiejar'}, AssertionError),
|
({'cookiejar': 'notacookiejar'}, AssertionError),
|
||||||
|
@ -1220,6 +1312,9 @@ class TestRequestHandlerValidation:
|
||||||
({'impersonate': ImpersonateTarget(None, None, None, None)}, False),
|
({'impersonate': ImpersonateTarget(None, None, None, None)}, False),
|
||||||
({'impersonate': ImpersonateTarget()}, False),
|
({'impersonate': ImpersonateTarget()}, False),
|
||||||
({'impersonate': 'chrome'}, AssertionError),
|
({'impersonate': 'chrome'}, AssertionError),
|
||||||
|
({'legacy_ssl': False}, False),
|
||||||
|
({'legacy_ssl': True}, False),
|
||||||
|
({'legacy_ssl': 'notabool'}, AssertionError),
|
||||||
]),
|
]),
|
||||||
(NoCheckRH, 'http', [
|
(NoCheckRH, 'http', [
|
||||||
({'cookiejar': 'notacookiejar'}, False),
|
({'cookiejar': 'notacookiejar'}, False),
|
||||||
|
@ -1228,6 +1323,9 @@ class TestRequestHandlerValidation:
|
||||||
('Websockets', 'ws', [
|
('Websockets', 'ws', [
|
||||||
({'cookiejar': YoutubeDLCookieJar()}, False),
|
({'cookiejar': YoutubeDLCookieJar()}, False),
|
||||||
({'timeout': 2}, False),
|
({'timeout': 2}, False),
|
||||||
|
({'legacy_ssl': False}, False),
|
||||||
|
({'legacy_ssl': True}, False),
|
||||||
|
({'legacy_ssl': 'notabool'}, AssertionError),
|
||||||
]),
|
]),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ TEST_DATA_DIR = Path(os.path.dirname(os.path.abspath(__file__)), 'testdata')
|
||||||
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 @@ class TestPlugins(unittest.TestCase):
|
||||||
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,23 @@ import xml.etree.ElementTree
|
||||||
|
|
||||||
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,
|
||||||
|
join_nonempty,
|
||||||
|
str_or_none,
|
||||||
|
)
|
||||||
|
from yt_dlp.utils.traversal import (
|
||||||
|
find_element,
|
||||||
|
find_elements,
|
||||||
|
require,
|
||||||
|
subs_list_to_dict,
|
||||||
|
traverse_obj,
|
||||||
|
trim_str,
|
||||||
|
unpack,
|
||||||
|
)
|
||||||
|
|
||||||
_TEST_DATA = {
|
_TEST_DATA = {
|
||||||
100: 100,
|
100: 100,
|
||||||
|
@ -24,6 +39,14 @@ _TEST_DATA = {
|
||||||
'dict': {},
|
'dict': {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_TEST_HTML = '''<html><body>
|
||||||
|
<div class="a">1</div>
|
||||||
|
<div class="a" id="x" custom="z">2</div>
|
||||||
|
<div class="b" data-id="y" custom="z">3</div>
|
||||||
|
<p class="a">4</p>
|
||||||
|
<p id="d" custom="e">5</p>
|
||||||
|
</body></html>'''
|
||||||
|
|
||||||
|
|
||||||
class TestTraversal:
|
class TestTraversal:
|
||||||
def test_traversal_base(self):
|
def test_traversal_base(self):
|
||||||
|
@ -420,6 +443,138 @@ class TestTraversal:
|
||||||
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', {determine_ext(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'
|
||||||
|
|
||||||
|
def test_trim_str(self):
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
trim_str('positional')
|
||||||
|
|
||||||
|
assert callable(trim_str(start='a'))
|
||||||
|
assert trim_str(start='ab')('abc') == 'c'
|
||||||
|
assert trim_str(end='bc')('abc') == 'a'
|
||||||
|
assert trim_str(start='a', end='c')('abc') == 'b'
|
||||||
|
assert trim_str(start='ab', end='c')('abc') == ''
|
||||||
|
assert trim_str(start='a', end='bc')('abc') == ''
|
||||||
|
assert trim_str(start='ab', end='bc')('abc') == ''
|
||||||
|
assert trim_str(start='abc', end='abc')('abc') == ''
|
||||||
|
assert trim_str(start='', end='')('abc') == 'abc'
|
||||||
|
|
||||||
|
def test_unpack(self):
|
||||||
|
assert unpack(lambda *x: ''.join(map(str, x)))([1, 2, 3]) == '123'
|
||||||
|
assert unpack(join_nonempty)([1, 2, 3]) == '1-2-3'
|
||||||
|
assert unpack(join_nonempty(delim=' '))([1, 2, 3]) == '1 2 3'
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
unpack(join_nonempty)()
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
unpack()
|
||||||
|
|
||||||
|
def test_find_element(self):
|
||||||
|
for improper_kwargs in [
|
||||||
|
dict(attr='data-id'),
|
||||||
|
dict(value='y'),
|
||||||
|
dict(attr='data-id', value='y', cls='a'),
|
||||||
|
dict(attr='data-id', value='y', id='x'),
|
||||||
|
dict(cls='a', id='x'),
|
||||||
|
dict(cls='a', tag='p'),
|
||||||
|
dict(cls='[ab]', regex=True),
|
||||||
|
]:
|
||||||
|
with pytest.raises(AssertionError):
|
||||||
|
find_element(**improper_kwargs)(_TEST_HTML)
|
||||||
|
|
||||||
|
assert find_element(cls='a')(_TEST_HTML) == '1'
|
||||||
|
assert find_element(cls='a', html=True)(_TEST_HTML) == '<div class="a">1</div>'
|
||||||
|
assert find_element(id='x')(_TEST_HTML) == '2'
|
||||||
|
assert find_element(id='[ex]')(_TEST_HTML) is None
|
||||||
|
assert find_element(id='[ex]', regex=True)(_TEST_HTML) == '2'
|
||||||
|
assert find_element(id='x', html=True)(_TEST_HTML) == '<div class="a" id="x" custom="z">2</div>'
|
||||||
|
assert find_element(attr='data-id', value='y')(_TEST_HTML) == '3'
|
||||||
|
assert find_element(attr='data-id', value='y(?:es)?')(_TEST_HTML) is None
|
||||||
|
assert find_element(attr='data-id', value='y(?:es)?', regex=True)(_TEST_HTML) == '3'
|
||||||
|
assert find_element(
|
||||||
|
attr='data-id', value='y', html=True)(_TEST_HTML) == '<div class="b" data-id="y" custom="z">3</div>'
|
||||||
|
|
||||||
|
def test_find_elements(self):
|
||||||
|
for improper_kwargs in [
|
||||||
|
dict(tag='p'),
|
||||||
|
dict(attr='data-id'),
|
||||||
|
dict(value='y'),
|
||||||
|
dict(attr='data-id', value='y', cls='a'),
|
||||||
|
dict(cls='a', tag='div'),
|
||||||
|
dict(cls='[ab]', regex=True),
|
||||||
|
]:
|
||||||
|
with pytest.raises(AssertionError):
|
||||||
|
find_elements(**improper_kwargs)(_TEST_HTML)
|
||||||
|
|
||||||
|
assert find_elements(cls='a')(_TEST_HTML) == ['1', '2', '4']
|
||||||
|
assert find_elements(cls='a', html=True)(_TEST_HTML) == [
|
||||||
|
'<div class="a">1</div>', '<div class="a" id="x" custom="z">2</div>', '<p class="a">4</p>']
|
||||||
|
assert find_elements(attr='custom', value='z')(_TEST_HTML) == ['2', '3']
|
||||||
|
assert find_elements(attr='custom', value='[ez]')(_TEST_HTML) == []
|
||||||
|
assert find_elements(attr='custom', value='[ez]', regex=True)(_TEST_HTML) == ['2', '3', '5']
|
||||||
|
|
||||||
|
|
||||||
class TestDictGet:
|
class TestDictGet:
|
||||||
def test_dict_get(self):
|
def test_dict_get(self):
|
||||||
|
|
|
@ -82,16 +82,32 @@ TEST_LOCKFILE_V1 = rf'''{TEST_LOCKFILE_COMMENT}
|
||||||
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 @@ class TestUpdate(unittest.TestCase):
|
||||||
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()
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import unittest
|
import unittest
|
||||||
|
import unittest.mock
|
||||||
import warnings
|
import warnings
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
|
|
||||||
|
@ -71,6 +72,7 @@ from yt_dlp.utils import (
|
||||||
intlist_to_bytes,
|
intlist_to_bytes,
|
||||||
iri_to_uri,
|
iri_to_uri,
|
||||||
is_html,
|
is_html,
|
||||||
|
join_nonempty,
|
||||||
js_to_json,
|
js_to_json,
|
||||||
limit_length,
|
limit_length,
|
||||||
locked_file,
|
locked_file,
|
||||||
|
@ -130,6 +132,7 @@ from yt_dlp.utils import (
|
||||||
xpath_text,
|
xpath_text,
|
||||||
xpath_with_ns,
|
xpath_with_ns,
|
||||||
)
|
)
|
||||||
|
from yt_dlp.utils._utils import _UnsafeExtensionError
|
||||||
from yt_dlp.utils.networking import (
|
from yt_dlp.utils.networking import (
|
||||||
HTTPHeaderDict,
|
HTTPHeaderDict,
|
||||||
escape_rfc3986,
|
escape_rfc3986,
|
||||||
|
@ -220,9 +223,10 @@ class TestUtil(unittest.TestCase):
|
||||||
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')
|
||||||
|
@ -255,6 +259,11 @@ class TestUtil(unittest.TestCase):
|
||||||
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')
|
||||||
|
@ -281,6 +290,13 @@ class TestUtil(unittest.TestCase):
|
||||||
finally:
|
finally:
|
||||||
os.environ['HOME'] = old_home or ''
|
os.environ['HOME'] = old_home or ''
|
||||||
|
|
||||||
|
_uncommon_extensions = [
|
||||||
|
('exe', 'abc.exe.ext'),
|
||||||
|
('de', 'abc.de.ext'),
|
||||||
|
('../.mp4', None),
|
||||||
|
('..\\.mp4', None),
|
||||||
|
]
|
||||||
|
|
||||||
def test_prepend_extension(self):
|
def test_prepend_extension(self):
|
||||||
self.assertEqual(prepend_extension('abc.ext', 'temp'), 'abc.temp.ext')
|
self.assertEqual(prepend_extension('abc.ext', 'temp'), 'abc.temp.ext')
|
||||||
self.assertEqual(prepend_extension('abc.ext', 'temp', 'ext'), 'abc.temp.ext')
|
self.assertEqual(prepend_extension('abc.ext', 'temp', 'ext'), 'abc.temp.ext')
|
||||||
|
@ -289,6 +305,19 @@ class TestUtil(unittest.TestCase):
|
||||||
self.assertEqual(prepend_extension('.abc', 'temp'), '.abc.temp')
|
self.assertEqual(prepend_extension('.abc', 'temp'), '.abc.temp')
|
||||||
self.assertEqual(prepend_extension('.abc.ext', 'temp'), '.abc.temp.ext')
|
self.assertEqual(prepend_extension('.abc.ext', 'temp'), '.abc.temp.ext')
|
||||||
|
|
||||||
|
# Test uncommon extensions
|
||||||
|
self.assertEqual(prepend_extension('abc.ext', 'bin'), 'abc.bin.ext')
|
||||||
|
for ext, result in self._uncommon_extensions:
|
||||||
|
with self.assertRaises(_UnsafeExtensionError):
|
||||||
|
prepend_extension('abc', ext)
|
||||||
|
if result:
|
||||||
|
self.assertEqual(prepend_extension('abc.ext', ext, 'ext'), result)
|
||||||
|
else:
|
||||||
|
with self.assertRaises(_UnsafeExtensionError):
|
||||||
|
prepend_extension('abc.ext', ext, 'ext')
|
||||||
|
with self.assertRaises(_UnsafeExtensionError):
|
||||||
|
prepend_extension('abc.unexpected_ext', ext, 'ext')
|
||||||
|
|
||||||
def test_replace_extension(self):
|
def test_replace_extension(self):
|
||||||
self.assertEqual(replace_extension('abc.ext', 'temp'), 'abc.temp')
|
self.assertEqual(replace_extension('abc.ext', 'temp'), 'abc.temp')
|
||||||
self.assertEqual(replace_extension('abc.ext', 'temp', 'ext'), 'abc.temp')
|
self.assertEqual(replace_extension('abc.ext', 'temp', 'ext'), 'abc.temp')
|
||||||
|
@ -297,6 +326,16 @@ class TestUtil(unittest.TestCase):
|
||||||
self.assertEqual(replace_extension('.abc', 'temp'), '.abc.temp')
|
self.assertEqual(replace_extension('.abc', 'temp'), '.abc.temp')
|
||||||
self.assertEqual(replace_extension('.abc.ext', 'temp'), '.abc.temp')
|
self.assertEqual(replace_extension('.abc.ext', 'temp'), '.abc.temp')
|
||||||
|
|
||||||
|
# Test uncommon extensions
|
||||||
|
self.assertEqual(replace_extension('abc.ext', 'bin'), 'abc.unknown_video')
|
||||||
|
for ext, _ in self._uncommon_extensions:
|
||||||
|
with self.assertRaises(_UnsafeExtensionError):
|
||||||
|
replace_extension('abc', ext)
|
||||||
|
with self.assertRaises(_UnsafeExtensionError):
|
||||||
|
replace_extension('abc.ext', ext, 'ext')
|
||||||
|
with self.assertRaises(_UnsafeExtensionError):
|
||||||
|
replace_extension('abc.unexpected_ext', ext, 'ext')
|
||||||
|
|
||||||
def test_subtitles_filename(self):
|
def test_subtitles_filename(self):
|
||||||
self.assertEqual(subtitles_filename('abc.ext', 'en', 'vtt'), 'abc.en.vtt')
|
self.assertEqual(subtitles_filename('abc.ext', 'en', 'vtt'), 'abc.en.vtt')
|
||||||
self.assertEqual(subtitles_filename('abc.ext', 'en', 'vtt', 'ext'), 'abc.en.vtt')
|
self.assertEqual(subtitles_filename('abc.ext', 'en', 'vtt', 'ext'), 'abc.en.vtt')
|
||||||
|
@ -306,11 +345,13 @@ class TestUtil(unittest.TestCase):
|
||||||
self.assertEqual(remove_start(None, 'A - '), None)
|
self.assertEqual(remove_start(None, 'A - '), None)
|
||||||
self.assertEqual(remove_start('A - B', 'A - '), 'B')
|
self.assertEqual(remove_start('A - B', 'A - '), 'B')
|
||||||
self.assertEqual(remove_start('B - A', 'A - '), 'B - A')
|
self.assertEqual(remove_start('B - A', 'A - '), 'B - A')
|
||||||
|
self.assertEqual(remove_start('non-empty', ''), 'non-empty')
|
||||||
|
|
||||||
def test_remove_end(self):
|
def test_remove_end(self):
|
||||||
self.assertEqual(remove_end(None, ' - B'), None)
|
self.assertEqual(remove_end(None, ' - B'), None)
|
||||||
self.assertEqual(remove_end('A - B', ' - B'), 'A')
|
self.assertEqual(remove_end('A - B', ' - B'), 'A')
|
||||||
self.assertEqual(remove_end('B - A', ' - B'), 'B - A')
|
self.assertEqual(remove_end('B - A', ' - B'), 'B - A')
|
||||||
|
self.assertEqual(remove_end('non-empty', ''), 'non-empty')
|
||||||
|
|
||||||
def test_remove_quotes(self):
|
def test_remove_quotes(self):
|
||||||
self.assertEqual(remove_quotes(None), None)
|
self.assertEqual(remove_quotes(None), None)
|
||||||
|
@ -413,6 +454,8 @@ class TestUtil(unittest.TestCase):
|
||||||
self.assertEqual(unified_timestamp('Sep 11, 2013 | 5:49 AM'), 1378878540)
|
self.assertEqual(unified_timestamp('Sep 11, 2013 | 5:49 AM'), 1378878540)
|
||||||
self.assertEqual(unified_timestamp('December 15, 2017 at 7:49 am'), 1513324140)
|
self.assertEqual(unified_timestamp('December 15, 2017 at 7:49 am'), 1513324140)
|
||||||
self.assertEqual(unified_timestamp('2018-03-14T08:32:43.1493874+00:00'), 1521016363)
|
self.assertEqual(unified_timestamp('2018-03-14T08:32:43.1493874+00:00'), 1521016363)
|
||||||
|
self.assertEqual(unified_timestamp('Sunday, 26 Nov 2006, 19:00'), 1164567600)
|
||||||
|
self.assertEqual(unified_timestamp('wed, aug 16, 2008, 12:00pm'), 1218931200)
|
||||||
|
|
||||||
self.assertEqual(unified_timestamp('December 31 1969 20:00:01 EDT'), 1)
|
self.assertEqual(unified_timestamp('December 31 1969 20:00:01 EDT'), 1)
|
||||||
self.assertEqual(unified_timestamp('Wednesday 31 December 1969 18:01:26 MDT'), 86)
|
self.assertEqual(unified_timestamp('Wednesday 31 December 1969 18:01:26 MDT'), 86)
|
||||||
|
@ -888,6 +931,11 @@ class TestUtil(unittest.TestCase):
|
||||||
'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',
|
||||||
|
@ -898,6 +946,11 @@ class TestUtil(unittest.TestCase):
|
||||||
'acodec': 'none',
|
'acodec': 'none',
|
||||||
'dynamic_range': 'DV',
|
'dynamic_range': 'DV',
|
||||||
})
|
})
|
||||||
|
self.assertEqual(parse_codecs('fLaC'), {
|
||||||
|
'vcodec': 'none',
|
||||||
|
'acodec': 'flac',
|
||||||
|
'dynamic_range': None,
|
||||||
|
})
|
||||||
self.assertEqual(parse_codecs('theora, vorbis'), {
|
self.assertEqual(parse_codecs('theora, vorbis'), {
|
||||||
'vcodec': 'theora',
|
'vcodec': 'theora',
|
||||||
'acodec': 'vorbis',
|
'acodec': 'vorbis',
|
||||||
|
@ -2099,6 +2152,16 @@ Line 1
|
||||||
assert run_shell(args) == expected
|
assert run_shell(args) == expected
|
||||||
assert run_shell(shell_quote(args, shell=True)) == expected
|
assert run_shell(shell_quote(args, shell=True)) == expected
|
||||||
|
|
||||||
|
def test_partial_application(self):
|
||||||
|
assert callable(int_or_none(scale=10)), 'missing positional parameter should apply partially'
|
||||||
|
assert int_or_none(10, scale=0.1) == 100, 'positionally passed argument should call function'
|
||||||
|
assert int_or_none(v=10) == 10, 'keyword passed positional should call function'
|
||||||
|
assert int_or_none(scale=0.1)(10) == 100, 'call after partial application should call the function'
|
||||||
|
|
||||||
|
assert callable(join_nonempty(delim=', ')), 'varargs positional should apply partially'
|
||||||
|
assert callable(join_nonempty()), 'varargs positional should apply partially'
|
||||||
|
assert join_nonempty(None, delim=', ') == '', 'passed varargs should call the function'
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
@ -61,6 +61,10 @@ def process_request(self, request):
|
||||||
return websockets.http11.Response(
|
return websockets.http11.Response(
|
||||||
status.value, status.phrase, websockets.datastructures.Headers([('Location', '/')]), b'')
|
status.value, status.phrase, websockets.datastructures.Headers([('Location', '/')]), b'')
|
||||||
return self.protocol.reject(status.value, status.phrase)
|
return self.protocol.reject(status.value, status.phrase)
|
||||||
|
elif request.path.startswith('/get_cookie'):
|
||||||
|
response = self.protocol.accept(request)
|
||||||
|
response.headers['Set-Cookie'] = 'test=ytdlp'
|
||||||
|
return response
|
||||||
return self.protocol.accept(request)
|
return self.protocol.accept(request)
|
||||||
|
|
||||||
|
|
||||||
|
@ -84,7 +88,7 @@ def create_wss_websocket_server():
|
||||||
certfn = os.path.join(TEST_DIR, 'testcert.pem')
|
certfn = os.path.join(TEST_DIR, 'testcert.pem')
|
||||||
sslctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
sslctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||||
sslctx.load_cert_chain(certfn, None)
|
sslctx.load_cert_chain(certfn, None)
|
||||||
return create_websocket_server(ssl_context=sslctx)
|
return create_websocket_server(ssl=sslctx)
|
||||||
|
|
||||||
|
|
||||||
MTLS_CERT_DIR = os.path.join(TEST_DIR, 'testdata', 'certificate')
|
MTLS_CERT_DIR = os.path.join(TEST_DIR, 'testdata', 'certificate')
|
||||||
|
@ -99,7 +103,16 @@ def create_mtls_wss_websocket_server():
|
||||||
sslctx.load_verify_locations(cafile=cacertfn)
|
sslctx.load_verify_locations(cafile=cacertfn)
|
||||||
sslctx.load_cert_chain(certfn, None)
|
sslctx.load_cert_chain(certfn, None)
|
||||||
|
|
||||||
return create_websocket_server(ssl_context=sslctx)
|
return create_websocket_server(ssl=sslctx)
|
||||||
|
|
||||||
|
|
||||||
|
def create_legacy_wss_websocket_server():
|
||||||
|
certfn = os.path.join(TEST_DIR, 'testcert.pem')
|
||||||
|
sslctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||||
|
sslctx.maximum_version = ssl.TLSVersion.TLSv1_2
|
||||||
|
sslctx.set_ciphers('SHA1:AESCCM:aDSS:eNULL:aNULL')
|
||||||
|
sslctx.load_cert_chain(certfn, None)
|
||||||
|
return create_websocket_server(ssl=sslctx)
|
||||||
|
|
||||||
|
|
||||||
def ws_validate_and_send(rh, req):
|
def ws_validate_and_send(rh, req):
|
||||||
|
@ -126,12 +139,15 @@ class TestWebsSocketRequestHandlerConformance:
|
||||||
cls.wss_thread, cls.wss_port = create_wss_websocket_server()
|
cls.wss_thread, cls.wss_port = create_wss_websocket_server()
|
||||||
cls.wss_base_url = f'wss://127.0.0.1:{cls.wss_port}'
|
cls.wss_base_url = f'wss://127.0.0.1:{cls.wss_port}'
|
||||||
|
|
||||||
cls.bad_wss_thread, cls.bad_wss_port = create_websocket_server(ssl_context=ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER))
|
cls.bad_wss_thread, cls.bad_wss_port = create_websocket_server(ssl=ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER))
|
||||||
cls.bad_wss_host = f'wss://127.0.0.1:{cls.bad_wss_port}'
|
cls.bad_wss_host = f'wss://127.0.0.1:{cls.bad_wss_port}'
|
||||||
|
|
||||||
cls.mtls_wss_thread, cls.mtls_wss_port = create_mtls_wss_websocket_server()
|
cls.mtls_wss_thread, cls.mtls_wss_port = create_mtls_wss_websocket_server()
|
||||||
cls.mtls_wss_base_url = f'wss://127.0.0.1:{cls.mtls_wss_port}'
|
cls.mtls_wss_base_url = f'wss://127.0.0.1:{cls.mtls_wss_port}'
|
||||||
|
|
||||||
|
cls.legacy_wss_thread, cls.legacy_wss_port = create_legacy_wss_websocket_server()
|
||||||
|
cls.legacy_wss_host = f'wss://127.0.0.1:{cls.legacy_wss_port}'
|
||||||
|
|
||||||
def test_basic_websockets(self, handler):
|
def test_basic_websockets(self, handler):
|
||||||
with handler() as rh:
|
with handler() as rh:
|
||||||
ws = ws_validate_and_send(rh, Request(self.ws_base_url))
|
ws = ws_validate_and_send(rh, Request(self.ws_base_url))
|
||||||
|
@ -166,6 +182,22 @@ class TestWebsSocketRequestHandlerConformance:
|
||||||
ws_validate_and_send(rh, Request(self.bad_wss_host))
|
ws_validate_and_send(rh, Request(self.bad_wss_host))
|
||||||
assert not issubclass(exc_info.type, CertificateVerifyError)
|
assert not issubclass(exc_info.type, CertificateVerifyError)
|
||||||
|
|
||||||
|
def test_legacy_ssl_extension(self, handler):
|
||||||
|
with handler(verify=False) as rh:
|
||||||
|
ws = ws_validate_and_send(rh, Request(self.legacy_wss_host, extensions={'legacy_ssl': True}))
|
||||||
|
assert ws.status == 101
|
||||||
|
ws.close()
|
||||||
|
|
||||||
|
# Ensure only applies to request extension
|
||||||
|
with pytest.raises(SSLError):
|
||||||
|
ws_validate_and_send(rh, Request(self.legacy_wss_host))
|
||||||
|
|
||||||
|
def test_legacy_ssl_support(self, handler):
|
||||||
|
with handler(verify=False, legacy_ssl_support=True) as rh:
|
||||||
|
ws = ws_validate_and_send(rh, Request(self.legacy_wss_host))
|
||||||
|
assert ws.status == 101
|
||||||
|
ws.close()
|
||||||
|
|
||||||
@pytest.mark.parametrize('path,expected', [
|
@pytest.mark.parametrize('path,expected', [
|
||||||
# Unicode characters should be encoded with uppercase percent-encoding
|
# Unicode characters should be encoded with uppercase percent-encoding
|
||||||
('/中文', '/%E4%B8%AD%E6%96%87'),
|
('/中文', '/%E4%B8%AD%E6%96%87'),
|
||||||
|
@ -248,6 +280,32 @@ class TestWebsSocketRequestHandlerConformance:
|
||||||
assert json.loads(ws.recv())['cookie'] == 'test=ytdlp'
|
assert json.loads(ws.recv())['cookie'] == 'test=ytdlp'
|
||||||
ws.close()
|
ws.close()
|
||||||
|
|
||||||
|
@pytest.mark.skip_handler('Websockets', 'Set-Cookie not supported by websockets')
|
||||||
|
def test_cookie_sync_only_cookiejar(self, handler):
|
||||||
|
# Ensure that cookies are ONLY being handled by the cookiejar
|
||||||
|
with handler() as rh:
|
||||||
|
ws_validate_and_send(rh, Request(f'{self.ws_base_url}/get_cookie', extensions={'cookiejar': YoutubeDLCookieJar()}))
|
||||||
|
ws = ws_validate_and_send(rh, Request(self.ws_base_url, extensions={'cookiejar': YoutubeDLCookieJar()}))
|
||||||
|
ws.send('headers')
|
||||||
|
assert 'cookie' not in json.loads(ws.recv())
|
||||||
|
ws.close()
|
||||||
|
|
||||||
|
@pytest.mark.skip_handler('Websockets', 'Set-Cookie not supported by websockets')
|
||||||
|
def test_cookie_sync_delete_cookie(self, handler):
|
||||||
|
# Ensure that cookies are ONLY being handled by the cookiejar
|
||||||
|
cookiejar = YoutubeDLCookieJar()
|
||||||
|
with handler(verbose=True, cookiejar=cookiejar) as rh:
|
||||||
|
ws_validate_and_send(rh, Request(f'{self.ws_base_url}/get_cookie'))
|
||||||
|
ws = ws_validate_and_send(rh, Request(self.ws_base_url))
|
||||||
|
ws.send('headers')
|
||||||
|
assert json.loads(ws.recv())['cookie'] == 'test=ytdlp'
|
||||||
|
ws.close()
|
||||||
|
cookiejar.clear_session_cookies()
|
||||||
|
ws = ws_validate_and_send(rh, Request(self.ws_base_url))
|
||||||
|
ws.send('headers')
|
||||||
|
assert 'cookie' not in json.loads(ws.recv())
|
||||||
|
ws.close()
|
||||||
|
|
||||||
def test_source_address(self, handler):
|
def test_source_address(self, handler):
|
||||||
source_address = f'127.0.0.{random.randint(5, 255)}'
|
source_address = f'127.0.0.{random.randint(5, 255)}'
|
||||||
verify_address_availability(source_address)
|
verify_address_availability(source_address)
|
||||||
|
|
|
@ -163,6 +163,26 @@ _NSIG_TESTS = [
|
||||||
'https://www.youtube.com/s/player/b7910ca8/player_ias.vflset/en_US/base.js',
|
'https://www.youtube.com/s/player/b7910ca8/player_ias.vflset/en_US/base.js',
|
||||||
'_hXMCwMt9qE310D', 'LoZMgkkofRMCZQ',
|
'_hXMCwMt9qE310D', 'LoZMgkkofRMCZQ',
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
'https://www.youtube.com/s/player/590f65a6/player_ias.vflset/en_US/base.js',
|
||||||
|
'1tm7-g_A9zsI8_Lay_', 'xI4Vem4Put_rOg',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'https://www.youtube.com/s/player/b22ef6e7/player_ias.vflset/en_US/base.js',
|
||||||
|
'b6HcntHGkvBLk_FRf', 'kNPW6A7FyP2l8A',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'https://www.youtube.com/s/player/3400486c/player_ias.vflset/en_US/base.js',
|
||||||
|
'lL46g3XifCKUZn1Xfw', 'z767lhet6V2Skl',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'https://www.youtube.com/s/player/20dfca59/player_ias.vflset/en_US/base.js',
|
||||||
|
'-fLCxedkAk4LUTK2', 'O8kfRq1y1eyHGw',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'https://www.youtube.com/s/player/b12cc44b/player_ias.vflset/en_US/base.js',
|
||||||
|
'keLa5R2U00sR9SQK', 'N1OGyujjEwMnLw',
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
4
test/testdata/netrc/netrc
vendored
Normal file
4
test/testdata/netrc/netrc
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
machine normal_use login user password pass
|
||||||
|
machine empty_user login "" password pass
|
||||||
|
machine empty_pass login user password ""
|
||||||
|
machine both_empty login "" password ""
|
2
test/testdata/netrc/print_netrc.py
vendored
Normal file
2
test/testdata/netrc/print_netrc.py
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
with open('./test/testdata/netrc/netrc', encoding='utf-8') as fp:
|
||||||
|
print(fp.read())
|
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
|
|
@ -4,6 +4,7 @@ import copy
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
import errno
|
import errno
|
||||||
import fileinput
|
import fileinput
|
||||||
|
import functools
|
||||||
import http.cookiejar
|
import http.cookiejar
|
||||||
import io
|
import io
|
||||||
import itertools
|
import itertools
|
||||||
|
@ -24,9 +25,9 @@ import traceback
|
||||||
import unicodedata
|
import unicodedata
|
||||||
|
|
||||||
from .cache import Cache
|
from .cache import Cache
|
||||||
from .compat import functools, 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
|
||||||
|
@ -153,12 +154,11 @@ from .utils import (
|
||||||
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,
|
||||||
)
|
)
|
||||||
from .utils._utils import _YDLLogger
|
from .utils._utils import _UnsafeExtensionError, _YDLLogger
|
||||||
from .utils.networking import (
|
from .utils.networking import (
|
||||||
HTTPHeaderDict,
|
HTTPHeaderDict,
|
||||||
clean_headers,
|
clean_headers,
|
||||||
|
@ -171,6 +171,20 @@ if compat_os_name == 'nt':
|
||||||
import ctypes
|
import ctypes
|
||||||
|
|
||||||
|
|
||||||
|
def _catch_unsafe_extension_error(func):
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapper(self, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
return func(self, *args, **kwargs)
|
||||||
|
except _UnsafeExtensionError as error:
|
||||||
|
self.report_error(
|
||||||
|
f'The extracted extension ({error.extension!r}) is unusual '
|
||||||
|
'and will be skipped for safety reasons. '
|
||||||
|
f'If you believe this is an error{bug_reports_message(",")}')
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
class YoutubeDL:
|
class YoutubeDL:
|
||||||
"""YoutubeDL class.
|
"""YoutubeDL class.
|
||||||
|
|
||||||
|
@ -236,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
|
||||||
|
@ -270,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
|
||||||
|
@ -437,7 +451,8 @@ class YoutubeDL:
|
||||||
Can also just be a single color policy,
|
Can also just be a single color policy,
|
||||||
in which case it applies to all outputs.
|
in which case it applies to all outputs.
|
||||||
Valid stream names are 'stdout' and 'stderr'.
|
Valid stream names are 'stdout' and 'stderr'.
|
||||||
Valid color policies are one of 'always', 'auto', 'no_color' or 'never'.
|
Valid color policies are one of 'always', 'auto',
|
||||||
|
'no_color', 'never', 'auto-tty' or 'no_color-tty'.
|
||||||
geo_bypass: Bypass geographic restriction via faking X-Forwarded-For
|
geo_bypass: Bypass geographic restriction via faking X-Forwarded-For
|
||||||
HTTP header
|
HTTP header
|
||||||
geo_bypass_country:
|
geo_bypass_country:
|
||||||
|
@ -453,8 +468,9 @@ class YoutubeDL:
|
||||||
Set the value to 'native' to use the native downloader
|
Set the value to 'native' to use the native downloader
|
||||||
compat_opts: Compatibility options. See "Differences in default behavior".
|
compat_opts: Compatibility options. See "Differences in default behavior".
|
||||||
The following options do not work when used through the API:
|
The following options do not work when used through the API:
|
||||||
filename, abort-on-error, multistreams, no-live-chat, format-sort
|
filename, abort-on-error, multistreams, no-live-chat,
|
||||||
no-clean-infojson, no-playlist-metafiles, no-keep-subs, no-attach-info-json.
|
format-sort, no-clean-infojson, no-playlist-metafiles,
|
||||||
|
no-keep-subs, no-attach-info-json, allow-unsafe-ext, prefer-vp9-sort.
|
||||||
Refer __init__.py for their implementation
|
Refer __init__.py for their implementation
|
||||||
progress_template: Dictionary of templates for progress outputs.
|
progress_template: Dictionary of templates for progress outputs.
|
||||||
Allowed keys are 'download', 'postprocess',
|
Allowed keys are 'download', 'postprocess',
|
||||||
|
@ -496,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.
|
||||||
|
@ -536,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
|
||||||
|
@ -581,8 +597,9 @@ class YoutubeDL:
|
||||||
'vbr', 'fps', 'vcodec', 'container', 'filesize', 'filesize_approx', 'rows', 'columns',
|
'vbr', 'fps', 'vcodec', 'container', 'filesize', 'filesize_approx', 'rows', 'columns',
|
||||||
'player_url', 'protocol', 'fragment_base_url', 'fragments', 'is_from_start', 'is_dash_periods', 'request_data',
|
'player_url', 'protocol', 'fragment_base_url', 'fragments', 'is_from_start', 'is_dash_periods', 'request_data',
|
||||||
'preference', 'language', 'language_preference', 'quality', 'source_preference', 'cookies',
|
'preference', 'language', 'language_preference', 'quality', 'source_preference', 'cookies',
|
||||||
'http_headers', 'stretched_ratio', 'no_resume', 'has_drm', 'extra_param_to_segment_url', 'hls_aes', 'downloader_options',
|
'http_headers', 'stretched_ratio', 'no_resume', 'has_drm', 'extra_param_to_segment_url', 'extra_param_to_key_url',
|
||||||
'page_url', 'app', 'play_path', 'tc_url', 'flash_version', 'rtmp_live', 'rtmp_conn', 'rtmp_protocol', 'rtmp_real_time',
|
'hls_aes', 'downloader_options', 'page_url', 'app', 'play_path', 'tc_url', 'flash_version',
|
||||||
|
'rtmp_live', 'rtmp_conn', 'rtmp_protocol', 'rtmp_real_time',
|
||||||
}
|
}
|
||||||
_deprecated_multivalue_fields = {
|
_deprecated_multivalue_fields = {
|
||||||
'album_artist': 'album_artists',
|
'album_artist': 'album_artists',
|
||||||
|
@ -642,12 +659,15 @@ class YoutubeDL:
|
||||||
self.params['color'] = 'no_color'
|
self.params['color'] = 'no_color'
|
||||||
|
|
||||||
term_allow_color = os.getenv('TERM', '').lower() != 'dumb'
|
term_allow_color = os.getenv('TERM', '').lower() != 'dumb'
|
||||||
no_color = bool(os.getenv('NO_COLOR'))
|
base_no_color = bool(os.getenv('NO_COLOR'))
|
||||||
|
|
||||||
def process_color_policy(stream):
|
def process_color_policy(stream):
|
||||||
stream_name = {sys.stdout: 'stdout', sys.stderr: 'stderr'}[stream]
|
stream_name = {sys.stdout: 'stdout', sys.stderr: 'stderr'}[stream]
|
||||||
policy = traverse_obj(self.params, ('color', (stream_name, None), {str}), get_all=False)
|
policy = traverse_obj(self.params, ('color', (stream_name, None), {str}, any)) or 'auto'
|
||||||
if policy in ('auto', None):
|
if policy in ('auto', 'auto-tty', 'no_color-tty'):
|
||||||
|
no_color = base_no_color
|
||||||
|
if policy.endswith('tty'):
|
||||||
|
no_color = policy.startswith('no_color')
|
||||||
if term_allow_color and supports_terminal_sequences(stream):
|
if term_allow_color and supports_terminal_sequences(stream):
|
||||||
return 'no_color' if no_color else True
|
return 'no_color' if no_color else True
|
||||||
return False
|
return False
|
||||||
|
@ -1398,6 +1418,7 @@ class YoutubeDL:
|
||||||
outtmpl, info_dict = self.prepare_outtmpl(outtmpl, info_dict, *args, **kwargs)
|
outtmpl, info_dict = self.prepare_outtmpl(outtmpl, info_dict, *args, **kwargs)
|
||||||
return self.escape_outtmpl(outtmpl) % info_dict
|
return self.escape_outtmpl(outtmpl) % info_dict
|
||||||
|
|
||||||
|
@_catch_unsafe_extension_error
|
||||||
def _prepare_filename(self, info_dict, *, outtmpl=None, tmpl_type=None):
|
def _prepare_filename(self, info_dict, *, outtmpl=None, tmpl_type=None):
|
||||||
assert None in (outtmpl, tmpl_type), 'outtmpl and tmpl_type are mutually exclusive'
|
assert None in (outtmpl, tmpl_type), 'outtmpl and tmpl_type are mutually exclusive'
|
||||||
if outtmpl is None:
|
if outtmpl is None:
|
||||||
|
@ -1602,7 +1623,7 @@ class YoutubeDL:
|
||||||
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:
|
||||||
|
@ -1925,6 +1946,8 @@ class YoutubeDL:
|
||||||
'playlist_title': ie_result.get('title'),
|
'playlist_title': ie_result.get('title'),
|
||||||
'playlist_uploader': ie_result.get('uploader'),
|
'playlist_uploader': ie_result.get('uploader'),
|
||||||
'playlist_uploader_id': ie_result.get('uploader_id'),
|
'playlist_uploader_id': ie_result.get('uploader_id'),
|
||||||
|
'playlist_channel': ie_result.get('channel'),
|
||||||
|
'playlist_channel_id': ie_result.get('channel_id'),
|
||||||
**kwargs,
|
**kwargs,
|
||||||
}
|
}
|
||||||
if strict:
|
if strict:
|
||||||
|
@ -2170,9 +2193,8 @@ class YoutubeDL:
|
||||||
or all(f.get('acodec') == 'none' for f in formats)), # OR, No formats with audio
|
or all(f.get('acodec') == 'none' for f in formats)), # OR, No formats with audio
|
||||||
}))
|
}))
|
||||||
|
|
||||||
def _default_format_spec(self, info_dict, download=True):
|
def _default_format_spec(self, info_dict):
|
||||||
download = download and not self.params.get('simulate')
|
prefer_best = (
|
||||||
prefer_best = download and (
|
|
||||||
self.params['outtmpl']['default'] == '-'
|
self.params['outtmpl']['default'] == '-'
|
||||||
or info_dict.get('is_live') and not self.params.get('live_from_start'))
|
or info_dict.get('is_live') and not self.params.get('live_from_start'))
|
||||||
|
|
||||||
|
@ -2180,7 +2202,7 @@ class YoutubeDL:
|
||||||
merger = FFmpegMergerPP(self)
|
merger = FFmpegMergerPP(self)
|
||||||
return merger.available and merger.can_merge()
|
return merger.available and merger.can_merge()
|
||||||
|
|
||||||
if not prefer_best and download and not can_merge():
|
if not prefer_best and not can_merge():
|
||||||
prefer_best = True
|
prefer_best = True
|
||||||
formats = self._get_formats(info_dict)
|
formats = self._get_formats(info_dict)
|
||||||
evaluate_formats = lambda spec: self._select_formats(formats, self.build_format_selector(spec))
|
evaluate_formats = lambda spec: self._select_formats(formats, self.build_format_selector(spec))
|
||||||
|
@ -2827,13 +2849,10 @@ class YoutubeDL:
|
||||||
sanitize_string_field(fmt, 'format_id')
|
sanitize_string_field(fmt, 'format_id')
|
||||||
sanitize_numeric_fields(fmt)
|
sanitize_numeric_fields(fmt)
|
||||||
fmt['url'] = sanitize_url(fmt['url'])
|
fmt['url'] = sanitize_url(fmt['url'])
|
||||||
if fmt.get('ext') is None:
|
FormatSorter._fill_sorting_fields(fmt)
|
||||||
fmt['ext'] = determine_ext(fmt['url']).lower()
|
|
||||||
if fmt['ext'] in ('aac', 'opus', 'mp3', 'flac', 'vorbis'):
|
if fmt['ext'] in ('aac', 'opus', 'mp3', 'flac', 'vorbis'):
|
||||||
if fmt.get('acodec') is None:
|
if fmt.get('acodec') is None:
|
||||||
fmt['acodec'] = fmt['ext']
|
fmt['acodec'] = fmt['ext']
|
||||||
if fmt.get('protocol') is None:
|
|
||||||
fmt['protocol'] = determine_protocol(fmt)
|
|
||||||
if fmt.get('resolution') is None:
|
if fmt.get('resolution') is None:
|
||||||
fmt['resolution'] = self.format_resolution(fmt, default=None)
|
fmt['resolution'] = self.format_resolution(fmt, default=None)
|
||||||
if fmt.get('dynamic_range') is None and fmt.get('vcodec') != 'none':
|
if fmt.get('dynamic_range') is None and fmt.get('vcodec') != 'none':
|
||||||
|
@ -2939,7 +2958,7 @@ class YoutubeDL:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if format_selector is None:
|
if format_selector is None:
|
||||||
req_format = self._default_format_spec(info_dict, download=download)
|
req_format = self._default_format_spec(info_dict)
|
||||||
self.write_debug(f'Default format spec: {req_format}')
|
self.write_debug(f'Default format spec: {req_format}')
|
||||||
format_selector = self.build_format_selector(req_format)
|
format_selector = self.build_format_selector(req_format)
|
||||||
|
|
||||||
|
@ -3149,11 +3168,12 @@ class YoutubeDL:
|
||||||
|
|
||||||
if test:
|
if test:
|
||||||
verbose = self.params.get('verbose')
|
verbose = self.params.get('verbose')
|
||||||
|
quiet = self.params.get('quiet') or not verbose
|
||||||
params = {
|
params = {
|
||||||
'test': True,
|
'test': True,
|
||||||
'quiet': self.params.get('quiet') or not verbose,
|
'quiet': quiet,
|
||||||
'verbose': verbose,
|
'verbose': verbose,
|
||||||
'noprogress': not verbose,
|
'noprogress': quiet,
|
||||||
'nopart': True,
|
'nopart': True,
|
||||||
'skip_unavailable_fragments': False,
|
'skip_unavailable_fragments': False,
|
||||||
'keep_fragments': False,
|
'keep_fragments': False,
|
||||||
|
@ -3188,6 +3208,7 @@ class YoutubeDL:
|
||||||
os.remove(file)
|
os.remove(file)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@_catch_unsafe_extension_error
|
||||||
def process_info(self, info_dict):
|
def process_info(self, info_dict):
|
||||||
"""Process a single resolved IE result. (Modifies it in-place)"""
|
"""Process a single resolved IE result. (Modifies it in-place)"""
|
||||||
|
|
||||||
|
@ -3555,6 +3576,8 @@ class YoutubeDL:
|
||||||
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:
|
||||||
|
@ -4043,6 +4066,10 @@ class YoutubeDL:
|
||||||
|
|
||||||
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}')
|
||||||
|
@ -4058,17 +4085,6 @@ class YoutubeDL:
|
||||||
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"""
|
||||||
|
@ -4088,8 +4104,14 @@ class YoutubeDL:
|
||||||
@functools.cached_property
|
@functools.cached_property
|
||||||
def cookiejar(self):
|
def cookiejar(self):
|
||||||
"""Global cookiejar instance"""
|
"""Global cookiejar instance"""
|
||||||
return load_cookies(
|
try:
|
||||||
self.params.get('cookiefile'), self.params.get('cookiesfrombrowser'), self)
|
return load_cookies(
|
||||||
|
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 re
|
||||||
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 .postprocessor import (
|
||||||
)
|
)
|
||||||
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,
|
||||||
|
@ -64,6 +65,7 @@ from .utils import (
|
||||||
write_string,
|
write_string,
|
||||||
)
|
)
|
||||||
from .utils.networking import std_headers
|
from .utils.networking import std_headers
|
||||||
|
from .utils._utils import _UnsafeExtensionError
|
||||||
from .YoutubeDL import YoutubeDL
|
from .YoutubeDL import YoutubeDL
|
||||||
|
|
||||||
_IN_CLI = False
|
_IN_CLI = False
|
||||||
|
@ -157,6 +159,9 @@ def set_compat_opts(opts):
|
||||||
opts.embed_infojson = False
|
opts.embed_infojson = False
|
||||||
if 'format-sort' in opts.compat_opts:
|
if 'format-sort' in opts.compat_opts:
|
||||||
opts.format_sort.extend(FormatSorter.ytdl_default)
|
opts.format_sort.extend(FormatSorter.ytdl_default)
|
||||||
|
elif 'prefer-vp9-sort' in opts.compat_opts:
|
||||||
|
opts.format_sort.extend(FormatSorter._prefer_vp9_sort)
|
||||||
|
|
||||||
_video_multistreams_set = set_default_compat('multistreams', 'allow_multiple_video_streams', False, remove_compat=False)
|
_video_multistreams_set = set_default_compat('multistreams', 'allow_multiple_video_streams', False, remove_compat=False)
|
||||||
_audio_multistreams_set = set_default_compat('multistreams', 'allow_multiple_audio_streams', False, remove_compat=False)
|
_audio_multistreams_set = set_default_compat('multistreams', 'allow_multiple_audio_streams', False, remove_compat=False)
|
||||||
if _video_multistreams_set is False and _audio_multistreams_set is False:
|
if _video_multistreams_set is False and _audio_multistreams_set is False:
|
||||||
|
@ -234,6 +239,11 @@ def validate_options(opts):
|
||||||
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)
|
||||||
|
@ -467,7 +477,7 @@ def validate_options(opts):
|
||||||
default_downloader = ed.get_basename()
|
default_downloader = ed.get_basename()
|
||||||
|
|
||||||
for policy in opts.color.values():
|
for policy in opts.color.values():
|
||||||
if policy not in ('always', 'auto', 'no_color', 'never'):
|
if policy not in ('always', 'auto', 'auto-tty', 'no_color', 'no_color-tty', 'never'):
|
||||||
raise ValueError(f'"{policy}" is not a valid color policy')
|
raise ValueError(f'"{policy}" is not a valid color policy')
|
||||||
|
|
||||||
warnings, deprecation_warnings = [], []
|
warnings, deprecation_warnings = [], []
|
||||||
|
@ -593,6 +603,13 @@ def validate_options(opts):
|
||||||
if opts.ap_username is not None and opts.ap_password is None:
|
if opts.ap_username is not None and opts.ap_password is None:
|
||||||
opts.ap_password = getpass.getpass('Type TV provider account password and press [Return]: ')
|
opts.ap_password = getpass.getpass('Type TV provider account password and press [Return]: ')
|
||||||
|
|
||||||
|
# compat option changes global state destructively; only allow from cli
|
||||||
|
if 'allow-unsafe-ext' in opts.compat_opts:
|
||||||
|
warnings.append(
|
||||||
|
'Using allow-unsafe-ext opens you up to potential attacks. '
|
||||||
|
'Use with great care!')
|
||||||
|
_UnsafeExtensionError.sanitize_extension = lambda x, prepend=False: x
|
||||||
|
|
||||||
return warnings, deprecation_warnings
|
return warnings, deprecation_warnings
|
||||||
|
|
||||||
|
|
||||||
|
@ -954,6 +971,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'])
|
||||||
|
@ -1071,7 +1093,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}')
|
||||||
|
|
|
@ -230,11 +230,11 @@ def aes_gcm_decrypt_and_verify(data, key, tag, nonce):
|
||||||
iv_ctr = inc(j0)
|
iv_ctr = inc(j0)
|
||||||
|
|
||||||
decrypted_data = aes_ctr_decrypt(data, key, iv_ctr + [0] * (BLOCK_SIZE_BYTES - len(iv_ctr)))
|
decrypted_data = aes_ctr_decrypt(data, key, iv_ctr + [0] * (BLOCK_SIZE_BYTES - len(iv_ctr)))
|
||||||
pad_len = len(data) // 16 * 16
|
pad_len = (BLOCK_SIZE_BYTES - (len(data) % BLOCK_SIZE_BYTES)) % BLOCK_SIZE_BYTES
|
||||||
s_tag = ghash(
|
s_tag = ghash(
|
||||||
hash_subkey,
|
hash_subkey,
|
||||||
data
|
data
|
||||||
+ [0] * (BLOCK_SIZE_BYTES - len(data) + pad_len) # pad
|
+ [0] * pad_len # pad
|
||||||
+ bytes_to_intlist((0 * 8).to_bytes(8, 'big') # length of associated data
|
+ bytes_to_intlist((0 * 8).to_bytes(8, 'big') # length of associated data
|
||||||
+ ((len(data) * 8).to_bytes(8, 'big'))), # length of data
|
+ ((len(data) * 8).to_bytes(8, 'big'))), # length of data
|
||||||
)
|
)
|
||||||
|
|
|
@ -57,7 +57,7 @@ def passthrough_module(parent, child, allowed_attributes=(..., ), *, callback=la
|
||||||
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 @@ from .compat_utils import passthrough_module
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
|
@ -1,16 +1,22 @@
|
||||||
tests = {
|
|
||||||
'webp': lambda h: h[0:4] == b'RIFF' and h[8:] == b'WEBP',
|
|
||||||
'png': lambda h: h[:8] == b'\211PNG\r\n\032\n',
|
|
||||||
'jpeg': lambda h: h[6:10] in (b'JFIF', b'Exif'),
|
|
||||||
'gif': lambda h: h[:6] in (b'GIF87a', b'GIF89a'),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def what(file=None, h=None):
|
def what(file=None, h=None):
|
||||||
"""Detect format of image (Currently supports jpeg, png, webp, gif only)
|
"""Detect format of image (Currently supports jpeg, png, webp, gif only)
|
||||||
Ref: https://github.com/python/cpython/blob/3.10/Lib/imghdr.py
|
Ref: https://github.com/python/cpython/blob/3.11/Lib/imghdr.py
|
||||||
|
Ref: https://www.w3.org/Graphics/JPEG/itu-t81.pdf
|
||||||
"""
|
"""
|
||||||
if h is None:
|
if h is None:
|
||||||
with open(file, 'rb') as f:
|
with open(file, 'rb') as f:
|
||||||
h = f.read(12)
|
h = f.read(12)
|
||||||
return next((type_ for type_, test in tests.items() if test(h)), None)
|
|
||||||
|
if h.startswith(b'RIFF') and h.startswith(b'WEBP', 8):
|
||||||
|
return 'webp'
|
||||||
|
|
||||||
|
if h.startswith(b'\x89PNG'):
|
||||||
|
return 'png'
|
||||||
|
|
||||||
|
if h.startswith(b'\xFF\xD8\xFF'):
|
||||||
|
return 'jpeg'
|
||||||
|
|
||||||
|
if h.startswith(b'GIF'):
|
||||||
|
return 'gif'
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
|
@ -2,7 +2,9 @@ import base64
|
||||||
import collections
|
import collections
|
||||||
import contextlib
|
import contextlib
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
|
import functools
|
||||||
import glob
|
import glob
|
||||||
|
import hashlib
|
||||||
import http.cookiejar
|
import http.cookiejar
|
||||||
import http.cookies
|
import http.cookies
|
||||||
import io
|
import io
|
||||||
|
@ -17,14 +19,12 @@ import tempfile
|
||||||
import time
|
import time
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
from hashlib import pbkdf2_hmac
|
|
||||||
|
|
||||||
from .aes import (
|
from .aes import (
|
||||||
aes_cbc_decrypt_bytes,
|
aes_cbc_decrypt_bytes,
|
||||||
aes_gcm_decrypt_and_verify_bytes,
|
aes_gcm_decrypt_and_verify_bytes,
|
||||||
unpad_pkcs7,
|
unpad_pkcs7,
|
||||||
)
|
)
|
||||||
from .compat import functools # isort: split
|
|
||||||
from .compat import compat_os_name
|
from .compat import compat_os_name
|
||||||
from .dependencies import (
|
from .dependencies import (
|
||||||
_SECRETSTORAGE_UNAVAILABLE_REASON,
|
_SECRETSTORAGE_UNAVAILABLE_REASON,
|
||||||
|
@ -34,6 +34,7 @@ from .dependencies import (
|
||||||
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,24 +87,31 @@ 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):
|
||||||
cookie_jars = []
|
try:
|
||||||
if browser_specification is not None:
|
cookie_jars = []
|
||||||
browser_name, profile, keyring, container = _parse_browser_specification(*browser_specification)
|
if browser_specification is not None:
|
||||||
cookie_jars.append(
|
browser_name, profile, keyring, container = _parse_browser_specification(*browser_specification)
|
||||||
extract_cookies_from_browser(browser_name, profile, YDLLogger(ydl), keyring=keyring, container=container))
|
cookie_jars.append(
|
||||||
|
extract_cookies_from_browser(browser_name, profile, YDLLogger(ydl), keyring=keyring, container=container))
|
||||||
|
|
||||||
if cookie_file is not None:
|
if cookie_file is not None:
|
||||||
is_filename = is_path_like(cookie_file)
|
is_filename = is_path_like(cookie_file)
|
||||||
if is_filename:
|
if is_filename:
|
||||||
cookie_file = expand_path(cookie_file)
|
cookie_file = expand_path(cookie_file)
|
||||||
|
|
||||||
jar = YoutubeDLCookieJar(cookie_file)
|
jar = YoutubeDLCookieJar(cookie_file)
|
||||||
if not is_filename or os.access(cookie_file, os.R_OK):
|
if not is_filename or os.access(cookie_file, os.R_OK):
|
||||||
jar.load()
|
jar.load()
|
||||||
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):
|
||||||
|
@ -294,12 +302,18 @@ def _extract_chrome_cookies(browser_name, profile, keyring, logger):
|
||||||
raise FileNotFoundError(f'could not find {browser_name} cookies database in "{search_root}"')
|
raise FileNotFoundError(f'could not find {browser_name} cookies database in "{search_root}"')
|
||||||
logger.debug(f'Extracting cookies from: "{cookie_database_path}"')
|
logger.debug(f'Extracting cookies from: "{cookie_database_path}"')
|
||||||
|
|
||||||
decryptor = get_cookie_decryptor(config['browser_dir'], config['keyring_name'], logger, keyring=keyring)
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory(prefix='yt_dlp') as tmpdir:
|
with tempfile.TemporaryDirectory(prefix='yt_dlp') as tmpdir:
|
||||||
cursor = None
|
cursor = None
|
||||||
try:
|
try:
|
||||||
cursor = _open_database_copy(cookie_database_path, tmpdir)
|
cursor = _open_database_copy(cookie_database_path, tmpdir)
|
||||||
|
|
||||||
|
# meta_version is necessary to determine if we need to trim the hash prefix from the cookies
|
||||||
|
# Ref: https://chromium.googlesource.com/chromium/src/+/b02dcebd7cafab92770734dc2bc317bd07f1d891/net/extras/sqlite/sqlite_persistent_cookie_store.cc#223
|
||||||
|
meta_version = int(cursor.execute('SELECT value FROM meta WHERE key = "version"').fetchone()[0])
|
||||||
|
decryptor = get_cookie_decryptor(
|
||||||
|
config['browser_dir'], config['keyring_name'], logger,
|
||||||
|
keyring=keyring, meta_version=meta_version)
|
||||||
|
|
||||||
cursor.connection.text_factory = bytes
|
cursor.connection.text_factory = bytes
|
||||||
column_names = _get_column_names(cursor, 'cookies')
|
column_names = _get_column_names(cursor, 'cookies')
|
||||||
secure_column = 'is_secure' if 'is_secure' in column_names else 'secure'
|
secure_column = 'is_secure' if 'is_secure' in column_names else 'secure'
|
||||||
|
@ -397,22 +411,23 @@ class ChromeCookieDecryptor:
|
||||||
raise NotImplementedError('Must be implemented by sub classes')
|
raise NotImplementedError('Must be implemented by sub classes')
|
||||||
|
|
||||||
|
|
||||||
def get_cookie_decryptor(browser_root, browser_keyring_name, logger, *, keyring=None):
|
def get_cookie_decryptor(browser_root, browser_keyring_name, logger, *, keyring=None, meta_version=None):
|
||||||
if sys.platform == 'darwin':
|
if sys.platform == 'darwin':
|
||||||
return MacChromeCookieDecryptor(browser_keyring_name, logger)
|
return MacChromeCookieDecryptor(browser_keyring_name, logger, meta_version=meta_version)
|
||||||
elif sys.platform in ('win32', 'cygwin'):
|
elif sys.platform in ('win32', 'cygwin'):
|
||||||
return WindowsChromeCookieDecryptor(browser_root, logger)
|
return WindowsChromeCookieDecryptor(browser_root, logger, meta_version=meta_version)
|
||||||
return LinuxChromeCookieDecryptor(browser_keyring_name, logger, keyring=keyring)
|
return LinuxChromeCookieDecryptor(browser_keyring_name, logger, keyring=keyring, meta_version=meta_version)
|
||||||
|
|
||||||
|
|
||||||
class LinuxChromeCookieDecryptor(ChromeCookieDecryptor):
|
class LinuxChromeCookieDecryptor(ChromeCookieDecryptor):
|
||||||
def __init__(self, browser_keyring_name, logger, *, keyring=None):
|
def __init__(self, browser_keyring_name, logger, *, keyring=None, meta_version=None):
|
||||||
self._logger = logger
|
self._logger = logger
|
||||||
self._v10_key = self.derive_key(b'peanuts')
|
self._v10_key = self.derive_key(b'peanuts')
|
||||||
self._empty_key = self.derive_key(b'')
|
self._empty_key = self.derive_key(b'')
|
||||||
self._cookie_counts = {'v10': 0, 'v11': 0, 'other': 0}
|
self._cookie_counts = {'v10': 0, 'v11': 0, 'other': 0}
|
||||||
self._browser_keyring_name = browser_keyring_name
|
self._browser_keyring_name = browser_keyring_name
|
||||||
self._keyring = keyring
|
self._keyring = keyring
|
||||||
|
self._meta_version = meta_version or 0
|
||||||
|
|
||||||
@functools.cached_property
|
@functools.cached_property
|
||||||
def _v11_key(self):
|
def _v11_key(self):
|
||||||
|
@ -441,14 +456,18 @@ class LinuxChromeCookieDecryptor(ChromeCookieDecryptor):
|
||||||
|
|
||||||
if version == b'v10':
|
if version == b'v10':
|
||||||
self._cookie_counts['v10'] += 1
|
self._cookie_counts['v10'] += 1
|
||||||
return _decrypt_aes_cbc_multi(ciphertext, (self._v10_key, self._empty_key), self._logger)
|
return _decrypt_aes_cbc_multi(
|
||||||
|
ciphertext, (self._v10_key, self._empty_key), self._logger,
|
||||||
|
hash_prefix=self._meta_version >= 24)
|
||||||
|
|
||||||
elif version == b'v11':
|
elif version == b'v11':
|
||||||
self._cookie_counts['v11'] += 1
|
self._cookie_counts['v11'] += 1
|
||||||
if self._v11_key is None:
|
if self._v11_key is None:
|
||||||
self._logger.warning('cannot decrypt v11 cookies: no key found', only_once=True)
|
self._logger.warning('cannot decrypt v11 cookies: no key found', only_once=True)
|
||||||
return None
|
return None
|
||||||
return _decrypt_aes_cbc_multi(ciphertext, (self._v11_key, self._empty_key), self._logger)
|
return _decrypt_aes_cbc_multi(
|
||||||
|
ciphertext, (self._v11_key, self._empty_key), self._logger,
|
||||||
|
hash_prefix=self._meta_version >= 24)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self._logger.warning(f'unknown cookie version: "{version}"', only_once=True)
|
self._logger.warning(f'unknown cookie version: "{version}"', only_once=True)
|
||||||
|
@ -457,11 +476,12 @@ class LinuxChromeCookieDecryptor(ChromeCookieDecryptor):
|
||||||
|
|
||||||
|
|
||||||
class MacChromeCookieDecryptor(ChromeCookieDecryptor):
|
class MacChromeCookieDecryptor(ChromeCookieDecryptor):
|
||||||
def __init__(self, browser_keyring_name, logger):
|
def __init__(self, browser_keyring_name, logger, meta_version=None):
|
||||||
self._logger = logger
|
self._logger = logger
|
||||||
password = _get_mac_keyring_password(browser_keyring_name, logger)
|
password = _get_mac_keyring_password(browser_keyring_name, logger)
|
||||||
self._v10_key = None if password is None else self.derive_key(password)
|
self._v10_key = None if password is None else self.derive_key(password)
|
||||||
self._cookie_counts = {'v10': 0, 'other': 0}
|
self._cookie_counts = {'v10': 0, 'other': 0}
|
||||||
|
self._meta_version = meta_version or 0
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def derive_key(password):
|
def derive_key(password):
|
||||||
|
@ -479,7 +499,8 @@ class MacChromeCookieDecryptor(ChromeCookieDecryptor):
|
||||||
self._logger.warning('cannot decrypt v10 cookies: no key found', only_once=True)
|
self._logger.warning('cannot decrypt v10 cookies: no key found', only_once=True)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return _decrypt_aes_cbc_multi(ciphertext, (self._v10_key,), self._logger)
|
return _decrypt_aes_cbc_multi(
|
||||||
|
ciphertext, (self._v10_key,), self._logger, hash_prefix=self._meta_version >= 24)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self._cookie_counts['other'] += 1
|
self._cookie_counts['other'] += 1
|
||||||
|
@ -489,10 +510,11 @@ class MacChromeCookieDecryptor(ChromeCookieDecryptor):
|
||||||
|
|
||||||
|
|
||||||
class WindowsChromeCookieDecryptor(ChromeCookieDecryptor):
|
class WindowsChromeCookieDecryptor(ChromeCookieDecryptor):
|
||||||
def __init__(self, browser_root, logger):
|
def __init__(self, browser_root, logger, meta_version=None):
|
||||||
self._logger = logger
|
self._logger = logger
|
||||||
self._v10_key = _get_windows_v10_key(browser_root, logger)
|
self._v10_key = _get_windows_v10_key(browser_root, logger)
|
||||||
self._cookie_counts = {'v10': 0, 'other': 0}
|
self._cookie_counts = {'v10': 0, 'other': 0}
|
||||||
|
self._meta_version = meta_version or 0
|
||||||
|
|
||||||
def decrypt(self, encrypted_value):
|
def decrypt(self, encrypted_value):
|
||||||
version = encrypted_value[:3]
|
version = encrypted_value[:3]
|
||||||
|
@ -516,7 +538,9 @@ class WindowsChromeCookieDecryptor(ChromeCookieDecryptor):
|
||||||
ciphertext = raw_ciphertext[nonce_length:-authentication_tag_length]
|
ciphertext = raw_ciphertext[nonce_length:-authentication_tag_length]
|
||||||
authentication_tag = raw_ciphertext[-authentication_tag_length:]
|
authentication_tag = raw_ciphertext[-authentication_tag_length:]
|
||||||
|
|
||||||
return _decrypt_aes_gcm(ciphertext, self._v10_key, nonce, authentication_tag, self._logger)
|
return _decrypt_aes_gcm(
|
||||||
|
ciphertext, self._v10_key, nonce, authentication_tag, self._logger,
|
||||||
|
hash_prefix=self._meta_version >= 24)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self._cookie_counts['other'] += 1
|
self._cookie_counts['other'] += 1
|
||||||
|
@ -740,40 +764,38 @@ def _get_linux_desktop_environment(env, logger):
|
||||||
xdg_current_desktop = env.get('XDG_CURRENT_DESKTOP', None)
|
xdg_current_desktop = env.get('XDG_CURRENT_DESKTOP', None)
|
||||||
desktop_session = env.get('DESKTOP_SESSION', None)
|
desktop_session = env.get('DESKTOP_SESSION', None)
|
||||||
if xdg_current_desktop is not None:
|
if xdg_current_desktop is not None:
|
||||||
xdg_current_desktop = xdg_current_desktop.split(':')[0].strip()
|
for part in map(str.strip, xdg_current_desktop.split(':')):
|
||||||
|
if part == 'Unity':
|
||||||
if xdg_current_desktop == 'Unity':
|
if desktop_session is not None and 'gnome-fallback' in desktop_session:
|
||||||
if desktop_session is not None and 'gnome-fallback' in desktop_session:
|
return _LinuxDesktopEnvironment.GNOME
|
||||||
|
else:
|
||||||
|
return _LinuxDesktopEnvironment.UNITY
|
||||||
|
elif part == 'Deepin':
|
||||||
|
return _LinuxDesktopEnvironment.DEEPIN
|
||||||
|
elif part == 'GNOME':
|
||||||
return _LinuxDesktopEnvironment.GNOME
|
return _LinuxDesktopEnvironment.GNOME
|
||||||
else:
|
elif part == 'X-Cinnamon':
|
||||||
return _LinuxDesktopEnvironment.UNITY
|
return _LinuxDesktopEnvironment.CINNAMON
|
||||||
elif xdg_current_desktop == 'Deepin':
|
elif part == 'KDE':
|
||||||
return _LinuxDesktopEnvironment.DEEPIN
|
kde_version = env.get('KDE_SESSION_VERSION', None)
|
||||||
elif xdg_current_desktop == 'GNOME':
|
if kde_version == '5':
|
||||||
return _LinuxDesktopEnvironment.GNOME
|
return _LinuxDesktopEnvironment.KDE5
|
||||||
elif xdg_current_desktop == 'X-Cinnamon':
|
elif kde_version == '6':
|
||||||
return _LinuxDesktopEnvironment.CINNAMON
|
return _LinuxDesktopEnvironment.KDE6
|
||||||
elif xdg_current_desktop == 'KDE':
|
elif kde_version == '4':
|
||||||
kde_version = env.get('KDE_SESSION_VERSION', None)
|
return _LinuxDesktopEnvironment.KDE4
|
||||||
if kde_version == '5':
|
else:
|
||||||
return _LinuxDesktopEnvironment.KDE5
|
logger.info(f'unknown KDE version: "{kde_version}". Assuming KDE4')
|
||||||
elif kde_version == '6':
|
return _LinuxDesktopEnvironment.KDE4
|
||||||
return _LinuxDesktopEnvironment.KDE6
|
elif part == 'Pantheon':
|
||||||
elif kde_version == '4':
|
return _LinuxDesktopEnvironment.PANTHEON
|
||||||
return _LinuxDesktopEnvironment.KDE4
|
elif part == 'XFCE':
|
||||||
else:
|
return _LinuxDesktopEnvironment.XFCE
|
||||||
logger.info(f'unknown KDE version: "{kde_version}". Assuming KDE4')
|
elif part == 'UKUI':
|
||||||
return _LinuxDesktopEnvironment.KDE4
|
return _LinuxDesktopEnvironment.UKUI
|
||||||
elif xdg_current_desktop == 'Pantheon':
|
elif part == 'LXQt':
|
||||||
return _LinuxDesktopEnvironment.PANTHEON
|
return _LinuxDesktopEnvironment.LXQT
|
||||||
elif xdg_current_desktop == 'XFCE':
|
logger.info(f'XDG_CURRENT_DESKTOP is set to an unknown value: "{xdg_current_desktop}"')
|
||||||
return _LinuxDesktopEnvironment.XFCE
|
|
||||||
elif xdg_current_desktop == 'UKUI':
|
|
||||||
return _LinuxDesktopEnvironment.UKUI
|
|
||||||
elif xdg_current_desktop == 'LXQt':
|
|
||||||
return _LinuxDesktopEnvironment.LXQT
|
|
||||||
else:
|
|
||||||
logger.info(f'XDG_CURRENT_DESKTOP is set to an unknown value: "{xdg_current_desktop}"')
|
|
||||||
|
|
||||||
elif desktop_session is not None:
|
elif desktop_session is not None:
|
||||||
if desktop_session == 'deepin':
|
if desktop_session == 'deepin':
|
||||||
|
@ -1001,13 +1023,15 @@ def _get_windows_v10_key(browser_root, logger):
|
||||||
|
|
||||||
|
|
||||||
def pbkdf2_sha1(password, salt, iterations, key_length):
|
def pbkdf2_sha1(password, salt, iterations, key_length):
|
||||||
return pbkdf2_hmac('sha1', password, salt, iterations, key_length)
|
return hashlib.pbkdf2_hmac('sha1', password, salt, iterations, key_length)
|
||||||
|
|
||||||
|
|
||||||
def _decrypt_aes_cbc_multi(ciphertext, keys, logger, initialization_vector=b' ' * 16):
|
def _decrypt_aes_cbc_multi(ciphertext, keys, logger, initialization_vector=b' ' * 16, hash_prefix=False):
|
||||||
for key in keys:
|
for key in keys:
|
||||||
plaintext = unpad_pkcs7(aes_cbc_decrypt_bytes(ciphertext, key, initialization_vector))
|
plaintext = unpad_pkcs7(aes_cbc_decrypt_bytes(ciphertext, key, initialization_vector))
|
||||||
try:
|
try:
|
||||||
|
if hash_prefix:
|
||||||
|
return plaintext[32:].decode()
|
||||||
return plaintext.decode()
|
return plaintext.decode()
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
pass
|
pass
|
||||||
|
@ -1015,7 +1039,7 @@ def _decrypt_aes_cbc_multi(ciphertext, keys, logger, initialization_vector=b' '
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _decrypt_aes_gcm(ciphertext, key, nonce, authentication_tag, logger):
|
def _decrypt_aes_gcm(ciphertext, key, nonce, authentication_tag, logger, hash_prefix=False):
|
||||||
try:
|
try:
|
||||||
plaintext = aes_gcm_decrypt_and_verify_bytes(ciphertext, key, authentication_tag, nonce)
|
plaintext = aes_gcm_decrypt_and_verify_bytes(ciphertext, key, authentication_tag, nonce)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
@ -1023,6 +1047,8 @@ def _decrypt_aes_gcm(ciphertext, key, nonce, authentication_tag, logger):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if hash_prefix:
|
||||||
|
return plaintext[32:].decode()
|
||||||
return plaintext.decode()
|
return plaintext.decode()
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
logger.warning('failed to decrypt cookie (AES-GCM) because UTF-8 decoding failed. Possibly the key is wrong?', only_once=True)
|
logger.warning('failed to decrypt cookie (AES-GCM) because UTF-8 decoding failed. Possibly the key is wrong?', only_once=True)
|
||||||
|
@ -1055,8 +1081,9 @@ def _decrypt_windows_dpapi(ciphertext, logger):
|
||||||
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)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import enum
|
import enum
|
||||||
|
import functools
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
@ -9,7 +10,6 @@ import time
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from .fragment import FragmentFD
|
from .fragment import FragmentFD
|
||||||
from ..compat import functools
|
|
||||||
from ..networking import Request
|
from ..networking import Request
|
||||||
from ..postprocessor.ffmpeg import EXT_TO_OUT_FORMATS, FFmpegPostProcessor
|
from ..postprocessor.ffmpeg import EXT_TO_OUT_FORMATS, FFmpegPostProcessor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
|
@ -108,7 +108,7 @@ class ExternalFD(FragmentFD):
|
||||||
return all((
|
return all((
|
||||||
not info_dict.get('to_stdout') or Features.TO_STDOUT in cls.SUPPORTED_FEATURES,
|
not info_dict.get('to_stdout') or Features.TO_STDOUT in cls.SUPPORTED_FEATURES,
|
||||||
'+' not in info_dict['protocol'] or Features.MULTIPLE_FORMATS in cls.SUPPORTED_FEATURES,
|
'+' not in info_dict['protocol'] or Features.MULTIPLE_FORMATS in cls.SUPPORTED_FEATURES,
|
||||||
not traverse_obj(info_dict, ('hls_aes', ...), 'extra_param_to_segment_url'),
|
not traverse_obj(info_dict, ('hls_aes', ...), 'extra_param_to_segment_url', 'extra_param_to_key_url'),
|
||||||
all(proto in cls.SUPPORTED_PROTOCOLS for proto in info_dict['protocol'].split('+')),
|
all(proto in cls.SUPPORTED_PROTOCOLS for proto in info_dict['protocol'].split('+')),
|
||||||
))
|
))
|
||||||
|
|
||||||
|
@ -508,7 +508,7 @@ class FFmpegFD(ExternalFD):
|
||||||
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 @@ class FFmpegFD(ExternalFD):
|
||||||
|
|
||||||
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(
|
||||||
|
|
|
@ -160,10 +160,12 @@ class HlsFD(FragmentFD):
|
||||||
extra_state = ctx.setdefault('extra_state', {})
|
extra_state = ctx.setdefault('extra_state', {})
|
||||||
|
|
||||||
format_index = info_dict.get('format_index')
|
format_index = info_dict.get('format_index')
|
||||||
extra_query = None
|
extra_segment_query = None
|
||||||
extra_param_to_segment_url = info_dict.get('extra_param_to_segment_url')
|
if extra_param_to_segment_url := info_dict.get('extra_param_to_segment_url'):
|
||||||
if extra_param_to_segment_url:
|
extra_segment_query = urllib.parse.parse_qs(extra_param_to_segment_url)
|
||||||
extra_query = urllib.parse.parse_qs(extra_param_to_segment_url)
|
extra_key_query = None
|
||||||
|
if extra_param_to_key_url := info_dict.get('extra_param_to_key_url'):
|
||||||
|
extra_key_query = urllib.parse.parse_qs(extra_param_to_key_url)
|
||||||
i = 0
|
i = 0
|
||||||
media_sequence = 0
|
media_sequence = 0
|
||||||
decrypt_info = {'METHOD': 'NONE'}
|
decrypt_info = {'METHOD': 'NONE'}
|
||||||
|
@ -190,8 +192,8 @@ class HlsFD(FragmentFD):
|
||||||
if frag_index <= ctx['fragment_index']:
|
if frag_index <= ctx['fragment_index']:
|
||||||
continue
|
continue
|
||||||
frag_url = urljoin(man_url, line)
|
frag_url = urljoin(man_url, line)
|
||||||
if extra_query:
|
if extra_segment_query:
|
||||||
frag_url = update_url_query(frag_url, extra_query)
|
frag_url = update_url_query(frag_url, extra_segment_query)
|
||||||
|
|
||||||
fragments.append({
|
fragments.append({
|
||||||
'frag_index': frag_index,
|
'frag_index': frag_index,
|
||||||
|
@ -212,8 +214,8 @@ class HlsFD(FragmentFD):
|
||||||
frag_index += 1
|
frag_index += 1
|
||||||
map_info = parse_m3u8_attributes(line[11:])
|
map_info = parse_m3u8_attributes(line[11:])
|
||||||
frag_url = urljoin(man_url, map_info.get('URI'))
|
frag_url = urljoin(man_url, map_info.get('URI'))
|
||||||
if extra_query:
|
if extra_segment_query:
|
||||||
frag_url = update_url_query(frag_url, extra_query)
|
frag_url = update_url_query(frag_url, extra_segment_query)
|
||||||
|
|
||||||
if map_info.get('BYTERANGE'):
|
if map_info.get('BYTERANGE'):
|
||||||
splitted_byte_range = map_info.get('BYTERANGE').split('@')
|
splitted_byte_range = map_info.get('BYTERANGE').split('@')
|
||||||
|
@ -244,8 +246,10 @@ class HlsFD(FragmentFD):
|
||||||
decrypt_info['KEY'] = external_aes_key
|
decrypt_info['KEY'] = external_aes_key
|
||||||
else:
|
else:
|
||||||
decrypt_info['URI'] = urljoin(man_url, decrypt_info['URI'])
|
decrypt_info['URI'] = urljoin(man_url, decrypt_info['URI'])
|
||||||
if extra_query:
|
if extra_key_query or extra_segment_query:
|
||||||
decrypt_info['URI'] = update_url_query(decrypt_info['URI'], extra_query)
|
# Fall back to extra_segment_query to key for backwards compat
|
||||||
|
decrypt_info['URI'] = update_url_query(
|
||||||
|
decrypt_info['URI'], extra_key_query or extra_segment_query)
|
||||||
if decrypt_url != decrypt_info['URI']:
|
if decrypt_url != decrypt_info['URI']:
|
||||||
decrypt_info['KEY'] = None
|
decrypt_info['KEY'] = None
|
||||||
|
|
||||||
|
|
|
@ -76,6 +76,7 @@ from .aenetworks import (
|
||||||
)
|
)
|
||||||
from .aeonco import AeonCoIE
|
from .aeonco import AeonCoIE
|
||||||
from .afreecatv import (
|
from .afreecatv import (
|
||||||
|
AfreecaTVCatchStoryIE,
|
||||||
AfreecaTVIE,
|
AfreecaTVIE,
|
||||||
AfreecaTVLiveIE,
|
AfreecaTVLiveIE,
|
||||||
AfreecaTVUserIE,
|
AfreecaTVUserIE,
|
||||||
|
@ -216,9 +217,8 @@ from .bbc import (
|
||||||
BBCCoUkIPlayerGroupIE,
|
BBCCoUkIPlayerGroupIE,
|
||||||
BBCCoUkPlaylistIE,
|
BBCCoUkPlaylistIE,
|
||||||
)
|
)
|
||||||
from .bbcmaestro import (
|
from .bbcmaestro import BBCMaestroComIE
|
||||||
BBCMaestroComIE,
|
from .beacon import BeaconTvIE
|
||||||
)
|
|
||||||
from .beatbump import (
|
from .beatbump import (
|
||||||
BeatBumpPlaylistIE,
|
BeatBumpPlaylistIE,
|
||||||
BeatBumpVideoIE,
|
BeatBumpVideoIE,
|
||||||
|
@ -279,6 +279,7 @@ from .bleacherreport import (
|
||||||
from .blerp import BlerpIE
|
from .blerp import BlerpIE
|
||||||
from .blogger import BloggerIE
|
from .blogger import BloggerIE
|
||||||
from .bloomberg import BloombergIE
|
from .bloomberg import BloombergIE
|
||||||
|
from .bluesky import BlueskyIE
|
||||||
from .bokecc import BokeCCIE
|
from .bokecc import BokeCCIE
|
||||||
from .bongacams import BongaCamsIE
|
from .bongacams import BongaCamsIE
|
||||||
from .boosty import BoostyIE
|
from .boosty import BoostyIE
|
||||||
|
@ -364,7 +365,10 @@ from .ccc import (
|
||||||
)
|
)
|
||||||
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
|
||||||
|
@ -399,8 +403,6 @@ from .cmt import CMTIE
|
||||||
from .cnbc import CNBCVideoIE
|
from .cnbc import CNBCVideoIE
|
||||||
from .cnn import (
|
from .cnn import (
|
||||||
CNNIE,
|
CNNIE,
|
||||||
CNNArticleIE,
|
|
||||||
CNNBlogsIE,
|
|
||||||
CNNIndonesiaIE,
|
CNNIndonesiaIE,
|
||||||
)
|
)
|
||||||
from .comedycentral import (
|
from .comedycentral import (
|
||||||
|
@ -506,7 +508,6 @@ from .dhm import DHMIE
|
||||||
from .digitalconcerthall import DigitalConcertHallIE
|
from .digitalconcerthall import DigitalConcertHallIE
|
||||||
from .digiteka import DigitekaIE
|
from .digiteka import DigitekaIE
|
||||||
from .discogs import DiscogsReleasePlaylistIE
|
from .discogs import DiscogsReleasePlaylistIE
|
||||||
from .discovery import DiscoveryIE
|
|
||||||
from .disney import DisneyIE
|
from .disney import DisneyIE
|
||||||
from .dispeak import DigitallySpeakingIE
|
from .dispeak import DigitallySpeakingIE
|
||||||
from .dlf import (
|
from .dlf import (
|
||||||
|
@ -534,16 +535,12 @@ from .dplay import (
|
||||||
DiscoveryPlusIndiaShowIE,
|
DiscoveryPlusIndiaShowIE,
|
||||||
DiscoveryPlusItalyIE,
|
DiscoveryPlusItalyIE,
|
||||||
DiscoveryPlusItalyShowIE,
|
DiscoveryPlusItalyShowIE,
|
||||||
DIYNetworkIE,
|
|
||||||
DPlayIE,
|
DPlayIE,
|
||||||
FoodNetworkIE,
|
FoodNetworkIE,
|
||||||
GlobalCyclingNetworkPlusIE,
|
|
||||||
GoDiscoveryIE,
|
GoDiscoveryIE,
|
||||||
HGTVDeIE,
|
HGTVDeIE,
|
||||||
HGTVUsaIE,
|
HGTVUsaIE,
|
||||||
InvestigationDiscoveryIE,
|
InvestigationDiscoveryIE,
|
||||||
MotorTrendIE,
|
|
||||||
MotorTrendOnDemandIE,
|
|
||||||
ScienceChannelIE,
|
ScienceChannelIE,
|
||||||
TravelChannelIE,
|
TravelChannelIE,
|
||||||
)
|
)
|
||||||
|
@ -736,6 +733,7 @@ from .genius import (
|
||||||
GeniusIE,
|
GeniusIE,
|
||||||
GeniusLyricsIE,
|
GeniusLyricsIE,
|
||||||
)
|
)
|
||||||
|
from .germanupa import GermanupaIE
|
||||||
from .getcourseru import (
|
from .getcourseru import (
|
||||||
GetCourseRuIE,
|
GetCourseRuIE,
|
||||||
GetCourseRuPlayerIE,
|
GetCourseRuPlayerIE,
|
||||||
|
@ -782,6 +780,7 @@ from .gopro import GoProIE
|
||||||
from .goshgay import GoshgayIE
|
from .goshgay import GoshgayIE
|
||||||
from .gotostage import GoToStageIE
|
from .gotostage import GoToStageIE
|
||||||
from .gputechconf import GPUTechConfIE
|
from .gputechconf import GPUTechConfIE
|
||||||
|
from .graspop import GraspopIE
|
||||||
from .gronkh import (
|
from .gronkh import (
|
||||||
GronkhFeedIE,
|
GronkhFeedIE,
|
||||||
GronkhIE,
|
GronkhIE,
|
||||||
|
@ -828,7 +827,10 @@ from .hungama import (
|
||||||
HungamaIE,
|
HungamaIE,
|
||||||
HungamaSongIE,
|
HungamaSongIE,
|
||||||
)
|
)
|
||||||
from .huya import HuyaLiveIE
|
from .huya import (
|
||||||
|
HuyaLiveIE,
|
||||||
|
HuyaVideoIE,
|
||||||
|
)
|
||||||
from .hypem import HypemIE
|
from .hypem import HypemIE
|
||||||
from .hypergryph import MonsterSirenHypergryphMusicIE
|
from .hypergryph import MonsterSirenHypergryphMusicIE
|
||||||
from .hytale import HytaleIE
|
from .hytale import HytaleIE
|
||||||
|
@ -945,11 +947,13 @@ from .khanacademy import (
|
||||||
KhanAcademyUnitIE,
|
KhanAcademyUnitIE,
|
||||||
)
|
)
|
||||||
from .kick import (
|
from .kick import (
|
||||||
|
KickClipIE,
|
||||||
KickIE,
|
KickIE,
|
||||||
KickVODIE,
|
KickVODIE,
|
||||||
)
|
)
|
||||||
from .kicker import KickerIE
|
from .kicker import KickerIE
|
||||||
from .kickstarter import KickStarterIE
|
from .kickstarter import KickStarterIE
|
||||||
|
from .kika import KikaIE
|
||||||
from .kinja import KinjaEmbedIE
|
from .kinja import KinjaEmbedIE
|
||||||
from .kinopoisk import KinoPoiskIE
|
from .kinopoisk import KinoPoiskIE
|
||||||
from .kommunetv import KommunetvIE
|
from .kommunetv import KommunetvIE
|
||||||
|
@ -972,6 +976,10 @@ from .la7 import (
|
||||||
LA7PodcastEpisodeIE,
|
LA7PodcastEpisodeIE,
|
||||||
LA7PodcastIE,
|
LA7PodcastIE,
|
||||||
)
|
)
|
||||||
|
from .laracasts import (
|
||||||
|
LaracastsIE,
|
||||||
|
LaracastsPlaylistIE,
|
||||||
|
)
|
||||||
from .lastfm import (
|
from .lastfm import (
|
||||||
LastFMIE,
|
LastFMIE,
|
||||||
LastFMPlaylistIE,
|
LastFMPlaylistIE,
|
||||||
|
@ -988,6 +996,7 @@ from .lcp import (
|
||||||
LcpIE,
|
LcpIE,
|
||||||
LcpPlayIE,
|
LcpPlayIE,
|
||||||
)
|
)
|
||||||
|
from .learningonscreen import LearningOnScreenIE
|
||||||
from .lecture2go import Lecture2GoIE
|
from .lecture2go import Lecture2GoIE
|
||||||
from .lecturio import (
|
from .lecturio import (
|
||||||
LecturioCourseIE,
|
LecturioCourseIE,
|
||||||
|
@ -1036,10 +1045,7 @@ from .livestream import (
|
||||||
LivestreamShortenerIE,
|
LivestreamShortenerIE,
|
||||||
)
|
)
|
||||||
from .livestreamfails import LivestreamfailsIE
|
from .livestreamfails import LivestreamfailsIE
|
||||||
from .lnkgo import (
|
from .lnk import LnkIE
|
||||||
LnkGoIE,
|
|
||||||
LnkIE,
|
|
||||||
)
|
|
||||||
from .loom import (
|
from .loom import (
|
||||||
LoomFolderIE,
|
LoomFolderIE,
|
||||||
LoomIE,
|
LoomIE,
|
||||||
|
@ -1116,12 +1122,15 @@ from .meipai import MeipaiIE
|
||||||
from .melonvod import MelonVODIE
|
from .melonvod import MelonVODIE
|
||||||
from .metacritic import MetacriticIE
|
from .metacritic import MetacriticIE
|
||||||
from .mgtv import MGTVIE
|
from .mgtv import MGTVIE
|
||||||
from .microsoftembed import MicrosoftEmbedIE
|
from .microsoftembed import (
|
||||||
from .microsoftstream import MicrosoftStreamIE
|
MicrosoftBuildIE,
|
||||||
from .microsoftvirtualacademy import (
|
MicrosoftEmbedIE,
|
||||||
MicrosoftVirtualAcademyCourseIE,
|
MicrosoftLearnEpisodeIE,
|
||||||
MicrosoftVirtualAcademyIE,
|
MicrosoftLearnPlaylistIE,
|
||||||
|
MicrosoftLearnSessionIE,
|
||||||
|
MicrosoftMediusIE,
|
||||||
)
|
)
|
||||||
|
from .microsoftstream import MicrosoftStreamIE
|
||||||
from .mildom import (
|
from .mildom import (
|
||||||
MildomClipIE,
|
MildomClipIE,
|
||||||
MildomIE,
|
MildomIE,
|
||||||
|
@ -1161,6 +1170,7 @@ from .mlb import (
|
||||||
)
|
)
|
||||||
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 (
|
||||||
|
@ -1606,6 +1616,7 @@ from .qqmusic import (
|
||||||
QQMusicPlaylistIE,
|
QQMusicPlaylistIE,
|
||||||
QQMusicSingerIE,
|
QQMusicSingerIE,
|
||||||
QQMusicToplistIE,
|
QQMusicToplistIE,
|
||||||
|
QQMusicVideoIE,
|
||||||
)
|
)
|
||||||
from .r7 import (
|
from .r7 import (
|
||||||
R7IE,
|
R7IE,
|
||||||
|
@ -1758,7 +1769,10 @@ from .rtve import (
|
||||||
RTVETelevisionIE,
|
RTVETelevisionIE,
|
||||||
)
|
)
|
||||||
from .rtvs import RTVSIE
|
from .rtvs import RTVSIE
|
||||||
from .rtvslo import RTVSLOIE
|
from .rtvslo import (
|
||||||
|
RTVSLOIE,
|
||||||
|
RTVSLOShowIE,
|
||||||
|
)
|
||||||
from .rudovideo import RudoVideoIE
|
from .rudovideo import RudoVideoIE
|
||||||
from .rule34video import Rule34VideoIE
|
from .rule34video import Rule34VideoIE
|
||||||
from .rumble import (
|
from .rumble import (
|
||||||
|
@ -1803,6 +1817,7 @@ from .screen9 import Screen9IE
|
||||||
from .screencast import ScreencastIE
|
from .screencast import ScreencastIE
|
||||||
from .screencastify import ScreencastifyIE
|
from .screencastify import ScreencastifyIE
|
||||||
from .screencastomatic import ScreencastOMaticIE
|
from .screencastomatic import ScreencastOMaticIE
|
||||||
|
from .screenrec import ScreenRecIE
|
||||||
from .scrippsnetworks import (
|
from .scrippsnetworks import (
|
||||||
ScrippsNetworksIE,
|
ScrippsNetworksIE,
|
||||||
ScrippsNetworksWatchIE,
|
ScrippsNetworksWatchIE,
|
||||||
|
@ -1813,6 +1828,7 @@ from .scte import (
|
||||||
SCTECourseIE,
|
SCTECourseIE,
|
||||||
)
|
)
|
||||||
from .sejmpl import SejmIE
|
from .sejmpl import SejmIE
|
||||||
|
from .sen import SenIE
|
||||||
from .senalcolombia import SenalColombiaLiveIE
|
from .senalcolombia import SenalColombiaLiveIE
|
||||||
from .senategov import (
|
from .senategov import (
|
||||||
SenateGovIE,
|
SenateGovIE,
|
||||||
|
@ -1868,6 +1884,7 @@ from .slideshare import SlideshareIE
|
||||||
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,
|
||||||
|
@ -1928,6 +1945,10 @@ from .spreaker import (
|
||||||
)
|
)
|
||||||
from .springboardplatform import SpringboardPlatformIE
|
from .springboardplatform import SpringboardPlatformIE
|
||||||
from .sprout import SproutIE
|
from .sprout import SproutIE
|
||||||
|
from .sproutvideo import (
|
||||||
|
SproutVideoIE,
|
||||||
|
VidsIoIE,
|
||||||
|
)
|
||||||
from .srgssr import (
|
from .srgssr import (
|
||||||
SRGSSRIE,
|
SRGSSRIE,
|
||||||
SRGSSRPlayIE,
|
SRGSSRPlayIE,
|
||||||
|
@ -2160,10 +2181,7 @@ from .tv5unis import (
|
||||||
TV5UnisVideoIE,
|
TV5UnisVideoIE,
|
||||||
)
|
)
|
||||||
from .tv24ua import TV24UAVideoIE
|
from .tv24ua import TV24UAVideoIE
|
||||||
from .tva import (
|
from .tva import TVAIE
|
||||||
TVAIE,
|
|
||||||
QubIE,
|
|
||||||
)
|
|
||||||
from .tvanouvelles import (
|
from .tvanouvelles import (
|
||||||
TVANouvellesArticleIE,
|
TVANouvellesArticleIE,
|
||||||
TVANouvellesIE,
|
TVANouvellesIE,
|
||||||
|
@ -2303,6 +2321,7 @@ from .videomore import (
|
||||||
VideomoreVideoIE,
|
VideomoreVideoIE,
|
||||||
)
|
)
|
||||||
from .videopress import VideoPressIE
|
from .videopress import VideoPressIE
|
||||||
|
from .vidflex import VidflexIE
|
||||||
from .vidio import (
|
from .vidio import (
|
||||||
VidioIE,
|
VidioIE,
|
||||||
VidioLiveIE,
|
VidioLiveIE,
|
||||||
|
@ -2310,6 +2329,7 @@ from .vidio import (
|
||||||
)
|
)
|
||||||
from .vidlii import VidLiiIE
|
from .vidlii import VidLiiIE
|
||||||
from .vidly import VidlyIE
|
from .vidly import VidlyIE
|
||||||
|
from .vidyard import VidyardIE
|
||||||
from .viewlift import (
|
from .viewlift import (
|
||||||
ViewLiftEmbedIE,
|
ViewLiftEmbedIE,
|
||||||
ViewLiftIE,
|
ViewLiftIE,
|
||||||
|
@ -2375,6 +2395,10 @@ from .vrt import (
|
||||||
VrtNUIE,
|
VrtNUIE,
|
||||||
)
|
)
|
||||||
from .vtm import VTMIE
|
from .vtm import VTMIE
|
||||||
|
from .vtv import (
|
||||||
|
VTVIE,
|
||||||
|
VTVGoIE,
|
||||||
|
)
|
||||||
from .vuclip import VuClipIE
|
from .vuclip import VuClipIE
|
||||||
from .vvvvid import (
|
from .vvvvid import (
|
||||||
VVVVIDIE,
|
VVVVIDIE,
|
||||||
|
|
|
@ -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'):
|
||||||
|
|
|
@ -9,12 +9,12 @@ import re
|
||||||
import struct
|
import struct
|
||||||
import time
|
import time
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import urllib.request
|
|
||||||
import urllib.response
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..aes import aes_ecb_decrypt
|
from ..aes import aes_ecb_decrypt
|
||||||
|
from ..networking import RequestHandler, Response
|
||||||
|
from ..networking.exceptions import TransportError
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
OnDemandPagedList,
|
OnDemandPagedList,
|
||||||
|
@ -26,37 +26,36 @@ from ..utils import (
|
||||||
traverse_obj,
|
traverse_obj,
|
||||||
update_url_query,
|
update_url_query,
|
||||||
)
|
)
|
||||||
from ..utils.networking import clean_proxies
|
|
||||||
|
|
||||||
|
|
||||||
def add_opener(ydl, handler): # FIXME: Create proper API in .networking
|
class AbemaLicenseRH(RequestHandler):
|
||||||
"""Add a handler for opening URLs, like _download_webpage"""
|
_SUPPORTED_URL_SCHEMES = ('abematv-license',)
|
||||||
# https://github.com/python/cpython/blob/main/Lib/urllib/request.py#L426
|
_SUPPORTED_PROXY_SCHEMES = None
|
||||||
# https://github.com/python/cpython/blob/main/Lib/urllib/request.py#L605
|
_SUPPORTED_FEATURES = None
|
||||||
rh = ydl._request_director.handlers['Urllib']
|
RH_NAME = 'abematv_license'
|
||||||
if 'abematv-license' in rh._SUPPORTED_URL_SCHEMES:
|
|
||||||
return
|
|
||||||
headers = ydl.params['http_headers'].copy()
|
|
||||||
proxies = ydl.proxies.copy()
|
|
||||||
clean_proxies(proxies, headers)
|
|
||||||
opener = rh._get_instance(cookiejar=ydl.cookiejar, proxies=proxies)
|
|
||||||
assert isinstance(opener, urllib.request.OpenerDirector)
|
|
||||||
opener.add_handler(handler)
|
|
||||||
rh._SUPPORTED_URL_SCHEMES = (*rh._SUPPORTED_URL_SCHEMES, 'abematv-license')
|
|
||||||
|
|
||||||
|
_STRTABLE = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
|
||||||
|
_HKEY = b'3AF0298C219469522A313570E8583005A642E73EDD58E3EA2FB7339D3DF1597E'
|
||||||
|
|
||||||
class AbemaLicenseHandler(urllib.request.BaseHandler):
|
def __init__(self, *, ie: 'AbemaTVIE', **kwargs):
|
||||||
handler_order = 499
|
super().__init__(**kwargs)
|
||||||
STRTABLE = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
|
|
||||||
HKEY = b'3AF0298C219469522A313570E8583005A642E73EDD58E3EA2FB7339D3DF1597E'
|
|
||||||
|
|
||||||
def __init__(self, ie: 'AbemaTVIE'):
|
|
||||||
# the protocol that this should really handle is 'abematv-license://'
|
|
||||||
# abematv_license_open is just a placeholder for development purposes
|
|
||||||
# ref. https://github.com/python/cpython/blob/f4c03484da59049eb62a9bf7777b963e2267d187/Lib/urllib/request.py#L510
|
|
||||||
setattr(self, 'abematv-license_open', getattr(self, 'abematv_license_open', None))
|
|
||||||
self.ie = ie
|
self.ie = ie
|
||||||
|
|
||||||
|
def _send(self, request):
|
||||||
|
url = request.url
|
||||||
|
ticket = urllib.parse.urlparse(url).netloc
|
||||||
|
|
||||||
|
try:
|
||||||
|
response_data = self._get_videokey_from_ticket(ticket)
|
||||||
|
except ExtractorError as e:
|
||||||
|
raise TransportError(cause=e.cause) from e
|
||||||
|
except (IndexError, KeyError, TypeError) as e:
|
||||||
|
raise TransportError(cause=repr(e)) from e
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
io.BytesIO(response_data), url,
|
||||||
|
headers={'Content-Length': str(len(response_data))})
|
||||||
|
|
||||||
def _get_videokey_from_ticket(self, ticket):
|
def _get_videokey_from_ticket(self, ticket):
|
||||||
to_show = self.ie.get_param('verbose', False)
|
to_show = self.ie.get_param('verbose', False)
|
||||||
media_token = self.ie._get_media_token(to_show=to_show)
|
media_token = self.ie._get_media_token(to_show=to_show)
|
||||||
|
@ -72,25 +71,17 @@ class AbemaLicenseHandler(urllib.request.BaseHandler):
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
})
|
})
|
||||||
|
|
||||||
res = decode_base_n(license_response['k'], table=self.STRTABLE)
|
res = decode_base_n(license_response['k'], table=self._STRTABLE)
|
||||||
encvideokey = bytes_to_intlist(struct.pack('>QQ', res >> 64, res & 0xffffffffffffffff))
|
encvideokey = bytes_to_intlist(struct.pack('>QQ', res >> 64, res & 0xffffffffffffffff))
|
||||||
|
|
||||||
h = hmac.new(
|
h = hmac.new(
|
||||||
binascii.unhexlify(self.HKEY),
|
binascii.unhexlify(self._HKEY),
|
||||||
(license_response['cid'] + self.ie._DEVICE_ID).encode(),
|
(license_response['cid'] + self.ie._DEVICE_ID).encode(),
|
||||||
digestmod=hashlib.sha256)
|
digestmod=hashlib.sha256)
|
||||||
enckey = bytes_to_intlist(h.digest())
|
enckey = bytes_to_intlist(h.digest())
|
||||||
|
|
||||||
return intlist_to_bytes(aes_ecb_decrypt(encvideokey, enckey))
|
return intlist_to_bytes(aes_ecb_decrypt(encvideokey, enckey))
|
||||||
|
|
||||||
def abematv_license_open(self, url):
|
|
||||||
url = url.get_full_url() if isinstance(url, urllib.request.Request) else url
|
|
||||||
ticket = urllib.parse.urlparse(url).netloc
|
|
||||||
response_data = self._get_videokey_from_ticket(ticket)
|
|
||||||
return urllib.response.addinfourl(io.BytesIO(response_data), headers={
|
|
||||||
'Content-Length': str(len(response_data)),
|
|
||||||
}, url=url, code=200)
|
|
||||||
|
|
||||||
|
|
||||||
class AbemaTVBaseIE(InfoExtractor):
|
class AbemaTVBaseIE(InfoExtractor):
|
||||||
_NETRC_MACHINE = 'abematv'
|
_NETRC_MACHINE = 'abematv'
|
||||||
|
@ -139,7 +130,7 @@ class AbemaTVBaseIE(InfoExtractor):
|
||||||
if self._USERTOKEN:
|
if self._USERTOKEN:
|
||||||
return self._USERTOKEN
|
return self._USERTOKEN
|
||||||
|
|
||||||
add_opener(self._downloader, AbemaLicenseHandler(self))
|
self._downloader._request_director.add_handler(AbemaLicenseRH(ie=self, logger=None))
|
||||||
|
|
||||||
username, _ = self._get_login_info()
|
username, _ = self._get_login_info()
|
||||||
auth_cache = username and self.cache.load(self._NETRC_MACHINE, username, min_ver='2024.01.19')
|
auth_cache = username and self.cache.load(self._NETRC_MACHINE, username, min_ver='2024.01.19')
|
||||||
|
@ -368,6 +359,7 @@ class AbemaTVIE(AbemaTVBaseIE):
|
||||||
info['episode_number'] = epis if epis < 2000 else None
|
info['episode_number'] = epis if epis < 2000 else None
|
||||||
|
|
||||||
is_live, m3u8_url = False, None
|
is_live, m3u8_url = False, None
|
||||||
|
availability = 'public'
|
||||||
if video_type == 'now-on-air':
|
if video_type == 'now-on-air':
|
||||||
is_live = True
|
is_live = True
|
||||||
channel_url = 'https://api.abema.io/v1/channels'
|
channel_url = 'https://api.abema.io/v1/channels'
|
||||||
|
@ -385,10 +377,10 @@ class AbemaTVIE(AbemaTVBaseIE):
|
||||||
f'https://api.abema.io/v1/video/programs/{video_id}', video_id,
|
f'https://api.abema.io/v1/video/programs/{video_id}', video_id,
|
||||||
note='Checking playability',
|
note='Checking playability',
|
||||||
headers=headers)
|
headers=headers)
|
||||||
ondemand_types = traverse_obj(api_response, ('terms', ..., 'onDemandType'))
|
if not traverse_obj(api_response, ('label', 'free', {bool})):
|
||||||
if 3 not in ondemand_types:
|
|
||||||
# cannot acquire decryption key for these streams
|
# cannot acquire decryption key for these streams
|
||||||
self.report_warning('This is a premium-only stream')
|
self.report_warning('This is a premium-only stream')
|
||||||
|
availability = 'premium_only'
|
||||||
info.update(traverse_obj(api_response, {
|
info.update(traverse_obj(api_response, {
|
||||||
'series': ('series', 'title'),
|
'series': ('series', 'title'),
|
||||||
'season': ('season', 'name'),
|
'season': ('season', 'name'),
|
||||||
|
@ -408,6 +400,7 @@ class AbemaTVIE(AbemaTVBaseIE):
|
||||||
headers=headers)
|
headers=headers)
|
||||||
if not traverse_obj(api_response, ('slot', 'flags', 'timeshiftFree'), default=False):
|
if not traverse_obj(api_response, ('slot', 'flags', 'timeshiftFree'), default=False):
|
||||||
self.report_warning('This is a premium-only stream')
|
self.report_warning('This is a premium-only stream')
|
||||||
|
availability = 'premium_only'
|
||||||
|
|
||||||
m3u8_url = f'https://vod-abematv.akamaized.net/slot/{video_id}/playlist.m3u8'
|
m3u8_url = f'https://vod-abematv.akamaized.net/slot/{video_id}/playlist.m3u8'
|
||||||
else:
|
else:
|
||||||
|
@ -425,6 +418,7 @@ class AbemaTVIE(AbemaTVBaseIE):
|
||||||
'description': description,
|
'description': description,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
'is_live': is_live,
|
'is_live': is_live,
|
||||||
|
'availability': availability,
|
||||||
})
|
})
|
||||||
return info
|
return info
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ from .common import InfoExtractor
|
||||||
|
|
||||||
|
|
||||||
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/',
|
||||||
|
|
|
@ -16,6 +16,7 @@ from ..utils import (
|
||||||
float_or_none,
|
float_or_none,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
intlist_to_bytes,
|
intlist_to_bytes,
|
||||||
|
join_nonempty,
|
||||||
long_to_bytes,
|
long_to_bytes,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
pkcs1pad,
|
pkcs1pad,
|
||||||
|
@ -48,9 +49,9 @@ class ADNBaseIE(InfoExtractor):
|
||||||
|
|
||||||
|
|
||||||
class ADNIE(ADNBaseIE):
|
class ADNIE(ADNBaseIE):
|
||||||
_VALID_URL = r'https?://(?:www\.)?(?:animation|anime)digitalnetwork\.(?P<lang>fr|de)/video/[^/?#]+/(?P<id>\d+)'
|
_VALID_URL = r'https?://(?:www\.)?animationdigitalnetwork\.com/(?:(?P<lang>de)/)?video/[^/?#]+/(?P<id>\d+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://animationdigitalnetwork.fr/video/fruits-basket/9841-episode-1-a-ce-soir',
|
'url': 'https://animationdigitalnetwork.com/video/558-fruits-basket/9841-episode-1-a-ce-soir',
|
||||||
'md5': '1c9ef066ceb302c86f80c2b371615261',
|
'md5': '1c9ef066ceb302c86f80c2b371615261',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '9841',
|
'id': '9841',
|
||||||
|
@ -70,10 +71,7 @@ class ADNIE(ADNBaseIE):
|
||||||
},
|
},
|
||||||
'skip': 'Only available in French and German speaking Europe',
|
'skip': 'Only available in French and German speaking Europe',
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://animedigitalnetwork.fr/video/blue-exorcist-kyoto-saga/7778-episode-1-debut-des-hostilites',
|
'url': 'https://animationdigitalnetwork.com/de/video/973-the-eminence-in-shadow/23550-folge-1',
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'https://animationdigitalnetwork.de/video/the-eminence-in-shadow/23550-folge-1',
|
|
||||||
'md5': '5c5651bf5791fa6fcd7906012b9d94e8',
|
'md5': '5c5651bf5791fa6fcd7906012b9d94e8',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '23550',
|
'id': '23550',
|
||||||
|
@ -166,7 +164,7 @@ Format: Marked,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text'''
|
||||||
'username': username,
|
'username': username,
|
||||||
})) or {}).get('accessToken')
|
})) or {}).get('accessToken')
|
||||||
if access_token:
|
if access_token:
|
||||||
self._HEADERS = {'authorization': 'Bearer ' + access_token}
|
self._HEADERS['Authorization'] = f'Bearer {access_token}'
|
||||||
except ExtractorError as e:
|
except ExtractorError as e:
|
||||||
message = None
|
message = None
|
||||||
if isinstance(e.cause, HTTPError) and e.cause.status == 401:
|
if isinstance(e.cause, HTTPError) and e.cause.status == 401:
|
||||||
|
@ -177,6 +175,7 @@ Format: Marked,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text'''
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
lang, video_id = self._match_valid_url(url).group('lang', 'id')
|
lang, video_id = self._match_valid_url(url).group('lang', 'id')
|
||||||
|
self._HEADERS['X-Target-Distribution'] = lang or 'fr'
|
||||||
video_base_url = self._PLAYER_BASE_URL + f'video/{video_id}/'
|
video_base_url = self._PLAYER_BASE_URL + f'video/{video_id}/'
|
||||||
player = self._download_json(
|
player = self._download_json(
|
||||||
video_base_url + 'configuration', video_id,
|
video_base_url + 'configuration', video_id,
|
||||||
|
@ -217,7 +216,6 @@ Format: Marked,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text'''
|
||||||
links_data = self._download_json(
|
links_data = self._download_json(
|
||||||
links_url, video_id, 'Downloading links JSON metadata', headers={
|
links_url, video_id, 'Downloading links JSON metadata', headers={
|
||||||
'X-Player-Token': authorization,
|
'X-Player-Token': authorization,
|
||||||
'X-Target-Distribution': lang,
|
|
||||||
**self._HEADERS,
|
**self._HEADERS,
|
||||||
}, query={
|
}, query={
|
||||||
'freeWithAds': 'true',
|
'freeWithAds': 'true',
|
||||||
|
@ -256,6 +254,7 @@ Format: Marked,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text'''
|
||||||
load_balancer_data = self._download_json(
|
load_balancer_data = self._download_json(
|
||||||
load_balancer_url, video_id,
|
load_balancer_url, video_id,
|
||||||
f'Downloading {format_id} {quality} JSON metadata',
|
f'Downloading {format_id} {quality} JSON metadata',
|
||||||
|
headers=self._HEADERS,
|
||||||
fatal=False) or {}
|
fatal=False) or {}
|
||||||
m3u8_url = load_balancer_data.get('location')
|
m3u8_url = load_balancer_data.get('location')
|
||||||
if not m3u8_url:
|
if not m3u8_url:
|
||||||
|
@ -276,7 +275,7 @@ Format: Marked,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text'''
|
||||||
|
|
||||||
video = (self._download_json(
|
video = (self._download_json(
|
||||||
self._API_BASE_URL + f'video/{video_id}', video_id,
|
self._API_BASE_URL + f'video/{video_id}', video_id,
|
||||||
'Downloading additional video metadata', fatal=False) or {}).get('video') or {}
|
'Downloading additional video metadata', fatal=False, headers=self._HEADERS) or {}).get('video') or {}
|
||||||
show = video.get('show') or {}
|
show = video.get('show') or {}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -298,9 +297,9 @@ Format: Marked,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text'''
|
||||||
|
|
||||||
|
|
||||||
class ADNSeasonIE(ADNBaseIE):
|
class ADNSeasonIE(ADNBaseIE):
|
||||||
_VALID_URL = r'https?://(?:www\.)?(?:animation|anime)digitalnetwork\.(?P<lang>fr|de)/video/(?P<id>[^/?#]+)/?(?:$|[#?])'
|
_VALID_URL = r'https?://(?:www\.)?animationdigitalnetwork\.com/(?:(?P<lang>de)/)?video/(?P<id>\d+)[^/?#]*/?(?:$|[#?])'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://animationdigitalnetwork.fr/video/tokyo-mew-mew-new',
|
'url': 'https://animationdigitalnetwork.com/video/911-tokyo-mew-mew-new',
|
||||||
'playlist_count': 12,
|
'playlist_count': 12,
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '911',
|
'id': '911',
|
||||||
|
@ -311,24 +310,22 @@ class ADNSeasonIE(ADNBaseIE):
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
lang, video_show_slug = self._match_valid_url(url).group('lang', 'id')
|
lang, video_show_slug = self._match_valid_url(url).group('lang', 'id')
|
||||||
|
self._HEADERS['X-Target-Distribution'] = lang or 'fr'
|
||||||
show = self._download_json(
|
show = self._download_json(
|
||||||
f'{self._API_BASE_URL}show/{video_show_slug}/', video_show_slug,
|
f'{self._API_BASE_URL}show/{video_show_slug}/', video_show_slug,
|
||||||
'Downloading show JSON metadata', headers=self._HEADERS)['show']
|
'Downloading show JSON metadata', headers=self._HEADERS)['show']
|
||||||
show_id = str(show['id'])
|
show_id = str(show['id'])
|
||||||
episodes = self._download_json(
|
episodes = self._download_json(
|
||||||
f'{self._API_BASE_URL}video/show/{show_id}', video_show_slug,
|
f'{self._API_BASE_URL}video/show/{show_id}', video_show_slug,
|
||||||
'Downloading episode list', headers={
|
'Downloading episode list', headers=self._HEADERS, query={
|
||||||
'X-Target-Distribution': lang,
|
|
||||||
**self._HEADERS,
|
|
||||||
}, query={
|
|
||||||
'order': 'asc',
|
'order': 'asc',
|
||||||
'limit': '-1',
|
'limit': '-1',
|
||||||
})
|
})
|
||||||
|
|
||||||
def entries():
|
def entries():
|
||||||
for episode_id in traverse_obj(episodes, ('videos', ..., 'id', {str_or_none})):
|
for episode_id in traverse_obj(episodes, ('videos', ..., 'id', {str_or_none})):
|
||||||
yield self.url_result(
|
yield self.url_result(join_nonempty(
|
||||||
f'https://animationdigitalnetwork.{lang}/video/{video_show_slug}/{episode_id}',
|
'https://animationdigitalnetwork.com', lang, 'video',
|
||||||
ADNIE, episode_id)
|
video_show_slug, episode_id, delim='/'), ADNIE, episode_id)
|
||||||
|
|
||||||
return self.playlist_result(entries(), show_id, show.get('title'))
|
return self.playlist_result(entries(), show_id, show.get('title'))
|
||||||
|
|
|
@ -1355,6 +1355,7 @@ MSO_INFO = {
|
||||||
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 @@ class AdobePassIE(InfoExtractor): # XXX: Conventionally, base classes should en
|
||||||
'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()
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import functools
|
import functools
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
|
from ..networking import Request
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
OnDemandPagedList,
|
OnDemandPagedList,
|
||||||
|
@ -32,21 +33,21 @@ class AfreecaTVBaseIE(InfoExtractor):
|
||||||
}
|
}
|
||||||
|
|
||||||
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.',
|
||||||
}
|
}
|
||||||
|
@ -58,71 +59,42 @@ class AfreecaTVBaseIE(InfoExtractor):
|
||||||
f'Unable to login: {self.IE_NAME} said: {error}',
|
f'Unable to login: {self.IE_NAME} said: {error}',
|
||||||
expected=True)
|
expected=True)
|
||||||
|
|
||||||
|
def _call_api(self, endpoint, display_id, data=None, headers=None, query=None):
|
||||||
|
return self._download_json(Request(
|
||||||
|
f'https://api.m.sooplive.co.kr/{endpoint}',
|
||||||
|
data=data, headers=headers, query=query,
|
||||||
|
extensions={'legacy_ssl': True}), display_id,
|
||||||
|
'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',
|
||||||
|
@ -134,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',
|
||||||
|
@ -149,44 +121,25 @@ 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,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
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._call_api(
|
||||||
'https://api.m.afreecatv.com/station/video/a/view', video_id,
|
'station/video/a/view', video_id, headers={'Referer': url},
|
||||||
headers={'Referer': url}, data=urlencode_postdata({
|
data=urlencode_postdata({
|
||||||
'nTitleNo': video_id,
|
'nTitleNo': video_id,
|
||||||
'nApiLevel': 10,
|
'nApiLevel': 10,
|
||||||
}))['data']
|
}))['data']
|
||||||
|
@ -201,7 +154,7 @@ class AfreecaTVIE(AfreecaTVBaseIE):
|
||||||
'title': ('title', {str}),
|
'title': ('title', {str}),
|
||||||
'uploader': ('writer_nick', {str}),
|
'uploader': ('writer_nick', {str}),
|
||||||
'uploader_id': ('bj_id', {str}),
|
'uploader_id': ('bj_id', {str}),
|
||||||
'duration': ('total_file_duration', {functools.partial(int_or_none, scale=1000)}),
|
'duration': ('total_file_duration', {int_or_none(scale=1000)}),
|
||||||
'thumbnail': ('thumb', {url_or_none}),
|
'thumbnail': ('thumb', {url_or_none}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -225,7 +178,7 @@ class AfreecaTVIE(AfreecaTVBaseIE):
|
||||||
'title': f'{common_info.get("title") or "Untitled"} (part {file_num})',
|
'title': f'{common_info.get("title") or "Untitled"} (part {file_num})',
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
**traverse_obj(file_element, {
|
**traverse_obj(file_element, {
|
||||||
'duration': ('duration', {functools.partial(int_or_none, scale=1000)}),
|
'duration': ('duration', {int_or_none(scale=1000)}),
|
||||||
'timestamp': ('file_start', {unified_timestamp}),
|
'timestamp': ('file_start', {unified_timestamp}),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
@ -253,12 +206,49 @@ class AfreecaTVIE(AfreecaTVBaseIE):
|
||||||
return self.playlist_result(entries, video_id, multi_video=True, **common_info)
|
return self.playlist_result(entries, video_id, multi_video=True, **common_info)
|
||||||
|
|
||||||
|
|
||||||
class AfreecaTVLiveIE(AfreecaTVBaseIE):
|
class AfreecaTVCatchStoryIE(AfreecaTVBaseIE):
|
||||||
IE_NAME = 'afreecatv:live'
|
IE_NAME = 'soop:catchstory'
|
||||||
IE_DESC = 'afreecatv.com livestreams'
|
IE_DESC = 'sooplive.co.kr catch story'
|
||||||
_VALID_URL = r'https?://play\.afreeca(?:tv)?\.com/(?P<id>[^/]+)(?:/(?P<bno>\d+))?'
|
_VALID_URL = r'https?://vod\.(?:sooplive\.co\.kr|afreecatv\.com)/player/(?P<id>\d+)/catchstory'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://play.afreecatv.com/pyh3646/237852185',
|
'url': 'https://vod.sooplive.co.kr/player/103247/catchstory',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '103247',
|
||||||
|
},
|
||||||
|
'playlist_count': 2,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
data = self._call_api(
|
||||||
|
'catchstory/a/view', video_id, headers={'Referer': url},
|
||||||
|
query={'aStoryListIdx': '', 'nStoryIdx': video_id})
|
||||||
|
|
||||||
|
return self.playlist_result(self._entries(data), video_id)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _entries(data):
|
||||||
|
# 'files' is always a list with 1 element
|
||||||
|
yield from traverse_obj(data, (
|
||||||
|
'data', lambda _, v: v['story_type'] == 'catch',
|
||||||
|
'catch_list', lambda _, v: v['files'][0]['file'], {
|
||||||
|
'id': ('files', 0, 'file_info_key', {str}),
|
||||||
|
'url': ('files', 0, 'file', {url_or_none}),
|
||||||
|
'duration': ('files', 0, 'duration', {int_or_none(scale=1000)}),
|
||||||
|
'title': ('title', {str}),
|
||||||
|
'uploader': ('writer_nick', {str}),
|
||||||
|
'uploader_id': ('writer_id', {str}),
|
||||||
|
'thumbnail': ('thumb', {url_or_none}),
|
||||||
|
'timestamp': ('write_timestamp', {int_or_none}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
|
||||||
|
class AfreecaTVLiveIE(AfreecaTVBaseIE):
|
||||||
|
IE_NAME = 'soop:live'
|
||||||
|
IE_DESC = 'sooplive.co.kr livestreams'
|
||||||
|
_VALID_URL = r'https?://play\.(?:sooplive\.co\.kr|afreecatv\.com)/(?P<id>[^/?#]+)(?:/(?P<bno>\d+))?'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://play.sooplive.co.kr/pyh3646/237852185',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '237852185',
|
'id': '237852185',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
|
@ -270,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([
|
||||||
|
@ -313,7 +303,7 @@ class AfreecaTVLiveIE(AfreecaTVBaseIE):
|
||||||
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
|
||||||
|
@ -329,7 +319,13 @@ class AfreecaTVLiveIE(AfreecaTVBaseIE):
|
||||||
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:
|
||||||
raise UserNotLive(video_id=broadcaster_id)
|
result = channel_info.get('RESULT')
|
||||||
|
if result == 0:
|
||||||
|
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:
|
||||||
|
@ -358,7 +354,7 @@ class AfreecaTVLiveIE(AfreecaTVBaseIE):
|
||||||
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 {}
|
||||||
|
|
||||||
|
@ -374,11 +370,11 @@ class AfreecaTVLiveIE(AfreecaTVBaseIE):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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',
|
||||||
|
@ -386,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',
|
||||||
|
@ -394,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',
|
||||||
|
@ -402,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',
|
||||||
|
@ -414,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')
|
||||||
|
|
|
@ -71,7 +71,7 @@ class AllstarBaseIE(InfoExtractor):
|
||||||
'thumbnails': (('clipImageThumb', 'clipImageSource'), {'url': {media_url_or_none}}),
|
'thumbnails': (('clipImageThumb', 'clipImageSource'), {'url': {media_url_or_none}}),
|
||||||
'duration': ('clipLength', {int_or_none}),
|
'duration': ('clipLength', {int_or_none}),
|
||||||
'filesize': ('clipSizeBytes', {int_or_none}),
|
'filesize': ('clipSizeBytes', {int_or_none}),
|
||||||
'timestamp': ('createdDate', {functools.partial(int_or_none, scale=1000)}),
|
'timestamp': ('createdDate', {int_or_none(scale=1000)}),
|
||||||
'uploader': ('username', {str}),
|
'uploader': ('username', {str}),
|
||||||
'uploader_id': ('user', '_id', {str}),
|
'uploader_id': ('user', '_id', {str}),
|
||||||
'view_count': ('views', {int_or_none}),
|
'view_count': ('views', {int_or_none}),
|
||||||
|
|
|
@ -33,24 +33,6 @@ class AnvatoIE(InfoExtractor):
|
||||||
_AUTH_KEY = b'\x31\xc2\x42\x84\x9e\x73\xa0\xce' # from anvplayer.min.js
|
_AUTH_KEY = b'\x31\xc2\x42\x84\x9e\x73\xa0\xce' # from anvplayer.min.js
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
# from https://www.nfl.com/videos/baker-mayfield-s-game-changing-plays-from-3-td-game-week-14
|
|
||||||
'url': 'anvato:GXvEgwyJeWem8KCYXfeoHWknwP48Mboj:899441',
|
|
||||||
'md5': '921919dab3cd0b849ff3d624831ae3e2',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '899441',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Baker Mayfield\'s game-changing plays from 3-TD game Week 14',
|
|
||||||
'description': 'md5:85e05a3cc163f8c344340f220521136d',
|
|
||||||
'upload_date': '20201215',
|
|
||||||
'timestamp': 1608009755,
|
|
||||||
'thumbnail': r're:^https?://.*\.jpg',
|
|
||||||
'uploader': 'NFL',
|
|
||||||
'tags': ['Baltimore Ravens at Cleveland Browns (2020-REG-14)', 'Baker Mayfield', 'Game Highlights',
|
|
||||||
'Player Highlights', 'Cleveland Browns', 'league'],
|
|
||||||
'duration': 157,
|
|
||||||
'categories': ['Entertainment', 'Game', 'Highlights'],
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
# from https://ktla.com/news/99-year-old-woman-learns-to-fly-in-torrance-checks-off-bucket-list-dream/
|
# from https://ktla.com/news/99-year-old-woman-learns-to-fly-in-torrance-checks-off-bucket-list-dream/
|
||||||
'url': 'anvato:X8POa4zpGZMmeiq0wqiO8IP5rMqQM9VN:8032455',
|
'url': 'anvato:X8POa4zpGZMmeiq0wqiO8IP5rMqQM9VN:8032455',
|
||||||
'md5': '837718bcfb3a7778d022f857f7a9b19e',
|
'md5': '837718bcfb3a7778d022f857f7a9b19e',
|
||||||
|
@ -241,31 +223,6 @@ class AnvatoIE(InfoExtractor):
|
||||||
'telemundo': 'anvato_mcp_telemundo_web_prod_c5278d51ad46fda4b6ca3d0ea44a7846a054f582',
|
'telemundo': 'anvato_mcp_telemundo_web_prod_c5278d51ad46fda4b6ca3d0ea44a7846a054f582',
|
||||||
}
|
}
|
||||||
|
|
||||||
def _generate_nfl_token(self, anvack, mcp_id):
|
|
||||||
reroute = self._download_json(
|
|
||||||
'https://api.nfl.com/v1/reroute', mcp_id, data=b'grant_type=client_credentials',
|
|
||||||
headers={'X-Domain-Id': 100}, note='Fetching token info')
|
|
||||||
token_type = reroute.get('token_type') or 'Bearer'
|
|
||||||
auth_token = f'{token_type} {reroute["access_token"]}'
|
|
||||||
response = self._download_json(
|
|
||||||
'https://api.nfl.com/v3/shield/', mcp_id, data=json.dumps({
|
|
||||||
'query': '''{
|
|
||||||
viewer {
|
|
||||||
mediaToken(anvack: "%s", id: %s) {
|
|
||||||
token
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}''' % (anvack, mcp_id), # noqa: UP031
|
|
||||||
}).encode(), headers={
|
|
||||||
'Authorization': auth_token,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}, note='Fetching NFL API token')
|
|
||||||
return traverse_obj(response, ('data', 'viewer', 'mediaToken', 'token'))
|
|
||||||
|
|
||||||
_TOKEN_GENERATORS = {
|
|
||||||
'GXvEgwyJeWem8KCYXfeoHWknwP48Mboj': _generate_nfl_token,
|
|
||||||
}
|
|
||||||
|
|
||||||
def _server_time(self, access_key, video_id):
|
def _server_time(self, access_key, video_id):
|
||||||
return int_or_none(traverse_obj(self._download_json(
|
return int_or_none(traverse_obj(self._download_json(
|
||||||
f'{self._API_BASE_URL}/server_time', video_id, query={'anvack': access_key},
|
f'{self._API_BASE_URL}/server_time', video_id, query={'anvack': access_key},
|
||||||
|
@ -290,8 +247,6 @@ class AnvatoIE(InfoExtractor):
|
||||||
}
|
}
|
||||||
if extracted_token is not None:
|
if extracted_token is not None:
|
||||||
api['anvstk2'] = extracted_token
|
api['anvstk2'] = extracted_token
|
||||||
elif self._TOKEN_GENERATORS.get(access_key) is not None:
|
|
||||||
api['anvstk2'] = self._TOKEN_GENERATORS[access_key](self, access_key, video_id)
|
|
||||||
elif self._ANVACK_TABLE.get(access_key) is not None:
|
elif self._ANVACK_TABLE.get(access_key) is not None:
|
||||||
api['anvstk'] = md5_text(f'{access_key}|{anvrid}|{server_time}|{self._ANVACK_TABLE[access_key]}')
|
api['anvstk'] = md5_text(f'{access_key}|{anvrid}|{server_time}|{self._ANVACK_TABLE[access_key]}')
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -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',
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
extract_attributes,
|
extract_attributes,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
|
join_nonempty,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
try_get,
|
try_get,
|
||||||
)
|
)
|
||||||
|
@ -136,7 +137,7 @@ class ArcPublishingIE(InfoExtractor):
|
||||||
else:
|
else:
|
||||||
vbr = int_or_none(s.get('bitrate'))
|
vbr = int_or_none(s.get('bitrate'))
|
||||||
formats.append({
|
formats.append({
|
||||||
'format_id': f'{stream_type}-{vbr}' if vbr else stream_type,
|
'format_id': join_nonempty(stream_type, vbr),
|
||||||
'vbr': vbr,
|
'vbr': vbr,
|
||||||
'width': int_or_none(s.get('width')),
|
'width': int_or_none(s.get('width')),
|
||||||
'height': int_or_none(s.get('height')),
|
'height': int_or_none(s.get('height')),
|
||||||
|
|
|
@ -231,7 +231,7 @@ class ARDIE(InfoExtractor):
|
||||||
|
|
||||||
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)/
|
||||||
|
@ -299,7 +299,7 @@ class ARDBetaMediathekIE(InfoExtractor):
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '94834686',
|
'id': '94834686',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'duration': 2700,
|
'duration': 2670,
|
||||||
'episode': '7 Tage ... unter harten Jungs',
|
'episode': '7 Tage ... unter harten Jungs',
|
||||||
'description': 'md5:0f215470dcd2b02f59f4bd10c963f072',
|
'description': 'md5:0f215470dcd2b02f59f4bd10c963f072',
|
||||||
'upload_date': '20231005',
|
'upload_date': '20231005',
|
||||||
|
@ -307,10 +307,28 @@ class ARDBetaMediathekIE(InfoExtractor):
|
||||||
'display_id': 'N2I2YmM5MzgtNWFlOS00ZGFlLTg2NzMtYzNjM2JlNjk4MDg3',
|
'display_id': 'N2I2YmM5MzgtNWFlOS00ZGFlLTg2NzMtYzNjM2JlNjk4MDg3',
|
||||||
'series': '7 Tage ...',
|
'series': '7 Tage ...',
|
||||||
'channel': 'HR',
|
'channel': 'HR',
|
||||||
'thumbnail': 'https://api.ardmediathek.de/image-service/images/urn:ard:image:f6e6d5ffac41925c?w=960&ch=fa32ba69bc87989a',
|
'thumbnail': 'https://api.ardmediathek.de/image-service/images/urn:ard:image:430c86d233afa42d?w=960&ch=fa32ba69bc87989a',
|
||||||
'title': '7 Tage ... unter harten Jungs',
|
'title': '7 Tage ... unter harten Jungs',
|
||||||
'_old_archive_ids': ['ardbetamediathek N2I2YmM5MzgtNWFlOS00ZGFlLTg2NzMtYzNjM2JlNjk4MDg3'],
|
'_old_archive_ids': ['ardbetamediathek N2I2YmM5MzgtNWFlOS00ZGFlLTg2NzMtYzNjM2JlNjk4MDg3'],
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.ardmediathek.de/video/lokalzeit-aus-duesseldorf/lokalzeit-aus-duesseldorf-oder-31-10-2024/wdr-duesseldorf/Y3JpZDovL3dkci5kZS9CZWl0cmFnLXNvcGhvcmEtOWFkMTc0ZWMtMDA5ZS00ZDEwLWFjYjctMGNmNTdhNzVmNzUz',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '13847165',
|
||||||
|
'chapters': 'count:8',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'channel': 'WDR',
|
||||||
|
'display_id': 'Y3JpZDovL3dkci5kZS9CZWl0cmFnLXNvcGhvcmEtOWFkMTc0ZWMtMDA5ZS00ZDEwLWFjYjctMGNmNTdhNzVmNzUz',
|
||||||
|
'episode': 'Lokalzeit aus Düsseldorf | 31.10.2024',
|
||||||
|
'series': 'Lokalzeit aus Düsseldorf',
|
||||||
|
'thumbnail': 'https://api.ardmediathek.de/image-service/images/urn:ard:image:f02ec9bd9b7bd5f6?w=960&ch=612491dcd5e09b0c',
|
||||||
|
'title': 'Lokalzeit aus Düsseldorf | 31.10.2024',
|
||||||
|
'upload_date': '20241031',
|
||||||
|
'timestamp': 1730399400,
|
||||||
|
'description': 'md5:12db30b3b706314efe3778b8df1a7058',
|
||||||
|
'duration': 1759,
|
||||||
|
'_old_archive_ids': ['ardbetamediathek Y3JpZDovL3dkci5kZS9CZWl0cmFnLXNvcGhvcmEtOWFkMTc0ZWMtMDA5ZS00ZDEwLWFjYjctMGNmNTdhNzVmNzUz'],
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://beta.ardmediathek.de/ard/video/Y3JpZDovL2Rhc2Vyc3RlLmRlL3RhdG9ydC9mYmM4NGM1NC0xNzU4LTRmZGYtYWFhZS0wYzcyZTIxNGEyMDE',
|
'url': 'https://beta.ardmediathek.de/ard/video/Y3JpZDovL2Rhc2Vyc3RlLmRlL3RhdG9ydC9mYmM4NGM1NC0xNzU4LTRmZGYtYWFhZS0wYzcyZTIxNGEyMDE',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
@ -455,6 +473,12 @@ class ARDBetaMediathekIE(InfoExtractor):
|
||||||
'subtitles': subtitles,
|
'subtitles': subtitles,
|
||||||
'is_live': is_live,
|
'is_live': is_live,
|
||||||
'age_limit': age_limit,
|
'age_limit': age_limit,
|
||||||
|
**traverse_obj(media_data, {
|
||||||
|
'chapters': ('pluginData', 'jumpmarks@all', 'chapterArray', lambda _, v: int_or_none(v['chapterTime']), {
|
||||||
|
'start_time': ('chapterTime', {int_or_none}),
|
||||||
|
'title': ('chapterTitle', {str}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
**traverse_obj(media_data, ('meta', {
|
**traverse_obj(media_data, ('meta', {
|
||||||
'title': 'title',
|
'title': 'title',
|
||||||
'description': 'synopsis',
|
'description': 'synopsis',
|
||||||
|
@ -470,7 +494,7 @@ class ARDBetaMediathekIE(InfoExtractor):
|
||||||
|
|
||||||
|
|
||||||
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)/
|
||||||
|
|
|
@ -131,8 +131,8 @@ class ArkenaIE(InfoExtractor):
|
||||||
formats.extend(self._extract_f4m_formats(
|
formats.extend(self._extract_f4m_formats(
|
||||||
href, video_id, f4m_id='hds', fatal=False))
|
href, video_id, f4m_id='hds', fatal=False))
|
||||||
elif mime_type == 'application/dash+xml':
|
elif mime_type == 'application/dash+xml':
|
||||||
formats.extend(self._extract_f4m_formats(
|
formats.extend(self._extract_mpd_formats(
|
||||||
href, video_id, f4m_id='hds', fatal=False))
|
href, video_id, mpd_id='dash', fatal=False))
|
||||||
elif mime_type == 'application/vnd.ms-sstr+xml':
|
elif mime_type == 'application/vnd.ms-sstr+xml':
|
||||||
formats.extend(self._extract_ism_formats(
|
formats.extend(self._extract_ism_formats(
|
||||||
href, video_id, ism_id='mss', fatal=False))
|
href, video_id, ism_id='mss', fatal=False))
|
||||||
|
|
|
@ -101,9 +101,10 @@ class AsobiStageIE(InfoExtractor):
|
||||||
self._HEADERS['Authorization'] = f'Bearer {token}'
|
self._HEADERS['Authorization'] = f'Bearer {token}'
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id, event, type_, slug = self._match_valid_url(url).group('id', 'event', 'type', 'slug')
|
webpage, urlh = self._download_webpage_handle(url, self._match_id(url))
|
||||||
|
video_id, event, type_, slug = self._match_valid_url(urlh.url).group('id', 'event', 'type', 'slug')
|
||||||
video_type = {'archive': 'archives', 'player': 'broadcasts'}[type_]
|
video_type = {'archive': 'archives', 'player': 'broadcasts'}[type_]
|
||||||
webpage = self._download_webpage(url, video_id)
|
|
||||||
event_data = traverse_obj(
|
event_data = traverse_obj(
|
||||||
self._search_nextjs_data(webpage, video_id, default={}),
|
self._search_nextjs_data(webpage, video_id, default={}),
|
||||||
('props', 'pageProps', 'eventCMSData', {
|
('props', 'pageProps', 'eventCMSData', {
|
||||||
|
|
|
@ -33,14 +33,6 @@ class AtresPlayerIE(InfoExtractor):
|
||||||
]
|
]
|
||||||
_API_BASE = 'https://api.atresplayer.com/'
|
_API_BASE = 'https://api.atresplayer.com/'
|
||||||
|
|
||||||
def _handle_error(self, e, code):
|
|
||||||
if isinstance(e.cause, HTTPError) and e.cause.status == code:
|
|
||||||
error = self._parse_json(e.cause.response.read(), None)
|
|
||||||
if error.get('error') == 'required_registered':
|
|
||||||
self.raise_login_required()
|
|
||||||
raise ExtractorError(error['error_description'], expected=True)
|
|
||||||
raise
|
|
||||||
|
|
||||||
def _perform_login(self, username, password):
|
def _perform_login(self, username, password):
|
||||||
self._request_webpage(
|
self._request_webpage(
|
||||||
self._API_BASE + 'login', None, 'Downloading login page')
|
self._API_BASE + 'login', None, 'Downloading login page')
|
||||||
|
@ -55,7 +47,9 @@ class AtresPlayerIE(InfoExtractor):
|
||||||
'password': password,
|
'password': password,
|
||||||
}))['targetUrl']
|
}))['targetUrl']
|
||||||
except ExtractorError as e:
|
except ExtractorError as e:
|
||||||
self._handle_error(e, 400)
|
if isinstance(e.cause, HTTPError) and e.cause.status == 400:
|
||||||
|
raise ExtractorError('Invalid username and/or password', expected=True)
|
||||||
|
raise
|
||||||
|
|
||||||
self._request_webpage(target_url, None, 'Following Target URL')
|
self._request_webpage(target_url, None, 'Following Target URL')
|
||||||
|
|
||||||
|
@ -66,7 +60,12 @@ class AtresPlayerIE(InfoExtractor):
|
||||||
episode = self._download_json(
|
episode = self._download_json(
|
||||||
self._API_BASE + 'client/v1/player/episode/' + video_id, video_id)
|
self._API_BASE + 'client/v1/player/episode/' + video_id, video_id)
|
||||||
except ExtractorError as e:
|
except ExtractorError as e:
|
||||||
self._handle_error(e, 403)
|
if isinstance(e.cause, HTTPError) and e.cause.status == 403:
|
||||||
|
error = self._parse_json(e.cause.response.read(), None)
|
||||||
|
if error.get('error') == 'required_registered':
|
||||||
|
self.raise_login_required()
|
||||||
|
raise ExtractorError(error['error_description'], expected=True)
|
||||||
|
raise
|
||||||
|
|
||||||
title = episode['titulo']
|
title = episode['titulo']
|
||||||
|
|
||||||
|
|
|
@ -4,9 +4,13 @@ import urllib.parse
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
InAdvancePagedList,
|
InAdvancePagedList,
|
||||||
|
determine_ext,
|
||||||
format_field,
|
format_field,
|
||||||
|
int_or_none,
|
||||||
|
join_nonempty,
|
||||||
traverse_obj,
|
traverse_obj,
|
||||||
unified_timestamp,
|
unified_timestamp,
|
||||||
|
url_or_none,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -30,6 +34,7 @@ class BanByeBaseIE(InfoExtractor):
|
||||||
class BanByeIE(BanByeBaseIE):
|
class BanByeIE(BanByeBaseIE):
|
||||||
_VALID_URL = r'https?://(?:www\.)?banbye\.com/(?:en/)?watch/(?P<id>[\w-]+)'
|
_VALID_URL = r'https?://(?:www\.)?banbye\.com/(?:en/)?watch/(?P<id>[\w-]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
|
# ['src']['mp4']['levels'] direct mp4 urls only
|
||||||
'url': 'https://banbye.com/watch/v_ytfmvkVYLE8T',
|
'url': 'https://banbye.com/watch/v_ytfmvkVYLE8T',
|
||||||
'md5': '2f4ea15c5ca259a73d909b2cfd558eb5',
|
'md5': '2f4ea15c5ca259a73d909b2cfd558eb5',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
|
@ -58,6 +63,7 @@ class BanByeIE(BanByeBaseIE):
|
||||||
},
|
},
|
||||||
'playlist_mincount': 9,
|
'playlist_mincount': 9,
|
||||||
}, {
|
}, {
|
||||||
|
# ['src']['mp4']['levels'] direct mp4 urls only
|
||||||
'url': 'https://banbye.com/watch/v_kb6_o1Kyq-CD',
|
'url': 'https://banbye.com/watch/v_kb6_o1Kyq-CD',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'v_kb6_o1Kyq-CD',
|
'id': 'v_kb6_o1Kyq-CD',
|
||||||
|
@ -77,6 +83,48 @@ class BanByeIE(BanByeBaseIE):
|
||||||
'view_count': int,
|
'view_count': int,
|
||||||
'comment_count': int,
|
'comment_count': int,
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
# ['src']['hls']['levels'] variant m3u8 urls only; master m3u8 is 404
|
||||||
|
'url': 'https://banbye.com/watch/v_a_gPFuC9LoW5',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'v_a_gPFuC9LoW5',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'md5:183524056bebdfa245fd6d214f63c0fe',
|
||||||
|
'description': 'md5:943ac87287ca98d28d8b8797719827c6',
|
||||||
|
'uploader': 'wRealu24',
|
||||||
|
'channel_id': 'ch_wrealu24',
|
||||||
|
'channel_url': 'https://banbye.com/channel/ch_wrealu24',
|
||||||
|
'upload_date': '20231113',
|
||||||
|
'timestamp': 1699874062,
|
||||||
|
'view_count': int,
|
||||||
|
'like_count': int,
|
||||||
|
'dislike_count': int,
|
||||||
|
'comment_count': int,
|
||||||
|
'thumbnail': 'https://cdn.banbye.com/video/v_a_gPFuC9LoW5/96.webp',
|
||||||
|
'tags': ['jaszczur', 'sejm', 'lewica', 'polska', 'ukrainizacja', 'pierwszeposiedzeniesejmu'],
|
||||||
|
},
|
||||||
|
'expected_warnings': ['Failed to download m3u8'],
|
||||||
|
}, {
|
||||||
|
# ['src']['hls']['masterPlaylist'] m3u8 only
|
||||||
|
'url': 'https://banbye.com/watch/v_B0rsKWsr-aaa',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'v_B0rsKWsr-aaa',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'md5:00b254164b82101b3f9e5326037447ed',
|
||||||
|
'description': 'md5:3fd8b48aa81954ba024bc60f5de6e167',
|
||||||
|
'uploader': 'PSTV Piotr Szlachtowicz ',
|
||||||
|
'channel_id': 'ch_KV9EVObkB9wB',
|
||||||
|
'channel_url': 'https://banbye.com/channel/ch_KV9EVObkB9wB',
|
||||||
|
'upload_date': '20240629',
|
||||||
|
'timestamp': 1719646816,
|
||||||
|
'duration': 2377,
|
||||||
|
'view_count': int,
|
||||||
|
'like_count': int,
|
||||||
|
'dislike_count': int,
|
||||||
|
'comment_count': int,
|
||||||
|
'thumbnail': 'https://cdn.banbye.com/video/v_B0rsKWsr-aaa/96.webp',
|
||||||
|
'tags': ['Biden', 'Trump', 'Wybory', 'USA'],
|
||||||
|
},
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
|
@ -91,11 +139,24 @@ class BanByeIE(BanByeBaseIE):
|
||||||
'id': f'{quality}p',
|
'id': f'{quality}p',
|
||||||
'url': f'{self._CDN_BASE}/video/{video_id}/{quality}.webp',
|
'url': f'{self._CDN_BASE}/video/{video_id}/{quality}.webp',
|
||||||
} for quality in [48, 96, 144, 240, 512, 1080]]
|
} for quality in [48, 96, 144, 240, 512, 1080]]
|
||||||
formats = [{
|
|
||||||
'format_id': f'http-{quality}p',
|
formats = []
|
||||||
'quality': quality,
|
url_data = self._download_json(f'{self._API_BASE}/videos/{video_id}/url', video_id, data=b'')
|
||||||
'url': f'{self._CDN_BASE}/video/{video_id}/{quality}.mp4',
|
if master_url := traverse_obj(url_data, ('src', 'hls', 'masterPlaylist', {url_or_none})):
|
||||||
} for quality in data['quality']]
|
formats = self._extract_m3u8_formats(master_url, video_id, 'mp4', m3u8_id='hls', fatal=False)
|
||||||
|
|
||||||
|
for format_id, format_url in traverse_obj(url_data, (
|
||||||
|
'src', ('mp4', 'hls'), 'levels', {dict.items}, lambda _, v: url_or_none(v[1]))):
|
||||||
|
ext = determine_ext(format_url)
|
||||||
|
is_hls = ext == 'm3u8'
|
||||||
|
formats.append({
|
||||||
|
'url': format_url,
|
||||||
|
'ext': 'mp4' if is_hls else ext,
|
||||||
|
'format_id': join_nonempty(is_hls and 'hls', format_id),
|
||||||
|
'protocol': 'm3u8_native' if is_hls else 'https',
|
||||||
|
'height': int_or_none(format_id),
|
||||||
|
})
|
||||||
|
self._remove_duplicate_formats(formats)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import json
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
|
@ -6,6 +7,7 @@ from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
KNOWN_EXTENSIONS,
|
KNOWN_EXTENSIONS,
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
|
extract_attributes,
|
||||||
float_or_none,
|
float_or_none,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
parse_filesize,
|
parse_filesize,
|
||||||
|
@ -17,6 +19,7 @@ from ..utils import (
|
||||||
url_or_none,
|
url_or_none,
|
||||||
urljoin,
|
urljoin,
|
||||||
)
|
)
|
||||||
|
from ..utils.traversal import find_element, traverse_obj
|
||||||
|
|
||||||
|
|
||||||
class BandcampIE(InfoExtractor):
|
class BandcampIE(InfoExtractor):
|
||||||
|
@ -40,8 +43,10 @@ class BandcampIE(InfoExtractor):
|
||||||
'uploader_url': 'https://youtube-dl.bandcamp.com',
|
'uploader_url': 'https://youtube-dl.bandcamp.com',
|
||||||
'uploader_id': 'youtube-dl',
|
'uploader_id': 'youtube-dl',
|
||||||
'thumbnail': 'https://f4.bcbits.com/img/a3216802731_5.jpg',
|
'thumbnail': 'https://f4.bcbits.com/img/a3216802731_5.jpg',
|
||||||
|
'artists': ['youtube-dl "\'/\\ä↭'],
|
||||||
|
'album_artists': ['youtube-dl "\'/\\ä↭'],
|
||||||
},
|
},
|
||||||
'_skip': 'There is a limit of 200 free downloads / month for the test song',
|
'skip': 'There is a limit of 200 free downloads / month for the test song',
|
||||||
}, {
|
}, {
|
||||||
# free download
|
# free download
|
||||||
'url': 'http://benprunty.bandcamp.com/track/lanius-battle',
|
'url': 'http://benprunty.bandcamp.com/track/lanius-battle',
|
||||||
|
@ -266,6 +271,18 @@ class BandcampAlbumIE(BandcampIE): # XXX: Do not subclass from concrete IE
|
||||||
'timestamp': 1311756226,
|
'timestamp': 1311756226,
|
||||||
'upload_date': '20110727',
|
'upload_date': '20110727',
|
||||||
'uploader': 'Blazo',
|
'uploader': 'Blazo',
|
||||||
|
'thumbnail': 'https://f4.bcbits.com/img/a1721150828_5.jpg',
|
||||||
|
'album_artists': ['Blazo'],
|
||||||
|
'uploader_url': 'https://blazo.bandcamp.com',
|
||||||
|
'release_date': '20110727',
|
||||||
|
'release_timestamp': 1311724800.0,
|
||||||
|
'track': 'Intro',
|
||||||
|
'uploader_id': 'blazo',
|
||||||
|
'track_number': 1,
|
||||||
|
'album': 'Jazz Format Mixtape vol.1',
|
||||||
|
'artists': ['Blazo'],
|
||||||
|
'duration': 19.335,
|
||||||
|
'track_id': '1353101989',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -277,6 +294,18 @@ class BandcampAlbumIE(BandcampIE): # XXX: Do not subclass from concrete IE
|
||||||
'timestamp': 1311757238,
|
'timestamp': 1311757238,
|
||||||
'upload_date': '20110727',
|
'upload_date': '20110727',
|
||||||
'uploader': 'Blazo',
|
'uploader': 'Blazo',
|
||||||
|
'track': 'Kero One - Keep It Alive (Blazo remix)',
|
||||||
|
'release_date': '20110727',
|
||||||
|
'track_id': '38097443',
|
||||||
|
'track_number': 2,
|
||||||
|
'duration': 181.467,
|
||||||
|
'uploader_url': 'https://blazo.bandcamp.com',
|
||||||
|
'album': 'Jazz Format Mixtape vol.1',
|
||||||
|
'uploader_id': 'blazo',
|
||||||
|
'album_artists': ['Blazo'],
|
||||||
|
'artists': ['Blazo'],
|
||||||
|
'thumbnail': 'https://f4.bcbits.com/img/a1721150828_5.jpg',
|
||||||
|
'release_timestamp': 1311724800.0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -284,6 +313,7 @@ class BandcampAlbumIE(BandcampIE): # XXX: Do not subclass from concrete IE
|
||||||
'title': 'Jazz Format Mixtape vol.1',
|
'title': 'Jazz Format Mixtape vol.1',
|
||||||
'id': 'jazz-format-mixtape-vol-1',
|
'id': 'jazz-format-mixtape-vol-1',
|
||||||
'uploader_id': 'blazo',
|
'uploader_id': 'blazo',
|
||||||
|
'description': 'md5:38052a93217f3ffdc033cd5dbbce2989',
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'playlistend': 2,
|
'playlistend': 2,
|
||||||
|
@ -358,10 +388,10 @@ class BandcampWeeklyIE(BandcampIE): # XXX: Do not subclass from concrete IE
|
||||||
_VALID_URL = r'https?://(?:www\.)?bandcamp\.com/?\?(?:.*?&)?show=(?P<id>\d+)'
|
_VALID_URL = r'https?://(?:www\.)?bandcamp\.com/?\?(?:.*?&)?show=(?P<id>\d+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://bandcamp.com/?show=224',
|
'url': 'https://bandcamp.com/?show=224',
|
||||||
'md5': 'b00df799c733cf7e0c567ed187dea0fd',
|
'md5': '61acc9a002bed93986b91168aa3ab433',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '224',
|
'id': '224',
|
||||||
'ext': 'opus',
|
'ext': 'mp3',
|
||||||
'title': 'BC Weekly April 4th 2017 - Magic Moments',
|
'title': 'BC Weekly April 4th 2017 - Magic Moments',
|
||||||
'description': 'md5:5d48150916e8e02d030623a48512c874',
|
'description': 'md5:5d48150916e8e02d030623a48512c874',
|
||||||
'duration': 5829.77,
|
'duration': 5829.77,
|
||||||
|
@ -371,7 +401,7 @@ class BandcampWeeklyIE(BandcampIE): # XXX: Do not subclass from concrete IE
|
||||||
'episode_id': '224',
|
'episode_id': '224',
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'format': 'opus-lo',
|
'format': 'mp3-128',
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://bandcamp.com/?blah/blah@&show=228',
|
'url': 'https://bandcamp.com/?blah/blah@&show=228',
|
||||||
|
@ -459,7 +489,7 @@ class BandcampUserIE(InfoExtractor):
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://coldworldofficial.bandcamp.com/music',
|
'url': 'https://coldworldofficial.bandcamp.com/music',
|
||||||
'playlist_mincount': 10,
|
'playlist_mincount': 7,
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'coldworldofficial',
|
'id': 'coldworldofficial',
|
||||||
'title': 'Discography of coldworldofficial',
|
'title': 'Discography of coldworldofficial',
|
||||||
|
@ -473,12 +503,19 @@ class BandcampUserIE(InfoExtractor):
|
||||||
},
|
},
|
||||||
}]
|
}]
|
||||||
|
|
||||||
|
def _yield_items(self, webpage):
|
||||||
|
yield from (
|
||||||
|
re.findall(r'<li data-item-id=["\'][^>]+>\s*<a href=["\'](?![^"\'/]*?/merch)([^"\']+)', webpage)
|
||||||
|
or re.findall(r'<div[^>]+trackTitle["\'][^"\']+["\']([^"\']+)', webpage))
|
||||||
|
|
||||||
|
yield from traverse_obj(webpage, (
|
||||||
|
{find_element(id='music-grid', html=True)}, {extract_attributes},
|
||||||
|
'data-client-items', {json.loads}, ..., 'page_url', {str}))
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
uploader = self._match_id(url)
|
uploader = self._match_id(url)
|
||||||
webpage = self._download_webpage(url, uploader)
|
webpage = self._download_webpage(url, uploader)
|
||||||
|
|
||||||
discography_data = (re.findall(r'<li data-item-id=["\'][^>]+>\s*<a href=["\'](?![^"\'/]*?/merch)([^"\']+)', webpage)
|
|
||||||
or re.findall(r'<div[^>]+trackTitle["\'][^"\']+["\']([^"\']+)', webpage))
|
|
||||||
|
|
||||||
return self.playlist_from_matches(
|
return self.playlist_from_matches(
|
||||||
discography_data, uploader, f'Discography of {uploader}', getter=lambda x: urljoin(url, x))
|
self._yield_items(webpage), uploader, f'Discography of {uploader}',
|
||||||
|
getter=urljoin(url))
|
||||||
|
|
|
@ -1284,9 +1284,9 @@ class BBCIE(BBCCoUkIE): # XXX: Do not subclass from concrete IE
|
||||||
**traverse_obj(model, {
|
**traverse_obj(model, {
|
||||||
'title': ('title', {str}),
|
'title': ('title', {str}),
|
||||||
'thumbnail': ('imageUrl', {lambda u: urljoin(url, u.replace('$recipe', 'raw'))}),
|
'thumbnail': ('imageUrl', {lambda u: urljoin(url, u.replace('$recipe', 'raw'))}),
|
||||||
'description': ('synopses', ('long', 'medium', 'short'), {str}, {lambda x: x or None}, any),
|
'description': ('synopses', ('long', 'medium', 'short'), {str}, filter, any),
|
||||||
'duration': ('versions', 0, 'duration', {int}),
|
'duration': ('versions', 0, 'duration', {int}),
|
||||||
'timestamp': ('versions', 0, 'availableFrom', {functools.partial(int_or_none, scale=1000)}),
|
'timestamp': ('versions', 0, 'availableFrom', {int_or_none(scale=1000)}),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1386,7 +1386,7 @@ class BBCIE(BBCCoUkIE): # XXX: Do not subclass from concrete IE
|
||||||
formats = traverse_obj(media_data, ('playlist', lambda _, v: url_or_none(v['url']), {
|
formats = traverse_obj(media_data, ('playlist', lambda _, v: url_or_none(v['url']), {
|
||||||
'url': ('url', {url_or_none}),
|
'url': ('url', {url_or_none}),
|
||||||
'ext': ('format', {str}),
|
'ext': ('format', {str}),
|
||||||
'tbr': ('bitrate', {functools.partial(int_or_none, scale=1000)}),
|
'tbr': ('bitrate', {int_or_none(scale=1000)}),
|
||||||
}))
|
}))
|
||||||
if formats:
|
if formats:
|
||||||
entry = {
|
entry = {
|
||||||
|
@ -1398,7 +1398,7 @@ class BBCIE(BBCCoUkIE): # XXX: Do not subclass from concrete IE
|
||||||
'title': ('title', {str}),
|
'title': ('title', {str}),
|
||||||
'thumbnail': ('imageUrl', {lambda u: urljoin(url, u.replace('$recipe', 'raw'))}),
|
'thumbnail': ('imageUrl', {lambda u: urljoin(url, u.replace('$recipe', 'raw'))}),
|
||||||
'description': ('synopses', ('long', 'medium', 'short'), {str}, any),
|
'description': ('synopses', ('long', 'medium', 'short'), {str}, any),
|
||||||
'timestamp': ('firstPublished', {functools.partial(int_or_none, scale=1000)}),
|
'timestamp': ('firstPublished', {int_or_none(scale=1000)}),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
done = True
|
done = True
|
||||||
|
@ -1428,7 +1428,7 @@ class BBCIE(BBCCoUkIE): # XXX: Do not subclass from concrete IE
|
||||||
if not entry.get('timestamp'):
|
if not entry.get('timestamp'):
|
||||||
entry['timestamp'] = traverse_obj(next_data, (
|
entry['timestamp'] = traverse_obj(next_data, (
|
||||||
..., 'contents', is_type('timestamp'), 'model',
|
..., 'contents', is_type('timestamp'), 'model',
|
||||||
'timestamp', {functools.partial(int_or_none, scale=1000)}, any))
|
'timestamp', {int_or_none(scale=1000)}, any))
|
||||||
entries.append(entry)
|
entries.append(entry)
|
||||||
return self.playlist_result(
|
return self.playlist_result(
|
||||||
entries, playlist_id, playlist_title, playlist_description)
|
entries, playlist_id, playlist_title, playlist_description)
|
||||||
|
|
68
yt_dlp/extractor/beacon.py
Normal file
68
yt_dlp/extractor/beacon.py
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import json
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
ExtractorError,
|
||||||
|
parse_iso8601,
|
||||||
|
traverse_obj,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BeaconTvIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?beacon\.tv/content/(?P<id>[\w-]+)'
|
||||||
|
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://beacon.tv/content/welcome-to-beacon',
|
||||||
|
'md5': 'b3f5932d437f288e662f10f3bfc5bd04',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'welcome-to-beacon',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'upload_date': '20240509',
|
||||||
|
'description': 'md5:ea2bd32e71acf3f9fca6937412cc3563',
|
||||||
|
'thumbnail': 'https://cdn.jwplayer.com/v2/media/I4CkkEvN/poster.jpg?width=720',
|
||||||
|
'title': 'Your home for Critical Role!',
|
||||||
|
'timestamp': 1715227200,
|
||||||
|
'duration': 105.494,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://beacon.tv/content/re-slayers-take-trailer',
|
||||||
|
'md5': 'd879b091485dbed2245094c8152afd89',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 're-slayers-take-trailer',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'The Re-Slayer’s Take | Official Trailer',
|
||||||
|
'timestamp': 1715189040,
|
||||||
|
'upload_date': '20240508',
|
||||||
|
'duration': 53.249,
|
||||||
|
'thumbnail': 'https://cdn.jwplayer.com/v2/media/PW5ApIw3/poster.jpg?width=720',
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
|
content_data = traverse_obj(self._search_nextjs_data(webpage, video_id), (
|
||||||
|
'props', 'pageProps', '__APOLLO_STATE__',
|
||||||
|
lambda k, v: k.startswith('Content:') and v['slug'] == video_id, any))
|
||||||
|
if not content_data:
|
||||||
|
raise ExtractorError('Failed to extract content data')
|
||||||
|
|
||||||
|
jwplayer_data = traverse_obj(content_data, (
|
||||||
|
(('contentVideo', 'video', 'videoData'),
|
||||||
|
('contentPodcast', 'podcast', 'audioData')), {json.loads}, {dict}, any))
|
||||||
|
if not jwplayer_data:
|
||||||
|
if content_data.get('contentType') not in ('videoPodcast', 'video', 'podcast'):
|
||||||
|
raise ExtractorError('Content is not a video/podcast', expected=True)
|
||||||
|
if traverse_obj(content_data, ('contentTier', '__ref')) != 'MemberTier:65b258d178f89be87b4dc0a4':
|
||||||
|
self.raise_login_required('This video/podcast is for members only')
|
||||||
|
raise ExtractorError('Failed to extract content')
|
||||||
|
|
||||||
|
return {
|
||||||
|
**self._parse_jwplayer_data(jwplayer_data, video_id),
|
||||||
|
**traverse_obj(content_data, {
|
||||||
|
'title': ('title', {str}),
|
||||||
|
'description': ('description', {str}),
|
||||||
|
'timestamp': ('publishedAt', {parse_iso8601}),
|
||||||
|
}),
|
||||||
|
}
|
|
@ -1,18 +1,33 @@
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import extract_attributes
|
from ..utils import ExtractorError, extract_attributes
|
||||||
|
|
||||||
|
|
||||||
class BFMTVBaseIE(InfoExtractor):
|
class BFMTVBaseIE(InfoExtractor):
|
||||||
_VALID_URL_BASE = r'https?://(?:www\.|rmc\.)?bfmtv\.com/'
|
_VALID_URL_BASE = r'https?://(?:www\.|rmc\.)?bfmtv\.com/'
|
||||||
_VALID_URL_TMPL = _VALID_URL_BASE + r'(?:[^/]+/)*[^/?&#]+_%s[A-Z]-(?P<id>\d{12})\.html'
|
_VALID_URL_TMPL = _VALID_URL_BASE + r'(?:[^/]+/)*[^/?&#]+_%s[A-Z]-(?P<id>\d{12})\.html'
|
||||||
_VIDEO_BLOCK_REGEX = r'(<div[^>]+class="video_block[^"]*"[^>]*>)'
|
_VIDEO_BLOCK_REGEX = r'(<div[^>]+class="video_block[^"]*"[^>]*>.*?</div>)'
|
||||||
|
_VIDEO_ELEMENT_REGEX = r'(<video-js[^>]+>)'
|
||||||
BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/%s/%s_default/index.html?videoId=%s'
|
BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/%s/%s_default/index.html?videoId=%s'
|
||||||
|
|
||||||
def _brightcove_url_result(self, video_id, video_block):
|
def _extract_video(self, video_block):
|
||||||
account_id = video_block.get('accountid') or '876450612001'
|
video_element = self._search_regex(
|
||||||
player_id = video_block.get('playerid') or 'I2qBTln4u'
|
self._VIDEO_ELEMENT_REGEX, video_block, 'video element', default=None)
|
||||||
|
if video_element:
|
||||||
|
video_element_attrs = extract_attributes(video_element)
|
||||||
|
video_id = video_element_attrs.get('data-video-id')
|
||||||
|
if not video_id:
|
||||||
|
return
|
||||||
|
account_id = video_element_attrs.get('data-account') or '876450610001'
|
||||||
|
player_id = video_element_attrs.get('adjustplayer') or '19dszYXgm'
|
||||||
|
else:
|
||||||
|
video_block_attrs = extract_attributes(video_block)
|
||||||
|
video_id = video_block_attrs.get('videoid')
|
||||||
|
if not video_id:
|
||||||
|
return
|
||||||
|
account_id = video_block_attrs.get('accountid') or '876630703001'
|
||||||
|
player_id = video_block_attrs.get('playerid') or 'KbPwEbuHx'
|
||||||
return self.url_result(
|
return self.url_result(
|
||||||
self.BRIGHTCOVE_URL_TEMPLATE % (account_id, player_id, video_id),
|
self.BRIGHTCOVE_URL_TEMPLATE % (account_id, player_id, video_id),
|
||||||
'BrightcoveNew', video_id)
|
'BrightcoveNew', video_id)
|
||||||
|
@ -40,23 +55,25 @@ class BFMTVIE(BFMTVBaseIE):
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
bfmtv_id = self._match_id(url)
|
bfmtv_id = self._match_id(url)
|
||||||
webpage = self._download_webpage(url, bfmtv_id)
|
webpage = self._download_webpage(url, bfmtv_id)
|
||||||
video_block = extract_attributes(self._search_regex(
|
video = self._extract_video(self._search_regex(
|
||||||
self._VIDEO_BLOCK_REGEX, webpage, 'video block'))
|
self._VIDEO_BLOCK_REGEX, webpage, 'video block'))
|
||||||
return self._brightcove_url_result(video_block['videoid'], video_block)
|
if not video:
|
||||||
|
raise ExtractorError('Failed to extract video')
|
||||||
|
return video
|
||||||
|
|
||||||
|
|
||||||
class BFMTVLiveIE(BFMTVIE): # XXX: Do not subclass from concrete IE
|
class BFMTVLiveIE(BFMTVBaseIE):
|
||||||
IE_NAME = 'bfmtv:live'
|
IE_NAME = 'bfmtv:live'
|
||||||
_VALID_URL = BFMTVBaseIE._VALID_URL_BASE + '(?P<id>(?:[^/]+/)?en-direct)'
|
_VALID_URL = BFMTVBaseIE._VALID_URL_BASE + '(?P<id>(?:[^/]+/)?en-direct)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.bfmtv.com/en-direct/',
|
'url': 'https://www.bfmtv.com/en-direct/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '5615950982001',
|
'id': '6346069778112',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': r're:^le direct BFMTV WEB \d{4}-\d{2}-\d{2} \d{2}:\d{2}$',
|
'title': r're:^Le Live BFM TV \d{4}-\d{2}-\d{2} \d{2}:\d{2}$',
|
||||||
'uploader_id': '876450610001',
|
'uploader_id': '876450610001',
|
||||||
'upload_date': '20220926',
|
'upload_date': '20240202',
|
||||||
'timestamp': 1664207191,
|
'timestamp': 1706887572,
|
||||||
'live_status': 'is_live',
|
'live_status': 'is_live',
|
||||||
'thumbnail': r're:https://.+/image\.jpg',
|
'thumbnail': r're:https://.+/image\.jpg',
|
||||||
'tags': [],
|
'tags': [],
|
||||||
|
@ -69,6 +86,15 @@ class BFMTVLiveIE(BFMTVIE): # XXX: Do not subclass from concrete IE
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
bfmtv_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, bfmtv_id)
|
||||||
|
video = self._extract_video(self._search_regex(
|
||||||
|
self._VIDEO_BLOCK_REGEX, webpage, 'video block'))
|
||||||
|
if not video:
|
||||||
|
raise ExtractorError('Failed to extract video')
|
||||||
|
return video
|
||||||
|
|
||||||
|
|
||||||
class BFMTVArticleIE(BFMTVBaseIE):
|
class BFMTVArticleIE(BFMTVBaseIE):
|
||||||
IE_NAME = 'bfmtv:article'
|
IE_NAME = 'bfmtv:article'
|
||||||
|
@ -102,18 +128,16 @@ class BFMTVArticleIE(BFMTVBaseIE):
|
||||||
},
|
},
|
||||||
}]
|
}]
|
||||||
|
|
||||||
|
def _entries(self, webpage):
|
||||||
|
for video_block_el in re.findall(self._VIDEO_BLOCK_REGEX, webpage):
|
||||||
|
video = self._extract_video(video_block_el)
|
||||||
|
if video:
|
||||||
|
yield video
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
bfmtv_id = self._match_id(url)
|
bfmtv_id = self._match_id(url)
|
||||||
webpage = self._download_webpage(url, bfmtv_id)
|
webpage = self._download_webpage(url, bfmtv_id)
|
||||||
|
|
||||||
entries = []
|
|
||||||
for video_block_el in re.findall(self._VIDEO_BLOCK_REGEX, webpage):
|
|
||||||
video_block = extract_attributes(video_block_el)
|
|
||||||
video_id = video_block.get('videoid')
|
|
||||||
if not video_id:
|
|
||||||
continue
|
|
||||||
entries.append(self._brightcove_url_result(video_id, video_block))
|
|
||||||
|
|
||||||
return self.playlist_result(
|
return self.playlist_result(
|
||||||
entries, bfmtv_id, self._og_search_title(webpage, fatal=False),
|
self._entries(webpage), bfmtv_id, self._og_search_title(webpage, fatal=False),
|
||||||
self._html_search_meta(['og:description', 'description'], webpage))
|
self._html_search_meta(['og:description', 'description'], webpage))
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import functools
|
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
|
@ -50,7 +49,7 @@ class BibelTVBaseIE(InfoExtractor):
|
||||||
**traverse_obj(data, {
|
**traverse_obj(data, {
|
||||||
'title': 'title',
|
'title': 'title',
|
||||||
'description': 'description',
|
'description': 'description',
|
||||||
'duration': ('duration', {functools.partial(int_or_none, scale=1000)}),
|
'duration': ('duration', {int_or_none(scale=1000)}),
|
||||||
'timestamp': ('schedulingStart', {parse_iso8601}),
|
'timestamp': ('schedulingStart', {parse_iso8601}),
|
||||||
'season_number': 'seasonNumber',
|
'season_number': 'seasonNumber',
|
||||||
'episode_number': 'episodeNumber',
|
'episode_number': 'episodeNumber',
|
||||||
|
|
|
@ -31,12 +31,12 @@ from ..utils import (
|
||||||
mimetype2ext,
|
mimetype2ext,
|
||||||
parse_count,
|
parse_count,
|
||||||
parse_qs,
|
parse_qs,
|
||||||
|
parse_resolution,
|
||||||
qualities,
|
qualities,
|
||||||
smuggle_url,
|
smuggle_url,
|
||||||
srt_subtitles_timecode,
|
srt_subtitles_timecode,
|
||||||
str_or_none,
|
str_or_none,
|
||||||
traverse_obj,
|
traverse_obj,
|
||||||
try_call,
|
|
||||||
unified_timestamp,
|
unified_timestamp,
|
||||||
unsmuggle_url,
|
unsmuggle_url,
|
||||||
url_or_none,
|
url_or_none,
|
||||||
|
@ -46,7 +46,25 @@ from ..utils import (
|
||||||
|
|
||||||
|
|
||||||
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 = {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_logged_in(self):
|
||||||
|
return bool(self._get_cookies('https://api.bilibili.com').get('SESSDATA'))
|
||||||
|
|
||||||
|
def _check_missing_formats(self, play_info, formats):
|
||||||
|
parsed_qualities = set(traverse_obj(formats, (..., 'quality')))
|
||||||
|
missing_formats = join_nonempty(*[
|
||||||
|
traverse_obj(fmt, 'new_description', 'display_desc', 'quality')
|
||||||
|
for fmt in traverse_obj(play_info, (
|
||||||
|
'support_formats', lambda _, v: v['quality'] not in parsed_qualities))], delim=', ')
|
||||||
|
if missing_formats:
|
||||||
|
self.to_screen(
|
||||||
|
f'Format(s) {missing_formats} are missing; you have to login or '
|
||||||
|
f'become a premium member to download them. {self._login_hint()}')
|
||||||
|
|
||||||
def extract_formats(self, play_info):
|
def extract_formats(self, play_info):
|
||||||
format_names = {
|
format_names = {
|
||||||
|
@ -86,18 +104,75 @@ class BilibiliBaseIE(InfoExtractor):
|
||||||
'format': format_names.get(video.get('id')),
|
'format': format_names.get(video.get('id')),
|
||||||
} for video in traverse_obj(play_info, ('dash', 'video', ...)))
|
} for video in traverse_obj(play_info, ('dash', 'video', ...)))
|
||||||
|
|
||||||
missing_formats = format_names.keys() - set(traverse_obj(formats, (..., 'quality')))
|
if formats:
|
||||||
if missing_formats:
|
self._check_missing_formats(play_info, formats)
|
||||||
self.to_screen(f'Format(s) {", ".join(format_names[i] for i in missing_formats)} are missing; '
|
|
||||||
f'you have to login or become premium member to download them. {self._login_hint()}')
|
|
||||||
|
|
||||||
|
fragments = traverse_obj(play_info, ('durl', lambda _, v: url_or_none(v['url']), {
|
||||||
|
'url': ('url', {url_or_none}),
|
||||||
|
'duration': ('length', {float_or_none(scale=1000)}),
|
||||||
|
'filesize': ('size', {int_or_none}),
|
||||||
|
}))
|
||||||
|
if fragments:
|
||||||
|
formats.append({
|
||||||
|
'url': fragments[0]['url'],
|
||||||
|
'filesize': sum(traverse_obj(fragments, (..., 'filesize'))),
|
||||||
|
**({
|
||||||
|
'fragments': fragments,
|
||||||
|
'protocol': 'http_dash_segments',
|
||||||
|
} if len(fragments) > 1 else {}),
|
||||||
|
**traverse_obj(play_info, {
|
||||||
|
'quality': ('quality', {int_or_none}),
|
||||||
|
'format_id': ('quality', {str_or_none}),
|
||||||
|
'format_note': ('quality', {lambda x: format_names.get(x)}),
|
||||||
|
'duration': ('timelength', {float_or_none(scale=1000)}),
|
||||||
|
}),
|
||||||
|
**parse_resolution(format_names.get(play_info.get('quality'))),
|
||||||
|
})
|
||||||
return formats
|
return formats
|
||||||
|
|
||||||
def _download_playinfo(self, video_id, cid, headers=None):
|
def _get_wbi_key(self, video_id):
|
||||||
|
if time.time() < self._wbi_key_cache.get('ts', 0) + self._WBI_KEY_CACHE_TIMEOUT:
|
||||||
|
return self._wbi_key_cache['key']
|
||||||
|
|
||||||
|
session_data = self._download_json(
|
||||||
|
'https://api.bilibili.com/x/web-interface/nav', video_id, note='Downloading wbi sign')
|
||||||
|
|
||||||
|
lookup = ''.join(traverse_obj(session_data, (
|
||||||
|
'data', 'wbi_img', ('img_url', 'sub_url'),
|
||||||
|
{lambda x: x.rpartition('/')[2].partition('.')[0]})))
|
||||||
|
|
||||||
|
# from getMixinKey() in the vendor js
|
||||||
|
mixin_key_enc_tab = [
|
||||||
|
46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49,
|
||||||
|
33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40,
|
||||||
|
61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11,
|
||||||
|
36, 20, 34, 44, 52,
|
||||||
|
]
|
||||||
|
|
||||||
|
self._wbi_key_cache.update({
|
||||||
|
'key': ''.join(lookup[i] for i in mixin_key_enc_tab)[:32],
|
||||||
|
'ts': time.time(),
|
||||||
|
})
|
||||||
|
return self._wbi_key_cache['key']
|
||||||
|
|
||||||
|
def _sign_wbi(self, params, video_id):
|
||||||
|
params['wts'] = round(time.time())
|
||||||
|
params = {
|
||||||
|
k: ''.join(filter(lambda char: char not in "!'()*", str(v)))
|
||||||
|
for k, v in sorted(params.items())
|
||||||
|
}
|
||||||
|
query = urllib.parse.urlencode(params)
|
||||||
|
params['w_rid'] = hashlib.md5(f'{query}{self._get_wbi_key(video_id)}'.encode()).hexdigest()
|
||||||
|
return params
|
||||||
|
|
||||||
|
def _download_playinfo(self, bvid, cid, headers=None, qn=None):
|
||||||
|
params = {'bvid': bvid, 'cid': cid, 'fnval': 4048}
|
||||||
|
if qn:
|
||||||
|
params['qn'] = qn
|
||||||
return self._download_json(
|
return self._download_json(
|
||||||
'https://api.bilibili.com/x/player/playurl', video_id,
|
'https://api.bilibili.com/x/player/wbi/playurl', bvid,
|
||||||
query={'bvid': video_id, 'cid': cid, 'fnval': 4048},
|
query=self._sign_wbi(params, bvid), headers=headers,
|
||||||
note=f'Downloading video formats for cid {cid}', headers=headers)['data']
|
note=f'Downloading video formats for cid {cid} {qn or ""}')['data']
|
||||||
|
|
||||||
def json2srt(self, json_data):
|
def json2srt(self, json_data):
|
||||||
srt_data = ''
|
srt_data = ''
|
||||||
|
@ -115,15 +190,15 @@ class BilibiliBaseIE(InfoExtractor):
|
||||||
}],
|
}],
|
||||||
}
|
}
|
||||||
|
|
||||||
subtitle_info = traverse_obj(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}'), ('data', 'subtitle'))
|
note=f'Extracting subtitle info {cid}', headers=self._HEADERS)
|
||||||
subs_list = traverse_obj(subtitle_info, ('subtitles', lambda _, v: v['subtitle_url'] and v['lan']))
|
if traverse_obj(video_info, ('data', 'need_login_subtitle')):
|
||||||
if not subs_list and traverse_obj(subtitle_info, 'allow_submit'):
|
self.report_warning(
|
||||||
if not self._get_cookies('https://api.bilibili.com').get('SESSDATA'): # no login session cookie
|
f'Subtitles are only available when logged in. {self._login_hint()}', only_once=True)
|
||||||
self.report_warning(f'CC subtitles (if any) are only visible when logged in. {self._login_hint()}', only_once=True)
|
for s in traverse_obj(video_info, (
|
||||||
for s in subs_list:
|
'data', 'subtitle', 'subtitles', lambda _, v: v['subtitle_url'] and v['lan'])):
|
||||||
subtitles.setdefault(s['lan'], []).append({
|
subtitles.setdefault(s['lan'], []).append({
|
||||||
'ext': 'srt',
|
'ext': 'srt',
|
||||||
'data': self.json2srt(self._download_json(s['subtitle_url'], video_id)),
|
'data': self.json2srt(self._download_json(s['subtitle_url'], video_id)),
|
||||||
|
@ -133,7 +208,7 @@ class BilibiliBaseIE(InfoExtractor):
|
||||||
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',
|
||||||
|
@ -203,15 +278,15 @@ class BilibiliBaseIE(InfoExtractor):
|
||||||
self._get_divisions(video_id, graph_version, edges, choice['edge_id'], cid_edges=cid_edges)
|
self._get_divisions(video_id, graph_version, edges, choice['edge_id'], cid_edges=cid_edges)
|
||||||
return cid_edges
|
return cid_edges
|
||||||
|
|
||||||
def _get_interactive_entries(self, video_id, cid, metainfo):
|
def _get_interactive_entries(self, video_id, cid, metainfo, headers=None):
|
||||||
graph_version = traverse_obj(
|
graph_version = traverse_obj(
|
||||||
self._download_json(
|
self._download_json(
|
||||||
'https://api.bilibili.com/x/player/wbi/v2', video_id,
|
'https://api.bilibili.com/x/player/wbi/v2', video_id,
|
||||||
'Extracting graph version', query={'bvid': video_id, 'cid': cid}),
|
'Extracting graph version', query={'bvid': video_id, 'cid': cid}, headers=headers),
|
||||||
('data', 'interaction', 'graph_version', {int_or_none}))
|
('data', 'interaction', 'graph_version', {int_or_none}))
|
||||||
cid_edges = self._get_divisions(video_id, graph_version, {1: {'cid': cid}}, 1)
|
cid_edges = self._get_divisions(video_id, graph_version, {1: {'cid': cid}}, 1)
|
||||||
for cid, edges in cid_edges.items():
|
for cid, edges in cid_edges.items():
|
||||||
play_info = self._download_playinfo(video_id, cid)
|
play_info = self._download_playinfo(video_id, cid, headers=headers)
|
||||||
yield {
|
yield {
|
||||||
**metainfo,
|
**metainfo,
|
||||||
'id': f'{video_id}_{cid}',
|
'id': f'{video_id}_{cid}',
|
||||||
|
@ -224,7 +299,7 @@ class BilibiliBaseIE(InfoExtractor):
|
||||||
|
|
||||||
|
|
||||||
class BiliBiliIE(BilibiliBaseIE):
|
class BiliBiliIE(BilibiliBaseIE):
|
||||||
_VALID_URL = r'https?://(?:www\.)?bilibili\.com/(?:video/|festival/\w+\?(?:[^#]*&)?bvid=)[aAbB][vV](?P<id>[^/?#&]+)'
|
_VALID_URL = r'https?://(?:www\.)?bilibili\.com/(?:video/|festival/[^/?#]+\?(?:[^#]*&)?bvid=)[aAbB][vV](?P<id>[^/?#&]+)'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.bilibili.com/video/BV13x41117TL',
|
'url': 'https://www.bilibili.com/video/BV13x41117TL',
|
||||||
|
@ -243,17 +318,17 @@ class BiliBiliIE(BilibiliBaseIE):
|
||||||
'timestamp': 1488353834,
|
'timestamp': 1488353834,
|
||||||
'like_count': int,
|
'like_count': int,
|
||||||
'view_count': int,
|
'view_count': int,
|
||||||
|
'_old_archive_ids': ['bilibili 8903802_part1'],
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'note': 'old av URL version',
|
'note': 'old av URL version',
|
||||||
'url': 'http://www.bilibili.com/video/av1074402/',
|
'url': 'http://www.bilibili.com/video/av1074402/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'thumbnail': r're:^https?://.*\.(jpg|jpeg)$',
|
'id': 'BV11x411K7CN',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
|
'title': '【金坷垃】金泡沫',
|
||||||
'uploader': '菊子桑',
|
'uploader': '菊子桑',
|
||||||
'uploader_id': '156160',
|
'uploader_id': '156160',
|
||||||
'id': 'BV11x411K7CN',
|
|
||||||
'title': '【金坷垃】金泡沫',
|
|
||||||
'duration': 308.36,
|
'duration': 308.36,
|
||||||
'upload_date': '20140420',
|
'upload_date': '20140420',
|
||||||
'timestamp': 1397983878,
|
'timestamp': 1397983878,
|
||||||
|
@ -262,6 +337,8 @@ class BiliBiliIE(BilibiliBaseIE):
|
||||||
'comment_count': int,
|
'comment_count': int,
|
||||||
'view_count': int,
|
'view_count': int,
|
||||||
'tags': list,
|
'tags': list,
|
||||||
|
'thumbnail': r're:^https?://.*\.(jpg|jpeg)$',
|
||||||
|
'_old_archive_ids': ['bilibili 1074402_part1'],
|
||||||
},
|
},
|
||||||
'params': {'skip_download': True},
|
'params': {'skip_download': True},
|
||||||
}, {
|
}, {
|
||||||
|
@ -288,6 +365,7 @@ class BiliBiliIE(BilibiliBaseIE):
|
||||||
'view_count': int,
|
'view_count': int,
|
||||||
'description': 'md5:e3c401cf7bc363118d1783dd74068a68',
|
'description': 'md5:e3c401cf7bc363118d1783dd74068a68',
|
||||||
'duration': 90.314,
|
'duration': 90.314,
|
||||||
|
'_old_archive_ids': ['bilibili 498159642_part1'],
|
||||||
},
|
},
|
||||||
}],
|
}],
|
||||||
}, {
|
}, {
|
||||||
|
@ -308,28 +386,8 @@ class BiliBiliIE(BilibiliBaseIE):
|
||||||
'view_count': int,
|
'view_count': int,
|
||||||
'description': 'md5:e3c401cf7bc363118d1783dd74068a68',
|
'description': 'md5:e3c401cf7bc363118d1783dd74068a68',
|
||||||
'duration': 90.314,
|
'duration': 90.314,
|
||||||
|
'_old_archive_ids': ['bilibili 498159642_part1'],
|
||||||
},
|
},
|
||||||
}, {
|
|
||||||
'note': 'video has subtitles',
|
|
||||||
'url': 'https://www.bilibili.com/video/BV12N4y1M7rh',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'BV12N4y1M7rh',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'md5:96e8bb42c2b432c0d4ce3434a61479c1',
|
|
||||||
'tags': list,
|
|
||||||
'description': 'md5:afde2b7ba9025c01d9e3dde10de221e4',
|
|
||||||
'duration': 313.557,
|
|
||||||
'upload_date': '20220709',
|
|
||||||
'uploader': '小夫太渴',
|
|
||||||
'timestamp': 1657347907,
|
|
||||||
'uploader_id': '1326814124',
|
|
||||||
'comment_count': int,
|
|
||||||
'view_count': int,
|
|
||||||
'like_count': int,
|
|
||||||
'thumbnail': r're:^https?://.*\.(jpg|jpeg|png)$',
|
|
||||||
'subtitles': 'count:2',
|
|
||||||
},
|
|
||||||
'params': {'listsubtitles': True},
|
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.bilibili.com/video/av8903802/',
|
'url': 'https://www.bilibili.com/video/av8903802/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
|
@ -347,6 +405,7 @@ class BiliBiliIE(BilibiliBaseIE):
|
||||||
'comment_count': int,
|
'comment_count': int,
|
||||||
'view_count': int,
|
'view_count': int,
|
||||||
'like_count': int,
|
'like_count': int,
|
||||||
|
'_old_archive_ids': ['bilibili 8903802_part1'],
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
|
@ -370,6 +429,7 @@ class BiliBiliIE(BilibiliBaseIE):
|
||||||
'view_count': int,
|
'view_count': int,
|
||||||
'like_count': int,
|
'like_count': int,
|
||||||
'thumbnail': r're:^https?://.*\.(jpg|jpeg|png)$',
|
'thumbnail': r're:^https?://.*\.(jpg|jpeg|png)$',
|
||||||
|
'_old_archive_ids': ['bilibili 463665680_part1'],
|
||||||
},
|
},
|
||||||
'params': {'skip_download': True},
|
'params': {'skip_download': True},
|
||||||
}, {
|
}, {
|
||||||
|
@ -388,8 +448,8 @@ class BiliBiliIE(BilibiliBaseIE):
|
||||||
'view_count': int,
|
'view_count': int,
|
||||||
'like_count': int,
|
'like_count': int,
|
||||||
'thumbnail': r're:^https?://.*\.(jpg|jpeg|png)$',
|
'thumbnail': r're:^https?://.*\.(jpg|jpeg|png)$',
|
||||||
|
'_old_archive_ids': ['bilibili 893839363_part1'],
|
||||||
},
|
},
|
||||||
'params': {'skip_download': True},
|
|
||||||
}, {
|
}, {
|
||||||
'note': 'newer festival video',
|
'note': 'newer festival video',
|
||||||
'url': 'https://www.bilibili.com/festival/2023honkaiimpact3gala?bvid=BV1ay4y1d77f',
|
'url': 'https://www.bilibili.com/festival/2023honkaiimpact3gala?bvid=BV1ay4y1d77f',
|
||||||
|
@ -406,8 +466,57 @@ class BiliBiliIE(BilibiliBaseIE):
|
||||||
'view_count': int,
|
'view_count': int,
|
||||||
'like_count': int,
|
'like_count': int,
|
||||||
'thumbnail': r're:^https?://.*\.(jpg|jpeg|png)$',
|
'thumbnail': r're:^https?://.*\.(jpg|jpeg|png)$',
|
||||||
|
'_old_archive_ids': ['bilibili 778246196_part1'],
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'note': 'legacy flv/mp4 video',
|
||||||
|
'url': 'https://www.bilibili.com/video/BV1ms411Q7vw/?p=4',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'BV1ms411Q7vw_p4',
|
||||||
|
'title': '[搞笑]【动画】云南方言快乐生产线出品 p04 新烧包谷之漫游桃花岛',
|
||||||
|
'timestamp': 1458222815,
|
||||||
|
'upload_date': '20160317',
|
||||||
|
'description': '云南方言快乐生产线出品',
|
||||||
|
'duration': float,
|
||||||
|
'uploader': '一笑颠天',
|
||||||
|
'uploader_id': '3916081',
|
||||||
|
'view_count': int,
|
||||||
|
'comment_count': int,
|
||||||
|
'like_count': int,
|
||||||
|
'tags': list,
|
||||||
|
'thumbnail': r're:^https?://.*\.(jpg|jpeg|png)$',
|
||||||
|
'_old_archive_ids': ['bilibili 4120229_part4'],
|
||||||
|
},
|
||||||
|
'params': {'extractor_args': {'bilibili': {'prefer_multi_flv': ['32']}}},
|
||||||
|
'playlist_count': 19,
|
||||||
|
'playlist': [{
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'BV1ms411Q7vw_p4_0',
|
||||||
|
'ext': 'flv',
|
||||||
|
'title': '[搞笑]【动画】云南方言快乐生产线出品 p04 新烧包谷之漫游桃花岛',
|
||||||
|
'duration': 399.102,
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
}, {
|
||||||
|
'note': 'legacy mp4-only video',
|
||||||
|
'url': 'https://www.bilibili.com/video/BV1nx411u79K',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'BV1nx411u79K',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': '【练习室】201603声乐练习《No Air》with VigoVan',
|
||||||
|
'timestamp': 1508893551,
|
||||||
|
'upload_date': '20171025',
|
||||||
|
'description': '@ZERO-G伯远\n声乐练习 《No Air》with Vigo Van',
|
||||||
|
'duration': 80.384,
|
||||||
|
'uploader': '伯远',
|
||||||
|
'uploader_id': '10584494',
|
||||||
|
'comment_count': int,
|
||||||
|
'view_count': int,
|
||||||
|
'like_count': int,
|
||||||
|
'tags': list,
|
||||||
|
'thumbnail': r're:^https?://.*\.(jpg|jpeg|png)$',
|
||||||
|
'_old_archive_ids': ['bilibili 15700301_part1'],
|
||||||
},
|
},
|
||||||
'params': {'skip_download': True},
|
|
||||||
}, {
|
}, {
|
||||||
'note': 'interactive/split-path video',
|
'note': 'interactive/split-path video',
|
||||||
'url': 'https://www.bilibili.com/video/BV1af4y1H7ga/',
|
'url': 'https://www.bilibili.com/video/BV1af4y1H7ga/',
|
||||||
|
@ -425,6 +534,7 @@ class BiliBiliIE(BilibiliBaseIE):
|
||||||
'view_count': int,
|
'view_count': int,
|
||||||
'like_count': int,
|
'like_count': int,
|
||||||
'thumbnail': r're:^https?://.*\.(jpg|jpeg|png)$',
|
'thumbnail': r're:^https?://.*\.(jpg|jpeg|png)$',
|
||||||
|
'_old_archive_ids': ['bilibili 292734508_part1'],
|
||||||
},
|
},
|
||||||
'playlist_count': 33,
|
'playlist_count': 33,
|
||||||
'playlist': [{
|
'playlist': [{
|
||||||
|
@ -443,6 +553,7 @@ class BiliBiliIE(BilibiliBaseIE):
|
||||||
'view_count': int,
|
'view_count': int,
|
||||||
'like_count': int,
|
'like_count': int,
|
||||||
'thumbnail': r're:^https?://.*\.(jpg|jpeg|png)$',
|
'thumbnail': r're:^https?://.*\.(jpg|jpeg|png)$',
|
||||||
|
'_old_archive_ids': ['bilibili 292734508_part1'],
|
||||||
},
|
},
|
||||||
}],
|
}],
|
||||||
}, {
|
}, {
|
||||||
|
@ -465,6 +576,29 @@ class BiliBiliIE(BilibiliBaseIE):
|
||||||
'upload_date': '20191021',
|
'upload_date': '20191021',
|
||||||
'thumbnail': r're:^https?://.*\.(jpg|jpeg|png)$',
|
'thumbnail': r're:^https?://.*\.(jpg|jpeg|png)$',
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
'note': 'video has subtitles, which requires login',
|
||||||
|
'url': 'https://www.bilibili.com/video/BV12N4y1M7rh',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'BV12N4y1M7rh',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'md5:96e8bb42c2b432c0d4ce3434a61479c1',
|
||||||
|
'tags': list,
|
||||||
|
'description': 'md5:afde2b7ba9025c01d9e3dde10de221e4',
|
||||||
|
'duration': 313.557,
|
||||||
|
'upload_date': '20220709',
|
||||||
|
'uploader': '小夫太渴',
|
||||||
|
'timestamp': 1657347907,
|
||||||
|
'uploader_id': '1326814124',
|
||||||
|
'comment_count': int,
|
||||||
|
'view_count': int,
|
||||||
|
'like_count': int,
|
||||||
|
'thumbnail': r're:^https?://.*\.(jpg|jpeg|png)$',
|
||||||
|
'subtitles': 'count:2', # login required for CC subtitle
|
||||||
|
'_old_archive_ids': ['bilibili 898179753_part1'],
|
||||||
|
},
|
||||||
|
'params': {'listsubtitles': True},
|
||||||
|
'skip': 'login required for subtitle',
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.bilibili.com/video/BV1jL41167ZG/',
|
'url': 'https://www.bilibili.com/video/BV1jL41167ZG/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
|
@ -489,6 +623,10 @@ class BiliBiliIE(BilibiliBaseIE):
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
},
|
},
|
||||||
'skip': 'geo-restricted',
|
'skip': 'geo-restricted',
|
||||||
|
}, {
|
||||||
|
'note': 'has - in the last path segment of the url',
|
||||||
|
'url': 'https://www.bilibili.com/festival/bh3-7th?bvid=BV1tr4y1f7p2&',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
|
@ -498,8 +636,9 @@ class BiliBiliIE(BilibiliBaseIE):
|
||||||
if not self._match_valid_url(urlh.url):
|
if not self._match_valid_url(urlh.url):
|
||||||
return self.url_result(urlh.url)
|
return self.url_result(urlh.url)
|
||||||
|
|
||||||
initial_state = self._search_json(r'window\.__INITIAL_STATE__\s*=', webpage, 'initial state', video_id)
|
headers['Referer'] = url
|
||||||
|
|
||||||
|
initial_state = self._search_json(r'window\.__INITIAL_STATE__\s*=', webpage, 'initial state', video_id)
|
||||||
is_festival = 'videoData' not in initial_state
|
is_festival = 'videoData' not in initial_state
|
||||||
if is_festival:
|
if is_festival:
|
||||||
video_data = initial_state['videoInfo']
|
video_data = initial_state['videoInfo']
|
||||||
|
@ -548,7 +687,6 @@ class BiliBiliIE(BilibiliBaseIE):
|
||||||
|
|
||||||
aid = video_data.get('aid')
|
aid = video_data.get('aid')
|
||||||
old_video_id = format_field(aid, None, f'%s_part{part_id or 1}')
|
old_video_id = format_field(aid, None, f'%s_part{part_id or 1}')
|
||||||
|
|
||||||
cid = traverse_obj(video_data, ('pages', part_id - 1, 'cid')) if part_id else video_data.get('cid')
|
cid = traverse_obj(video_data, ('pages', part_id - 1, 'cid')) if part_id else video_data.get('cid')
|
||||||
|
|
||||||
festival_info = {}
|
festival_info = {}
|
||||||
|
@ -586,18 +724,65 @@ class BiliBiliIE(BilibiliBaseIE):
|
||||||
is_interactive = traverse_obj(video_data, ('rights', 'is_stein_gate'))
|
is_interactive = traverse_obj(video_data, ('rights', 'is_stein_gate'))
|
||||||
if is_interactive:
|
if is_interactive:
|
||||||
return self.playlist_result(
|
return self.playlist_result(
|
||||||
self._get_interactive_entries(video_id, cid, metainfo), **metainfo,
|
self._get_interactive_entries(video_id, cid, metainfo, headers=headers), **metainfo,
|
||||||
duration=traverse_obj(initial_state, ('videoData', 'duration', {int_or_none})),
|
duration=traverse_obj(initial_state, ('videoData', 'duration', {int_or_none})),
|
||||||
__post_extractor=self.extract_comments(aid))
|
__post_extractor=self.extract_comments(aid))
|
||||||
else:
|
else:
|
||||||
return {
|
formats = self.extract_formats(play_info)
|
||||||
**metainfo,
|
|
||||||
'duration': float_or_none(play_info.get('timelength'), scale=1000),
|
if not traverse_obj(play_info, ('dash')):
|
||||||
'chapters': self._get_chapters(aid, cid),
|
# we only have legacy formats and need additional work
|
||||||
'subtitles': self.extract_subtitles(video_id, cid),
|
has_qn = lambda x: x in traverse_obj(formats, (..., 'quality'))
|
||||||
'formats': self.extract_formats(play_info),
|
for qn in traverse_obj(play_info, ('accept_quality', lambda _, v: not has_qn(v), {int})):
|
||||||
'__post_extractor': self.extract_comments(aid),
|
formats.extend(traverse_obj(
|
||||||
}
|
self.extract_formats(self._download_playinfo(video_id, cid, headers=headers, qn=qn)),
|
||||||
|
lambda _, v: not has_qn(v['quality'])))
|
||||||
|
self._check_missing_formats(play_info, formats)
|
||||||
|
flv_formats = traverse_obj(formats, lambda _, v: v['fragments'])
|
||||||
|
if flv_formats and len(flv_formats) < len(formats):
|
||||||
|
# Flv and mp4 are incompatible due to `multi_video` workaround, so drop one
|
||||||
|
if not self._configuration_arg('prefer_multi_flv'):
|
||||||
|
dropped_fmts = ', '.join(
|
||||||
|
f'{f.get("format_note")} ({f.get("format_id")})' for f in flv_formats)
|
||||||
|
formats = traverse_obj(formats, lambda _, v: not v.get('fragments'))
|
||||||
|
if dropped_fmts:
|
||||||
|
self.to_screen(
|
||||||
|
f'Dropping incompatible flv format(s) {dropped_fmts} since mp4 is available. '
|
||||||
|
'To extract flv, pass --extractor-args "bilibili:prefer_multi_flv"')
|
||||||
|
else:
|
||||||
|
formats = traverse_obj(
|
||||||
|
# XXX: Filtering by extractor-arg is for testing purposes
|
||||||
|
formats, lambda _, v: v['quality'] == int(self._configuration_arg('prefer_multi_flv')[0]),
|
||||||
|
) or [max(flv_formats, key=lambda x: x['quality'])]
|
||||||
|
|
||||||
|
if traverse_obj(formats, (0, 'fragments')):
|
||||||
|
# We have flv formats, which are individual short videos with their own timestamps and metainfo
|
||||||
|
# Binary concatenation corrupts their timestamps, so we need a `multi_video` workaround
|
||||||
|
return {
|
||||||
|
**metainfo,
|
||||||
|
'_type': 'multi_video',
|
||||||
|
'entries': [{
|
||||||
|
'id': f'{metainfo["id"]}_{idx}',
|
||||||
|
'title': metainfo['title'],
|
||||||
|
'http_headers': metainfo['http_headers'],
|
||||||
|
'formats': [{
|
||||||
|
**fragment,
|
||||||
|
'format_id': formats[0].get('format_id'),
|
||||||
|
}],
|
||||||
|
'subtitles': self.extract_subtitles(video_id, cid) if idx == 0 else None,
|
||||||
|
'__post_extractor': self.extract_comments(aid) if idx == 0 else None,
|
||||||
|
} for idx, fragment in enumerate(formats[0]['fragments'])],
|
||||||
|
'duration': float_or_none(play_info.get('timelength'), scale=1000),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
**metainfo,
|
||||||
|
'formats': formats,
|
||||||
|
'duration': float_or_none(play_info.get('timelength'), scale=1000),
|
||||||
|
'chapters': self._get_chapters(aid, cid),
|
||||||
|
'subtitles': self.extract_subtitles(video_id, cid),
|
||||||
|
'__post_extractor': self.extract_comments(aid),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class BiliBiliBangumiIE(BilibiliBaseIE):
|
class BiliBiliBangumiIE(BilibiliBaseIE):
|
||||||
|
@ -837,8 +1022,6 @@ class BiliBiliBangumiSeasonIE(BilibiliBaseIE):
|
||||||
|
|
||||||
|
|
||||||
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)
|
||||||
|
@ -968,7 +1151,7 @@ class BilibiliCheeseSeasonIE(BilibiliCheeseBaseIE):
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|
||||||
class BilibiliSpaceBaseIE(InfoExtractor):
|
class BilibiliSpaceBaseIE(BilibiliBaseIE):
|
||||||
def _extract_playlist(self, fetch_page, get_metadata, get_entries):
|
def _extract_playlist(self, fetch_page, get_metadata, get_entries):
|
||||||
first_page = fetch_page(0)
|
first_page = fetch_page(0)
|
||||||
metadata = get_metadata(first_page)
|
metadata = get_metadata(first_page)
|
||||||
|
@ -988,73 +1171,53 @@ class BilibiliSpaceVideoIE(BilibiliSpaceBaseIE):
|
||||||
'id': '3985676',
|
'id': '3985676',
|
||||||
},
|
},
|
||||||
'playlist_mincount': 178,
|
'playlist_mincount': 178,
|
||||||
|
'skip': 'login required',
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://space.bilibili.com/313580179/video',
|
'url': 'https://space.bilibili.com/313580179/video',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '313580179',
|
'id': '313580179',
|
||||||
},
|
},
|
||||||
'playlist_mincount': 92,
|
'playlist_mincount': 92,
|
||||||
|
'skip': 'login required',
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _extract_signature(self, playlist_id):
|
|
||||||
session_data = self._download_json('https://api.bilibili.com/x/web-interface/nav', playlist_id, fatal=False)
|
|
||||||
|
|
||||||
key_from_url = lambda x: x[x.rfind('/') + 1:].split('.')[0]
|
|
||||||
img_key = traverse_obj(
|
|
||||||
session_data, ('data', 'wbi_img', 'img_url', {key_from_url})) or '34478ba821254d9d93542680e3b86100'
|
|
||||||
sub_key = traverse_obj(
|
|
||||||
session_data, ('data', 'wbi_img', 'sub_url', {key_from_url})) or '7e16a90d190a4355a78fd00b32a38de6'
|
|
||||||
|
|
||||||
session_key = img_key + sub_key
|
|
||||||
|
|
||||||
signature_values = []
|
|
||||||
for position in (
|
|
||||||
46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49, 33, 9, 42, 19, 29, 28, 14, 39,
|
|
||||||
12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40, 61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63,
|
|
||||||
57, 62, 11, 36, 20, 34, 44, 52,
|
|
||||||
):
|
|
||||||
char_at_position = try_call(lambda: session_key[position])
|
|
||||||
if char_at_position:
|
|
||||||
signature_values.append(char_at_position)
|
|
||||||
|
|
||||||
return ''.join(signature_values)[:32]
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
playlist_id, is_video_url = self._match_valid_url(url).group('id', 'video')
|
playlist_id, is_video_url = self._match_valid_url(url).group('id', 'video')
|
||||||
if not is_video_url:
|
if not is_video_url:
|
||||||
self.to_screen('A channel URL was given. Only the channel\'s videos will be downloaded. '
|
self.to_screen('A channel URL was given. Only the channel\'s videos will be downloaded. '
|
||||||
'To download audios, add a "/audio" to the URL')
|
'To download audios, add a "/audio" to the URL')
|
||||||
|
|
||||||
signature = self._extract_signature(playlist_id)
|
|
||||||
|
|
||||||
def fetch_page(page_idx):
|
def fetch_page(page_idx):
|
||||||
query = {
|
query = {
|
||||||
'keyword': '',
|
'keyword': '',
|
||||||
'mid': playlist_id,
|
'mid': playlist_id,
|
||||||
'order': 'pubdate',
|
'order': traverse_obj(parse_qs(url), ('order', 0)) or 'pubdate',
|
||||||
'order_avoided': 'true',
|
'order_avoided': 'true',
|
||||||
'platform': 'web',
|
'platform': 'web',
|
||||||
'pn': page_idx + 1,
|
'pn': page_idx + 1,
|
||||||
'ps': 30,
|
'ps': 30,
|
||||||
'tid': 0,
|
'tid': 0,
|
||||||
'web_location': 1550101,
|
'web_location': 1550101,
|
||||||
'wts': int(time.time()),
|
|
||||||
}
|
}
|
||||||
query['w_rid'] = hashlib.md5(f'{urllib.parse.urlencode(query)}{signature}'.encode()).hexdigest()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = self._download_json('https://api.bilibili.com/x/space/wbi/arc/search',
|
response = self._download_json(
|
||||||
playlist_id, note=f'Downloading page {page_idx}', query=query,
|
'https://api.bilibili.com/x/space/wbi/arc/search', playlist_id,
|
||||||
headers={'referer': url})
|
query=self._sign_wbi(query, playlist_id),
|
||||||
|
note=f'Downloading space page {page_idx}', headers={'Referer': url})
|
||||||
except ExtractorError as e:
|
except ExtractorError as e:
|
||||||
if isinstance(e.cause, HTTPError) and e.cause.status == 412:
|
if isinstance(e.cause, HTTPError) and e.cause.status == 412:
|
||||||
raise ExtractorError(
|
raise ExtractorError(
|
||||||
'Request is blocked by server (412), please add cookies, wait and try later.', expected=True)
|
'Request is blocked by server (412), please add cookies, wait and try later.', expected=True)
|
||||||
raise
|
raise
|
||||||
if response['code'] in (-352, -401):
|
status_code = response['code']
|
||||||
|
if status_code == -401:
|
||||||
raise ExtractorError(
|
raise ExtractorError(
|
||||||
f'Request is blocked by server ({-response["code"]}), '
|
'Request is blocked by server (401), please add cookies, wait and try later.', expected=True)
|
||||||
'please add cookies, wait and try later.', expected=True)
|
elif status_code == -352 and not self.is_logged_in:
|
||||||
|
self.raise_login_required('Request is rejected, you need to login to access playlist')
|
||||||
|
elif status_code != 0:
|
||||||
|
raise ExtractorError(f'Request failed ({status_code}): {response.get("message") or "Unknown error"}')
|
||||||
return response['data']
|
return response['data']
|
||||||
|
|
||||||
def get_metadata(page_data):
|
def get_metadata(page_data):
|
||||||
|
@ -1280,7 +1443,10 @@ class BilibiliWatchlaterIE(BilibiliSpaceListBaseIE):
|
||||||
_VALID_URL = r'https?://(?:www\.)?bilibili\.com/watchlater/?(?:[?#]|$)'
|
_VALID_URL = r'https?://(?:www\.)?bilibili\.com/watchlater/?(?:[?#]|$)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.bilibili.com/watchlater/#/list',
|
'url': 'https://www.bilibili.com/watchlater/#/list',
|
||||||
'info_dict': {'id': 'watchlater'},
|
'info_dict': {
|
||||||
|
'id': r're:\d+',
|
||||||
|
'title': '稍后再看',
|
||||||
|
},
|
||||||
'playlist_mincount': 0,
|
'playlist_mincount': 0,
|
||||||
'skip': 'login required',
|
'skip': 'login required',
|
||||||
}]
|
}]
|
||||||
|
@ -1356,14 +1522,19 @@ class BilibiliPlaylistIE(BilibiliSpaceListBaseIE):
|
||||||
'skip': 'redirect url',
|
'skip': 'redirect url',
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.bilibili.com/list/watchlater',
|
'url': 'https://www.bilibili.com/list/watchlater',
|
||||||
'info_dict': {'id': 'watchlater'},
|
'info_dict': {
|
||||||
|
'id': r're:2_\d+',
|
||||||
|
'title': '稍后再看',
|
||||||
|
'uploader': str,
|
||||||
|
'uploader_id': str,
|
||||||
|
},
|
||||||
'playlist_mincount': 0,
|
'playlist_mincount': 0,
|
||||||
'skip': 'login required',
|
'skip': 'login required',
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.bilibili.com/medialist/play/watchlater',
|
'url': 'https://www.bilibili.com/medialist/play/watchlater',
|
||||||
'info_dict': {'id': 'watchlater'},
|
'info_dict': {'id': 'watchlater'},
|
||||||
'playlist_mincount': 0,
|
'playlist_mincount': 0,
|
||||||
'skip': 'login required',
|
'skip': 'redirect url & login required',
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _extract_medialist(self, query, list_id):
|
def _extract_medialist(self, query, list_id):
|
||||||
|
@ -1414,7 +1585,7 @@ class BilibiliPlaylistIE(BilibiliSpaceListBaseIE):
|
||||||
'title': ('title', {str}),
|
'title': ('title', {str}),
|
||||||
'uploader': ('upper', 'name', {str}),
|
'uploader': ('upper', 'name', {str}),
|
||||||
'uploader_id': ('upper', 'mid', {str_or_none}),
|
'uploader_id': ('upper', 'mid', {str_or_none}),
|
||||||
'timestamp': ('ctime', {int_or_none}),
|
'timestamp': ('ctime', {int_or_none}, filter),
|
||||||
'thumbnail': ('cover', {url_or_none}),
|
'thumbnail': ('cover', {url_or_none}),
|
||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
|
@ -1680,7 +1851,7 @@ class BiliBiliPlayerIE(InfoExtractor):
|
||||||
class BiliIntlBaseIE(InfoExtractor):
|
class BiliIntlBaseIE(InfoExtractor):
|
||||||
_API_URL = 'https://api.bilibili.tv/intl/gateway'
|
_API_URL = 'https://api.bilibili.tv/intl/gateway'
|
||||||
_NETRC_MACHINE = 'biliintl'
|
_NETRC_MACHINE = 'biliintl'
|
||||||
_HEADERS = {'Referer': 'https://www.bilibili.com/'}
|
_HEADERS = {'Referer': 'https://www.bilibili.tv/'}
|
||||||
|
|
||||||
def _call_api(self, endpoint, *args, **kwargs):
|
def _call_api(self, endpoint, *args, **kwargs):
|
||||||
json = self._download_json(self._API_URL + endpoint, *args, **kwargs)
|
json = self._download_json(self._API_URL + endpoint, *args, **kwargs)
|
||||||
|
@ -1808,7 +1979,8 @@ class BiliIntlBaseIE(InfoExtractor):
|
||||||
public_key = Cryptodome.RSA.importKey(key_data['key'])
|
public_key = Cryptodome.RSA.importKey(key_data['key'])
|
||||||
password_hash = Cryptodome.PKCS1_v1_5.new(public_key).encrypt((key_data['hash'] + password).encode())
|
password_hash = Cryptodome.PKCS1_v1_5.new(public_key).encrypt((key_data['hash'] + password).encode())
|
||||||
login_post = self._download_json(
|
login_post = self._download_json(
|
||||||
'https://passport.bilibili.tv/x/intl/passport-login/web/login/password?lang=en-US', None, data=urlencode_postdata({
|
'https://passport.bilibili.tv/x/intl/passport-login/web/login/password?lang=en-US', None,
|
||||||
|
data=urlencode_postdata({
|
||||||
'username': username,
|
'username': username,
|
||||||
'password': base64.b64encode(password_hash).decode('ascii'),
|
'password': base64.b64encode(password_hash).decode('ascii'),
|
||||||
'keep_me': 'true',
|
'keep_me': 'true',
|
||||||
|
@ -2140,7 +2312,8 @@ class BiliIntlSeriesIE(BiliIntlBaseIE):
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
series_id = self._match_id(url)
|
series_id = self._match_id(url)
|
||||||
series_info = self._call_api(f'/web/v2/ogv/play/season_info?season_id={series_id}&platform=web', series_id).get('season') or {}
|
series_info = self._call_api(
|
||||||
|
f'/web/v2/ogv/play/season_info?season_id={series_id}&platform=web', series_id).get('season') or {}
|
||||||
return self.playlist_result(
|
return self.playlist_result(
|
||||||
self._entries(series_id), series_id, series_info.get('title'), series_info.get('description'),
|
self._entries(series_id), series_id, series_info.get('title'), series_info.get('description'),
|
||||||
categories=traverse_obj(series_info, ('styles', ..., 'title'), expected_type=str_or_none),
|
categories=traverse_obj(series_info, ('styles', ..., 'title'), expected_type=str_or_none),
|
||||||
|
|
|
@ -24,7 +24,7 @@ from ..utils import (
|
||||||
|
|
||||||
|
|
||||||
class BitChuteIE(InfoExtractor):
|
class BitChuteIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?bitchute\.com/(?:video|embed|torrent/[^/]+)/(?P<id>[^/?#&]+)'
|
_VALID_URL = r'https?://(?:(?:www|old)\.)?bitchute\.com/(?:video|embed|torrent/[^/]+)/(?P<id>[^/?#&]+)'
|
||||||
_EMBED_REGEX = [rf'<(?:script|iframe)[^>]+\bsrc=(["\'])(?P<url>{_VALID_URL})']
|
_EMBED_REGEX = [rf'<(?:script|iframe)[^>]+\bsrc=(["\'])(?P<url>{_VALID_URL})']
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.bitchute.com/video/UGlrF9o9b-Q/',
|
'url': 'https://www.bitchute.com/video/UGlrF9o9b-Q/',
|
||||||
|
@ -91,6 +91,9 @@ class BitChuteIE(InfoExtractor):
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.bitchute.com/torrent/Zee5BE49045h/szoMrox2JEI.webtorrent',
|
'url': 'https://www.bitchute.com/torrent/Zee5BE49045h/szoMrox2JEI.webtorrent',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://old.bitchute.com/video/UGlrF9o9b-Q/',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
_GEO_BYPASS = False
|
_GEO_BYPASS = False
|
||||||
|
|
||||||
|
@ -132,7 +135,7 @@ class BitChuteIE(InfoExtractor):
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
webpage = self._download_webpage(
|
webpage = self._download_webpage(
|
||||||
f'https://www.bitchute.com/video/{video_id}', video_id, headers=self._HEADERS)
|
f'https://old.bitchute.com/video/{video_id}', video_id, headers=self._HEADERS)
|
||||||
|
|
||||||
self._raise_if_restricted(webpage)
|
self._raise_if_restricted(webpage)
|
||||||
publish_date = clean_html(get_element_by_class('video-publish-date', webpage))
|
publish_date = clean_html(get_element_by_class('video-publish-date', webpage))
|
||||||
|
@ -171,13 +174,13 @@ class BitChuteIE(InfoExtractor):
|
||||||
|
|
||||||
|
|
||||||
class BitChuteChannelIE(InfoExtractor):
|
class BitChuteChannelIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?bitchute\.com/(?P<type>channel|playlist)/(?P<id>[^/?#&]+)'
|
_VALID_URL = r'https?://(?:(?:www|old)\.)?bitchute\.com/(?P<type>channel|playlist)/(?P<id>[^/?#&]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.bitchute.com/channel/bitchute/',
|
'url': 'https://www.bitchute.com/channel/bitchute/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'bitchute',
|
'id': 'bitchute',
|
||||||
'title': 'BitChute',
|
'title': 'BitChute',
|
||||||
'description': 'md5:5329fb3866125afa9446835594a9b138',
|
'description': 'md5:2134c37d64fc3a4846787c402956adac',
|
||||||
},
|
},
|
||||||
'playlist': [
|
'playlist': [
|
||||||
{
|
{
|
||||||
|
@ -210,6 +213,9 @@ class BitChuteChannelIE(InfoExtractor):
|
||||||
'title': 'Bruce MacDonald and "The Light of Darkness"',
|
'title': 'Bruce MacDonald and "The Light of Darkness"',
|
||||||
'description': 'md5:747724ef404eebdfc04277714f81863e',
|
'description': 'md5:747724ef404eebdfc04277714f81863e',
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://old.bitchute.com/playlist/wV9Imujxasw9/',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
_TOKEN = 'zyG6tQcGPE5swyAEFLqKUwMuMMuF6IO2DZ6ZDQjGfsL0e4dcTLwqkTTul05Jdve7'
|
_TOKEN = 'zyG6tQcGPE5swyAEFLqKUwMuMMuF6IO2DZ6ZDQjGfsL0e4dcTLwqkTTul05Jdve7'
|
||||||
|
@ -230,7 +236,7 @@ class BitChuteChannelIE(InfoExtractor):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _make_url(playlist_id, playlist_type):
|
def _make_url(playlist_id, playlist_type):
|
||||||
return f'https://www.bitchute.com/{playlist_type}/{playlist_id}/'
|
return f'https://old.bitchute.com/{playlist_type}/{playlist_id}/'
|
||||||
|
|
||||||
def _fetch_page(self, playlist_id, playlist_type, page_num):
|
def _fetch_page(self, playlist_id, playlist_type, page_num):
|
||||||
playlist_url = self._make_url(playlist_id, playlist_type)
|
playlist_url = self._make_url(playlist_id, playlist_type)
|
||||||
|
|
388
yt_dlp/extractor/bluesky.py
Normal file
388
yt_dlp/extractor/bluesky.py
Normal file
|
@ -0,0 +1,388 @@
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
ExtractorError,
|
||||||
|
format_field,
|
||||||
|
int_or_none,
|
||||||
|
mimetype2ext,
|
||||||
|
orderedSet,
|
||||||
|
parse_iso8601,
|
||||||
|
truncate_string,
|
||||||
|
update_url_query,
|
||||||
|
url_basename,
|
||||||
|
url_or_none,
|
||||||
|
variadic,
|
||||||
|
)
|
||||||
|
from ..utils.traversal import traverse_obj
|
||||||
|
|
||||||
|
|
||||||
|
class BlueskyIE(InfoExtractor):
|
||||||
|
_VALID_URL = [
|
||||||
|
r'https?://(?:www\.)?(?:bsky\.app|main\.bsky\.dev)/profile/(?P<handle>[\w.:%-]+)/post/(?P<id>\w+)',
|
||||||
|
r'at://(?P<handle>[\w.:%-]+)/app\.bsky\.feed\.post/(?P<id>\w+)',
|
||||||
|
]
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://bsky.app/profile/blu3blue.bsky.social/post/3l4omssdl632g',
|
||||||
|
'md5': '375539c1930ab05d15585ed772ab54fd',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '3l4omssdl632g',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'uploader': 'Blu3Blu3Lilith',
|
||||||
|
'uploader_id': 'blu3blue.bsky.social',
|
||||||
|
'uploader_url': 'https://bsky.app/profile/blu3blue.bsky.social',
|
||||||
|
'channel_id': 'did:plc:pzdr5ylumf7vmvwasrpr5bf2',
|
||||||
|
'channel_url': 'https://bsky.app/profile/did:plc:pzdr5ylumf7vmvwasrpr5bf2',
|
||||||
|
'thumbnail': r're:https://video.bsky.app/watch/.*\.jpg$',
|
||||||
|
'title': 'OMG WE HAVE VIDEOS NOW',
|
||||||
|
'description': 'OMG WE HAVE VIDEOS NOW',
|
||||||
|
'upload_date': '20240921',
|
||||||
|
'timestamp': 1726940605,
|
||||||
|
'like_count': int,
|
||||||
|
'repost_count': int,
|
||||||
|
'comment_count': int,
|
||||||
|
'tags': [],
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://bsky.app/profile/bsky.app/post/3l3vgf77uco2g',
|
||||||
|
'md5': 'b9e344fdbce9f2852c668a97efefb105',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '3l3vgf77uco2g',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'uploader': 'Bluesky',
|
||||||
|
'uploader_id': 'bsky.app',
|
||||||
|
'uploader_url': 'https://bsky.app/profile/bsky.app',
|
||||||
|
'channel_id': 'did:plc:z72i7hdynmk6r22z27h6tvur',
|
||||||
|
'channel_url': 'https://bsky.app/profile/did:plc:z72i7hdynmk6r22z27h6tvur',
|
||||||
|
'thumbnail': r're:https://video.bsky.app/watch/.*\.jpg$',
|
||||||
|
'title': 'Bluesky now has video! Update your app to versi...',
|
||||||
|
'alt_title': 'Bluesky video feature announcement',
|
||||||
|
'description': r're:(?s)Bluesky now has video! .{239}',
|
||||||
|
'upload_date': '20240911',
|
||||||
|
'timestamp': 1726074716,
|
||||||
|
'like_count': int,
|
||||||
|
'repost_count': int,
|
||||||
|
'comment_count': int,
|
||||||
|
'tags': [],
|
||||||
|
'subtitles': {
|
||||||
|
'en': 'mincount:1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://main.bsky.dev/profile/souris.moe/post/3l4qhp7bcs52c',
|
||||||
|
'md5': '5f2df8c200b5633eb7fb2c984d29772f',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '3l4qhp7bcs52c',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'uploader': 'souris',
|
||||||
|
'uploader_id': 'souris.moe',
|
||||||
|
'uploader_url': 'https://bsky.app/profile/souris.moe',
|
||||||
|
'channel_id': 'did:plc:tj7g244gl5v6ai6cm4f4wlqp',
|
||||||
|
'channel_url': 'https://bsky.app/profile/did:plc:tj7g244gl5v6ai6cm4f4wlqp',
|
||||||
|
'thumbnail': r're:https://video.bsky.app/watch/.*\.jpg$',
|
||||||
|
'title': 'Bluesky video #3l4qhp7bcs52c',
|
||||||
|
'upload_date': '20240922',
|
||||||
|
'timestamp': 1727003838,
|
||||||
|
'like_count': int,
|
||||||
|
'repost_count': int,
|
||||||
|
'comment_count': int,
|
||||||
|
'tags': [],
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://bsky.app/profile/de1.pds.tentacle.expert/post/3l3w4tnezek2e',
|
||||||
|
'md5': '1af9c7fda061cf7593bbffca89e43d1c',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '3l3w4tnezek2e',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'uploader': 'clean',
|
||||||
|
'uploader_id': 'de1.pds.tentacle.expert',
|
||||||
|
'uploader_url': 'https://bsky.app/profile/de1.pds.tentacle.expert',
|
||||||
|
'channel_id': 'did:web:de1.tentacle.expert',
|
||||||
|
'channel_url': 'https://bsky.app/profile/did:web:de1.tentacle.expert',
|
||||||
|
'thumbnail': r're:https://video.bsky.app/watch/.*\.jpg$',
|
||||||
|
'title': 'Bluesky video #3l3w4tnezek2e',
|
||||||
|
'upload_date': '20240911',
|
||||||
|
'timestamp': 1726098823,
|
||||||
|
'like_count': int,
|
||||||
|
'repost_count': int,
|
||||||
|
'comment_count': int,
|
||||||
|
'tags': [],
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://bsky.app/profile/yunayuispink.bsky.social/post/3l7gqcfes742o',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'XxK3t_5V3ao',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'uploader': 'yunayu',
|
||||||
|
'uploader_id': '@yunayuispink',
|
||||||
|
'uploader_url': 'https://www.youtube.com/@yunayuispink',
|
||||||
|
'channel': 'yunayu',
|
||||||
|
'channel_id': 'UCPLvXnHa7lTyNoR_dGsU14w',
|
||||||
|
'channel_url': 'https://www.youtube.com/channel/UCPLvXnHa7lTyNoR_dGsU14w',
|
||||||
|
'thumbnail': 'https://i.ytimg.com/vi_webp/XxK3t_5V3ao/maxresdefault.webp',
|
||||||
|
'description': r're:Have a good goodx10000day',
|
||||||
|
'title': '5min vs 5hours drawing',
|
||||||
|
'availability': 'public',
|
||||||
|
'live_status': 'not_live',
|
||||||
|
'playable_in_embed': True,
|
||||||
|
'upload_date': '20241026',
|
||||||
|
'timestamp': 1729967784,
|
||||||
|
'duration': 321,
|
||||||
|
'age_limit': 0,
|
||||||
|
'like_count': int,
|
||||||
|
'view_count': int,
|
||||||
|
'comment_count': int,
|
||||||
|
'channel_follower_count': int,
|
||||||
|
'categories': ['Entertainment'],
|
||||||
|
'tags': [],
|
||||||
|
},
|
||||||
|
'add_ie': ['Youtube'],
|
||||||
|
}, {
|
||||||
|
'url': 'https://bsky.app/profile/endshark.bsky.social/post/3jzxjkcemae2m',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '222792849',
|
||||||
|
'ext': 'mp3',
|
||||||
|
'uploader': 'LASERBAT',
|
||||||
|
'uploader_id': 'laserbatx',
|
||||||
|
'uploader_url': 'https://laserbatx.bandcamp.com',
|
||||||
|
'artists': ['LASERBAT'],
|
||||||
|
'album_artists': ['LASERBAT'],
|
||||||
|
'album': 'Hari Nezumi [EP]',
|
||||||
|
'track': 'Forward to the End',
|
||||||
|
'title': 'LASERBAT - Forward to the End',
|
||||||
|
'thumbnail': 'https://f4.bcbits.com/img/a2507705510_5.jpg',
|
||||||
|
'duration': 228.571,
|
||||||
|
'track_id': '222792849',
|
||||||
|
'release_date': '20230423',
|
||||||
|
'upload_date': '20230423',
|
||||||
|
'timestamp': 1682276040.0,
|
||||||
|
'release_timestamp': 1682276040.0,
|
||||||
|
'track_number': 1,
|
||||||
|
},
|
||||||
|
'add_ie': ['Bandcamp'],
|
||||||
|
}, {
|
||||||
|
'url': 'https://bsky.app/profile/dannybhoix.bsky.social/post/3l6oe5mtr2c2j',
|
||||||
|
'md5': 'b9e344fdbce9f2852c668a97efefb105',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '3l3vgf77uco2g',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'uploader': 'Bluesky',
|
||||||
|
'uploader_id': 'bsky.app',
|
||||||
|
'uploader_url': 'https://bsky.app/profile/bsky.app',
|
||||||
|
'channel_id': 'did:plc:z72i7hdynmk6r22z27h6tvur',
|
||||||
|
'channel_url': 'https://bsky.app/profile/did:plc:z72i7hdynmk6r22z27h6tvur',
|
||||||
|
'thumbnail': r're:https://video.bsky.app/watch/.*\.jpg$',
|
||||||
|
'title': 'Bluesky now has video! Update your app to versi...',
|
||||||
|
'alt_title': 'Bluesky video feature announcement',
|
||||||
|
'description': r're:(?s)Bluesky now has video! .{239}',
|
||||||
|
'upload_date': '20240911',
|
||||||
|
'timestamp': 1726074716,
|
||||||
|
'like_count': int,
|
||||||
|
'repost_count': int,
|
||||||
|
'comment_count': int,
|
||||||
|
'tags': [],
|
||||||
|
'subtitles': {
|
||||||
|
'en': 'mincount:1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://bsky.app/profile/alt.bun.how/post/3l7rdfxhyds2f',
|
||||||
|
'md5': '8775118b235cf9fa6b5ad30f95cda75c',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '3l7rdfxhyds2f',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'uploader': 'cinnamon',
|
||||||
|
'uploader_id': 'alt.bun.how',
|
||||||
|
'uploader_url': 'https://bsky.app/profile/alt.bun.how',
|
||||||
|
'channel_id': 'did:plc:7x6rtuenkuvxq3zsvffp2ide',
|
||||||
|
'channel_url': 'https://bsky.app/profile/did:plc:7x6rtuenkuvxq3zsvffp2ide',
|
||||||
|
'thumbnail': r're:https://video.bsky.app/watch/.*\.jpg$',
|
||||||
|
'title': 'crazy that i look like this tbh',
|
||||||
|
'description': 'crazy that i look like this tbh',
|
||||||
|
'upload_date': '20241030',
|
||||||
|
'timestamp': 1730332128,
|
||||||
|
'like_count': int,
|
||||||
|
'repost_count': int,
|
||||||
|
'comment_count': int,
|
||||||
|
'tags': ['sexual'],
|
||||||
|
'age_limit': 18,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'at://did:plc:ia76kvnndjutgedggx2ibrem/app.bsky.feed.post/3l6zrz6zyl2dr',
|
||||||
|
'md5': '71b0eb6d85d03145e6af6642c7fc6d78',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '3l6zrz6zyl2dr',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'uploader': 'mary🐇',
|
||||||
|
'uploader_id': 'mary.my.id',
|
||||||
|
'uploader_url': 'https://bsky.app/profile/mary.my.id',
|
||||||
|
'channel_id': 'did:plc:ia76kvnndjutgedggx2ibrem',
|
||||||
|
'channel_url': 'https://bsky.app/profile/did:plc:ia76kvnndjutgedggx2ibrem',
|
||||||
|
'thumbnail': r're:https://video.bsky.app/watch/.*\.jpg$',
|
||||||
|
'title': 'Bluesky video #3l6zrz6zyl2dr',
|
||||||
|
'upload_date': '20241021',
|
||||||
|
'timestamp': 1729523172,
|
||||||
|
'like_count': int,
|
||||||
|
'repost_count': int,
|
||||||
|
'comment_count': int,
|
||||||
|
'tags': [],
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://bsky.app/profile/purpleicetea.bsky.social/post/3l7gv55dc2o2w',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '3l7gv55dc2o2w',
|
||||||
|
},
|
||||||
|
'playlist': [{
|
||||||
|
'info_dict': {
|
||||||
|
'id': '3l7gv55dc2o2w',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'upload_date': '20241026',
|
||||||
|
'description': 'One of my favorite videos',
|
||||||
|
'comment_count': int,
|
||||||
|
'uploader_url': 'https://bsky.app/profile/purpleicetea.bsky.social',
|
||||||
|
'uploader': 'Purple.Ice.Tea',
|
||||||
|
'thumbnail': r're:https://video.bsky.app/watch/.*\.jpg$',
|
||||||
|
'channel_url': 'https://bsky.app/profile/did:plc:bjh5ffwya5f53dfy47dezuwx',
|
||||||
|
'like_count': int,
|
||||||
|
'channel_id': 'did:plc:bjh5ffwya5f53dfy47dezuwx',
|
||||||
|
'repost_count': int,
|
||||||
|
'timestamp': 1729973202,
|
||||||
|
'tags': [],
|
||||||
|
'uploader_id': 'purpleicetea.bsky.social',
|
||||||
|
'title': 'One of my favorite videos',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'info_dict': {
|
||||||
|
'id': '3l77u64l7le2e',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'hearing people on twitter say that bluesky isn\'...',
|
||||||
|
'like_count': int,
|
||||||
|
'uploader_id': 'thafnine.net',
|
||||||
|
'uploader_url': 'https://bsky.app/profile/thafnine.net',
|
||||||
|
'upload_date': '20241024',
|
||||||
|
'channel_url': 'https://bsky.app/profile/did:plc:6ttyq36rhiyed7wu3ws7dmqj',
|
||||||
|
'description': r're:(?s)hearing people on twitter say that bluesky .{93}',
|
||||||
|
'tags': [],
|
||||||
|
'alt_title': 'md5:9b1ee1937fb3d1a81e932f9ec14d560e',
|
||||||
|
'uploader': 'T9',
|
||||||
|
'channel_id': 'did:plc:6ttyq36rhiyed7wu3ws7dmqj',
|
||||||
|
'thumbnail': r're:https://video.bsky.app/watch/.*\.jpg$',
|
||||||
|
'timestamp': 1729731642,
|
||||||
|
'comment_count': int,
|
||||||
|
'repost_count': int,
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
}]
|
||||||
|
_BLOB_URL_TMPL = '{}/xrpc/com.atproto.sync.getBlob'
|
||||||
|
|
||||||
|
def _get_service_endpoint(self, did, video_id):
|
||||||
|
if did.startswith('did:web:'):
|
||||||
|
url = f'https://{did[8:]}/.well-known/did.json'
|
||||||
|
else:
|
||||||
|
url = f'https://plc.directory/{did}'
|
||||||
|
services = self._download_json(
|
||||||
|
url, video_id, 'Fetching service endpoint', 'Falling back to bsky.social', fatal=False)
|
||||||
|
return traverse_obj(
|
||||||
|
services, ('service', lambda _, x: x['type'] == 'AtprotoPersonalDataServer',
|
||||||
|
'serviceEndpoint', {url_or_none}, any)) or 'https://bsky.social'
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
handle, video_id = self._match_valid_url(url).group('handle', 'id')
|
||||||
|
|
||||||
|
post = self._download_json(
|
||||||
|
'https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread',
|
||||||
|
video_id, query={
|
||||||
|
'uri': f'at://{handle}/app.bsky.feed.post/{video_id}',
|
||||||
|
'depth': 0,
|
||||||
|
'parentHeight': 0,
|
||||||
|
})['thread']['post']
|
||||||
|
|
||||||
|
entries = []
|
||||||
|
# app.bsky.embed.video.view/app.bsky.embed.external.view
|
||||||
|
entries.extend(self._extract_videos(post, video_id))
|
||||||
|
# app.bsky.embed.recordWithMedia.view
|
||||||
|
entries.extend(self._extract_videos(
|
||||||
|
post, video_id, embed_path=('embed', 'media'), record_subpath=('embed', 'media')))
|
||||||
|
# app.bsky.embed.record.view
|
||||||
|
if nested_post := traverse_obj(post, ('embed', 'record', ('record', None), {dict}, any)):
|
||||||
|
entries.extend(self._extract_videos(
|
||||||
|
nested_post, video_id, embed_path=('embeds', 0), record_path='value'))
|
||||||
|
|
||||||
|
if not entries:
|
||||||
|
raise ExtractorError('No video could be found in this post', expected=True)
|
||||||
|
if len(entries) == 1:
|
||||||
|
return entries[0]
|
||||||
|
return self.playlist_result(entries, video_id)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _build_profile_url(path):
|
||||||
|
return format_field(path, None, 'https://bsky.app/profile/%s', default=None)
|
||||||
|
|
||||||
|
def _extract_videos(self, root, video_id, embed_path='embed', record_path='record', record_subpath='embed'):
|
||||||
|
embed_path = variadic(embed_path, (str, bytes, dict, set))
|
||||||
|
record_path = variadic(record_path, (str, bytes, dict, set))
|
||||||
|
record_subpath = variadic(record_subpath, (str, bytes, dict, set))
|
||||||
|
|
||||||
|
entries = []
|
||||||
|
if external_uri := traverse_obj(root, (
|
||||||
|
((*record_path, *record_subpath), embed_path), 'external', 'uri', {url_or_none}, any)):
|
||||||
|
entries.append(self.url_result(external_uri))
|
||||||
|
if playlist := traverse_obj(root, (*embed_path, 'playlist', {url_or_none})):
|
||||||
|
formats, subtitles = self._extract_m3u8_formats_and_subtitles(
|
||||||
|
playlist, video_id, 'mp4', m3u8_id='hls', fatal=False)
|
||||||
|
else:
|
||||||
|
return entries
|
||||||
|
|
||||||
|
video_cid = traverse_obj(
|
||||||
|
root, (*embed_path, 'cid', {str}),
|
||||||
|
(*record_path, *record_subpath, 'video', 'ref', '$link', {str}))
|
||||||
|
did = traverse_obj(root, ('author', 'did', {str}))
|
||||||
|
|
||||||
|
if did and video_cid:
|
||||||
|
endpoint = self._get_service_endpoint(did, video_id)
|
||||||
|
|
||||||
|
formats.append({
|
||||||
|
'format_id': 'blob',
|
||||||
|
'url': update_url_query(
|
||||||
|
self._BLOB_URL_TMPL.format(endpoint), {'did': did, 'cid': video_cid}),
|
||||||
|
**traverse_obj(root, (*embed_path, 'aspectRatio', {
|
||||||
|
'width': ('width', {int_or_none}),
|
||||||
|
'height': ('height', {int_or_none}),
|
||||||
|
})),
|
||||||
|
**traverse_obj(root, (*record_path, *record_subpath, 'video', {
|
||||||
|
'filesize': ('size', {int_or_none}),
|
||||||
|
'ext': ('mimeType', {mimetype2ext}),
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
|
||||||
|
for sub_data in traverse_obj(root, (
|
||||||
|
*record_path, *record_subpath, 'captions', lambda _, v: v['file']['ref']['$link'])):
|
||||||
|
subtitles.setdefault(sub_data.get('lang') or 'und', []).append({
|
||||||
|
'url': update_url_query(
|
||||||
|
self._BLOB_URL_TMPL.format(endpoint), {'did': did, 'cid': sub_data['file']['ref']['$link']}),
|
||||||
|
'ext': traverse_obj(sub_data, ('file', 'mimeType', {mimetype2ext})),
|
||||||
|
})
|
||||||
|
|
||||||
|
entries.append({
|
||||||
|
'id': video_id,
|
||||||
|
'formats': formats,
|
||||||
|
'subtitles': subtitles,
|
||||||
|
**traverse_obj(root, {
|
||||||
|
'id': ('uri', {url_basename}),
|
||||||
|
'thumbnail': (*embed_path, 'thumbnail', {url_or_none}),
|
||||||
|
'alt_title': (*embed_path, 'alt', {str}, filter),
|
||||||
|
'uploader': ('author', 'displayName', {str}),
|
||||||
|
'uploader_id': ('author', 'handle', {str}),
|
||||||
|
'uploader_url': ('author', 'handle', {self._build_profile_url}),
|
||||||
|
'channel_id': ('author', 'did', {str}),
|
||||||
|
'channel_url': ('author', 'did', {self._build_profile_url}),
|
||||||
|
'like_count': ('likeCount', {int_or_none}),
|
||||||
|
'repost_count': ('repostCount', {int_or_none}),
|
||||||
|
'comment_count': ('replyCount', {int_or_none}),
|
||||||
|
'timestamp': ('indexedAt', {parse_iso8601}),
|
||||||
|
'tags': ('labels', ..., 'val', {str}, all, {orderedSet}),
|
||||||
|
'age_limit': (
|
||||||
|
'labels', ..., 'val', {lambda x: 18 if x in ('sexual', 'porn', 'graphic-media') else None}, any),
|
||||||
|
'description': (*record_path, 'text', {str}, filter),
|
||||||
|
'title': (*record_path, 'text', {lambda x: x.replace('\n', ' ')}, {truncate_string(left=50)}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
return entries
|
|
@ -12,7 +12,7 @@ from ..utils.traversal import traverse_obj
|
||||||
|
|
||||||
|
|
||||||
class BoxIE(InfoExtractor):
|
class BoxIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:[^.]+\.)?app\.box\.com/s/(?P<shared_name>[^/?#]+)(?:/file/(?P<id>\d+))?'
|
_VALID_URL = r'https?://(?:[^.]+\.)?(?P<service>app|ent)\.box\.com/s/(?P<shared_name>[^/?#]+)(?:/file/(?P<id>\d+))?'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://mlssoccer.app.box.com/s/0evd2o3e08l60lr4ygukepvnkord1o1x/file/510727257538',
|
'url': 'https://mlssoccer.app.box.com/s/0evd2o3e08l60lr4ygukepvnkord1o1x/file/510727257538',
|
||||||
'md5': '1f81b2fd3960f38a40a3b8823e5fcd43',
|
'md5': '1f81b2fd3960f38a40a3b8823e5fcd43',
|
||||||
|
@ -38,10 +38,22 @@ class BoxIE(InfoExtractor):
|
||||||
'uploader_id': '239068974',
|
'uploader_id': '239068974',
|
||||||
},
|
},
|
||||||
'params': {'skip_download': 'dash fragment too small'},
|
'params': {'skip_download': 'dash fragment too small'},
|
||||||
|
}, {
|
||||||
|
'url': 'https://thejacksonlaboratory.ent.box.com/s/2x09dm6vcg6y28o0oox1so4l0t8wzt6l/file/1536173056065',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '1536173056065',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'uploader_id': '18523128264',
|
||||||
|
'uploader': 'Lexi Hennigan',
|
||||||
|
'title': 'iPSC Symposium recording part 1.mp4',
|
||||||
|
'timestamp': 1716228343,
|
||||||
|
'upload_date': '20240520',
|
||||||
|
},
|
||||||
|
'params': {'skip_download': 'dash fragment too small'},
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
shared_name, file_id = self._match_valid_url(url).groups()
|
shared_name, file_id, service = self._match_valid_url(url).group('shared_name', 'id', 'service')
|
||||||
webpage = self._download_webpage(url, file_id or shared_name)
|
webpage = self._download_webpage(url, file_id or shared_name)
|
||||||
|
|
||||||
if not file_id:
|
if not file_id:
|
||||||
|
@ -57,14 +69,14 @@ class BoxIE(InfoExtractor):
|
||||||
request_token = self._search_json(
|
request_token = self._search_json(
|
||||||
r'Box\.config\s*=', webpage, 'Box config', file_id)['requestToken']
|
r'Box\.config\s*=', webpage, 'Box config', file_id)['requestToken']
|
||||||
access_token = self._download_json(
|
access_token = self._download_json(
|
||||||
'https://app.box.com/app-api/enduserapp/elements/tokens', file_id,
|
f'https://{service}.box.com/app-api/enduserapp/elements/tokens', file_id,
|
||||||
'Downloading token JSON metadata',
|
'Downloading token JSON metadata',
|
||||||
data=json.dumps({'fileIDs': [file_id]}).encode(), headers={
|
data=json.dumps({'fileIDs': [file_id]}).encode(), headers={
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'X-Request-Token': request_token,
|
'X-Request-Token': request_token,
|
||||||
'X-Box-EndUser-API': 'sharedName=' + shared_name,
|
'X-Box-EndUser-API': 'sharedName=' + shared_name,
|
||||||
})[file_id]['read']
|
})[file_id]['read']
|
||||||
shared_link = 'https://app.box.com/s/' + shared_name
|
shared_link = f'https://{service}.box.com/s/{shared_name}'
|
||||||
f = self._download_json(
|
f = self._download_json(
|
||||||
'https://api.box.com/2.0/files/' + file_id, file_id,
|
'https://api.box.com/2.0/files/' + file_id, file_id,
|
||||||
'Downloading file JSON metadata', headers={
|
'Downloading file JSON metadata', headers={
|
||||||
|
|
|
@ -1,35 +1,20 @@
|
||||||
import functools
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
clean_html,
|
clean_html,
|
||||||
extract_attributes,
|
extract_attributes,
|
||||||
get_element_text_and_html_by_tag,
|
|
||||||
get_elements_by_class,
|
|
||||||
join_nonempty,
|
join_nonempty,
|
||||||
js_to_json,
|
js_to_json,
|
||||||
mimetype2ext,
|
mimetype2ext,
|
||||||
unified_strdate,
|
unified_strdate,
|
||||||
url_or_none,
|
url_or_none,
|
||||||
urljoin,
|
urljoin,
|
||||||
variadic,
|
|
||||||
)
|
)
|
||||||
from ..utils.traversal import traverse_obj
|
from ..utils.traversal import (
|
||||||
|
find_element,
|
||||||
|
traverse_obj,
|
||||||
def html_get_element(tag=None, cls=None):
|
)
|
||||||
assert tag or cls, 'One of tag or class is required'
|
|
||||||
|
|
||||||
if cls:
|
|
||||||
func = functools.partial(get_elements_by_class, cls, tag=tag)
|
|
||||||
else:
|
|
||||||
func = functools.partial(get_element_text_and_html_by_tag, tag)
|
|
||||||
|
|
||||||
def html_get_element_wrapper(html):
|
|
||||||
return variadic(func(html))[0]
|
|
||||||
|
|
||||||
return html_get_element_wrapper
|
|
||||||
|
|
||||||
|
|
||||||
class BpbIE(InfoExtractor):
|
class BpbIE(InfoExtractor):
|
||||||
|
@ -41,12 +26,12 @@ class BpbIE(InfoExtractor):
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '297',
|
'id': '297',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'creator': 'Kooperative Berlin',
|
'creators': ['Kooperative Berlin'],
|
||||||
'description': 'md5:f4f75885ba009d3e2b156247a8941ce6',
|
'description': r're:Joachim Gauck, .*\n\nKamera: .*',
|
||||||
'release_date': '20160115',
|
'release_date': '20150716',
|
||||||
'series': 'Interview auf dem Geschichtsforum 1989 | 2009',
|
'series': 'Interview auf dem Geschichtsforum 1989 | 2009',
|
||||||
'tags': ['Friedliche Revolution', 'Erinnerungskultur', 'Vergangenheitspolitik', 'DDR 1949 - 1990', 'Freiheitsrecht', 'BStU', 'Deutschland'],
|
'tags': [],
|
||||||
'thumbnail': 'https://www.bpb.de/cache/images/7/297_teaser_16x9_1240.jpg?8839D',
|
'thumbnail': r're:https?://www\.bpb\.de/cache/images/7/297_teaser_16x9_1240\.jpg.*',
|
||||||
'title': 'Joachim Gauck zu 1989 und die Erinnerung an die DDR',
|
'title': 'Joachim Gauck zu 1989 und die Erinnerung an die DDR',
|
||||||
'uploader': 'Bundeszentrale für politische Bildung',
|
'uploader': 'Bundeszentrale für politische Bildung',
|
||||||
},
|
},
|
||||||
|
@ -55,11 +40,12 @@ class BpbIE(InfoExtractor):
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '522184',
|
'id': '522184',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'creator': 'Institute for Strategic Dialogue Germany gGmbH (ISD)',
|
'creators': ['Institute for Strategic Dialogue Germany gGmbH (ISD)'],
|
||||||
'description': 'md5:f83c795ff8f825a69456a9e51fc15903',
|
'description': 'md5:f83c795ff8f825a69456a9e51fc15903',
|
||||||
'release_date': '20230621',
|
'release_date': '20230621',
|
||||||
'tags': ['Desinformation', 'Ukraine', 'Russland', 'Geflüchtete'],
|
'series': 'Narrative über den Krieg Russlands gegen die Ukraine (NUK)',
|
||||||
'thumbnail': 'https://www.bpb.de/cache/images/4/522184_teaser_16x9_1240.png?EABFB',
|
'tags': [],
|
||||||
|
'thumbnail': r're:https://www\.bpb\.de/cache/images/4/522184_teaser_16x9_1240\.png.*',
|
||||||
'title': 'md5:9b01ccdbf58dbf9e5c9f6e771a803b1c',
|
'title': 'md5:9b01ccdbf58dbf9e5c9f6e771a803b1c',
|
||||||
'uploader': 'Bundeszentrale für politische Bildung',
|
'uploader': 'Bundeszentrale für politische Bildung',
|
||||||
},
|
},
|
||||||
|
@ -68,11 +54,12 @@ class BpbIE(InfoExtractor):
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '518789',
|
'id': '518789',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'creator': 'Institute for Strategic Dialogue Germany gGmbH (ISD)',
|
'creators': ['Institute for Strategic Dialogue Germany gGmbH (ISD)'],
|
||||||
'description': 'md5:85228aed433e84ff0ff9bc582abd4ea8',
|
'description': 'md5:85228aed433e84ff0ff9bc582abd4ea8',
|
||||||
'release_date': '20230302',
|
'release_date': '20230302',
|
||||||
'tags': ['Desinformation', 'Ukraine', 'Russland', 'Geflüchtete'],
|
'series': 'Narrative über den Krieg Russlands gegen die Ukraine (NUK)',
|
||||||
'thumbnail': 'https://www.bpb.de/cache/images/9/518789_teaser_16x9_1240.jpeg?56D0D',
|
'tags': [],
|
||||||
|
'thumbnail': r're:https://www\.bpb\.de/cache/images/9/518789_teaser_16x9_1240\.jpeg.*',
|
||||||
'title': 'md5:3e956f264bb501f6383f10495a401da4',
|
'title': 'md5:3e956f264bb501f6383f10495a401da4',
|
||||||
'uploader': 'Bundeszentrale für politische Bildung',
|
'uploader': 'Bundeszentrale für politische Bildung',
|
||||||
},
|
},
|
||||||
|
@ -84,12 +71,12 @@ class BpbIE(InfoExtractor):
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '315813',
|
'id': '315813',
|
||||||
'ext': 'mp3',
|
'ext': 'mp3',
|
||||||
'creator': 'Axel Schröder',
|
'creators': ['Axel Schröder'],
|
||||||
'description': 'md5:eda9d1af34e5912efef5baf54fba4427',
|
'description': 'md5:eda9d1af34e5912efef5baf54fba4427',
|
||||||
'release_date': '20200921',
|
'release_date': '20200921',
|
||||||
'series': 'Auf Endlagersuche. Der deutsche Weg zu einem sicheren Atommülllager',
|
'series': 'Auf Endlagersuche. Der deutsche Weg zu einem sicheren Atommülllager',
|
||||||
'tags': ['Atomenergie', 'Endlager', 'hoch-radioaktiver Abfall', 'Endlagersuche', 'Atommüll', 'Atomendlager', 'Gorleben', 'Deutschland'],
|
'tags': ['Atomenergie', 'Endlager', 'hoch-radioaktiver Abfall', 'Endlagersuche', 'Atommüll', 'Atomendlager', 'Gorleben', 'Deutschland'],
|
||||||
'thumbnail': 'https://www.bpb.de/cache/images/3/315813_teaser_16x9_1240.png?92A94',
|
'thumbnail': r're:https://www\.bpb\.de/cache/images/3/315813_teaser_16x9_1240\.png.*',
|
||||||
'title': 'Folge 1: Eine Einführung',
|
'title': 'Folge 1: Eine Einführung',
|
||||||
'uploader': 'Bundeszentrale für politische Bildung',
|
'uploader': 'Bundeszentrale für politische Bildung',
|
||||||
},
|
},
|
||||||
|
@ -98,12 +85,12 @@ class BpbIE(InfoExtractor):
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '517806',
|
'id': '517806',
|
||||||
'ext': 'mp3',
|
'ext': 'mp3',
|
||||||
'creator': 'Bundeszentrale für politische Bildung',
|
'creators': ['Bundeszentrale für politische Bildung'],
|
||||||
'description': 'md5:594689600e919912aade0b2871cc3fed',
|
'description': 'md5:594689600e919912aade0b2871cc3fed',
|
||||||
'release_date': '20230127',
|
'release_date': '20230127',
|
||||||
'series': 'Vorträge des Fachtags "Modernisierer. Grenzgänger. Anstifter. Sechs Jahrzehnte \'Neue Rechte\'"',
|
'series': 'Vorträge des Fachtags "Modernisierer. Grenzgänger. Anstifter. Sechs Jahrzehnte \'Neue Rechte\'"',
|
||||||
'tags': ['Rechtsextremismus', 'Konservatismus', 'Konservativismus', 'neue Rechte', 'Rechtspopulismus', 'Schnellroda', 'Deutschland'],
|
'tags': ['Rechtsextremismus', 'Konservatismus', 'Konservativismus', 'neue Rechte', 'Rechtspopulismus', 'Schnellroda', 'Deutschland'],
|
||||||
'thumbnail': 'https://www.bpb.de/cache/images/6/517806_teaser_16x9_1240.png?7A7A0',
|
'thumbnail': r're:https://www\.bpb\.de/cache/images/6/517806_teaser_16x9_1240\.png.*',
|
||||||
'title': 'Die Weltanschauung der "Neuen Rechten"',
|
'title': 'Die Weltanschauung der "Neuen Rechten"',
|
||||||
'uploader': 'Bundeszentrale für politische Bildung',
|
'uploader': 'Bundeszentrale für politische Bildung',
|
||||||
},
|
},
|
||||||
|
@ -147,7 +134,7 @@ class BpbIE(InfoExtractor):
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
webpage = self._download_webpage(url, video_id)
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
title_result = traverse_obj(webpage, ({html_get_element(cls='opening-header__title')}, {self._TITLE_RE.match}))
|
title_result = traverse_obj(webpage, ({find_element(cls='opening-header__title')}, {self._TITLE_RE.match}))
|
||||||
json_lds = list(self._yield_json_ld(webpage, video_id, fatal=False))
|
json_lds = list(self._yield_json_ld(webpage, video_id, fatal=False))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -156,15 +143,15 @@ class BpbIE(InfoExtractor):
|
||||||
# This metadata could be interpreted otherwise, but it fits "series" the most
|
# This metadata could be interpreted otherwise, but it fits "series" the most
|
||||||
'series': traverse_obj(title_result, ('series', {str.strip})) or None,
|
'series': traverse_obj(title_result, ('series', {str.strip})) or None,
|
||||||
'description': join_nonempty(*traverse_obj(webpage, [(
|
'description': join_nonempty(*traverse_obj(webpage, [(
|
||||||
{html_get_element(cls='opening-intro')},
|
{find_element(cls='opening-intro')},
|
||||||
[{html_get_element(tag='bpb-accordion-item')}, {html_get_element(cls='text-content')}],
|
[{find_element(tag='bpb-accordion-item')}, {find_element(cls='text-content')}],
|
||||||
), {clean_html}]), delim='\n\n') or None,
|
), {clean_html}]), delim='\n\n') or None,
|
||||||
'creator': self._html_search_meta('author', webpage),
|
'creators': traverse_obj(self._html_search_meta('author', webpage), all),
|
||||||
'uploader': self._html_search_meta('publisher', webpage),
|
'uploader': self._html_search_meta('publisher', webpage),
|
||||||
'release_date': unified_strdate(self._html_search_meta('date', webpage)),
|
'release_date': unified_strdate(self._html_search_meta('date', webpage)),
|
||||||
'tags': traverse_obj(json_lds, (..., 'keywords', {lambda x: x.split(',')}, ...)),
|
'tags': traverse_obj(json_lds, (..., 'keywords', {lambda x: x.split(',')}, ...)),
|
||||||
**traverse_obj(self._parse_vue_attributes('bpb-player', webpage, video_id), {
|
**traverse_obj(self._parse_vue_attributes('bpb-player', webpage, video_id), {
|
||||||
'formats': (':sources', ..., {self._process_source}),
|
'formats': (':sources', ..., {self._process_source}),
|
||||||
'thumbnail': ('poster', {lambda x: urljoin(url, x)}),
|
'thumbnail': ('poster', {urljoin(url)}),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
|
@ -145,10 +145,9 @@ class BravoTVIE(AdobePassIE):
|
||||||
tp_metadata = self._download_json(
|
tp_metadata = self._download_json(
|
||||||
update_url_query(tp_url, {'format': 'preview'}), video_id, fatal=False)
|
update_url_query(tp_url, {'format': 'preview'}), video_id, fatal=False)
|
||||||
|
|
||||||
seconds_or_none = lambda x: float_or_none(x, 1000)
|
|
||||||
chapters = traverse_obj(tp_metadata, ('chapters', ..., {
|
chapters = traverse_obj(tp_metadata, ('chapters', ..., {
|
||||||
'start_time': ('startTime', {seconds_or_none}),
|
'start_time': ('startTime', {float_or_none(scale=1000)}),
|
||||||
'end_time': ('endTime', {seconds_or_none}),
|
'end_time': ('endTime', {float_or_none(scale=1000)}),
|
||||||
}))
|
}))
|
||||||
# prune pointless single chapters that span the entire duration from short videos
|
# prune pointless single chapters that span the entire duration from short videos
|
||||||
if len(chapters) == 1 and not traverse_obj(chapters, (0, 'end_time')):
|
if len(chapters) == 1 and not traverse_obj(chapters, (0, 'end_time')):
|
||||||
|
@ -168,8 +167,8 @@ class BravoTVIE(AdobePassIE):
|
||||||
**merge_dicts(traverse_obj(tp_metadata, {
|
**merge_dicts(traverse_obj(tp_metadata, {
|
||||||
'title': 'title',
|
'title': 'title',
|
||||||
'description': 'description',
|
'description': 'description',
|
||||||
'duration': ('duration', {seconds_or_none}),
|
'duration': ('duration', {float_or_none(scale=1000)}),
|
||||||
'timestamp': ('pubDate', {seconds_or_none}),
|
'timestamp': ('pubDate', {float_or_none(scale=1000)}),
|
||||||
'season_number': (('pl1$seasonNumber', 'nbcu$seasonNumber'), {int_or_none}),
|
'season_number': (('pl1$seasonNumber', 'nbcu$seasonNumber'), {int_or_none}),
|
||||||
'episode_number': (('pl1$episodeNumber', 'nbcu$episodeNumber'), {int_or_none}),
|
'episode_number': (('pl1$episodeNumber', 'nbcu$episodeNumber'), {int_or_none}),
|
||||||
'series': (('pl1$show', 'nbcu$show'), (None, ...), {str}),
|
'series': (('pl1$show', 'nbcu$show'), (None, ...), {str}),
|
||||||
|
|
|
@ -18,6 +18,7 @@ from ..utils import (
|
||||||
fix_xml_ampersands,
|
fix_xml_ampersands,
|
||||||
float_or_none,
|
float_or_none,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
|
join_nonempty,
|
||||||
js_to_json,
|
js_to_json,
|
||||||
mimetype2ext,
|
mimetype2ext,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
|
@ -386,7 +387,7 @@ class BrightcoveLegacyIE(InfoExtractor):
|
||||||
@classmethod
|
@classmethod
|
||||||
def _make_brightcove_url(cls, params):
|
def _make_brightcove_url(cls, params):
|
||||||
return update_url_query(
|
return update_url_query(
|
||||||
'http://c.brightcove.com/services/viewer/htmlFederated', params)
|
'https://c.brightcove.com/services/viewer/htmlFederated', params)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _extract_brightcove_url(cls, webpage):
|
def _extract_brightcove_url(cls, webpage):
|
||||||
|
@ -470,7 +471,7 @@ class BrightcoveLegacyIE(InfoExtractor):
|
||||||
if referer:
|
if referer:
|
||||||
headers['Referer'] = referer
|
headers['Referer'] = referer
|
||||||
player_page = self._download_webpage(
|
player_page = self._download_webpage(
|
||||||
'http://link.brightcove.com/services/player/bcpid' + player_id[0],
|
'https://link.brightcove.com/services/player/bcpid' + player_id[0],
|
||||||
video_id, headers=headers, fatal=False)
|
video_id, headers=headers, fatal=False)
|
||||||
if player_page:
|
if player_page:
|
||||||
player_key = self._search_regex(
|
player_key = self._search_regex(
|
||||||
|
@ -480,7 +481,7 @@ class BrightcoveLegacyIE(InfoExtractor):
|
||||||
enc_pub_id = player_key.split(',')[1].replace('~', '=')
|
enc_pub_id = player_key.split(',')[1].replace('~', '=')
|
||||||
publisher_id = struct.unpack('>Q', base64.urlsafe_b64decode(enc_pub_id))[0]
|
publisher_id = struct.unpack('>Q', base64.urlsafe_b64decode(enc_pub_id))[0]
|
||||||
if publisher_id:
|
if publisher_id:
|
||||||
brightcove_new_url = f'http://players.brightcove.net/{publisher_id}/default_default/index.html?videoId={video_id}'
|
brightcove_new_url = f'https://players.brightcove.net/{publisher_id}/default_default/index.html?videoId={video_id}'
|
||||||
if referer:
|
if referer:
|
||||||
brightcove_new_url = smuggle_url(brightcove_new_url, {'referrer': referer})
|
brightcove_new_url = smuggle_url(brightcove_new_url, {'referrer': referer})
|
||||||
return self.url_result(brightcove_new_url, BrightcoveNewIE.ie_key(), video_id)
|
return self.url_result(brightcove_new_url, BrightcoveNewIE.ie_key(), video_id)
|
||||||
|
@ -538,12 +539,7 @@ class BrightcoveNewBaseIE(AdobePassIE):
|
||||||
})
|
})
|
||||||
|
|
||||||
def build_format_id(kind):
|
def build_format_id(kind):
|
||||||
format_id = kind
|
return join_nonempty(kind, tbr and f'{int(tbr)}k', height and f'{height}p')
|
||||||
if tbr:
|
|
||||||
format_id += f'-{int(tbr)}k'
|
|
||||||
if height:
|
|
||||||
format_id += f'-{height}p'
|
|
||||||
return format_id
|
|
||||||
|
|
||||||
if src or streaming_src:
|
if src or streaming_src:
|
||||||
f.update({
|
f.update({
|
||||||
|
@ -801,7 +797,7 @@ class BrightcoveNewIE(BrightcoveNewBaseIE):
|
||||||
# Look for iframe embeds [1]
|
# Look for iframe embeds [1]
|
||||||
for _, url in re.findall(
|
for _, url in re.findall(
|
||||||
r'<iframe[^>]+src=(["\'])((?:https?:)?//players\.brightcove\.net/\d+/[^/]+/index\.html.+?)\1', webpage):
|
r'<iframe[^>]+src=(["\'])((?:https?:)?//players\.brightcove\.net/\d+/[^/]+/index\.html.+?)\1', webpage):
|
||||||
entries.append(url if url.startswith('http') else 'http:' + url)
|
entries.append(url if url.startswith(('http:', 'https:')) else 'https:' + url)
|
||||||
|
|
||||||
# Look for <video> tags [2] and embed_in_page embeds [3]
|
# Look for <video> tags [2] and embed_in_page embeds [3]
|
||||||
# [2] looks like:
|
# [2] looks like:
|
||||||
|
@ -830,7 +826,7 @@ class BrightcoveNewIE(BrightcoveNewBaseIE):
|
||||||
player_id = player_id or attrs.get('data-player') or 'default'
|
player_id = player_id or attrs.get('data-player') or 'default'
|
||||||
embed = embed or attrs.get('data-embed') or 'default'
|
embed = embed or attrs.get('data-embed') or 'default'
|
||||||
|
|
||||||
bc_url = f'http://players.brightcove.net/{account_id}/{player_id}_{embed}/index.html?videoId={video_id}'
|
bc_url = f'https://players.brightcove.net/{account_id}/{player_id}_{embed}/index.html?videoId={video_id}'
|
||||||
|
|
||||||
# Some brightcove videos may be embedded with video tag only and
|
# Some brightcove videos may be embedded with video tag only and
|
||||||
# without script tag or any mentioning of brightcove at all. Such
|
# without script tag or any mentioning of brightcove at all. Such
|
||||||
|
@ -867,7 +863,7 @@ class BrightcoveNewIE(BrightcoveNewBaseIE):
|
||||||
store_pk = lambda x: self.cache.store('brightcove', policy_key_id, x)
|
store_pk = lambda x: self.cache.store('brightcove', policy_key_id, x)
|
||||||
|
|
||||||
def extract_policy_key():
|
def extract_policy_key():
|
||||||
base_url = f'http://players.brightcove.net/{account_id}/{player_id}_{embed}/'
|
base_url = f'https://players.brightcove.net/{account_id}/{player_id}_{embed}/'
|
||||||
config = self._download_json(
|
config = self._download_json(
|
||||||
base_url + 'config.json', video_id, fatal=False) or {}
|
base_url + 'config.json', video_id, fatal=False) or {}
|
||||||
policy_key = try_get(
|
policy_key = try_get(
|
||||||
|
|
|
@ -8,11 +8,13 @@ from ..utils import (
|
||||||
bug_reports_message,
|
bug_reports_message,
|
||||||
clean_html,
|
clean_html,
|
||||||
format_field,
|
format_field,
|
||||||
get_element_text_and_html_by_tag,
|
|
||||||
int_or_none,
|
int_or_none,
|
||||||
url_or_none,
|
url_or_none,
|
||||||
)
|
)
|
||||||
from ..utils.traversal import traverse_obj
|
from ..utils.traversal import (
|
||||||
|
find_element,
|
||||||
|
traverse_obj,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BundestagIE(InfoExtractor):
|
class BundestagIE(InfoExtractor):
|
||||||
|
@ -115,9 +117,8 @@ class BundestagIE(InfoExtractor):
|
||||||
note='Downloading metadata overlay', fatal=False,
|
note='Downloading metadata overlay', fatal=False,
|
||||||
), {
|
), {
|
||||||
'title': (
|
'title': (
|
||||||
{functools.partial(get_element_text_and_html_by_tag, 'h3')}, 0,
|
{find_element(tag='h3')}, {functools.partial(re.sub, r'<span[^>]*>[^<]+</span>', '')}, {clean_html}),
|
||||||
{functools.partial(re.sub, r'<span[^>]*>[^<]+</span>', '')}, {clean_html}),
|
'description': ({find_element(tag='p')}, {clean_html}),
|
||||||
'description': ({functools.partial(get_element_text_and_html_by_tag, 'p')}, 0, {clean_html}),
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
|
@ -53,7 +53,7 @@ class CaffeineTVIE(InfoExtractor):
|
||||||
'like_count': ('like_count', {int_or_none}),
|
'like_count': ('like_count', {int_or_none}),
|
||||||
'view_count': ('view_count', {int_or_none}),
|
'view_count': ('view_count', {int_or_none}),
|
||||||
'comment_count': ('comment_count', {int_or_none}),
|
'comment_count': ('comment_count', {int_or_none}),
|
||||||
'tags': ('tags', ..., {str}, {lambda x: x or None}),
|
'tags': ('tags', ..., {str}, filter),
|
||||||
'uploader': ('user', 'name', {str}),
|
'uploader': ('user', 'name', {str}),
|
||||||
'uploader_id': (((None, 'user'), 'username'), {str}, any),
|
'uploader_id': (((None, 'user'), 'username'), {str}, any),
|
||||||
'is_live': ('is_live', {bool}),
|
'is_live': ('is_live', {bool}),
|
||||||
|
@ -62,7 +62,7 @@ class CaffeineTVIE(InfoExtractor):
|
||||||
'title': ('broadcast_title', {str}),
|
'title': ('broadcast_title', {str}),
|
||||||
'duration': ('content_duration', {int_or_none}),
|
'duration': ('content_duration', {int_or_none}),
|
||||||
'timestamp': ('broadcast_start_time', {parse_iso8601}),
|
'timestamp': ('broadcast_start_time', {parse_iso8601}),
|
||||||
'thumbnail': ('preview_image_path', {lambda x: urljoin(url, x)}),
|
'thumbnail': ('preview_image_path', {urljoin(url)}),
|
||||||
}),
|
}),
|
||||||
'age_limit': {
|
'age_limit': {
|
||||||
# assume Apple Store ratings: https://en.wikipedia.org/wiki/Mobile_software_content_rating_system
|
# assume Apple Store ratings: https://en.wikipedia.org/wiki/Mobile_software_content_rating_system
|
||||||
|
|
|
@ -3,7 +3,7 @@ from ..utils import float_or_none, int_or_none, make_archive_id, traverse_obj
|
||||||
|
|
||||||
|
|
||||||
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': {
|
||||||
|
|
|
@ -1,22 +1,28 @@
|
||||||
import base64
|
import base64
|
||||||
|
import functools
|
||||||
import json
|
import json
|
||||||
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 ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
|
float_or_none,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
join_nonempty,
|
|
||||||
js_to_json,
|
js_to_json,
|
||||||
|
mimetype2ext,
|
||||||
orderedSet,
|
orderedSet,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
|
replace_extension,
|
||||||
smuggle_url,
|
smuggle_url,
|
||||||
strip_or_none,
|
strip_or_none,
|
||||||
traverse_obj,
|
traverse_obj,
|
||||||
try_get,
|
try_get,
|
||||||
|
update_url,
|
||||||
|
url_basename,
|
||||||
|
url_or_none,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -149,6 +155,7 @@ class CBCIE(InfoExtractor):
|
||||||
class CBCPlayerIE(InfoExtractor):
|
class CBCPlayerIE(InfoExtractor):
|
||||||
IE_NAME = 'cbc.ca:player'
|
IE_NAME = 'cbc.ca:player'
|
||||||
_VALID_URL = r'(?:cbcplayer:|https?://(?:www\.)?cbc\.ca/(?:player/play/(?:video/)?|i/caffeine/syndicate/\?mediaId=))(?P<id>(?:\d\.)?\d+)'
|
_VALID_URL = r'(?:cbcplayer:|https?://(?:www\.)?cbc\.ca/(?:player/play/(?:video/)?|i/caffeine/syndicate/\?mediaId=))(?P<id>(?:\d\.)?\d+)'
|
||||||
|
_GEO_COUNTRIES = ['CA']
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.cbc.ca/player/play/2683190193',
|
'url': 'http://www.cbc.ca/player/play/2683190193',
|
||||||
'md5': '64d25f841ddf4ddb28a235338af32e2c',
|
'md5': '64d25f841ddf4ddb28a235338af32e2c',
|
||||||
|
@ -172,21 +179,20 @@ class CBCPlayerIE(InfoExtractor):
|
||||||
'description': 'md5:dd3b692f0a139b0369943150bd1c46a9',
|
'description': 'md5:dd3b692f0a139b0369943150bd1c46a9',
|
||||||
'timestamp': 1425704400,
|
'timestamp': 1425704400,
|
||||||
'upload_date': '20150307',
|
'upload_date': '20150307',
|
||||||
'uploader': 'CBCC-NEW',
|
'thumbnail': 'https://i.cbc.ca/ais/1.2985700,1717262248558/full/max/0/default.jpg',
|
||||||
'thumbnail': 'http://thumbnails.cbc.ca/maven_legacy/thumbnails/sonali-karnick-220.jpg',
|
|
||||||
'chapters': [],
|
'chapters': [],
|
||||||
'duration': 494.811,
|
'duration': 494.811,
|
||||||
'categories': ['AudioMobile/All in a Weekend Montreal'],
|
'categories': ['All in a Weekend Montreal'],
|
||||||
'tags': 'count:8',
|
'tags': 'count:11',
|
||||||
'location': 'Quebec',
|
'location': 'Quebec',
|
||||||
'series': 'All in a Weekend Montreal',
|
'series': 'All in a Weekend Montreal',
|
||||||
'season': 'Season 2015',
|
'season': 'Season 2015',
|
||||||
'season_number': 2015,
|
'season_number': 2015,
|
||||||
'media_type': 'Excerpt',
|
'media_type': 'Excerpt',
|
||||||
|
'genres': ['Other'],
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://www.cbc.ca/i/caffeine/syndicate/?mediaId=2164402062',
|
'url': 'http://www.cbc.ca/i/caffeine/syndicate/?mediaId=2164402062',
|
||||||
'md5': '33fcd8f6719b9dd60a5e73adcb83b9f6',
|
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '2164402062',
|
'id': '2164402062',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
|
@ -194,107 +200,168 @@ class CBCPlayerIE(InfoExtractor):
|
||||||
'description': 'Tim Mayer has beaten three different forms of cancer four times in five years.',
|
'description': 'Tim Mayer has beaten three different forms of cancer four times in five years.',
|
||||||
'timestamp': 1320410746,
|
'timestamp': 1320410746,
|
||||||
'upload_date': '20111104',
|
'upload_date': '20111104',
|
||||||
'uploader': 'CBCC-NEW',
|
'thumbnail': 'https://i.cbc.ca/ais/1.1711287,1717139372111/full/max/0/default.jpg',
|
||||||
'thumbnail': 'https://thumbnails.cbc.ca/maven_legacy/thumbnails/277/67/cancer_852x480_2164412612.jpg',
|
|
||||||
'chapters': [],
|
'chapters': [],
|
||||||
'duration': 186.867,
|
'duration': 186.867,
|
||||||
'series': 'CBC News: Windsor at 6:00',
|
'series': 'CBC News: Windsor at 6:00',
|
||||||
'categories': ['News/Canada/Windsor'],
|
'categories': ['Windsor'],
|
||||||
'location': 'Windsor',
|
'location': 'Windsor',
|
||||||
'tags': ['cancer'],
|
'tags': ['Cancer', 'News/Canada/Windsor', 'Windsor'],
|
||||||
'creators': ['Allison Johnson'],
|
|
||||||
'media_type': 'Excerpt',
|
'media_type': 'Excerpt',
|
||||||
|
'genres': ['News'],
|
||||||
},
|
},
|
||||||
|
'params': {'skip_download': 'm3u8'},
|
||||||
}, {
|
}, {
|
||||||
# Redirected from http://www.cbc.ca/player/AudioMobile/All%20in%20a%20Weekend%20Montreal/ID/2657632011/
|
# Redirected from http://www.cbc.ca/player/AudioMobile/All%20in%20a%20Weekend%20Montreal/ID/2657632011/
|
||||||
'url': 'https://www.cbc.ca/player/play/1.2985700',
|
'url': 'https://www.cbc.ca/player/play/1.2985700',
|
||||||
'md5': 'e5e708c34ae6fca156aafe17c43e8b75',
|
'md5': 'e5e708c34ae6fca156aafe17c43e8b75',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '2657631896',
|
'id': '1.2985700',
|
||||||
'ext': 'mp3',
|
'ext': 'mp3',
|
||||||
'title': 'CBC Montreal is organizing its first ever community hackathon!',
|
'title': 'CBC Montreal is organizing its first ever community hackathon!',
|
||||||
'description': 'The modern technology we tend to depend on so heavily, is never without it\'s share of hiccups and headaches. Next weekend - CBC Montreal will be getting members of the public for its first Hackathon.',
|
'description': 'The modern technology we tend to depend on so heavily, is never without it\'s share of hiccups and headaches. Next weekend - CBC Montreal will be getting members of the public for its first Hackathon.',
|
||||||
'timestamp': 1425704400,
|
'timestamp': 1425704400,
|
||||||
'upload_date': '20150307',
|
'upload_date': '20150307',
|
||||||
'uploader': 'CBCC-NEW',
|
'thumbnail': 'https://i.cbc.ca/ais/1.2985700,1717262248558/full/max/0/default.jpg',
|
||||||
'thumbnail': 'http://thumbnails.cbc.ca/maven_legacy/thumbnails/sonali-karnick-220.jpg',
|
|
||||||
'chapters': [],
|
'chapters': [],
|
||||||
'duration': 494.811,
|
'duration': 494.811,
|
||||||
'categories': ['AudioMobile/All in a Weekend Montreal'],
|
'categories': ['All in a Weekend Montreal'],
|
||||||
'tags': 'count:8',
|
'tags': 'count:11',
|
||||||
'location': 'Quebec',
|
'location': 'Quebec',
|
||||||
'series': 'All in a Weekend Montreal',
|
'series': 'All in a Weekend Montreal',
|
||||||
'season': 'Season 2015',
|
'season': 'Season 2015',
|
||||||
'season_number': 2015,
|
'season_number': 2015,
|
||||||
'media_type': 'Excerpt',
|
'media_type': 'Excerpt',
|
||||||
|
'genres': ['Other'],
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.cbc.ca/player/play/1.1711287',
|
'url': 'https://www.cbc.ca/player/play/1.1711287',
|
||||||
'md5': '33fcd8f6719b9dd60a5e73adcb83b9f6',
|
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '2164402062',
|
'id': '1.1711287',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Cancer survivor four times over',
|
'title': 'Cancer survivor four times over',
|
||||||
'description': 'Tim Mayer has beaten three different forms of cancer four times in five years.',
|
'description': 'Tim Mayer has beaten three different forms of cancer four times in five years.',
|
||||||
'timestamp': 1320410746,
|
'timestamp': 1320410746,
|
||||||
'upload_date': '20111104',
|
'upload_date': '20111104',
|
||||||
'uploader': 'CBCC-NEW',
|
'thumbnail': 'https://i.cbc.ca/ais/1.1711287,1717139372111/full/max/0/default.jpg',
|
||||||
'thumbnail': 'https://thumbnails.cbc.ca/maven_legacy/thumbnails/277/67/cancer_852x480_2164412612.jpg',
|
|
||||||
'chapters': [],
|
'chapters': [],
|
||||||
'duration': 186.867,
|
'duration': 186.867,
|
||||||
'series': 'CBC News: Windsor at 6:00',
|
'series': 'CBC News: Windsor at 6:00',
|
||||||
'categories': ['News/Canada/Windsor'],
|
'categories': ['Windsor'],
|
||||||
'location': 'Windsor',
|
'location': 'Windsor',
|
||||||
'tags': ['cancer'],
|
'tags': ['Cancer', 'News/Canada/Windsor', 'Windsor'],
|
||||||
'creators': ['Allison Johnson'],
|
|
||||||
'media_type': 'Excerpt',
|
'media_type': 'Excerpt',
|
||||||
|
'genres': ['News'],
|
||||||
},
|
},
|
||||||
|
'params': {'skip_download': 'm3u8'},
|
||||||
}, {
|
}, {
|
||||||
# Has subtitles
|
# Has subtitles
|
||||||
# These broadcasts expire after ~1 month, can find new test URL here:
|
# These broadcasts expire after ~1 month, can find new test URL here:
|
||||||
# https://www.cbc.ca/player/news/TV%20Shows/The%20National/Latest%20Broadcast
|
# https://www.cbc.ca/player/news/TV%20Shows/The%20National/Latest%20Broadcast
|
||||||
'url': 'https://www.cbc.ca/player/play/1.7159484',
|
'url': 'https://www.cbc.ca/player/play/video/9.6424403',
|
||||||
'md5': '6ed6cd0fc2ef568d2297ba68a763d455',
|
'md5': '8025909eaffcf0adf59922904def9a5e',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '2324213316001',
|
'id': '9.6424403',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'The National | School boards sue social media giants',
|
'title': 'The National | N.W.T. wildfire emergency',
|
||||||
'description': 'md5:4b4db69322fa32186c3ce426da07402c',
|
'description': 'md5:ada33d36d1df69347ed575905bfd496c',
|
||||||
'timestamp': 1711681200,
|
'timestamp': 1718589600,
|
||||||
'duration': 2743.400,
|
'duration': 2692.833,
|
||||||
'subtitles': {'eng': [{'ext': 'vtt', 'protocol': 'm3u8_native'}]},
|
'subtitles': {
|
||||||
'thumbnail': 'https://thumbnails.cbc.ca/maven_legacy/thumbnails/607/559/thumbnail.jpeg',
|
'en-US': [{
|
||||||
'uploader': 'CBCC-NEW',
|
'name': 'English Captions',
|
||||||
|
'url': 'https://cbchls.akamaized.net/delivery/news-shows/2024/06/17/NAT_JUN16-00-55-00/NAT_JUN16_cc.vtt',
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
'thumbnail': 'https://i.cbc.ca/ais/6272b5c6-5e78-4c05-915d-0e36672e33d1,1714756287822/full/max/0/default.jpg',
|
||||||
'chapters': 'count:5',
|
'chapters': 'count:5',
|
||||||
'upload_date': '20240329',
|
'upload_date': '20240617',
|
||||||
'categories': 'count:4',
|
'categories': ['News', 'The National', 'The National Latest Broadcasts'],
|
||||||
'series': 'The National - Full Show',
|
'series': 'The National - Full Show',
|
||||||
'tags': 'count:1',
|
'tags': ['The National'],
|
||||||
'creators': ['News'],
|
|
||||||
'location': 'Canada',
|
'location': 'Canada',
|
||||||
'media_type': 'Full Program',
|
'media_type': 'Full Program',
|
||||||
|
'genres': ['News'],
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.cbc.ca/player/play/video/1.7194274',
|
'url': 'https://www.cbc.ca/player/play/video/1.7194274',
|
||||||
'md5': '188b96cf6bdcb2540e178a6caa957128',
|
'md5': '188b96cf6bdcb2540e178a6caa957128',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '2334524995812',
|
'id': '1.7194274',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': '#TheMoment a rare white spirit moose was spotted in Alberta',
|
'title': '#TheMoment a rare white spirit moose was spotted in Alberta',
|
||||||
'description': 'md5:18ae269a2d0265c5b0bbe4b2e1ac61a3',
|
'description': 'md5:18ae269a2d0265c5b0bbe4b2e1ac61a3',
|
||||||
'timestamp': 1714788791,
|
'timestamp': 1714788791,
|
||||||
'duration': 77.678,
|
'duration': 77.678,
|
||||||
'subtitles': {'eng': [{'ext': 'vtt', 'protocol': 'm3u8_native'}]},
|
'subtitles': {'eng': [{'ext': 'vtt', 'protocol': 'm3u8_native'}]},
|
||||||
'thumbnail': 'https://thumbnails.cbc.ca/maven_legacy/thumbnails/201/543/THE_MOMENT.jpg',
|
'thumbnail': 'https://i.cbc.ca/ais/1.7194274,1717224990425/full/max/0/default.jpg',
|
||||||
'uploader': 'CBCC-NEW',
|
'chapters': [],
|
||||||
'chapters': 'count:0',
|
|
||||||
'upload_date': '20240504',
|
|
||||||
'categories': 'count:3',
|
'categories': 'count:3',
|
||||||
'series': 'The National',
|
'series': 'The National',
|
||||||
'tags': 'count:15',
|
'tags': 'count:17',
|
||||||
'creators': ['encoder'],
|
|
||||||
'location': 'Canada',
|
'location': 'Canada',
|
||||||
'media_type': 'Excerpt',
|
'media_type': 'Excerpt',
|
||||||
|
'upload_date': '20240504',
|
||||||
|
'genres': ['News'],
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.cbc.ca/player/play/video/9.6427282',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '9.6427282',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Men\'s Soccer - Argentina vs Morocco',
|
||||||
|
'description': 'Argentina faces Morocco on the football pitch at Saint Etienne Stadium.',
|
||||||
|
'series': 'CBC Sports',
|
||||||
|
'media_type': 'Event Coverage',
|
||||||
|
'thumbnail': 'https://i.cbc.ca/ais/a4c5c0c2-99fa-4bd3-8061-5a63879c1b33,1718828053500/full/max/0/default.jpg',
|
||||||
|
'timestamp': 1721825400.0,
|
||||||
|
'upload_date': '20240724',
|
||||||
|
'duration': 10568.0,
|
||||||
|
'chapters': [],
|
||||||
|
'genres': [],
|
||||||
|
'tags': ['2024 Paris Olympic Games'],
|
||||||
|
'categories': ['Olympics Summer Soccer', 'Summer Olympics Replays', 'Summer Olympics Soccer Replays'],
|
||||||
|
'location': 'Canada',
|
||||||
|
},
|
||||||
|
'params': {'skip_download': 'm3u8'},
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.cbc.ca/player/play/video/9.6459530',
|
||||||
|
'md5': '6c1bb76693ab321a2e99c347a1d5ecbc',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '9.6459530',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Parts of Jasper incinerated as wildfire rages',
|
||||||
|
'description': 'md5:6f1caa8d128ad3f629257ef5fecf0962',
|
||||||
|
'series': 'The National',
|
||||||
|
'media_type': 'Excerpt',
|
||||||
|
'thumbnail': 'https://i.cbc.ca/ais/507c0086-31a2-494d-96e4-bffb1048d045,1721953984375/full/max/0/default.jpg',
|
||||||
|
'timestamp': 1721964091.012,
|
||||||
|
'upload_date': '20240726',
|
||||||
|
'duration': 952.285,
|
||||||
|
'chapters': [],
|
||||||
|
'genres': [],
|
||||||
|
'tags': 'count:23',
|
||||||
|
'categories': ['News (FAST)', 'News', 'The National', 'TV News Shows', 'The National '],
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.cbc.ca/player/play/video/9.6420651',
|
||||||
|
'md5': '71a850c2c6ee5e912de169f5311bb533',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '9.6420651',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Is it a breath of fresh air? Measuring air quality in Edmonton',
|
||||||
|
'description': 'md5:3922b92cc8b69212d739bd9dd095b1c3',
|
||||||
|
'series': 'CBC News Edmonton',
|
||||||
|
'media_type': 'Excerpt',
|
||||||
|
'thumbnail': 'https://i.cbc.ca/ais/73c4ab9c-7ad4-46ee-bb9b-020fdc01c745,1718214547576/full/max/0/default.jpg',
|
||||||
|
'timestamp': 1718220065.768,
|
||||||
|
'upload_date': '20240612',
|
||||||
|
'duration': 286.086,
|
||||||
|
'chapters': [],
|
||||||
|
'genres': ['News'],
|
||||||
|
'categories': ['News', 'Edmonton'],
|
||||||
|
'tags': 'count:7',
|
||||||
|
'location': 'Edmonton',
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'cbcplayer:1.7159484',
|
'url': 'cbcplayer:1.7159484',
|
||||||
|
@ -307,23 +374,113 @@ class CBCPlayerIE(InfoExtractor):
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
|
def _parse_param(self, asset_data, name):
|
||||||
|
return traverse_obj(asset_data, ('params', lambda _, v: v['name'] == name, 'value', {str}, any))
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
if '.' in video_id:
|
webpage = self._download_webpage(f'https://www.cbc.ca/player/play/{video_id}', video_id)
|
||||||
webpage = self._download_webpage(f'https://www.cbc.ca/player/play/{video_id}', video_id)
|
data = self._search_json(
|
||||||
video_id = self._search_json(
|
r'window\.__INITIAL_STATE__\s*=', webpage, 'initial state', video_id)['video']['currentClip']
|
||||||
r'window\.__INITIAL_STATE__\s*=', webpage,
|
assets = traverse_obj(
|
||||||
'initial state', video_id)['video']['currentClip']['mediaId']
|
data, ('media', 'assets', lambda _, v: url_or_none(v['key']) and v['type']))
|
||||||
|
|
||||||
|
if not assets and (media_id := traverse_obj(data, ('mediaId', {str}))):
|
||||||
|
# XXX: Deprecated; CBC is migrating off of ThePlatform
|
||||||
|
return {
|
||||||
|
'_type': 'url_transparent',
|
||||||
|
'ie_key': 'ThePlatform',
|
||||||
|
'url': smuggle_url(
|
||||||
|
f'http://link.theplatform.com/s/ExhSPC/media/guid/2655402169/{media_id}?mbr=true&formats=MPEG4,FLV,MP3', {
|
||||||
|
'force_smil_url': True,
|
||||||
|
}),
|
||||||
|
'id': media_id,
|
||||||
|
'_format_sort_fields': ('res', 'proto'), # Prioritize direct http formats over HLS
|
||||||
|
}
|
||||||
|
|
||||||
|
is_live = traverse_obj(data, ('media', 'streamType', {str})) == 'Live'
|
||||||
|
formats, subtitles = [], {}
|
||||||
|
|
||||||
|
for sub in traverse_obj(data, ('media', 'textTracks', lambda _, v: url_or_none(v['src']))):
|
||||||
|
subtitles.setdefault(sub.get('language') or 'und', []).append({
|
||||||
|
'url': sub['src'],
|
||||||
|
'name': sub.get('label'),
|
||||||
|
})
|
||||||
|
|
||||||
|
for asset in assets:
|
||||||
|
asset_key = asset['key']
|
||||||
|
asset_type = asset['type']
|
||||||
|
if asset_type != 'medianet':
|
||||||
|
self.report_warning(f'Skipping unsupported asset type "{asset_type}": {asset_key}')
|
||||||
|
continue
|
||||||
|
asset_data = self._download_json(asset_key, video_id, f'Downloading {asset_type} JSON')
|
||||||
|
ext = mimetype2ext(self._parse_param(asset_data, 'contentType'))
|
||||||
|
if ext == 'm3u8':
|
||||||
|
fmts, subs = self._extract_m3u8_formats_and_subtitles(
|
||||||
|
asset_data['url'], video_id, 'mp4', m3u8_id='hls', live=is_live)
|
||||||
|
formats.extend(fmts)
|
||||||
|
# Avoid slow/error-prone webvtt-over-m3u8 if direct https vtt is available
|
||||||
|
if not subtitles:
|
||||||
|
self._merge_subtitles(subs, target=subtitles)
|
||||||
|
if is_live or not fmts:
|
||||||
|
continue
|
||||||
|
# Check for direct https mp4 format
|
||||||
|
best_video_fmt = traverse_obj(fmts, (
|
||||||
|
lambda _, v: v.get('vcodec') != 'none' and v['tbr'], all,
|
||||||
|
{functools.partial(sorted, key=lambda x: x['tbr'])}, -1, {dict})) or {}
|
||||||
|
base_url = self._search_regex(
|
||||||
|
r'(https?://[^?#]+?/)hdntl=', best_video_fmt.get('url'), 'base url', default=None)
|
||||||
|
if not base_url or '/live/' in base_url:
|
||||||
|
continue
|
||||||
|
mp4_url = base_url + replace_extension(url_basename(best_video_fmt['url']), 'mp4')
|
||||||
|
if self._request_webpage(
|
||||||
|
HEADRequest(mp4_url), video_id, 'Checking for https format',
|
||||||
|
errnote=False, fatal=False):
|
||||||
|
formats.append({
|
||||||
|
**best_video_fmt,
|
||||||
|
'url': mp4_url,
|
||||||
|
'format_id': 'https-mp4',
|
||||||
|
'protocol': 'https',
|
||||||
|
'manifest_url': None,
|
||||||
|
'acodec': None,
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
formats.append({
|
||||||
|
'url': asset_data['url'],
|
||||||
|
'ext': ext,
|
||||||
|
'vcodec': 'none' if self._parse_param(asset_data, 'mediaType') == 'audio' else None,
|
||||||
|
})
|
||||||
|
|
||||||
|
chapters = traverse_obj(data, (
|
||||||
|
'media', 'chapters', lambda _, v: float(v['startTime']) is not None, {
|
||||||
|
'start_time': ('startTime', {float_or_none(scale=1000)}),
|
||||||
|
'end_time': ('endTime', {float_or_none(scale=1000)}),
|
||||||
|
'title': ('name', {str}),
|
||||||
|
}))
|
||||||
|
# Filter out pointless single chapters with start_time==0 and no end_time
|
||||||
|
if len(chapters) == 1 and not (chapters[0].get('start_time') or chapters[0].get('end_time')):
|
||||||
|
chapters = []
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'_type': 'url_transparent',
|
**traverse_obj(data, {
|
||||||
'ie_key': 'ThePlatform',
|
'title': ('title', {str}),
|
||||||
'url': smuggle_url(
|
'description': ('description', {str.strip}),
|
||||||
f'http://link.theplatform.com/s/ExhSPC/media/guid/2655402169/{video_id}?mbr=true&formats=MPEG4,FLV,MP3', {
|
'thumbnail': ('image', 'url', {url_or_none}, {update_url(query=None)}),
|
||||||
'force_smil_url': True,
|
'timestamp': ('publishedAt', {float_or_none(scale=1000)}),
|
||||||
}),
|
'media_type': ('media', 'clipType', {str}),
|
||||||
|
'series': ('showName', {str}),
|
||||||
|
'season_number': ('media', 'season', {int_or_none}),
|
||||||
|
'duration': ('media', 'duration', {float_or_none}, {lambda x: None if is_live else x}),
|
||||||
|
'location': ('media', 'region', {str}),
|
||||||
|
'tags': ('tags', ..., 'name', {str}),
|
||||||
|
'genres': ('media', 'genre', all),
|
||||||
|
'categories': ('categories', ..., 'name', {str}),
|
||||||
|
}),
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'_format_sort_fields': ('res', 'proto'), # Prioritize direct http formats over HLS
|
'formats': formats,
|
||||||
|
'subtitles': subtitles,
|
||||||
|
'chapters': chapters,
|
||||||
|
'is_live': is_live,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -365,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',
|
||||||
|
@ -380,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',
|
||||||
|
@ -401,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,
|
||||||
|
@ -455,10 +616,8 @@ class CBCGemIE(InfoExtractor):
|
||||||
|
|
||||||
def claims_token_expired(self):
|
def claims_token_expired(self):
|
||||||
exp = self._get_claims_token_expiry()
|
exp = self._get_claims_token_expiry()
|
||||||
if exp - time.time() < 10:
|
# It will expire in less than 10 seconds, or has already expired
|
||||||
# It will expire in less than 10 seconds, or has already expired
|
return exp - time.time() < 10
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def claims_token_valid(self):
|
def claims_token_valid(self):
|
||||||
return self._claims_token is not None and not self.claims_token_expired()
|
return self._claims_token is not None and not self.claims_token_expired()
|
||||||
|
@ -474,38 +633,6 @@ class CBCGemIE(InfoExtractor):
|
||||||
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(
|
||||||
|
@ -519,7 +646,6 @@ class CBCGemIE(InfoExtractor):
|
||||||
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'])
|
||||||
|
@ -528,9 +654,9 @@ class CBCGemIE(InfoExtractor):
|
||||||
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':
|
||||||
|
@ -546,20 +672,21 @@ class CBCGemIE(InfoExtractor):
|
||||||
|
|
||||||
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)}),
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -649,11 +776,11 @@ class CBCGemLiveIE(InfoExtractor):
|
||||||
'title': 'Ottawa',
|
'title': 'Ottawa',
|
||||||
'description': 'The live TV channel and local programming from Ottawa',
|
'description': 'The live TV channel and local programming from Ottawa',
|
||||||
'thumbnail': 'https://thumbnails.cbc.ca/maven_legacy/thumbnails/CBC_OTT_VMS/Live_Channel_Static_Images/Ottawa_2880x1620.jpg',
|
'thumbnail': 'https://thumbnails.cbc.ca/maven_legacy/thumbnails/CBC_OTT_VMS/Live_Channel_Static_Images/Ottawa_2880x1620.jpg',
|
||||||
'is_live': True,
|
'live_status': 'is_live',
|
||||||
'id': 'AyqZwxRqh8EH',
|
'id': 'AyqZwxRqh8EH',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'timestamp': 1492106160,
|
'release_timestamp': 1492106160,
|
||||||
'upload_date': '20170413',
|
'release_date': '20170413',
|
||||||
'uploader': 'CBCC-NEW',
|
'uploader': 'CBCC-NEW',
|
||||||
},
|
},
|
||||||
'skip': 'Live might have ended',
|
'skip': 'Live might have ended',
|
||||||
|
@ -682,49 +809,84 @@ class CBCGemLiveIE(InfoExtractor):
|
||||||
'description': 'March 24, 2023 | President Biden’s Ottawa visit ends with big pledges from both countries. Plus, Gwyneth Paltrow testifies in her ski collision trial.',
|
'description': 'March 24, 2023 | President Biden’s Ottawa visit ends with big pledges from both countries. Plus, Gwyneth Paltrow testifies in her ski collision trial.',
|
||||||
'live_status': 'is_live',
|
'live_status': 'is_live',
|
||||||
'thumbnail': r're:https://images.gem.cbc.ca/v1/cbc-gem/live/.*',
|
'thumbnail': r're:https://images.gem.cbc.ca/v1/cbc-gem/live/.*',
|
||||||
'timestamp': 1679706000,
|
'release_timestamp': 1679706000,
|
||||||
'upload_date': '20230325',
|
'release_date': '20230325',
|
||||||
},
|
},
|
||||||
'params': {'skip_download': True},
|
'params': {'skip_download': True},
|
||||||
'skip': 'Live might have ended',
|
'skip': 'Live might have ended',
|
||||||
},
|
},
|
||||||
|
{ # event replay (medianetlive)
|
||||||
|
'url': 'https://gem.cbc.ca/live-event/42314',
|
||||||
|
'md5': '297a9600f554f2258aed01514226a697',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '42314',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'live_status': 'was_live',
|
||||||
|
'title': 'Women\'s Soccer - Canada vs New Zealand',
|
||||||
|
'description': 'md5:36200e5f1a70982277b5a6ecea86155d',
|
||||||
|
'thumbnail': r're:https://.+default\.jpg',
|
||||||
|
'release_timestamp': 1721917200,
|
||||||
|
'release_date': '20240725',
|
||||||
|
},
|
||||||
|
'params': {'skip_download': True},
|
||||||
|
'skip': 'Replay might no longer be available',
|
||||||
|
},
|
||||||
|
{ # event replay (medianetlive)
|
||||||
|
'url': 'https://gem.cbc.ca/live-event/43273',
|
||||||
|
'only_matching': True,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
_GEO_COUNTRIES = ['CA']
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
webpage = self._download_webpage(url, video_id)
|
webpage = self._download_webpage(url, video_id)
|
||||||
video_info = self._search_nextjs_data(webpage, video_id)['props']['pageProps']['data']
|
video_info = self._search_nextjs_data(webpage, video_id)['props']['pageProps']['data']
|
||||||
|
|
||||||
# Two types of metadata JSON
|
# Three types of video_info JSON: info in root, freeTv stream/item, event replay
|
||||||
if not video_info.get('formattedIdMedia'):
|
if not video_info.get('formattedIdMedia'):
|
||||||
video_info = traverse_obj(
|
if traverse_obj(video_info, ('event', 'key')) == video_id:
|
||||||
video_info, (('freeTv', ('streams', ...)), 'items', lambda _, v: v['key'] == video_id, {dict}),
|
video_info = video_info['event']
|
||||||
get_all=False, default={})
|
else:
|
||||||
|
video_info = traverse_obj(video_info, (
|
||||||
|
('freeTv', ('streams', ...)), 'items',
|
||||||
|
lambda _, v: v['key'].partition('-')[0] == video_id, any)) or {}
|
||||||
|
|
||||||
video_stream_id = video_info.get('formattedIdMedia')
|
video_stream_id = video_info.get('formattedIdMedia')
|
||||||
if not video_stream_id:
|
if not video_stream_id:
|
||||||
raise ExtractorError('Couldn\'t find video metadata, maybe this livestream is now offline', expected=True)
|
raise ExtractorError(
|
||||||
|
'Couldn\'t find video metadata, maybe this livestream is now offline', expected=True)
|
||||||
|
|
||||||
stream_data = self._download_json(
|
live_status = 'was_live' if video_info.get('isVodEnabled') else 'is_live'
|
||||||
'https://services.radio-canada.ca/media/validation/v2/', video_id, query={
|
release_timestamp = traverse_obj(video_info, ('airDate', {parse_iso8601}))
|
||||||
'appCode': 'mpx',
|
|
||||||
'connectionType': 'hd',
|
if live_status == 'is_live' and release_timestamp and release_timestamp > time.time():
|
||||||
'deviceType': 'ipad',
|
formats = []
|
||||||
'idMedia': video_stream_id,
|
live_status = 'is_upcoming'
|
||||||
'multibitrate': 'true',
|
self.raise_no_formats('This livestream has not yet started', expected=True)
|
||||||
'output': 'json',
|
else:
|
||||||
'tech': 'hls',
|
stream_data = self._download_json(
|
||||||
'manifestType': 'desktop',
|
'https://services.radio-canada.ca/media/validation/v2/', video_id, query={
|
||||||
})
|
'appCode': 'medianetlive',
|
||||||
|
'connectionType': 'hd',
|
||||||
|
'deviceType': 'ipad',
|
||||||
|
'idMedia': video_stream_id,
|
||||||
|
'multibitrate': 'true',
|
||||||
|
'output': 'json',
|
||||||
|
'tech': 'hls',
|
||||||
|
'manifestType': 'desktop',
|
||||||
|
})
|
||||||
|
formats = self._extract_m3u8_formats(
|
||||||
|
stream_data['url'], video_id, 'mp4', live=live_status == 'is_live')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'formats': self._extract_m3u8_formats(stream_data['url'], video_id, 'mp4', live=True),
|
'formats': formats,
|
||||||
'is_live': True,
|
'live_status': live_status,
|
||||||
|
'release_timestamp': release_timestamp,
|
||||||
**traverse_obj(video_info, {
|
**traverse_obj(video_info, {
|
||||||
'title': 'title',
|
'title': ('title', {str}),
|
||||||
'description': 'description',
|
'description': ('description', {str}),
|
||||||
'thumbnail': ('images', 'card', 'url'),
|
'thumbnail': ('images', 'card', 'url'),
|
||||||
'timestamp': ('airDate', {parse_iso8601}),
|
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import base64
|
import base64
|
||||||
import re
|
import re
|
||||||
import urllib.error
|
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import zlib
|
import zlib
|
||||||
|
|
||||||
|
@ -97,7 +96,7 @@ class CBSNewsBaseIE(InfoExtractor):
|
||||||
**traverse_obj(item, {
|
**traverse_obj(item, {
|
||||||
'title': (None, ('fulltitle', 'title')),
|
'title': (None, ('fulltitle', 'title')),
|
||||||
'description': 'dek',
|
'description': 'dek',
|
||||||
'timestamp': ('timestamp', {lambda x: float_or_none(x, 1000)}),
|
'timestamp': ('timestamp', {float_or_none(scale=1000)}),
|
||||||
'duration': ('duration', {float_or_none}),
|
'duration': ('duration', {float_or_none}),
|
||||||
'subtitles': ('captions', {get_subtitles}),
|
'subtitles': ('captions', {get_subtitles}),
|
||||||
'thumbnail': ('images', ('hd', 'sd'), {url_or_none}),
|
'thumbnail': ('images', ('hd', 'sd'), {url_or_none}),
|
||||||
|
|
|
@ -12,53 +12,86 @@ from ..utils import (
|
||||||
|
|
||||||
|
|
||||||
class CCMAIE(InfoExtractor):
|
class CCMAIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?ccma\.cat/(?:[^/]+/)*?(?P<type>video|audio)/(?P<id>\d+)'
|
IE_DESC = '3Cat, TV3 and Catalunya Ràdio'
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?3cat\.cat/(?:3cat|tv3/sx3)/[^/?#]+/(?P<type>video|audio)/(?P<id>\d+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.ccma.cat/tv3/alacarta/lespot-de-la-marato-de-tv3/lespot-de-la-marato-de-tv3/video/5630208/',
|
# ccma.cat/tv3/alacarta/ URLs redirect to 3cat.cat/3cat/
|
||||||
|
'url': 'https://www.3cat.cat/3cat/lespot-de-la-marato-de-tv3/video/5630208/',
|
||||||
'md5': '7296ca43977c8ea4469e719c609b0871',
|
'md5': '7296ca43977c8ea4469e719c609b0871',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '5630208',
|
'id': '5630208',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'L\'espot de La Marató de TV3',
|
'title': 'L\'espot de La Marató 2016: Ictus i les lesions medul·lars i cerebrals traumàtiques',
|
||||||
'description': 'md5:f12987f320e2f6e988e9908e4fe97765',
|
'description': 'md5:f12987f320e2f6e988e9908e4fe97765',
|
||||||
'timestamp': 1478608140,
|
'timestamp': 1478608140,
|
||||||
'upload_date': '20161108',
|
'upload_date': '20161108',
|
||||||
'age_limit': 0,
|
'age_limit': 0,
|
||||||
|
'alt_title': 'EsportMarató2016WEB_PerPublicar',
|
||||||
|
'duration': 79,
|
||||||
|
'thumbnail': 'https://img.3cat.cat/multimedia/jpg/4/6/1478536106664.jpg',
|
||||||
|
'series': 'Dedicada a l\'ictus i les lesions medul·lars i cerebrals traumàtiques',
|
||||||
|
'categories': ['Divulgació'],
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://www.ccma.cat/catradio/alacarta/programa/el-consell-de-savis-analitza-el-derbi/audio/943685/',
|
# ccma.cat/catradio/alacarta/ URLs redirect to 3cat.cat/3cat/
|
||||||
|
'url': 'https://www.3cat.cat/3cat/el-consell-de-savis-analitza-el-derbi/audio/943685/',
|
||||||
'md5': 'fa3e38f269329a278271276330261425',
|
'md5': 'fa3e38f269329a278271276330261425',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '943685',
|
'id': '943685',
|
||||||
'ext': 'mp3',
|
'ext': 'mp3',
|
||||||
'title': 'El Consell de Savis analitza el derbi',
|
'title': 'El Consell de Savis analitza el derbi',
|
||||||
'description': 'md5:e2a3648145f3241cb9c6b4b624033e53',
|
'description': 'md5:e2a3648145f3241cb9c6b4b624033e53',
|
||||||
'upload_date': '20170512',
|
'upload_date': '20161217',
|
||||||
'timestamp': 1494622500,
|
'timestamp': 1482011700,
|
||||||
'vcodec': 'none',
|
'vcodec': 'none',
|
||||||
'categories': ['Esports'],
|
'categories': ['Esports'],
|
||||||
|
'series': 'Tot gira',
|
||||||
|
'duration': 821,
|
||||||
|
'thumbnail': 'https://img.3cat.cat/multimedia/jpg/8/9/1482002602598.jpg',
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://www.ccma.cat/tv3/alacarta/crims/crims-josep-tallada-lespereu-me-capitol-1/video/6031387/',
|
'url': 'https://www.3cat.cat/3cat/crims-josep-tallada-lespereu-me-part-1/video/6031387/',
|
||||||
'md5': 'b43c3d3486f430f3032b5b160d80cbc3',
|
'md5': '27493513d08a3e5605814aee9bb778d2',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '6031387',
|
'id': '6031387',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Crims - Josep Talleda, l\'"Espereu-me" (capítol 1)',
|
'title': 'T1xC5 - Josep Talleda, l\'"Espereu-me" (part 1)',
|
||||||
'description': 'md5:7cbdafb640da9d0d2c0f62bad1e74e60',
|
'description': 'md5:7cbdafb640da9d0d2c0f62bad1e74e60',
|
||||||
'timestamp': 1582577700,
|
'timestamp': 1582577919,
|
||||||
'upload_date': '20200224',
|
'upload_date': '20200224',
|
||||||
'subtitles': 'mincount:4',
|
'subtitles': 'mincount:1',
|
||||||
'age_limit': 16,
|
'age_limit': 13,
|
||||||
'series': 'Crims',
|
'series': 'Crims',
|
||||||
|
'thumbnail': 'https://img.3cat.cat/multimedia/jpg/1/9/1582564376991.jpg',
|
||||||
|
'duration': 3203,
|
||||||
|
'categories': ['Divulgació'],
|
||||||
|
'alt_title': 'Crims - 5 - Josep Talleda, l\'"Espereu-me" (1a part) - Josep Talleda, l\'"Espereu-me" (part 1)',
|
||||||
|
'episode_number': 5,
|
||||||
|
'episode': 'Episode 5',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.3cat.cat/tv3/sx3/una-mosca-volava-per-la-llum/video/5759227/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '5759227',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Una mosca volava per la llum',
|
||||||
|
'alt_title': '17Z004Ç UNA MOSCA VOLAVA PER LA LLUM',
|
||||||
|
'description': 'md5:9ab64276944b0825336f4147f13f7854',
|
||||||
|
'series': 'Mic',
|
||||||
|
'upload_date': '20180411',
|
||||||
|
'timestamp': 1523440105,
|
||||||
|
'duration': 160,
|
||||||
|
'age_limit': 0,
|
||||||
|
'thumbnail': 'https://img.3cat.cat/multimedia/jpg/6/1/1524071667216.jpg',
|
||||||
|
'categories': ['Música'],
|
||||||
},
|
},
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
media_type, media_id = self._match_valid_url(url).groups()
|
media_type, media_id = self._match_valid_url(url).group('type', 'id')
|
||||||
|
|
||||||
media = self._download_json(
|
media = self._download_json(
|
||||||
'http://dinamics.ccma.cat/pvideo/media.jsp', media_id, query={
|
'http://api-media.3cat.cat/pvideo/media.jsp', media_id, query={
|
||||||
'media': media_type,
|
'media': media_type,
|
||||||
'idint': media_id,
|
'idint': media_id,
|
||||||
'format': 'dm',
|
'format': 'dm',
|
||||||
|
|
|
@ -12,6 +12,7 @@ from .common import InfoExtractor
|
||||||
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 @@ class CDAIE(InfoExtractor):
|
||||||
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))
|
||||||
|
|
|
@ -1,63 +1,50 @@
|
||||||
from .common import InfoExtractor
|
from .vidyard import VidyardBaseIE, VidyardIE
|
||||||
from ..utils import traverse_obj
|
from ..utils import ExtractorError, make_archive_id, url_basename
|
||||||
|
|
||||||
|
|
||||||
class CellebriteIE(InfoExtractor):
|
class CellebriteIE(VidyardBaseIE):
|
||||||
_VALID_URL = r'https?://cellebrite\.com/(?:\w+)?/(?P<id>[\w-]+)'
|
_VALID_URL = r'https?://cellebrite\.com/(?:\w+)?/(?P<id>[\w-]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://cellebrite.com/en/collect-data-from-android-devices-with-cellebrite-ufed/',
|
'url': 'https://cellebrite.com/en/collect-data-from-android-devices-with-cellebrite-ufed/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '16025876',
|
'id': 'ZqmUss3dQfEMGpauambPuH',
|
||||||
|
'display_id': '16025876',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'description': 'md5:174571cb97083fd1d457d75c684f4e2b',
|
|
||||||
'thumbnail': 'https://cellebrite.com/wp-content/uploads/2021/05/Chat-Capture-1024x559.png',
|
|
||||||
'title': 'Ask the Expert: Chat Capture - Collect Data from Android Devices in Cellebrite UFED',
|
'title': 'Ask the Expert: Chat Capture - Collect Data from Android Devices in Cellebrite UFED',
|
||||||
'duration': 455,
|
'description': 'md5:dee48fe12bbae5c01fe6a053f7676da4',
|
||||||
'tags': [],
|
'thumbnail': 'https://cellebrite.com/wp-content/uploads/2021/05/Chat-Capture-1024x559.png',
|
||||||
|
'duration': 455.979,
|
||||||
|
'_old_archive_ids': ['cellebrite 16025876'],
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://cellebrite.com/en/how-to-lawfully-collect-the-maximum-amount-of-data-from-android-devices/',
|
'url': 'https://cellebrite.com/en/how-to-lawfully-collect-the-maximum-amount-of-data-from-android-devices/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '29018255',
|
'id': 'QV1U8a2yzcxigw7VFnqKyg',
|
||||||
|
'display_id': '29018255',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'duration': 134,
|
'title': 'How to Lawfully Collect the Maximum Amount of Data From Android Devices',
|
||||||
'tags': [],
|
'description': 'md5:0e943a9ac14c374d5d74faed634d773c',
|
||||||
'description': 'md5:e9a3d124c7287b0b07bad2547061cacf',
|
|
||||||
'thumbnail': 'https://cellebrite.com/wp-content/uploads/2022/07/How-to-Lawfully-Collect-the-Maximum-Amount-of-Data-From-Android-Devices.png',
|
'thumbnail': 'https://cellebrite.com/wp-content/uploads/2022/07/How-to-Lawfully-Collect-the-Maximum-Amount-of-Data-From-Android-Devices.png',
|
||||||
'title': 'Android Extractions Explained',
|
'duration': 134.315,
|
||||||
|
'_old_archive_ids': ['cellebrite 29018255'],
|
||||||
},
|
},
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _get_formats_and_subtitles(self, json_data, display_id):
|
|
||||||
formats = [{'url': url} for url in traverse_obj(json_data, ('mp4', ..., 'url')) or []]
|
|
||||||
subtitles = {}
|
|
||||||
|
|
||||||
for url in traverse_obj(json_data, ('hls', ..., 'url')) or []:
|
|
||||||
fmt, sub = self._extract_m3u8_formats_and_subtitles(
|
|
||||||
url, display_id, ext='mp4', headers={'Referer': 'https://play.vidyard.com/'})
|
|
||||||
formats.extend(fmt)
|
|
||||||
self._merge_subtitles(sub, target=subtitles)
|
|
||||||
|
|
||||||
return formats, subtitles
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
display_id = self._match_id(url)
|
slug = self._match_id(url)
|
||||||
webpage = self._download_webpage(url, display_id)
|
webpage = self._download_webpage(url, slug)
|
||||||
|
vidyard_url = next(VidyardIE._extract_embed_urls(url, webpage), None)
|
||||||
|
if not vidyard_url:
|
||||||
|
raise ExtractorError('No Vidyard video embeds found on page')
|
||||||
|
|
||||||
player_uuid = self._search_regex(
|
video_id = url_basename(vidyard_url)
|
||||||
r'<img\s[^>]*\bdata-uuid\s*=\s*"([^"\?]+)', webpage, 'player UUID')
|
info = self._process_video_json(self._fetch_video_json(video_id)['chapters'][0], video_id)
|
||||||
json_data = self._download_json(
|
if info.get('display_id'):
|
||||||
f'https://play.vidyard.com/player/{player_uuid}.json', display_id)['payload']['chapters'][0]
|
info['_old_archive_ids'] = [make_archive_id(self, info['display_id'])]
|
||||||
|
if thumbnail := self._og_search_thumbnail(webpage, default=None):
|
||||||
|
info.setdefault('thumbnails', []).append({'url': thumbnail})
|
||||||
|
|
||||||
formats, subtitles = self._get_formats_and_subtitles(json_data['sources'], display_id)
|
|
||||||
return {
|
return {
|
||||||
'id': str(json_data['videoId']),
|
'description': self._og_search_description(webpage, default=None),
|
||||||
'title': json_data.get('name') or self._og_search_title(webpage),
|
**info,
|
||||||
'formats': formats,
|
|
||||||
'subtitles': subtitles,
|
|
||||||
'description': json_data.get('description') or self._og_search_description(webpage),
|
|
||||||
'duration': json_data.get('seconds'),
|
|
||||||
'tags': json_data.get('tags'),
|
|
||||||
'thumbnail': self._og_search_thumbnail(webpage),
|
|
||||||
'http_headers': {'Referer': 'https://play.vidyard.com/'},
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import functools
|
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
UserNotLive,
|
UserNotLive,
|
||||||
|
@ -36,7 +34,7 @@ class CHZZKLiveIE(InfoExtractor):
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
channel_id = self._match_id(url)
|
channel_id = self._match_id(url)
|
||||||
live_detail = self._download_json(
|
live_detail = self._download_json(
|
||||||
f'https://api.chzzk.naver.com/service/v2/channels/{channel_id}/live-detail', channel_id,
|
f'https://api.chzzk.naver.com/service/v3/channels/{channel_id}/live-detail', channel_id,
|
||||||
note='Downloading channel info', errnote='Unable to download channel info')['content']
|
note='Downloading channel info', errnote='Unable to download channel info')['content']
|
||||||
|
|
||||||
if live_detail.get('status') == 'CLOSE':
|
if live_detail.get('status') == 'CLOSE':
|
||||||
|
@ -77,7 +75,7 @@ class CHZZKLiveIE(InfoExtractor):
|
||||||
'thumbnails': thumbnails,
|
'thumbnails': thumbnails,
|
||||||
**traverse_obj(live_detail, {
|
**traverse_obj(live_detail, {
|
||||||
'title': ('liveTitle', {str}),
|
'title': ('liveTitle', {str}),
|
||||||
'timestamp': ('openDate', {functools.partial(parse_iso8601, delimiter=' ')}),
|
'timestamp': ('openDate', {parse_iso8601(delimiter=' ')}),
|
||||||
'concurrent_view_count': ('concurrentUserCount', {int_or_none}),
|
'concurrent_view_count': ('concurrentUserCount', {int_or_none}),
|
||||||
'view_count': ('accumulateCount', {int_or_none}),
|
'view_count': ('accumulateCount', {int_or_none}),
|
||||||
'channel': ('channel', 'channelName', {str}),
|
'channel': ('channel', 'channelName', {str}),
|
||||||
|
@ -106,30 +104,77 @@ class CHZZKVideoIE(InfoExtractor):
|
||||||
'upload_date': '20231219',
|
'upload_date': '20231219',
|
||||||
'view_count': int,
|
'view_count': int,
|
||||||
},
|
},
|
||||||
|
'skip': 'Replay video is expired',
|
||||||
|
}, {
|
||||||
|
# Manually uploaded video
|
||||||
|
'url': 'https://chzzk.naver.com/video/1980',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '1980',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': '※시청주의※한번보면 잊기 힘든 영상',
|
||||||
|
'channel': '라디유radiyu',
|
||||||
|
'channel_id': '68f895c59a1043bc5019b5e08c83a5c5',
|
||||||
|
'channel_is_verified': False,
|
||||||
|
'thumbnail': r're:^https?://.*\.jpg$',
|
||||||
|
'duration': 95,
|
||||||
|
'timestamp': 1703102631.722,
|
||||||
|
'upload_date': '20231220',
|
||||||
|
'view_count': int,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
# Partner channel replay video
|
||||||
|
'url': 'https://chzzk.naver.com/video/2458',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '2458',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': '첫 방송',
|
||||||
|
'channel': '강지',
|
||||||
|
'channel_id': 'b5ed5db484d04faf4d150aedd362f34b',
|
||||||
|
'channel_is_verified': True,
|
||||||
|
'thumbnail': r're:^https?://.*\.jpg$',
|
||||||
|
'duration': 4433,
|
||||||
|
'timestamp': 1703307460.214,
|
||||||
|
'upload_date': '20231223',
|
||||||
|
'view_count': int,
|
||||||
|
},
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
video_meta = self._download_json(
|
video_meta = self._download_json(
|
||||||
f'https://api.chzzk.naver.com/service/v2/videos/{video_id}', video_id,
|
f'https://api.chzzk.naver.com/service/v3/videos/{video_id}', video_id,
|
||||||
note='Downloading video info', errnote='Unable to download video info')['content']
|
note='Downloading video info', errnote='Unable to download video info')['content']
|
||||||
formats, subtitles = self._extract_mpd_formats_and_subtitles(
|
|
||||||
f'https://apis.naver.com/neonplayer/vodplay/v1/playback/{video_meta["videoId"]}', video_id,
|
live_status = 'was_live' if video_meta.get('liveOpenDate') else 'not_live'
|
||||||
query={
|
video_status = video_meta.get('vodStatus')
|
||||||
'key': video_meta['inKey'],
|
if video_status == 'UPLOAD':
|
||||||
'env': 'real',
|
playback = self._parse_json(video_meta['liveRewindPlaybackJson'], video_id)
|
||||||
'lc': 'en_US',
|
formats, subtitles = self._extract_m3u8_formats_and_subtitles(
|
||||||
'cpl': 'en_US',
|
playback['media'][0]['path'], video_id, 'mp4', m3u8_id='hls')
|
||||||
}, note='Downloading video playback', errnote='Unable to download video playback')
|
elif video_status == 'ABR_HLS':
|
||||||
|
formats, subtitles = self._extract_mpd_formats_and_subtitles(
|
||||||
|
f'https://apis.naver.com/neonplayer/vodplay/v1/playback/{video_meta["videoId"]}',
|
||||||
|
video_id, query={
|
||||||
|
'key': video_meta['inKey'],
|
||||||
|
'env': 'real',
|
||||||
|
'lc': 'en_US',
|
||||||
|
'cpl': 'en_US',
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
self.raise_no_formats(
|
||||||
|
f'Unknown video status detected: "{video_status}"', expected=True, video_id=video_id)
|
||||||
|
formats, subtitles = [], {}
|
||||||
|
live_status = 'post_live' if live_status == 'was_live' else None
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
'subtitles': subtitles,
|
'subtitles': subtitles,
|
||||||
|
'live_status': live_status,
|
||||||
**traverse_obj(video_meta, {
|
**traverse_obj(video_meta, {
|
||||||
'title': ('videoTitle', {str}),
|
'title': ('videoTitle', {str}),
|
||||||
'thumbnail': ('thumbnailImageUrl', {url_or_none}),
|
'thumbnail': ('thumbnailImageUrl', {url_or_none}),
|
||||||
'timestamp': ('publishDateAt', {functools.partial(float_or_none, scale=1000)}),
|
'timestamp': ('publishDateAt', {float_or_none(scale=1000)}),
|
||||||
'view_count': ('readCount', {int_or_none}),
|
'view_count': ('readCount', {int_or_none}),
|
||||||
'duration': ('duration', {int_or_none}),
|
'duration': ('duration', {int_or_none}),
|
||||||
'channel': ('channel', 'channelName', {str}),
|
'channel': ('channel', 'channelName', {str}),
|
||||||
|
|
|
@ -3,6 +3,7 @@ import re
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
filter_dict,
|
filter_dict,
|
||||||
|
float_or_none,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
parse_age_limit,
|
parse_age_limit,
|
||||||
smuggle_url,
|
smuggle_url,
|
||||||
|
@ -85,7 +86,7 @@ class CineverseIE(CineverseBaseIE):
|
||||||
'title': 'title',
|
'title': 'title',
|
||||||
'id': ('details', 'item_id'),
|
'id': ('details', 'item_id'),
|
||||||
'description': ('details', 'description'),
|
'description': ('details', 'description'),
|
||||||
'duration': ('duration', {lambda x: x / 1000}),
|
'duration': ('duration', {float_or_none(scale=1000)}),
|
||||||
'cast': ('details', 'cast', {lambda x: x.split(', ')}),
|
'cast': ('details', 'cast', {lambda x: x.split(', ')}),
|
||||||
'modified_timestamp': ('details', 'updated_by', 0, 'update_time', 'time', {int_or_none}),
|
'modified_timestamp': ('details', 'updated_by', 0, 'update_time', 'time', {int_or_none}),
|
||||||
'season_number': ('details', 'season', {int_or_none}),
|
'season_number': ('details', 'season', {int_or_none}),
|
||||||
|
|
|
@ -6,11 +6,11 @@ from .common import InfoExtractor
|
||||||
class CloudflareStreamIE(InfoExtractor):
|
class CloudflareStreamIE(InfoExtractor):
|
||||||
_SUBDOMAIN_RE = r'(?:(?:watch|iframe|customer-\w+)\.)?'
|
_SUBDOMAIN_RE = r'(?:(?:watch|iframe|customer-\w+)\.)?'
|
||||||
_DOMAIN_RE = r'(?:cloudflarestream\.com|(?:videodelivery|bytehighway)\.net)'
|
_DOMAIN_RE = r'(?:cloudflarestream\.com|(?:videodelivery|bytehighway)\.net)'
|
||||||
_EMBED_RE = rf'embed\.{_DOMAIN_RE}/embed/[^/]+\.js\?.*?\bvideo='
|
_EMBED_RE = rf'(?:embed\.|{_SUBDOMAIN_RE}){_DOMAIN_RE}/embed/[^/?#]+\.js\?(?:[^#]+&)?video='
|
||||||
_ID_RE = r'[\da-f]{32}|[\w-]+\.[\w-]+\.[\w-]+'
|
_ID_RE = r'[\da-f]{32}|eyJ[\w-]+\.[\w-]+\.[\w-]+'
|
||||||
_VALID_URL = rf'https?://(?:{_SUBDOMAIN_RE}{_DOMAIN_RE}/|{_EMBED_RE})(?P<id>{_ID_RE})'
|
_VALID_URL = rf'https?://(?:{_SUBDOMAIN_RE}{_DOMAIN_RE}/|{_EMBED_RE})(?P<id>{_ID_RE})'
|
||||||
_EMBED_REGEX = [
|
_EMBED_REGEX = [
|
||||||
rf'<script[^>]+\bsrc=(["\'])(?P<url>(?:https?:)?//{_EMBED_RE}(?:{_ID_RE}).*?)\1',
|
rf'<script[^>]+\bsrc=(["\'])(?P<url>(?:https?:)?//{_EMBED_RE}(?:{_ID_RE})(?:(?!\1).)*)\1',
|
||||||
rf'<iframe[^>]+\bsrc=["\'](?P<url>https?://{_SUBDOMAIN_RE}{_DOMAIN_RE}/[\da-f]{{32}})',
|
rf'<iframe[^>]+\bsrc=["\'](?P<url>https?://{_SUBDOMAIN_RE}{_DOMAIN_RE}/[\da-f]{{32}})',
|
||||||
]
|
]
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
|
@ -24,6 +24,14 @@ class CloudflareStreamIE(InfoExtractor):
|
||||||
'params': {
|
'params': {
|
||||||
'skip_download': 'm3u8',
|
'skip_download': 'm3u8',
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://watch.cloudflarestream.com/embed/sdk-iframe-integration.fla9.latest.js?video=0e8e040aec776862e1d632a699edf59e',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '0e8e040aec776862e1d632a699edf59e',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': '0e8e040aec776862e1d632a699edf59e',
|
||||||
|
'thumbnail': 'https://videodelivery.net/0e8e040aec776862e1d632a699edf59e/thumbnails/thumbnail.jpg',
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://watch.cloudflarestream.com/9df17203414fd1db3e3ed74abbe936c1',
|
'url': 'https://watch.cloudflarestream.com/9df17203414fd1db3e3ed74abbe936c1',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
@ -36,6 +44,9 @@ class CloudflareStreamIE(InfoExtractor):
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://customer-aw5py76sw8wyqzmh.cloudflarestream.com/2463f6d3e06fa29710a337f5f5389fd8/iframe',
|
'url': 'https://customer-aw5py76sw8wyqzmh.cloudflarestream.com/2463f6d3e06fa29710a337f5f5389fd8/iframe',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://watch.cloudflarestream.com/eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJraWQiOiJmYTA0YjViMzQ2NDkwYTM5NWJiNzQ1NWFhZTA2YzYwZSIsInN1YiI6Ijg4ZDQxMDhhMzY0MjA3M2VhYmFhZjg3ZGExODJkMjYzIiwiZXhwIjoxNjAwNjA5MzE5fQ.xkRJwLGkt0nZ%5F0BlPiwU7iW4pqb4lKkznbKfAhGg0tGcxSS6ZBA3lcTUwu7W%2DyCFbnAl%2Dhqk3Fn%5FqeQS%5FQydP27qTHpB9iIFFsMtk1tqzGZV5v4yrYDnwLSKzEKvVd6QwJnfABtxH2JdpSNuWlMUiVXFxGWgjOw6QeTNDDklTQYXV%5FNLV7sErSn5CeOPeRRkdXb%2D8ip%5FVOcfk1nDsFoOo4fctFtGP0wYMyY5ae8nhhatydHwevuvJCcEvEfh%2D4qjq9mCZOodevmtSQ4YWmggf4BxtWnDWYrGW8Otp6oqezrR8oY4%2DbKdV6PaqBj49aJdcls6xK7PmM8%5Fvjy3xfm0Mg',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
_WEBPAGE_TESTS = [{
|
_WEBPAGE_TESTS = [{
|
||||||
'url': 'https://upride.cc/incident/shoulder-pass-at-light/',
|
'url': 'https://upride.cc/incident/shoulder-pass-at-light/',
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
int_or_none,
|
int_or_none,
|
||||||
|
@ -35,6 +37,20 @@ class CloudyCDNIE(InfoExtractor):
|
||||||
'duration': 1205,
|
'duration': 1205,
|
||||||
'upload_date': '20221130',
|
'upload_date': '20221130',
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
# Video-only m3u8 formats need manual fixup
|
||||||
|
'url': 'https://embed.cloudycdn.services/ltv/media/08j_d24-6000-074',
|
||||||
|
'md5': 'fc472e40f6e6238446509be411c920e2',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '08j_d24-6000-074',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'upload_date': '20240620',
|
||||||
|
'duration': 1673,
|
||||||
|
'title': 'D24-6000-074-cetstud',
|
||||||
|
'timestamp': 1718902233,
|
||||||
|
'thumbnail': 'https://store.cloudycdn.services/tmsp00060/assets/media/788392/placeholder1718903938.jpg',
|
||||||
|
},
|
||||||
|
'params': {'format': 'bv'},
|
||||||
}]
|
}]
|
||||||
_WEBPAGE_TESTS = [{
|
_WEBPAGE_TESTS = [{
|
||||||
'url': 'https://www.tavaklase.lv/video/es-esmu-mina-um-2/',
|
'url': 'https://www.tavaklase.lv/video/es-esmu-mina-um-2/',
|
||||||
|
@ -63,6 +79,9 @@ class CloudyCDNIE(InfoExtractor):
|
||||||
formats, subtitles = [], {}
|
formats, subtitles = [], {}
|
||||||
for m3u8_url in traverse_obj(data, ('source', 'sources', ..., 'src', {url_or_none})):
|
for m3u8_url in traverse_obj(data, ('source', 'sources', ..., 'src', {url_or_none})):
|
||||||
fmts, subs = self._extract_m3u8_formats_and_subtitles(m3u8_url, video_id, fatal=False)
|
fmts, subs = self._extract_m3u8_formats_and_subtitles(m3u8_url, video_id, fatal=False)
|
||||||
|
for fmt in fmts:
|
||||||
|
if re.search(r'chunklist_b\d+_vo_', fmt['url']):
|
||||||
|
fmt['acodec'] = 'none'
|
||||||
formats.extend(fmts)
|
formats.extend(fmts)
|
||||||
self._merge_subtitles(subs, target=subtitles)
|
self._merge_subtitles(subs, target=subtitles)
|
||||||
|
|
||||||
|
|
|
@ -1,146 +1,225 @@
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from .turner import TurnerBaseIE
|
from ..utils import (
|
||||||
from ..utils import merge_dicts, try_call, url_basename
|
clean_html,
|
||||||
|
extract_attributes,
|
||||||
|
int_or_none,
|
||||||
|
merge_dicts,
|
||||||
|
parse_duration,
|
||||||
|
parse_iso8601,
|
||||||
|
parse_resolution,
|
||||||
|
try_call,
|
||||||
|
update_url,
|
||||||
|
url_or_none,
|
||||||
|
)
|
||||||
|
from ..utils.traversal import find_elements, traverse_obj
|
||||||
|
|
||||||
|
|
||||||
class CNNIE(TurnerBaseIE):
|
class CNNIE(InfoExtractor):
|
||||||
_VALID_URL = r'''(?x)https?://(?:(?P<sub_domain>edition|www|money)\.)?cnn\.com/(?:video/(?:data/.+?|\?)/)?videos?/
|
_VALID_URL = r'https?://(?:(?:edition|www|money|cnnespanol)\.)?cnn\.com/(?!audio/)(?P<display_id>[^?#]+?)(?:[?#]|$|/index\.html)'
|
||||||
(?P<path>.+?/(?P<title>[^/]+?)(?:\.(?:[a-z\-]+)|(?=&)))'''
|
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://edition.cnn.com/video/?/video/sports/2013/06/09/nadal-1-on-1.cnn',
|
'url': 'https://www.cnn.com/2024/05/31/sport/video/jadon-sancho-borussia-dortmund-champions-league-exclusive-spt-intl',
|
||||||
'md5': '3e6121ea48df7e2259fe73a0628605c4',
|
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'sports/2013/06/09/nadal-1-on-1.cnn',
|
'id': 'med0e97ad0d154f56e29aa96e57192a14226734b6b',
|
||||||
|
'display_id': '2024/05/31/sport/video/jadon-sancho-borussia-dortmund-champions-league-exclusive-spt-intl',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Nadal wins 8th French Open title',
|
'upload_date': '20240531',
|
||||||
'description': 'World Sport\'s Amanda Davies chats with 2013 French Open champion Rafael Nadal.',
|
'description': 'md5:844bcdb0629e1877a7a466c913f4c19c',
|
||||||
'duration': 135,
|
'thumbnail': 'https://media.cnn.com/api/v1/images/stellar/prod/gettyimages-2151936122.jpg?c=original',
|
||||||
'upload_date': '20130609',
|
'duration': 373.0,
|
||||||
|
'timestamp': 1717148586,
|
||||||
|
'title': 'Borussia Dortmund star Jadon Sancho seeks Wembley redemption after 2020 Euros hurt',
|
||||||
|
'modified_date': '20240531',
|
||||||
|
'modified_timestamp': 1717150140,
|
||||||
},
|
},
|
||||||
'expected_warnings': ['Failed to download m3u8 information'],
|
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://edition.cnn.com/video/?/video/us/2013/08/21/sot-student-gives-epic-speech.georgia-institute-of-technology&utm_source=feedburner&utm_medium=feed&utm_campaign=Feed%3A+rss%2Fcnn_topstories+%28RSS%3A+Top+Stories%29',
|
'url': 'https://edition.cnn.com/2024/06/11/politics/video/inmates-vote-jail-nevada-murray-dnt-ac360-digvid',
|
||||||
'md5': 'b5cc60c60a3477d185af8f19a2a26f4e',
|
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'us/2013/08/21/sot-student-gives-epic-speech.georgia-institute-of-technology',
|
'id': 'me522945c4709b299e5cb8657900a7a21ad3b559f9',
|
||||||
|
'display_id': '2024/06/11/politics/video/inmates-vote-jail-nevada-murray-dnt-ac360-digvid',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': "Student's epic speech stuns new freshmen",
|
'description': 'md5:e0120fe5da9ad8259fd707c1cbb64a60',
|
||||||
'description': 'A Georgia Tech student welcomes the incoming freshmen with an epic speech backed by music from "2001: A Space Odyssey."',
|
'title': 'Here’s how some inmates in closely divided state are now able to vote from jail',
|
||||||
'upload_date': '20130821',
|
'timestamp': 1718158269,
|
||||||
|
'upload_date': '20240612',
|
||||||
|
'thumbnail': 'https://media.cnn.com/api/v1/images/stellar/prod/still-20701554-13565-571-still.jpg?c=original',
|
||||||
|
'duration': 202.0,
|
||||||
|
'modified_date': '20240612',
|
||||||
|
'modified_timestamp': 1718158509,
|
||||||
},
|
},
|
||||||
'expected_warnings': ['Failed to download m3u8 information'],
|
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://www.cnn.com/video/data/2.0/video/living/2014/12/22/growing-america-nashville-salemtown-board-episode-1.hln.html',
|
'url': 'https://edition.cnn.com/2024/06/11/style/king-charles-portrait-vandalized/index.html',
|
||||||
'md5': 'f14d02ebd264df951feb2400e2c25a1b',
|
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'living/2014/12/22/growing-america-nashville-salemtown-board-episode-1.hln',
|
'id': 'mef5f52b9e1fe28b1ad192afcbc9206ae984894b68',
|
||||||
|
'display_id': '2024/06/11/style/king-charles-portrait-vandalized',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Nashville Ep. 1: Hand crafted skateboards',
|
'thumbnail': 'https://media.cnn.com/api/v1/images/stellar/prod/still-20701257-8846-816-still.jpg?c=original',
|
||||||
'description': 'md5:e7223a503315c9f150acac52e76de086',
|
'description': 'md5:19f78338ccec533db0fa8a4511012dae',
|
||||||
'upload_date': '20141222',
|
'title': 'Video shows King Charles\' portrait being vandalized by activists',
|
||||||
|
'timestamp': 1718113852,
|
||||||
|
'upload_date': '20240611',
|
||||||
|
'duration': 51.0,
|
||||||
|
'modified_timestamp': 1718116193,
|
||||||
|
'modified_date': '20240611',
|
||||||
},
|
},
|
||||||
'expected_warnings': ['Failed to download m3u8 information'],
|
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://money.cnn.com/video/news/2016/08/19/netflix-stunning-stats.cnnmoney/index.html',
|
'url': 'https://edition.cnn.com/videos/media/2022/12/05/robin-meade-final-sign-off-broadcast-hln-mxp-contd-vpx.hln',
|
||||||
'md5': '52a515dc1b0f001cd82e4ceda32be9d1',
|
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '/video/news/2016/08/19/netflix-stunning-stats.cnnmoney',
|
'id': 'mefba13799201b084ea3b1d0f7ca820ae94d4bb5b2',
|
||||||
|
'display_id': 'videos/media/2022/12/05/robin-meade-final-sign-off-broadcast-hln-mxp-contd-vpx.hln',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': '5 stunning stats about Netflix',
|
'thumbnail': 'https://media.cnn.com/api/v1/images/stellar/prod/221205163510-robin-meade-sign-off.jpg?c=original',
|
||||||
'description': 'Did you know that Netflix has more than 80 million members? Here are five facts about the online video distributor that you probably didn\'t know.',
|
'duration': 158.0,
|
||||||
'upload_date': '20160819',
|
'title': 'Robin Meade signs off after HLN\'s last broadcast',
|
||||||
|
'description': 'md5:cff3c62d18d2fbc6c5c75cb029b7353b',
|
||||||
|
'upload_date': '20221205',
|
||||||
|
'timestamp': 1670284296,
|
||||||
|
'modified_timestamp': 1670332404,
|
||||||
|
'modified_date': '20221206',
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {'format': 'direct'},
|
||||||
# m3u8 download
|
}, {
|
||||||
'skip_download': True,
|
'url': 'https://cnnespanol.cnn.com/video/ataque-misil-israel-beirut-libano-octubre-trax',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'me484a43722642aa00627b812fe928f2e99c6e2997',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'display_id': 'video/ataque-misil-israel-beirut-libano-octubre-trax',
|
||||||
|
'timestamp': 1729501452,
|
||||||
|
'thumbnail': 'https://media.cnn.com/api/v1/images/stellar/prod/ataqeubeirut-1.jpg?c=original',
|
||||||
|
'description': 'md5:256ee7137d161f776cda429654135e52',
|
||||||
|
'upload_date': '20241021',
|
||||||
|
'duration': 31.0,
|
||||||
|
'title': 'VIDEO | Israel lanza un nuevo ataque sobre Beirut',
|
||||||
|
'modified_date': '20241021',
|
||||||
|
'modified_timestamp': 1729501530,
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://cnn.com/video/?/video/politics/2015/03/27/pkg-arizona-senator-church-attendance-mandatory.ktvk',
|
'url': 'https://edition.cnn.com/2024/10/16/politics/kamala-harris-fox-news-interview/index.html',
|
||||||
'only_matching': True,
|
'info_dict': {
|
||||||
}, {
|
'id': '2024/10/16/politics/kamala-harris-fox-news-interview',
|
||||||
'url': 'http://cnn.com/video/?/video/us/2015/04/06/dnt-baker-refuses-anti-gay-order.wkmg',
|
},
|
||||||
'only_matching': True,
|
'playlist_count': 2,
|
||||||
}, {
|
'playlist': [{
|
||||||
'url': 'http://edition.cnn.com/videos/arts/2016/04/21/olympic-games-cultural-a-z-brazil.cnn',
|
'md5': '073ffab87b8bef97c9913e71cc18ef9e',
|
||||||
'only_matching': True,
|
'info_dict': {
|
||||||
|
'id': 'me19d548fdd54df0924087039283128ef473ab397d',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': '\'I\'m not finished\': Harris interview with Fox News gets heated',
|
||||||
|
'display_id': 'kamala-harris-fox-news-interview-ebof-digvid',
|
||||||
|
'description': 'md5:e7dd3d1a04df916062230b60ca419a0a',
|
||||||
|
'thumbnail': 'https://media.cnn.com/api/v1/images/stellar/prod/harris-20241016234916617.jpg?c=original',
|
||||||
|
'duration': 173.0,
|
||||||
|
'timestamp': 1729122182,
|
||||||
|
'upload_date': '20241016',
|
||||||
|
'modified_timestamp': 1729194706,
|
||||||
|
'modified_date': '20241017',
|
||||||
|
},
|
||||||
|
'params': {'format': 'direct'},
|
||||||
|
}, {
|
||||||
|
'md5': '11604ab4af83b650826753f1ccb8ecff',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'med04507d8ca3da827001f63d22af321ec29c7d97b',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': '\'Wise\': Buttigieg on Harris\' handling of interview question about gender transition surgery',
|
||||||
|
'display_id': 'pete-buttigieg-harris-fox-newssrc-digvid',
|
||||||
|
'description': 'md5:602a8a7e853ed5e574acd3159428c98e',
|
||||||
|
'thumbnail': 'https://media.cnn.com/api/v1/images/stellar/prod/buttigieg-20241017040412074.jpg?c=original',
|
||||||
|
'duration': 145.0,
|
||||||
|
'timestamp': 1729137765,
|
||||||
|
'upload_date': '20241017',
|
||||||
|
'modified_timestamp': 1729138184,
|
||||||
|
'modified_date': '20241017',
|
||||||
|
},
|
||||||
|
'params': {'format': 'direct'},
|
||||||
|
}],
|
||||||
}]
|
}]
|
||||||
|
|
||||||
_CONFIG = {
|
|
||||||
# http://edition.cnn.com/.element/apps/cvp/3.0/cfg/spider/cnn/expansion/config.xml
|
|
||||||
'edition': {
|
|
||||||
'data_src': 'http://edition.cnn.com/video/data/3.0/video/%s/index.xml',
|
|
||||||
'media_src': 'http://pmd.cdn.turner.com/cnn/big',
|
|
||||||
},
|
|
||||||
# http://money.cnn.com/.element/apps/cvp2/cfg/config.xml
|
|
||||||
'money': {
|
|
||||||
'data_src': 'http://money.cnn.com/video/data/4.0/video/%s.xml',
|
|
||||||
'media_src': 'http://ht3.cdn.turner.com/money/big',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
def _extract_timestamp(self, video_data):
|
|
||||||
# TODO: fix timestamp extraction
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
sub_domain, path, page_title = self._match_valid_url(url).groups()
|
display_id = self._match_valid_url(url).group('display_id')
|
||||||
if sub_domain not in ('money', 'edition'):
|
webpage = self._download_webpage(url, display_id)
|
||||||
sub_domain = 'edition'
|
app_id = traverse_obj(
|
||||||
config = self._CONFIG[sub_domain]
|
self._search_json(r'window\.env\s*=', webpage, 'window env', display_id, default={}),
|
||||||
return self._extract_cvp_info(
|
('TOP_AUTH_SERVICE_APP_ID', {str}))
|
||||||
config['data_src'] % path, page_title, {
|
|
||||||
'default': {
|
entries = []
|
||||||
'media_src': config['media_src'],
|
for player_data in traverse_obj(webpage, (
|
||||||
},
|
{find_elements(tag='div', attr='data-component-name', value='video-player', html=True)},
|
||||||
'f4m': {
|
..., {extract_attributes}, all, lambda _, v: v['data-media-id'])):
|
||||||
'host': 'cnn-vh.akamaihd.net',
|
media_id = player_data['data-media-id']
|
||||||
},
|
parent_uri = player_data.get('data-video-resource-parent-uri')
|
||||||
|
formats, subtitles = [], {}
|
||||||
|
|
||||||
|
video_data = {}
|
||||||
|
if parent_uri:
|
||||||
|
video_data = self._download_json(
|
||||||
|
'https://fave.api.cnn.io/v1/video', media_id, fatal=False,
|
||||||
|
query={
|
||||||
|
'id': media_id,
|
||||||
|
'stellarUri': parent_uri,
|
||||||
|
})
|
||||||
|
for direct_url in traverse_obj(video_data, ('files', ..., 'fileUri', {url_or_none})):
|
||||||
|
resolution, bitrate = None, None
|
||||||
|
if mobj := re.search(r'-(?P<res>\d+x\d+)_(?P<tbr>\d+)k\.mp4', direct_url):
|
||||||
|
resolution, bitrate = mobj.group('res', 'tbr')
|
||||||
|
formats.append({
|
||||||
|
'url': direct_url,
|
||||||
|
'format_id': 'direct',
|
||||||
|
'quality': 1,
|
||||||
|
'tbr': int_or_none(bitrate),
|
||||||
|
**parse_resolution(resolution),
|
||||||
|
})
|
||||||
|
for sub_data in traverse_obj(video_data, (
|
||||||
|
'closedCaptions', 'types', lambda _, v: url_or_none(v['track']['url']), 'track')):
|
||||||
|
subtitles.setdefault(sub_data.get('lang') or 'en', []).append({
|
||||||
|
'url': sub_data['url'],
|
||||||
|
'name': sub_data.get('label'),
|
||||||
|
})
|
||||||
|
|
||||||
|
if app_id:
|
||||||
|
media_data = self._download_json(
|
||||||
|
f'https://medium.ngtv.io/v2/media/{media_id}/desktop', media_id, fatal=False,
|
||||||
|
query={'appId': app_id})
|
||||||
|
m3u8_url = traverse_obj(media_data, (
|
||||||
|
'media', 'desktop', 'unprotected', 'unencrypted', 'url', {url_or_none}))
|
||||||
|
if m3u8_url:
|
||||||
|
fmts, subs = self._extract_m3u8_formats_and_subtitles(
|
||||||
|
m3u8_url, media_id, 'mp4', m3u8_id='hls', fatal=False)
|
||||||
|
formats.extend(fmts)
|
||||||
|
self._merge_subtitles(subs, target=subtitles)
|
||||||
|
|
||||||
|
entries.append({
|
||||||
|
**traverse_obj(player_data, {
|
||||||
|
'title': ('data-headline', {clean_html}),
|
||||||
|
'description': ('data-description', {clean_html}),
|
||||||
|
'duration': ('data-duration', {parse_duration}),
|
||||||
|
'timestamp': ('data-publish-date', {parse_iso8601}),
|
||||||
|
'thumbnail': (
|
||||||
|
'data-poster-image-override', {json.loads}, 'big', 'uri', {url_or_none},
|
||||||
|
{update_url(query='c=original')}),
|
||||||
|
'display_id': 'data-video-slug',
|
||||||
|
}),
|
||||||
|
**traverse_obj(video_data, {
|
||||||
|
'timestamp': ('dateCreated', 'uts', {int_or_none(scale=1000)}),
|
||||||
|
'description': ('description', {clean_html}),
|
||||||
|
'title': ('headline', {str}),
|
||||||
|
'modified_timestamp': ('lastModified', 'uts', {int_or_none(scale=1000)}),
|
||||||
|
'duration': ('trt', {int_or_none}),
|
||||||
|
}),
|
||||||
|
'id': media_id,
|
||||||
|
'formats': formats,
|
||||||
|
'subtitles': subtitles,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if len(entries) == 1:
|
||||||
|
return {
|
||||||
|
**entries[0],
|
||||||
|
'display_id': display_id,
|
||||||
|
}
|
||||||
|
|
||||||
class CNNBlogsIE(InfoExtractor):
|
return self.playlist_result(entries, display_id)
|
||||||
_VALID_URL = r'https?://[^\.]+\.blogs\.cnn\.com/.+'
|
|
||||||
_TEST = {
|
|
||||||
'url': 'http://reliablesources.blogs.cnn.com/2014/02/09/criminalizing-journalism/',
|
|
||||||
'md5': '3e56f97b0b6ffb4b79f4ea0749551084',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'bestoftv/2014/02/09/criminalizing-journalism.cnn',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Criminalizing journalism?',
|
|
||||||
'description': 'Glenn Greenwald responds to comments made this week on Capitol Hill that journalists could be criminal accessories.',
|
|
||||||
'upload_date': '20140209',
|
|
||||||
},
|
|
||||||
'expected_warnings': ['Failed to download m3u8 information'],
|
|
||||||
'add_ie': ['CNN'],
|
|
||||||
}
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
webpage = self._download_webpage(url, url_basename(url))
|
|
||||||
cnn_url = self._html_search_regex(r'data-url="(.+?)"', webpage, 'cnn url')
|
|
||||||
return self.url_result(cnn_url, CNNIE.ie_key())
|
|
||||||
|
|
||||||
|
|
||||||
class CNNArticleIE(InfoExtractor):
|
|
||||||
_VALID_URL = r'https?://(?:(?:edition|www)\.)?cnn\.com/(?!videos?/)'
|
|
||||||
_TEST = {
|
|
||||||
'url': 'http://www.cnn.com/2014/12/21/politics/obama-north-koreas-hack-not-war-but-cyber-vandalism/',
|
|
||||||
'md5': '689034c2a3d9c6dc4aa72d65a81efd01',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'bestoftv/2014/12/21/ip-north-korea-obama.cnn',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Obama: Cyberattack not an act of war',
|
|
||||||
'description': 'md5:0a802a40d2376f60e6b04c8d5bcebc4b',
|
|
||||||
'upload_date': '20141221',
|
|
||||||
},
|
|
||||||
'expected_warnings': ['Failed to download m3u8 information'],
|
|
||||||
'add_ie': ['CNN'],
|
|
||||||
}
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
webpage = self._download_webpage(url, url_basename(url))
|
|
||||||
cnn_url = self._html_search_regex(r"video:\s*'([^']+)'", webpage, 'cnn url')
|
|
||||||
return self.url_result('http://cnn.com/video/?/video/' + cnn_url, CNNIE.ie_key())
|
|
||||||
|
|
||||||
|
|
||||||
class CNNIndonesiaIE(InfoExtractor):
|
class CNNIndonesiaIE(InfoExtractor):
|
||||||
|
|
|
@ -35,6 +35,7 @@ from ..networking import HEADRequest, Request
|
||||||
from ..networking.exceptions import (
|
from ..networking.exceptions import (
|
||||||
HTTPError,
|
HTTPError,
|
||||||
IncompleteRead,
|
IncompleteRead,
|
||||||
|
TransportError,
|
||||||
network_exceptions,
|
network_exceptions,
|
||||||
)
|
)
|
||||||
from ..networking.impersonate import ImpersonateTarget
|
from ..networking.impersonate import ImpersonateTarget
|
||||||
|
@ -46,6 +47,7 @@ from ..utils import (
|
||||||
FormatSorter,
|
FormatSorter,
|
||||||
GeoRestrictedError,
|
GeoRestrictedError,
|
||||||
GeoUtils,
|
GeoUtils,
|
||||||
|
ISO639Utils,
|
||||||
LenientJSONDecoder,
|
LenientJSONDecoder,
|
||||||
Popen,
|
Popen,
|
||||||
RegexNotFoundError,
|
RegexNotFoundError,
|
||||||
|
@ -234,7 +236,14 @@ class InfoExtractor:
|
||||||
'maybe' if the format may have DRM and has to be tested before download.
|
'maybe' if the format may have DRM and has to be tested before download.
|
||||||
* extra_param_to_segment_url A query string to append to each
|
* extra_param_to_segment_url A query string to append to each
|
||||||
fragment's URL, or to update each existing query string
|
fragment's URL, or to update each existing query string
|
||||||
with. Only applied by the native HLS/DASH downloaders.
|
with. If it is an HLS stream with an AES-128 decryption key,
|
||||||
|
the query paramaters will be passed to the key URI as well,
|
||||||
|
unless there is an `extra_param_to_key_url` given,
|
||||||
|
or unless an external key URI is provided via `hls_aes`.
|
||||||
|
Only applied by the native HLS/DASH downloaders.
|
||||||
|
* extra_param_to_key_url A query string to append to the URL
|
||||||
|
of the format's HLS AES-128 decryption key.
|
||||||
|
Only applied by the native HLS downloader.
|
||||||
* hls_aes A dictionary of HLS AES-128 decryption information
|
* hls_aes A dictionary of HLS AES-128 decryption information
|
||||||
used by the native HLS downloader to override the
|
used by the native HLS downloader to override the
|
||||||
values in the media playlist when an '#EXT-X-KEY' tag
|
values in the media playlist when an '#EXT-X-KEY' tag
|
||||||
|
@ -325,7 +334,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):
|
||||||
|
@ -512,7 +521,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
|
||||||
|
@ -565,13 +574,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):
|
||||||
|
@ -958,6 +967,9 @@ class InfoExtractor:
|
||||||
return False
|
return False
|
||||||
content = self._webpage_read_content(urlh, url_or_request, video_id, note, errnote, fatal,
|
content = self._webpage_read_content(urlh, url_or_request, video_id, note, errnote, fatal,
|
||||||
encoding=encoding, data=data)
|
encoding=encoding, data=data)
|
||||||
|
if content is False:
|
||||||
|
assert not fatal
|
||||||
|
return False
|
||||||
return (content, urlh)
|
return (content, urlh)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -1032,7 +1044,15 @@ class InfoExtractor:
|
||||||
|
|
||||||
def _webpage_read_content(self, urlh, url_or_request, video_id, note=None, errnote=None, fatal=True,
|
def _webpage_read_content(self, urlh, url_or_request, video_id, note=None, errnote=None, fatal=True,
|
||||||
prefix=None, encoding=None, data=None):
|
prefix=None, encoding=None, data=None):
|
||||||
webpage_bytes = urlh.read()
|
try:
|
||||||
|
webpage_bytes = urlh.read()
|
||||||
|
except TransportError as err:
|
||||||
|
errmsg = f'{video_id}: Error reading response: {err.msg}'
|
||||||
|
if fatal:
|
||||||
|
raise ExtractorError(errmsg, cause=err)
|
||||||
|
self.report_warning(errmsg)
|
||||||
|
return False
|
||||||
|
|
||||||
if prefix is not None:
|
if prefix is not None:
|
||||||
webpage_bytes = prefix + webpage_bytes
|
webpage_bytes = prefix + webpage_bytes
|
||||||
if self.get_param('dump_intermediate_pages', False):
|
if self.get_param('dump_intermediate_pages', False):
|
||||||
|
@ -1389,6 +1409,13 @@ class InfoExtractor:
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
self.write_debug(f'Using netrc for {netrc_machine} authentication')
|
self.write_debug(f'Using netrc for {netrc_machine} authentication')
|
||||||
|
|
||||||
|
# compat: <=py3.10: netrc cannot parse tokens as empty strings, will return `""` instead
|
||||||
|
# Ref: https://github.com/yt-dlp/yt-dlp/issues/11413
|
||||||
|
# https://github.com/python/cpython/commit/15409c720be0503131713e3d3abc1acd0da07378
|
||||||
|
if sys.version_info < (3, 11):
|
||||||
|
return tuple(x if x != '""' else '' for x in info[::2])
|
||||||
|
|
||||||
return info[0], info[2]
|
return info[0], info[2]
|
||||||
|
|
||||||
def _get_login_info(self, username_option='username', password_option='password', netrc_machine=None):
|
def _get_login_info(self, username_option='username', password_option='password', netrc_machine=None):
|
||||||
|
@ -1551,7 +1578,9 @@ class InfoExtractor:
|
||||||
if default is not NO_DEFAULT:
|
if default is not NO_DEFAULT:
|
||||||
fatal = False
|
fatal = False
|
||||||
for mobj in re.finditer(JSON_LD_RE, html):
|
for mobj in re.finditer(JSON_LD_RE, html):
|
||||||
json_ld_item = self._parse_json(mobj.group('json_ld'), video_id, fatal=fatal)
|
json_ld_item = self._parse_json(
|
||||||
|
mobj.group('json_ld'), video_id, fatal=fatal,
|
||||||
|
errnote=False if default is not NO_DEFAULT else None)
|
||||||
for json_ld in variadic(json_ld_item):
|
for json_ld in variadic(json_ld_item):
|
||||||
if isinstance(json_ld, dict):
|
if isinstance(json_ld, dict):
|
||||||
yield json_ld
|
yield json_ld
|
||||||
|
@ -1691,7 +1720,7 @@ class InfoExtractor:
|
||||||
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,
|
||||||
|
@ -2058,7 +2087,7 @@ class InfoExtractor:
|
||||||
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):
|
||||||
|
@ -2215,6 +2244,11 @@ class InfoExtractor:
|
||||||
'quality': quality,
|
'quality': quality,
|
||||||
'has_drm': has_drm,
|
'has_drm': has_drm,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# YouTube-specific
|
||||||
|
if yt_audio_content_id := last_stream_inf.get('YT-EXT-AUDIO-CONTENT-ID'):
|
||||||
|
f['language'] = yt_audio_content_id.split('.')[0]
|
||||||
|
|
||||||
resolution = last_stream_inf.get('RESOLUTION')
|
resolution = last_stream_inf.get('RESOLUTION')
|
||||||
if resolution:
|
if resolution:
|
||||||
mobj = re.search(r'(?P<width>\d+)[xX](?P<height>\d+)', resolution)
|
mobj = re.search(r'(?P<width>\d+)[xX](?P<height>\d+)', resolution)
|
||||||
|
@ -2788,11 +2822,11 @@ class InfoExtractor:
|
||||||
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
|
||||||
|
@ -2882,7 +2916,7 @@ class InfoExtractor:
|
||||||
}
|
}
|
||||||
|
|
||||||
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:
|
||||||
|
|
||||||
|
@ -3047,7 +3081,11 @@ class InfoExtractor:
|
||||||
url_pattern = stream.attrib['Url']
|
url_pattern = stream.attrib['Url']
|
||||||
stream_timescale = int_or_none(stream.get('TimeScale')) or timescale
|
stream_timescale = int_or_none(stream.get('TimeScale')) or timescale
|
||||||
stream_name = stream.get('Name')
|
stream_name = stream.get('Name')
|
||||||
stream_language = stream.get('Language', 'und')
|
# IsmFD expects ISO 639 Set 2 language codes (3-character length)
|
||||||
|
# See: https://github.com/yt-dlp/yt-dlp/issues/11356
|
||||||
|
stream_language = stream.get('Language') or 'und'
|
||||||
|
if len(stream_language) != 3:
|
||||||
|
stream_language = ISO639Utils.short2long(stream_language) or 'und'
|
||||||
for track in stream.findall('QualityLevel'):
|
for track in stream.findall('QualityLevel'):
|
||||||
KNOWN_TAGS = {'255': 'AACL', '65534': 'EC-3'}
|
KNOWN_TAGS = {'255': 'AACL', '65534': 'EC-3'}
|
||||||
fourcc = track.get('FourCC') or KNOWN_TAGS.get(track.get('AudioTag'))
|
fourcc = track.get('FourCC') or KNOWN_TAGS.get(track.get('AudioTag'))
|
||||||
|
@ -3138,7 +3176,7 @@ class InfoExtractor:
|
||||||
})
|
})
|
||||||
return formats, subtitles
|
return formats, subtitles
|
||||||
|
|
||||||
def _parse_html5_media_entries(self, base_url, webpage, video_id, m3u8_id=None, m3u8_entry_protocol='m3u8_native', mpd_id=None, preference=None, quality=None):
|
def _parse_html5_media_entries(self, base_url, webpage, video_id, m3u8_id=None, m3u8_entry_protocol='m3u8_native', mpd_id=None, preference=None, quality=None, _headers=None):
|
||||||
def absolute_url(item_url):
|
def absolute_url(item_url):
|
||||||
return urljoin(base_url, item_url)
|
return urljoin(base_url, item_url)
|
||||||
|
|
||||||
|
@ -3162,11 +3200,11 @@ class InfoExtractor:
|
||||||
formats = self._extract_m3u8_formats(
|
formats = self._extract_m3u8_formats(
|
||||||
full_url, video_id, ext='mp4',
|
full_url, video_id, ext='mp4',
|
||||||
entry_protocol=m3u8_entry_protocol, m3u8_id=m3u8_id,
|
entry_protocol=m3u8_entry_protocol, m3u8_id=m3u8_id,
|
||||||
preference=preference, quality=quality, fatal=False)
|
preference=preference, quality=quality, fatal=False, headers=_headers)
|
||||||
elif ext == 'mpd':
|
elif ext == 'mpd':
|
||||||
is_plain_url = False
|
is_plain_url = False
|
||||||
formats = self._extract_mpd_formats(
|
formats = self._extract_mpd_formats(
|
||||||
full_url, video_id, mpd_id=mpd_id, fatal=False)
|
full_url, video_id, mpd_id=mpd_id, fatal=False, headers=_headers)
|
||||||
else:
|
else:
|
||||||
is_plain_url = True
|
is_plain_url = True
|
||||||
formats = [{
|
formats = [{
|
||||||
|
@ -3260,6 +3298,8 @@ class InfoExtractor:
|
||||||
})
|
})
|
||||||
for f in media_info['formats']:
|
for f in media_info['formats']:
|
||||||
f.setdefault('http_headers', {})['Referer'] = base_url
|
f.setdefault('http_headers', {})['Referer'] = base_url
|
||||||
|
if _headers:
|
||||||
|
f['http_headers'].update(_headers)
|
||||||
if media_info['formats'] or media_info['subtitles']:
|
if media_info['formats'] or media_info['subtitles']:
|
||||||
entries.append(media_info)
|
entries.append(media_info)
|
||||||
return entries
|
return entries
|
||||||
|
@ -3475,7 +3515,7 @@ class InfoExtractor:
|
||||||
continue
|
continue
|
||||||
urls.add(source_url)
|
urls.add(source_url)
|
||||||
source_type = source.get('type') or ''
|
source_type = source.get('type') or ''
|
||||||
ext = mimetype2ext(source_type) or determine_ext(source_url)
|
ext = determine_ext(source_url, default_ext=mimetype2ext(source_type))
|
||||||
if source_type == 'hls' or ext == 'm3u8' or 'format=m3u8-aapl' in source_url:
|
if source_type == 'hls' or ext == 'm3u8' or 'format=m3u8-aapl' in source_url:
|
||||||
formats.extend(self._extract_m3u8_formats(
|
formats.extend(self._extract_m3u8_formats(
|
||||||
source_url, video_id, 'mp4', entry_protocol='m3u8_native',
|
source_url, video_id, 'mp4', entry_protocol='m3u8_native',
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue