mirror of
https://github.com/yt-dlp/yt-dlp.git
synced 2026-01-12 01:41:26 +00:00
Compare commits
50 Commits
2023.02.17
...
2023.03.04
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8729e7b57c | ||
|
|
392389b7df | ||
|
|
eb8fd6d044 | ||
|
|
f44cb4e77b | ||
|
|
46580ced56 | ||
|
|
b404712822 | ||
|
|
1f8489cccb | ||
|
|
ed4cc4ea79 | ||
|
|
cf60522652 | ||
|
|
45db357289 | ||
|
|
8a83baaf21 | ||
|
|
7accdd9845 | ||
|
|
283a0b5bc5 | ||
|
|
22ccd5420b | ||
|
|
08ff6d59f9 | ||
|
|
4a6272c6d1 | ||
|
|
640c934823 | ||
|
|
55676fe498 | ||
|
|
354d5fca7a | ||
|
|
9344964281 | ||
|
|
bfc861a91e | ||
|
|
fe2ce85aff | ||
|
|
d21056f4cf | ||
|
|
b2e0343ba0 | ||
|
|
4815bbfc41 | ||
|
|
776d1c3f0c | ||
|
|
12647e03d4 | ||
|
|
77df20f14c | ||
|
|
29cb20bd56 | ||
|
|
d400e261cf | ||
|
|
9acf1ee25f | ||
|
|
40d77d8902 | ||
|
|
2d5a8c5db2 | ||
|
|
77d6d13646 | ||
|
|
9fddc12ab0 | ||
|
|
b38cae49e6 | ||
|
|
7f51861b18 | ||
|
|
5b28cef72d | ||
|
|
31e183557f | ||
|
|
f34804b2f9 | ||
|
|
65f6e80780 | ||
|
|
b059188383 | ||
|
|
5038f6d713 | ||
|
|
4d248e29d2 | ||
|
|
8e9fe43cd3 | ||
|
|
43a3eaf963 | ||
|
|
cc09083636 | ||
|
|
da8e2912b1 | ||
|
|
18d295c9e0 | ||
|
|
17ca19ab60 |
14
.github/ISSUE_TEMPLATE/1_broken_site.yml
vendored
14
.github/ISSUE_TEMPLATE/1_broken_site.yml
vendored
@@ -1,5 +1,5 @@
|
||||
name: Broken site
|
||||
description: Report broken or misfunctioning site
|
||||
description: Report error in a supported site
|
||||
labels: [triage, site-bug]
|
||||
body:
|
||||
- type: checkboxes
|
||||
@@ -16,9 +16,9 @@ body:
|
||||
description: |
|
||||
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
|
||||
options:
|
||||
- label: I'm reporting a broken site
|
||||
- label: I'm reporting that a **supported** site is broken
|
||||
required: true
|
||||
- label: I've verified that I'm running yt-dlp version **2023.02.17** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
|
||||
- label: I've verified that I'm running yt-dlp version **2023.03.04** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
|
||||
required: true
|
||||
- label: I've checked that all provided URLs are playable in a browser with the same IP and same login details
|
||||
required: true
|
||||
@@ -50,6 +50,8 @@ body:
|
||||
options:
|
||||
- label: Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
|
||||
required: true
|
||||
- label: "If using API, add `'verbose': True` to `YoutubeDL` params instead"
|
||||
required: false
|
||||
- label: Copy the WHOLE output (starting with `[debug] Command-line config`) and insert it below
|
||||
required: true
|
||||
- type: textarea
|
||||
@@ -62,7 +64,7 @@ body:
|
||||
[debug] Command-line config: ['-vU', 'test:youtube']
|
||||
[debug] Portable config "yt-dlp.conf": ['-i']
|
||||
[debug] Encodings: locale cp65001, fs utf-8, pref cp65001, out utf-8, error utf-8, screen utf-8
|
||||
[debug] yt-dlp version 2023.02.17 [9d339c4] (win32_exe)
|
||||
[debug] yt-dlp version 2023.03.04 [9d339c4] (win32_exe)
|
||||
[debug] Python 3.8.10 (CPython 64bit) - Windows-10-10.0.22000-SP0
|
||||
[debug] Checking exe version: ffmpeg -bsfs
|
||||
[debug] Checking exe version: ffprobe -bsfs
|
||||
@@ -70,8 +72,8 @@ body:
|
||||
[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] Proxy map: {}
|
||||
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
|
||||
Latest version: 2023.02.17, Current version: 2023.02.17
|
||||
yt-dlp is up to date (2023.02.17)
|
||||
Latest version: 2023.03.04, Current version: 2023.03.04
|
||||
yt-dlp is up to date (2023.03.04)
|
||||
<more lines>
|
||||
render: shell
|
||||
validations:
|
||||
|
||||
@@ -18,7 +18,7 @@ body:
|
||||
options:
|
||||
- label: I'm reporting a new site support request
|
||||
required: true
|
||||
- label: I've verified that I'm running yt-dlp version **2023.02.17** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
|
||||
- label: I've verified that I'm running yt-dlp version **2023.03.04** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
|
||||
required: true
|
||||
- label: I've checked that all provided URLs are playable in a browser with the same IP and same login details
|
||||
required: true
|
||||
@@ -62,6 +62,8 @@ body:
|
||||
options:
|
||||
- label: Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
|
||||
required: true
|
||||
- label: "If using API, add `'verbose': True` to `YoutubeDL` params instead"
|
||||
required: false
|
||||
- label: Copy the WHOLE output (starting with `[debug] Command-line config`) and insert it below
|
||||
required: true
|
||||
- type: textarea
|
||||
@@ -74,7 +76,7 @@ body:
|
||||
[debug] Command-line config: ['-vU', 'test:youtube']
|
||||
[debug] Portable config "yt-dlp.conf": ['-i']
|
||||
[debug] Encodings: locale cp65001, fs utf-8, pref cp65001, out utf-8, error utf-8, screen utf-8
|
||||
[debug] yt-dlp version 2023.02.17 [9d339c4] (win32_exe)
|
||||
[debug] yt-dlp version 2023.03.04 [9d339c4] (win32_exe)
|
||||
[debug] Python 3.8.10 (CPython 64bit) - Windows-10-10.0.22000-SP0
|
||||
[debug] Checking exe version: ffmpeg -bsfs
|
||||
[debug] Checking exe version: ffprobe -bsfs
|
||||
@@ -82,8 +84,8 @@ body:
|
||||
[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] Proxy map: {}
|
||||
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
|
||||
Latest version: 2023.02.17, Current version: 2023.02.17
|
||||
yt-dlp is up to date (2023.02.17)
|
||||
Latest version: 2023.03.04, Current version: 2023.03.04
|
||||
yt-dlp is up to date (2023.03.04)
|
||||
<more lines>
|
||||
render: shell
|
||||
validations:
|
||||
|
||||
@@ -18,7 +18,7 @@ body:
|
||||
options:
|
||||
- label: I'm requesting a site-specific feature
|
||||
required: true
|
||||
- label: I've verified that I'm running yt-dlp version **2023.02.17** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
|
||||
- label: I've verified that I'm running yt-dlp version **2023.03.04** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
|
||||
required: true
|
||||
- label: I've checked that all provided URLs are playable in a browser with the same IP and same login details
|
||||
required: true
|
||||
@@ -58,6 +58,8 @@ body:
|
||||
options:
|
||||
- label: Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
|
||||
required: true
|
||||
- label: "If using API, add `'verbose': True` to `YoutubeDL` params instead"
|
||||
required: false
|
||||
- label: Copy the WHOLE output (starting with `[debug] Command-line config`) and insert it below
|
||||
required: true
|
||||
- type: textarea
|
||||
@@ -70,7 +72,7 @@ body:
|
||||
[debug] Command-line config: ['-vU', 'test:youtube']
|
||||
[debug] Portable config "yt-dlp.conf": ['-i']
|
||||
[debug] Encodings: locale cp65001, fs utf-8, pref cp65001, out utf-8, error utf-8, screen utf-8
|
||||
[debug] yt-dlp version 2023.02.17 [9d339c4] (win32_exe)
|
||||
[debug] yt-dlp version 2023.03.04 [9d339c4] (win32_exe)
|
||||
[debug] Python 3.8.10 (CPython 64bit) - Windows-10-10.0.22000-SP0
|
||||
[debug] Checking exe version: ffmpeg -bsfs
|
||||
[debug] Checking exe version: ffprobe -bsfs
|
||||
@@ -78,8 +80,8 @@ body:
|
||||
[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] Proxy map: {}
|
||||
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
|
||||
Latest version: 2023.02.17, Current version: 2023.02.17
|
||||
yt-dlp is up to date (2023.02.17)
|
||||
Latest version: 2023.03.04, Current version: 2023.03.04
|
||||
yt-dlp is up to date (2023.03.04)
|
||||
<more lines>
|
||||
render: shell
|
||||
validations:
|
||||
|
||||
10
.github/ISSUE_TEMPLATE/4_bug_report.yml
vendored
10
.github/ISSUE_TEMPLATE/4_bug_report.yml
vendored
@@ -18,7 +18,7 @@ body:
|
||||
options:
|
||||
- label: I'm reporting a bug unrelated to a specific site
|
||||
required: true
|
||||
- label: I've verified that I'm running yt-dlp version **2023.02.17** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
|
||||
- label: I've verified that I'm running yt-dlp version **2023.03.04** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
|
||||
required: true
|
||||
- label: I've checked that all provided URLs are playable in a browser with the same IP and same login details
|
||||
required: true
|
||||
@@ -43,6 +43,8 @@ body:
|
||||
options:
|
||||
- label: Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
|
||||
required: true
|
||||
- label: "If using API, add `'verbose': True` to `YoutubeDL` params instead"
|
||||
required: false
|
||||
- label: Copy the WHOLE output (starting with `[debug] Command-line config`) and insert it below
|
||||
required: true
|
||||
- type: textarea
|
||||
@@ -55,7 +57,7 @@ body:
|
||||
[debug] Command-line config: ['-vU', 'test:youtube']
|
||||
[debug] Portable config "yt-dlp.conf": ['-i']
|
||||
[debug] Encodings: locale cp65001, fs utf-8, pref cp65001, out utf-8, error utf-8, screen utf-8
|
||||
[debug] yt-dlp version 2023.02.17 [9d339c4] (win32_exe)
|
||||
[debug] yt-dlp version 2023.03.04 [9d339c4] (win32_exe)
|
||||
[debug] Python 3.8.10 (CPython 64bit) - Windows-10-10.0.22000-SP0
|
||||
[debug] Checking exe version: ffmpeg -bsfs
|
||||
[debug] Checking exe version: ffprobe -bsfs
|
||||
@@ -63,8 +65,8 @@ body:
|
||||
[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] Proxy map: {}
|
||||
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
|
||||
Latest version: 2023.02.17, Current version: 2023.02.17
|
||||
yt-dlp is up to date (2023.02.17)
|
||||
Latest version: 2023.03.04, Current version: 2023.03.04
|
||||
yt-dlp is up to date (2023.03.04)
|
||||
<more lines>
|
||||
render: shell
|
||||
validations:
|
||||
|
||||
10
.github/ISSUE_TEMPLATE/5_feature_request.yml
vendored
10
.github/ISSUE_TEMPLATE/5_feature_request.yml
vendored
@@ -20,7 +20,7 @@ body:
|
||||
required: true
|
||||
- label: I've looked through the [README](https://github.com/yt-dlp/yt-dlp#readme)
|
||||
required: true
|
||||
- label: I've verified that I'm running yt-dlp version **2023.02.17** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
|
||||
- label: I've verified that I'm running yt-dlp version **2023.03.04** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
|
||||
required: true
|
||||
- label: I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766) and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues **including closed ones**. DO NOT post duplicates
|
||||
required: true
|
||||
@@ -40,6 +40,8 @@ body:
|
||||
label: Provide verbose output that clearly demonstrates the problem
|
||||
options:
|
||||
- label: Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
|
||||
- label: "If using API, add `'verbose': True` to `YoutubeDL` params instead"
|
||||
required: false
|
||||
- label: Copy the WHOLE output (starting with `[debug] Command-line config`) and insert it below
|
||||
- type: textarea
|
||||
id: log
|
||||
@@ -51,7 +53,7 @@ body:
|
||||
[debug] Command-line config: ['-vU', 'test:youtube']
|
||||
[debug] Portable config "yt-dlp.conf": ['-i']
|
||||
[debug] Encodings: locale cp65001, fs utf-8, pref cp65001, out utf-8, error utf-8, screen utf-8
|
||||
[debug] yt-dlp version 2023.02.17 [9d339c4] (win32_exe)
|
||||
[debug] yt-dlp version 2023.03.04 [9d339c4] (win32_exe)
|
||||
[debug] Python 3.8.10 (CPython 64bit) - Windows-10-10.0.22000-SP0
|
||||
[debug] Checking exe version: ffmpeg -bsfs
|
||||
[debug] Checking exe version: ffprobe -bsfs
|
||||
@@ -59,7 +61,7 @@ body:
|
||||
[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] Proxy map: {}
|
||||
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
|
||||
Latest version: 2023.02.17, Current version: 2023.02.17
|
||||
yt-dlp is up to date (2023.02.17)
|
||||
Latest version: 2023.03.04, Current version: 2023.03.04
|
||||
yt-dlp is up to date (2023.03.04)
|
||||
<more lines>
|
||||
render: shell
|
||||
|
||||
10
.github/ISSUE_TEMPLATE/6_question.yml
vendored
10
.github/ISSUE_TEMPLATE/6_question.yml
vendored
@@ -26,7 +26,7 @@ body:
|
||||
required: true
|
||||
- label: I've looked through the [README](https://github.com/yt-dlp/yt-dlp#readme)
|
||||
required: true
|
||||
- label: I've verified that I'm running yt-dlp version **2023.02.17** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
|
||||
- label: I've verified that I'm running yt-dlp version **2023.03.04** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
|
||||
required: true
|
||||
- label: I've searched [known issues](https://github.com/yt-dlp/yt-dlp/issues/3766) and the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar questions **including closed ones**. DO NOT post duplicates
|
||||
required: true
|
||||
@@ -46,6 +46,8 @@ body:
|
||||
label: Provide verbose output that clearly demonstrates the problem
|
||||
options:
|
||||
- label: Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
|
||||
- label: "If using API, add `'verbose': True` to `YoutubeDL` params instead"
|
||||
required: false
|
||||
- label: Copy the WHOLE output (starting with `[debug] Command-line config`) and insert it below
|
||||
- type: textarea
|
||||
id: log
|
||||
@@ -57,7 +59,7 @@ body:
|
||||
[debug] Command-line config: ['-vU', 'test:youtube']
|
||||
[debug] Portable config "yt-dlp.conf": ['-i']
|
||||
[debug] Encodings: locale cp65001, fs utf-8, pref cp65001, out utf-8, error utf-8, screen utf-8
|
||||
[debug] yt-dlp version 2023.02.17 [9d339c4] (win32_exe)
|
||||
[debug] yt-dlp version 2023.03.04 [9d339c4] (win32_exe)
|
||||
[debug] Python 3.8.10 (CPython 64bit) - Windows-10-10.0.22000-SP0
|
||||
[debug] Checking exe version: ffmpeg -bsfs
|
||||
[debug] Checking exe version: ffprobe -bsfs
|
||||
@@ -65,7 +67,7 @@ body:
|
||||
[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] Proxy map: {}
|
||||
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest
|
||||
Latest version: 2023.02.17, Current version: 2023.02.17
|
||||
yt-dlp is up to date (2023.02.17)
|
||||
Latest version: 2023.03.04, Current version: 2023.03.04
|
||||
yt-dlp is up to date (2023.03.04)
|
||||
<more lines>
|
||||
render: shell
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: Broken site
|
||||
description: Report broken or misfunctioning site
|
||||
description: Report error in a supported site
|
||||
labels: [triage, site-bug]
|
||||
body:
|
||||
%(no_skip)s
|
||||
@@ -10,7 +10,7 @@ body:
|
||||
description: |
|
||||
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
|
||||
options:
|
||||
- label: I'm reporting a broken site
|
||||
- label: I'm reporting that a **supported** site is broken
|
||||
required: true
|
||||
- label: I've verified that I'm running yt-dlp version **%(version)s** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
|
||||
required: true
|
||||
|
||||
561
.github/workflows/build.yml
vendored
561
.github/workflows/build.yml
vendored
@@ -1,393 +1,356 @@
|
||||
name: Build
|
||||
on: workflow_dispatch
|
||||
name: Build Artifacts
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
required: true
|
||||
type: string
|
||||
channel:
|
||||
required: false
|
||||
default: stable
|
||||
type: string
|
||||
unix:
|
||||
default: true
|
||||
type: boolean
|
||||
linux_arm:
|
||||
default: true
|
||||
type: boolean
|
||||
macos:
|
||||
default: true
|
||||
type: boolean
|
||||
macos_legacy:
|
||||
default: true
|
||||
type: boolean
|
||||
windows:
|
||||
default: true
|
||||
type: boolean
|
||||
windows32:
|
||||
default: true
|
||||
type: boolean
|
||||
meta_files:
|
||||
default: true
|
||||
type: boolean
|
||||
secrets:
|
||||
GPG_SIGNING_KEY:
|
||||
required: false
|
||||
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: Version tag (YYYY.MM.DD[.REV])
|
||||
required: true
|
||||
type: string
|
||||
channel:
|
||||
description: Update channel (stable/nightly)
|
||||
required: true
|
||||
default: stable
|
||||
type: string
|
||||
unix:
|
||||
description: yt-dlp, yt-dlp.tar.gz, yt-dlp_linux, yt-dlp_linux.zip
|
||||
default: true
|
||||
type: boolean
|
||||
linux_arm:
|
||||
description: yt-dlp_linux_aarch64, yt-dlp_linux_armv7l
|
||||
default: true
|
||||
type: boolean
|
||||
macos:
|
||||
description: yt-dlp_macos, yt-dlp_macos.zip
|
||||
default: true
|
||||
type: boolean
|
||||
macos_legacy:
|
||||
description: yt-dlp_macos_legacy
|
||||
default: true
|
||||
type: boolean
|
||||
windows:
|
||||
description: yt-dlp.exe, yt-dlp_min.exe, yt-dlp_win.zip
|
||||
default: true
|
||||
type: boolean
|
||||
windows32:
|
||||
description: yt-dlp_x86.exe
|
||||
default: true
|
||||
type: boolean
|
||||
meta_files:
|
||||
description: SHA2-256SUMS, SHA2-512SUMS, _update_spec
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
permissions:
|
||||
contents: write # for push_release
|
||||
unix:
|
||||
if: inputs.unix
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version_suffix: ${{ steps.version_suffix.outputs.version_suffix }}
|
||||
ytdlp_version: ${{ steps.bump_version.outputs.ytdlp_version }}
|
||||
head_sha: ${{ steps.push_release.outputs.head_sha }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Set version suffix
|
||||
id: version_suffix
|
||||
env:
|
||||
PUSH_VERSION_COMMIT: ${{ secrets.PUSH_VERSION_COMMIT }}
|
||||
if: "env.PUSH_VERSION_COMMIT == ''"
|
||||
run: echo "version_suffix=$(date -u +"%H%M%S")" >> "$GITHUB_OUTPUT"
|
||||
- name: Bump version
|
||||
id: bump_version
|
||||
run: |
|
||||
python devscripts/update-version.py ${{ steps.version_suffix.outputs.version_suffix }}
|
||||
make issuetemplates
|
||||
|
||||
- name: Push to release
|
||||
id: push_release
|
||||
run: |
|
||||
git config --global user.name github-actions
|
||||
git config --global user.email github-actions@example.com
|
||||
git add -u
|
||||
git commit -m "[version] update" -m "Created by: ${{ github.event.sender.login }}" -m ":ci skip all :ci run dl"
|
||||
git push origin --force ${{ github.event.ref }}:release
|
||||
echo "head_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||||
- name: Update master
|
||||
env:
|
||||
PUSH_VERSION_COMMIT: ${{ secrets.PUSH_VERSION_COMMIT }}
|
||||
if: "env.PUSH_VERSION_COMMIT != ''"
|
||||
run: git push origin ${{ github.event.ref }}
|
||||
|
||||
|
||||
build_unix:
|
||||
needs: prepare
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- uses: conda-incubator/setup-miniconda@v2
|
||||
with:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.10"
|
||||
- uses: conda-incubator/setup-miniconda@v2
|
||||
with:
|
||||
miniforge-variant: Mambaforge
|
||||
use-mamba: true
|
||||
channels: conda-forge
|
||||
auto-update-conda: true
|
||||
activate-environment: ''
|
||||
activate-environment: ""
|
||||
auto-activate-base: false
|
||||
- name: Install Requirements
|
||||
run: |
|
||||
- name: Install Requirements
|
||||
run: |
|
||||
sudo apt-get -y install zip pandoc man sed
|
||||
python -m pip install -U pip setuptools wheel twine
|
||||
python -m pip install -U pip setuptools wheel
|
||||
python -m pip install -U Pyinstaller -r requirements.txt
|
||||
reqs=$(mktemp)
|
||||
echo -e 'python=3.10.*\npyinstaller' >$reqs
|
||||
sed 's/^brotli.*/brotli-python/' <requirements.txt >>$reqs
|
||||
cat > $reqs << EOF
|
||||
python=3.10.*
|
||||
pyinstaller
|
||||
cffi
|
||||
brotli-python
|
||||
EOF
|
||||
sed '/^brotli.*/d' requirements.txt >> $reqs
|
||||
mamba create -n build --file $reqs
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
python devscripts/update-version.py ${{ needs.prepare.outputs.version_suffix }}
|
||||
- name: Prepare
|
||||
run: |
|
||||
python devscripts/update-version.py -c ${{ inputs.channel }} ${{ inputs.version }}
|
||||
python devscripts/make_lazy_extractors.py
|
||||
- name: Build Unix platform-independent binary
|
||||
run: |
|
||||
- name: Build Unix platform-independent binary
|
||||
run: |
|
||||
make all tar
|
||||
- name: Build Unix standalone binary
|
||||
shell: bash -l {0}
|
||||
run: |
|
||||
- name: Build Unix standalone binary
|
||||
shell: bash -l {0}
|
||||
run: |
|
||||
unset LD_LIBRARY_PATH # Harmful; set by setup-python
|
||||
conda activate build
|
||||
python pyinst.py --onedir
|
||||
(cd ./dist/yt-dlp_linux && zip -r ../yt-dlp_linux.zip .)
|
||||
python pyinst.py
|
||||
mv ./dist/yt-dlp_linux ./yt-dlp_linux
|
||||
mv ./dist/yt-dlp_linux.zip ./yt-dlp_linux.zip
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: |
|
||||
yt-dlp
|
||||
yt-dlp.tar.gz
|
||||
dist/yt-dlp_linux
|
||||
dist/yt-dlp_linux.zip
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: |
|
||||
yt-dlp
|
||||
yt-dlp.tar.gz
|
||||
yt-dlp_linux
|
||||
yt-dlp_linux.zip
|
||||
|
||||
- name: Build and publish on PyPi
|
||||
env:
|
||||
TWINE_USERNAME: __token__
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
|
||||
if: "env.TWINE_PASSWORD != ''"
|
||||
run: |
|
||||
rm -rf dist/*
|
||||
python devscripts/set-variant.py pip -M "You installed yt-dlp with pip or using the wheel from PyPi; Use that to update"
|
||||
python setup.py sdist bdist_wheel
|
||||
twine upload dist/*
|
||||
|
||||
- name: Install SSH private key for Homebrew
|
||||
env:
|
||||
BREW_TOKEN: ${{ secrets.BREW_TOKEN }}
|
||||
if: "env.BREW_TOKEN != ''"
|
||||
uses: yt-dlp/ssh-agent@v0.5.3
|
||||
with:
|
||||
ssh-private-key: ${{ env.BREW_TOKEN }}
|
||||
- name: Update Homebrew Formulae
|
||||
env:
|
||||
BREW_TOKEN: ${{ secrets.BREW_TOKEN }}
|
||||
if: "env.BREW_TOKEN != ''"
|
||||
run: |
|
||||
git clone git@github.com:yt-dlp/homebrew-taps taps/
|
||||
python devscripts/update-formulae.py taps/Formula/yt-dlp.rb "${{ needs.prepare.outputs.ytdlp_version }}"
|
||||
git -C taps/ config user.name github-actions
|
||||
git -C taps/ config user.email github-actions@example.com
|
||||
git -C taps/ commit -am 'yt-dlp: ${{ needs.prepare.outputs.ytdlp_version }}'
|
||||
git -C taps/ push
|
||||
|
||||
|
||||
build_linux_arm:
|
||||
linux_arm:
|
||||
if: inputs.linux_arm
|
||||
permissions:
|
||||
packages: write # for Creating cache
|
||||
contents: read
|
||||
packages: write # for creating cache
|
||||
runs-on: ubuntu-latest
|
||||
needs: prepare
|
||||
strategy:
|
||||
matrix:
|
||||
architecture:
|
||||
- armv7
|
||||
- aarch64
|
||||
- armv7
|
||||
- aarch64
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
path: ./repo
|
||||
- name: Virtualized Install, Prepare & Build
|
||||
uses: yt-dlp/run-on-arch-action@v2
|
||||
with:
|
||||
githubToken: ${{ github.token }} # To cache image
|
||||
arch: ${{ matrix.architecture }}
|
||||
distro: ubuntu18.04 # Standalone executable should be built on minimum supported OS
|
||||
dockerRunArgs: --volume "${PWD}/repo:/repo"
|
||||
install: | # Installing Python 3.10 from the Deadsnakes repo raises errors
|
||||
apt update
|
||||
apt -y install zlib1g-dev python3.8 python3.8-dev python3.8-distutils python3-pip
|
||||
python3.8 -m pip install -U pip setuptools wheel
|
||||
# Cannot access requirements.txt from the repo directory at this stage
|
||||
python3.8 -m pip install -U Pyinstaller mutagen pycryptodomex websockets brotli certifi
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
path: ./repo
|
||||
- name: Virtualized Install, Prepare & Build
|
||||
uses: yt-dlp/run-on-arch-action@v2
|
||||
with:
|
||||
# Ref: https://github.com/uraimo/run-on-arch-action/issues/55
|
||||
env: |
|
||||
GITHUB_WORKFLOW: build
|
||||
githubToken: ${{ github.token }} # To cache image
|
||||
arch: ${{ matrix.architecture }}
|
||||
distro: ubuntu18.04 # Standalone executable should be built on minimum supported OS
|
||||
dockerRunArgs: --volume "${PWD}/repo:/repo"
|
||||
install: | # Installing Python 3.10 from the Deadsnakes repo raises errors
|
||||
apt update
|
||||
apt -y install zlib1g-dev python3.8 python3.8-dev python3.8-distutils python3-pip
|
||||
python3.8 -m pip install -U pip setuptools wheel
|
||||
# Cannot access requirements.txt from the repo directory at this stage
|
||||
python3.8 -m pip install -U Pyinstaller mutagen pycryptodomex websockets brotli certifi
|
||||
|
||||
run: |
|
||||
cd repo
|
||||
python3.8 -m pip install -U Pyinstaller -r requirements.txt # Cached version may be out of date
|
||||
python3.8 devscripts/update-version.py ${{ needs.prepare.outputs.version_suffix }}
|
||||
python3.8 devscripts/make_lazy_extractors.py
|
||||
python3.8 pyinst.py
|
||||
run: |
|
||||
cd repo
|
||||
python3.8 -m pip install -U Pyinstaller -r requirements.txt # Cached version may be out of date
|
||||
python3.8 devscripts/update-version.py -c ${{ inputs.channel }} ${{ inputs.version }}
|
||||
python3.8 devscripts/make_lazy_extractors.py
|
||||
python3.8 pyinst.py
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: | # run-on-arch-action designates armv7l as armv7
|
||||
repo/dist/yt-dlp_linux_${{ (matrix.architecture == 'armv7' && 'armv7l') || matrix.architecture }}
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: | # run-on-arch-action designates armv7l as armv7
|
||||
repo/dist/yt-dlp_linux_${{ (matrix.architecture == 'armv7' && 'armv7l') || matrix.architecture }}
|
||||
|
||||
|
||||
build_macos:
|
||||
macos:
|
||||
if: inputs.macos
|
||||
runs-on: macos-11
|
||||
needs: prepare
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
# NB: In order to create a universal2 application, the version of python3 in /usr/bin has to be used
|
||||
- name: Install Requirements
|
||||
run: |
|
||||
- uses: actions/checkout@v3
|
||||
# NB: In order to create a universal2 application, the version of python3 in /usr/bin has to be used
|
||||
- name: Install Requirements
|
||||
run: |
|
||||
brew install coreutils
|
||||
/usr/bin/python3 -m pip install -U --user pip Pyinstaller -r requirements.txt
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
/usr/bin/python3 devscripts/update-version.py ${{ needs.prepare.outputs.version_suffix }}
|
||||
- name: Prepare
|
||||
run: |
|
||||
/usr/bin/python3 devscripts/update-version.py -c ${{ inputs.channel }} ${{ inputs.version }}
|
||||
/usr/bin/python3 devscripts/make_lazy_extractors.py
|
||||
- name: Build
|
||||
run: |
|
||||
- name: Build
|
||||
run: |
|
||||
/usr/bin/python3 pyinst.py --target-architecture universal2 --onedir
|
||||
(cd ./dist/yt-dlp_macos && zip -r ../yt-dlp_macos.zip .)
|
||||
/usr/bin/python3 pyinst.py --target-architecture universal2
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: |
|
||||
dist/yt-dlp_macos
|
||||
dist/yt-dlp_macos.zip
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: |
|
||||
dist/yt-dlp_macos
|
||||
dist/yt-dlp_macos.zip
|
||||
|
||||
|
||||
build_macos_legacy:
|
||||
macos_legacy:
|
||||
if: inputs.macos_legacy
|
||||
runs-on: macos-latest
|
||||
needs: prepare
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Python
|
||||
# We need the official Python, because the GA ones only support newer macOS versions
|
||||
env:
|
||||
PYTHON_VERSION: 3.10.5
|
||||
MACOSX_DEPLOYMENT_TARGET: 10.9 # Used up by the Python build tools
|
||||
run: |
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Python
|
||||
# We need the official Python, because the GA ones only support newer macOS versions
|
||||
env:
|
||||
PYTHON_VERSION: 3.10.5
|
||||
MACOSX_DEPLOYMENT_TARGET: 10.9 # Used up by the Python build tools
|
||||
run: |
|
||||
# Hack to get the latest patch version. Uncomment if needed
|
||||
#brew install python@3.10
|
||||
#export PYTHON_VERSION=$( $(brew --prefix)/opt/python@3.10/bin/python3 --version | cut -d ' ' -f 2 )
|
||||
curl https://www.python.org/ftp/python/${PYTHON_VERSION}/python-${PYTHON_VERSION}-macos11.pkg -o "python.pkg"
|
||||
sudo installer -pkg python.pkg -target /
|
||||
python3 --version
|
||||
- name: Install Requirements
|
||||
run: |
|
||||
- name: Install Requirements
|
||||
run: |
|
||||
brew install coreutils
|
||||
python3 -m pip install -U --user pip Pyinstaller -r requirements.txt
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
python3 devscripts/update-version.py ${{ needs.prepare.outputs.version_suffix }}
|
||||
- name: Prepare
|
||||
run: |
|
||||
python3 devscripts/update-version.py -c ${{ inputs.channel }} ${{ inputs.version }}
|
||||
python3 devscripts/make_lazy_extractors.py
|
||||
- name: Build
|
||||
run: |
|
||||
- name: Build
|
||||
run: |
|
||||
python3 pyinst.py
|
||||
mv dist/yt-dlp_macos dist/yt-dlp_macos_legacy
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: |
|
||||
dist/yt-dlp_macos_legacy
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: |
|
||||
dist/yt-dlp_macos_legacy
|
||||
|
||||
|
||||
build_windows:
|
||||
windows:
|
||||
if: inputs.windows
|
||||
runs-on: windows-latest
|
||||
needs: prepare
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with: # 3.8 is used for Win7 support
|
||||
python-version: '3.8'
|
||||
- name: Install Requirements
|
||||
run: | # Custom pyinstaller built with https://github.com/yt-dlp/pyinstaller-builds
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with: # 3.8 is used for Win7 support
|
||||
python-version: "3.8"
|
||||
- name: Install Requirements
|
||||
run: | # Custom pyinstaller built with https://github.com/yt-dlp/pyinstaller-builds
|
||||
python -m pip install -U pip setuptools wheel py2exe
|
||||
pip install -U "https://yt-dlp.github.io/Pyinstaller-Builds/x86_64/pyinstaller-5.8.0-py3-none-any.whl" -r requirements.txt
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
python devscripts/update-version.py ${{ needs.prepare.outputs.version_suffix }}
|
||||
- name: Prepare
|
||||
run: |
|
||||
python devscripts/update-version.py -c ${{ inputs.channel }} ${{ inputs.version }}
|
||||
python devscripts/make_lazy_extractors.py
|
||||
- name: Build
|
||||
run: |
|
||||
- name: Build
|
||||
run: |
|
||||
python setup.py py2exe
|
||||
Move-Item ./dist/yt-dlp.exe ./dist/yt-dlp_min.exe
|
||||
python pyinst.py
|
||||
python pyinst.py --onedir
|
||||
Compress-Archive -Path ./dist/yt-dlp/* -DestinationPath ./dist/yt-dlp_win.zip
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: |
|
||||
dist/yt-dlp.exe
|
||||
dist/yt-dlp_min.exe
|
||||
dist/yt-dlp_win.zip
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: |
|
||||
dist/yt-dlp.exe
|
||||
dist/yt-dlp_min.exe
|
||||
dist/yt-dlp_win.zip
|
||||
|
||||
|
||||
build_windows32:
|
||||
windows32:
|
||||
if: inputs.windows32
|
||||
runs-on: windows-latest
|
||||
needs: prepare
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with: # 3.7 is used for Vista support. See https://github.com/yt-dlp/yt-dlp/issues/390
|
||||
python-version: '3.7'
|
||||
architecture: 'x86'
|
||||
- name: Install Requirements
|
||||
run: |
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with: # 3.7 is used for Vista support. See https://github.com/yt-dlp/yt-dlp/issues/390
|
||||
python-version: "3.7"
|
||||
architecture: "x86"
|
||||
- name: Install Requirements
|
||||
run: |
|
||||
python -m pip install -U pip setuptools wheel
|
||||
pip install -U "https://yt-dlp.github.io/Pyinstaller-Builds/i686/pyinstaller-5.8.0-py3-none-any.whl" -r requirements.txt
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
python devscripts/update-version.py ${{ needs.prepare.outputs.version_suffix }}
|
||||
- name: Prepare
|
||||
run: |
|
||||
python devscripts/update-version.py -c ${{ inputs.channel }} ${{ inputs.version }}
|
||||
python devscripts/make_lazy_extractors.py
|
||||
- name: Build
|
||||
run: |
|
||||
- name: Build
|
||||
run: |
|
||||
python pyinst.py
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: |
|
||||
dist/yt-dlp_x86.exe
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: |
|
||||
dist/yt-dlp_x86.exe
|
||||
|
||||
|
||||
publish_release:
|
||||
permissions:
|
||||
contents: write # for action-gh-release
|
||||
meta_files:
|
||||
if: inputs.meta_files && always()
|
||||
needs:
|
||||
- unix
|
||||
- linux_arm
|
||||
- macos
|
||||
- macos_legacy
|
||||
- windows
|
||||
- windows32
|
||||
runs-on: ubuntu-latest
|
||||
needs: [prepare, build_unix, build_linux_arm, build_windows, build_windows32, build_macos, build_macos_legacy]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/download-artifact@v3
|
||||
|
||||
- name: Get Changelog
|
||||
run: |
|
||||
changelog=$(grep -oPz '(?s)(?<=### ${{ needs.prepare.outputs.ytdlp_version }}\n{2}).+?(?=\n{2,3}###)' Changelog.md) || true
|
||||
echo "changelog<<EOF" >> $GITHUB_ENV
|
||||
echo "$changelog" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
- name: Make Update spec
|
||||
run: |
|
||||
echo "# This file is used for regulating self-update" >> _update_spec
|
||||
echo "lock 2022.07.18 .+ Python 3.6" >> _update_spec
|
||||
- name: Make SHA2-SUMS files
|
||||
run: |
|
||||
sha256sum artifact/yt-dlp | awk '{print $1 " yt-dlp"}' >> SHA2-256SUMS
|
||||
sha256sum artifact/yt-dlp.tar.gz | awk '{print $1 " yt-dlp.tar.gz"}' >> SHA2-256SUMS
|
||||
sha256sum artifact/yt-dlp.exe | awk '{print $1 " yt-dlp.exe"}' >> SHA2-256SUMS
|
||||
sha256sum artifact/yt-dlp_win.zip | awk '{print $1 " yt-dlp_win.zip"}' >> SHA2-256SUMS
|
||||
sha256sum artifact/yt-dlp_min.exe | awk '{print $1 " yt-dlp_min.exe"}' >> SHA2-256SUMS
|
||||
sha256sum artifact/yt-dlp_x86.exe | awk '{print $1 " yt-dlp_x86.exe"}' >> SHA2-256SUMS
|
||||
sha256sum artifact/yt-dlp_macos | awk '{print $1 " yt-dlp_macos"}' >> SHA2-256SUMS
|
||||
sha256sum artifact/yt-dlp_macos.zip | awk '{print $1 " yt-dlp_macos.zip"}' >> SHA2-256SUMS
|
||||
sha256sum artifact/yt-dlp_macos_legacy | awk '{print $1 " yt-dlp_macos_legacy"}' >> SHA2-256SUMS
|
||||
sha256sum artifact/yt-dlp_linux_armv7l | awk '{print $1 " yt-dlp_linux_armv7l"}' >> SHA2-256SUMS
|
||||
sha256sum artifact/yt-dlp_linux_aarch64 | awk '{print $1 " yt-dlp_linux_aarch64"}' >> SHA2-256SUMS
|
||||
sha256sum artifact/dist/yt-dlp_linux | awk '{print $1 " yt-dlp_linux"}' >> SHA2-256SUMS
|
||||
sha256sum artifact/dist/yt-dlp_linux.zip | awk '{print $1 " yt-dlp_linux.zip"}' >> SHA2-256SUMS
|
||||
sha512sum artifact/yt-dlp | awk '{print $1 " yt-dlp"}' >> SHA2-512SUMS
|
||||
sha512sum artifact/yt-dlp.tar.gz | awk '{print $1 " yt-dlp.tar.gz"}' >> SHA2-512SUMS
|
||||
sha512sum artifact/yt-dlp.exe | awk '{print $1 " yt-dlp.exe"}' >> SHA2-512SUMS
|
||||
sha512sum artifact/yt-dlp_win.zip | awk '{print $1 " yt-dlp_win.zip"}' >> SHA2-512SUMS
|
||||
sha512sum artifact/yt-dlp_min.exe | awk '{print $1 " yt-dlp_min.exe"}' >> SHA2-512SUMS
|
||||
sha512sum artifact/yt-dlp_x86.exe | awk '{print $1 " yt-dlp_x86.exe"}' >> SHA2-512SUMS
|
||||
sha512sum artifact/yt-dlp_macos | awk '{print $1 " yt-dlp_macos"}' >> SHA2-512SUMS
|
||||
sha512sum artifact/yt-dlp_macos.zip | awk '{print $1 " yt-dlp_macos.zip"}' >> SHA2-512SUMS
|
||||
sha512sum artifact/yt-dlp_macos_legacy | awk '{print $1 " yt-dlp_macos_legacy"}' >> SHA2-512SUMS
|
||||
sha512sum artifact/yt-dlp_linux_armv7l | awk '{print $1 " yt-dlp_linux_armv7l"}' >> SHA2-512SUMS
|
||||
sha512sum artifact/yt-dlp_linux_aarch64 | awk '{print $1 " yt-dlp_linux_aarch64"}' >> SHA2-512SUMS
|
||||
sha512sum artifact/dist/yt-dlp_linux | awk '{print $1 " yt-dlp_linux"}' >> SHA2-512SUMS
|
||||
sha512sum artifact/dist/yt-dlp_linux.zip | awk '{print $1 " yt-dlp_linux.zip"}' >> SHA2-512SUMS
|
||||
- name: Make SHA2-SUMS files
|
||||
run: |
|
||||
cd ./artifact/
|
||||
sha256sum * > ../SHA2-256SUMS
|
||||
sha512sum * > ../SHA2-512SUMS
|
||||
|
||||
- name: Publish Release
|
||||
uses: yt-dlp/action-gh-release@v1
|
||||
with:
|
||||
tag_name: ${{ needs.prepare.outputs.ytdlp_version }}
|
||||
name: yt-dlp ${{ needs.prepare.outputs.ytdlp_version }}
|
||||
target_commitish: ${{ needs.prepare.outputs.head_sha }}
|
||||
body: |
|
||||
#### [A description of the various files]((https://github.com/yt-dlp/yt-dlp#release-files)) are in the README
|
||||
- name: Make Update spec
|
||||
run: |
|
||||
cat >> _update_spec << EOF
|
||||
# This file is used for regulating self-update
|
||||
lock 2022.08.18.36 .+ Python 3.6
|
||||
EOF
|
||||
|
||||
---
|
||||
<details open><summary><h3>Changelog</summary>
|
||||
<p>
|
||||
- name: Sign checksum files
|
||||
env:
|
||||
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }}
|
||||
if: env.GPG_SIGNING_KEY != ''
|
||||
run: |
|
||||
gpg --batch --import <<< "${{ secrets.GPG_SIGNING_KEY }}"
|
||||
for signfile in ./SHA*SUMS; do
|
||||
gpg --batch --detach-sign "$signfile"
|
||||
done
|
||||
|
||||
${{ env.changelog }}
|
||||
|
||||
</p>
|
||||
</details>
|
||||
files: |
|
||||
SHA2-256SUMS
|
||||
SHA2-512SUMS
|
||||
artifact/yt-dlp
|
||||
artifact/yt-dlp.tar.gz
|
||||
artifact/yt-dlp.exe
|
||||
artifact/yt-dlp_win.zip
|
||||
artifact/yt-dlp_min.exe
|
||||
artifact/yt-dlp_x86.exe
|
||||
artifact/yt-dlp_macos
|
||||
artifact/yt-dlp_macos.zip
|
||||
artifact/yt-dlp_macos_legacy
|
||||
artifact/yt-dlp_linux_armv7l
|
||||
artifact/yt-dlp_linux_aarch64
|
||||
artifact/dist/yt-dlp_linux
|
||||
artifact/dist/yt-dlp_linux.zip
|
||||
_update_spec
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: |
|
||||
SHA*SUMS*
|
||||
_update_spec
|
||||
|
||||
81
.github/workflows/publish.yml
vendored
Normal file
81
.github/workflows/publish.yml
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
name: Publish
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
nightly:
|
||||
default: false
|
||||
required: false
|
||||
type: boolean
|
||||
version:
|
||||
required: true
|
||||
type: string
|
||||
target_commitish:
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
ARCHIVE_REPO_TOKEN:
|
||||
required: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Generate release notes
|
||||
run: |
|
||||
cat >> ./RELEASE_NOTES << EOF
|
||||
#### A description of the various files are in the [README](https://github.com/yt-dlp/yt-dlp#release-files)
|
||||
---
|
||||
<details><summary><h3>Changelog</h3></summary>
|
||||
$(python ./devscripts/make_changelog.py -vv)
|
||||
</details>
|
||||
EOF
|
||||
echo "**This is an automated nightly pre-release build**" >> ./PRERELEASE_NOTES
|
||||
cat ./RELEASE_NOTES >> ./PRERELEASE_NOTES
|
||||
echo "Generated from: https://github.com/${{ github.repository }}/commit/${{ inputs.target_commitish }}" >> ./ARCHIVE_NOTES
|
||||
cat ./RELEASE_NOTES >> ./ARCHIVE_NOTES
|
||||
|
||||
- name: Archive nightly release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.ARCHIVE_REPO_TOKEN }}
|
||||
GH_REPO: ${{ vars.ARCHIVE_REPO }}
|
||||
if: |
|
||||
inputs.nightly && env.GH_TOKEN != '' && env.GH_REPO != ''
|
||||
run: |
|
||||
gh release create \
|
||||
--notes-file ARCHIVE_NOTES \
|
||||
--title "yt-dlp nightly ${{ inputs.version }}" \
|
||||
${{ inputs.version }} \
|
||||
artifact/*
|
||||
|
||||
- name: Prune old nightly release
|
||||
if: inputs.nightly && !vars.ARCHIVE_REPO
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh release delete --yes --cleanup-tag "nightly" || true
|
||||
git tag --delete "nightly" || true
|
||||
sleep 5 # Enough time to cover deletion race condition
|
||||
|
||||
- name: Publish release${{ inputs.nightly && ' (nightly)' || '' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
if: (inputs.nightly && !vars.ARCHIVE_REPO) || !inputs.nightly
|
||||
run: |
|
||||
gh release create \
|
||||
--notes-file ${{ inputs.nightly && 'PRE' || '' }}RELEASE_NOTES \
|
||||
--target ${{ inputs.target_commitish }} \
|
||||
--title "yt-dlp ${{ inputs.nightly && 'nightly ' || '' }}${{ inputs.version }}" \
|
||||
${{ inputs.nightly && '--prerelease "nightly"' || inputs.version }} \
|
||||
artifact/*
|
||||
51
.github/workflows/release-nightly.yml
vendored
Normal file
51
.github/workflows/release-nightly.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
name: Release (nightly)
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- "yt_dlp/**.py"
|
||||
- "!yt_dlp/version.py"
|
||||
concurrency:
|
||||
group: release-nightly
|
||||
cancel-in-progress: true
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
if: vars.BUILD_NIGHTLY != ''
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.get_version.outputs.version }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Get version
|
||||
id: get_version
|
||||
run: |
|
||||
python devscripts/update-version.py "$(date -u +"%H%M%S")" | grep -Po "version=\d+(\.\d+){3}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
build:
|
||||
needs: prepare
|
||||
uses: ./.github/workflows/build.yml
|
||||
with:
|
||||
version: ${{ needs.prepare.outputs.version }}
|
||||
channel: nightly
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write # For package cache
|
||||
secrets:
|
||||
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }}
|
||||
|
||||
publish:
|
||||
needs: [prepare, build]
|
||||
uses: ./.github/workflows/publish.yml
|
||||
secrets:
|
||||
ARCHIVE_REPO_TOKEN: ${{ secrets.ARCHIVE_REPO_TOKEN }}
|
||||
permissions:
|
||||
contents: write
|
||||
with:
|
||||
nightly: true
|
||||
version: ${{ needs.prepare.outputs.version }}
|
||||
target_commitish: ${{ github.sha }}
|
||||
129
.github/workflows/release.yml
vendored
Normal file
129
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,129 @@
|
||||
name: Release
|
||||
on: workflow_dispatch
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.update_version.outputs.version }}
|
||||
head_sha: ${{ steps.push_release.outputs.head_sha }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Update version
|
||||
id: update_version
|
||||
run: |
|
||||
python devscripts/update-version.py ${{ vars.PUSH_VERSION_COMMIT == '' && '"$(date -u +"%H%M%S")"' || '' }} | \
|
||||
grep -Po "version=\d+\.\d+\.\d+(\.\d+)?" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Update documentation
|
||||
run: |
|
||||
make doc
|
||||
sed '/### /Q' Changelog.md >> ./CHANGELOG
|
||||
echo '### ${{ steps.update_version.outputs.version }}' >> ./CHANGELOG
|
||||
python ./devscripts/make_changelog.py -vv -c >> ./CHANGELOG
|
||||
echo >> ./CHANGELOG
|
||||
grep -Poz '(?s)### \d+\.\d+\.\d+.+' 'Changelog.md' | head -n -1 >> ./CHANGELOG
|
||||
cat ./CHANGELOG > Changelog.md
|
||||
|
||||
- name: Push to release
|
||||
id: push_release
|
||||
run: |
|
||||
git config --global user.name github-actions
|
||||
git config --global user.email github-actions@example.com
|
||||
git add -u
|
||||
git commit -m "Release ${{ steps.update_version.outputs.version }}" \
|
||||
-m "Created by: ${{ github.event.sender.login }}" -m ":ci skip all :ci run dl"
|
||||
git push origin --force ${{ github.event.ref }}:release
|
||||
echo "head_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Update master
|
||||
if: vars.PUSH_VERSION_COMMIT != ''
|
||||
run: git push origin ${{ github.event.ref }}
|
||||
|
||||
publish_pypi_homebrew:
|
||||
needs: prepare
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Install Requirements
|
||||
run: |
|
||||
sudo apt-get -y install pandoc man
|
||||
python -m pip install -U pip setuptools wheel twine
|
||||
python -m pip install -U -r requirements.txt
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
python devscripts/update-version.py ${{ needs.prepare.outputs.version }}
|
||||
python devscripts/make_lazy_extractors.py
|
||||
|
||||
- name: Build and publish on PyPI
|
||||
env:
|
||||
TWINE_USERNAME: __token__
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
|
||||
if: env.TWINE_PASSWORD != ''
|
||||
run: |
|
||||
rm -rf dist/*
|
||||
make pypi-files
|
||||
python devscripts/set-variant.py pip -M "You installed yt-dlp with pip or using the wheel from PyPi; Use that to update"
|
||||
python setup.py sdist bdist_wheel
|
||||
twine upload dist/*
|
||||
|
||||
- name: Checkout Homebrew repository
|
||||
env:
|
||||
BREW_TOKEN: ${{ secrets.BREW_TOKEN }}
|
||||
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
|
||||
if: env.BREW_TOKEN != '' && env.PYPI_TOKEN != ''
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: yt-dlp/homebrew-taps
|
||||
path: taps
|
||||
ssh-key: ${{ secrets.BREW_TOKEN }}
|
||||
|
||||
- name: Update Homebrew Formulae
|
||||
env:
|
||||
BREW_TOKEN: ${{ secrets.BREW_TOKEN }}
|
||||
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
|
||||
if: env.BREW_TOKEN != '' && env.PYPI_TOKEN != ''
|
||||
run: |
|
||||
python devscripts/update-formulae.py taps/Formula/yt-dlp.rb "${{ needs.prepare.outputs.version }}"
|
||||
git -C taps/ config user.name github-actions
|
||||
git -C taps/ config user.email github-actions@example.com
|
||||
git -C taps/ commit -am 'yt-dlp: ${{ needs.prepare.outputs.version }}'
|
||||
git -C taps/ push
|
||||
|
||||
build:
|
||||
needs: prepare
|
||||
uses: ./.github/workflows/build.yml
|
||||
with:
|
||||
version: ${{ needs.prepare.outputs.version }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write # For package cache
|
||||
secrets:
|
||||
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }}
|
||||
|
||||
publish:
|
||||
needs: [prepare, build]
|
||||
uses: ./.github/workflows/publish.yml
|
||||
permissions:
|
||||
contents: write
|
||||
with:
|
||||
version: ${{ needs.prepare.outputs.version }}
|
||||
target_commitish: ${{ needs.prepare.outputs.head_sha }}
|
||||
@@ -127,7 +127,7 @@ While these steps won't necessarily ensure that no misuse of the account takes p
|
||||
|
||||
### Is the website primarily used for piracy?
|
||||
|
||||
We follow [youtube-dl's policy](https://github.com/ytdl-org/youtube-dl#can-you-add-support-for-this-anime-video-site-or-site-which-shows-current-movies-for-free) to not support services that is primarily used for infringing copyright. Additionally, it has been decided to not to support porn sites that specialize in deep fake. We also cannot support any service that serves only [DRM protected content](https://en.wikipedia.org/wiki/Digital_rights_management).
|
||||
We follow [youtube-dl's policy](https://github.com/ytdl-org/youtube-dl#can-you-add-support-for-this-anime-video-site-or-site-which-shows-current-movies-for-free) to not support services that is primarily used for infringing copyright. Additionally, it has been decided to not to support porn sites that specialize in fakes. We also cannot support any service that serves only [DRM protected content](https://en.wikipedia.org/wiki/Digital_rights_management).
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -405,3 +405,7 @@ road-master
|
||||
rohieb
|
||||
sdht0
|
||||
seproDev
|
||||
Hill-98
|
||||
LXYan2333
|
||||
mushbite
|
||||
venkata-krishnas
|
||||
|
||||
282
Changelog.md
282
Changelog.md
@@ -1,16 +1,109 @@
|
||||
# Changelog
|
||||
|
||||
<!--
|
||||
# Instuctions for creating release
|
||||
|
||||
* Run `make doc`
|
||||
* Update Changelog.md and CONTRIBUTORS
|
||||
* Change "Based on ytdl" version in Readme.md if needed
|
||||
* Commit as `Release <version>` and push to master
|
||||
* Dispatch the workflow https://github.com/yt-dlp/yt-dlp/actions/workflows/build.yml on master
|
||||
# To create a release, dispatch the https://github.com/yt-dlp/yt-dlp/actions/workflows/release.yml workflow on master
|
||||
-->
|
||||
|
||||
# 2023.02.17
|
||||
### 2023.03.04
|
||||
|
||||
#### Extractor changes
|
||||
- bilibili
|
||||
- [Fix for downloading wrong subtitles](https://github.com/yt-dlp/yt-dlp/commit/8a83baaf218ab89e6e7faa76b7c7be3a2ec19e3a) ([#6358](https://github.com/yt-dlp/yt-dlp/issues/6358)) by [LXYan2333](https://github.com/LXYan2333)
|
||||
- ESPNcricinfo
|
||||
- [Handle new URL pattern](https://github.com/yt-dlp/yt-dlp/commit/640c934823fc2d1ec77ec932566078014058635f) ([#6321](https://github.com/yt-dlp/yt-dlp/issues/6321)) by [venkata-krishnas](https://github.com/venkata-krishnas)
|
||||
- lefigaro
|
||||
- [Add extractors](https://github.com/yt-dlp/yt-dlp/commit/eb8fd6d044e8926532772b72be0645c6b8ecb3aa) ([#6309](https://github.com/yt-dlp/yt-dlp/issues/6309)) by [elyse0](https://github.com/elyse0)
|
||||
- lumni
|
||||
- [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/1f8489cccbdc6e96027ef527b88717458f0900e8) ([#6302](https://github.com/yt-dlp/yt-dlp/issues/6302)) by [carusocr](https://github.com/carusocr)
|
||||
- Prankcast
|
||||
- [Fix tags](https://github.com/yt-dlp/yt-dlp/commit/ed4cc4ea793314c50ae3f82e98248c1de1c25694) ([#6316](https://github.com/yt-dlp/yt-dlp/issues/6316)) by [columndeeply](https://github.com/columndeeply)
|
||||
- rutube
|
||||
- [Extract chapters from description](https://github.com/yt-dlp/yt-dlp/commit/22ccd5420b3eb0782776071f12cccd1fedaa1fd0) ([#6345](https://github.com/yt-dlp/yt-dlp/issues/6345)) by [mushbite](https://github.com/mushbite)
|
||||
- SportDeutschland
|
||||
- [Rewrite extractor](https://github.com/yt-dlp/yt-dlp/commit/45db357289b4e1eec09093c8bc5446520378f426) by [pukkandan](https://github.com/pukkandan)
|
||||
- telecaribe
|
||||
- [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/b40471282286bd2b09c485bf79afd271d229272c) ([#6311](https://github.com/yt-dlp/yt-dlp/issues/6311)) by [elyse0](https://github.com/elyse0)
|
||||
- tubetugraz
|
||||
- [Support `--twofactor` (#6424)](https://github.com/yt-dlp/yt-dlp/commit/f44cb4e77bb9be8be291d02ab6f79dc0b4c0d4a1) ([#6427](https://github.com/yt-dlp/yt-dlp/issues/6427)) by [Ferdi265](https://github.com/Ferdi265)
|
||||
- tunein
|
||||
- [Fix extractors](https://github.com/yt-dlp/yt-dlp/commit/46580ced56c90b559885aded6aa8f46f20a9cdce) ([#6310](https://github.com/yt-dlp/yt-dlp/issues/6310)) by [elyse0](https://github.com/elyse0)
|
||||
- twitch
|
||||
- [Update for GraphQL API changes](https://github.com/yt-dlp/yt-dlp/commit/4a6272c6d1bff89969b67cd22b26ebe6d7e72279) ([#6318](https://github.com/yt-dlp/yt-dlp/issues/6318)) by [elyse0](https://github.com/elyse0)
|
||||
- twitter
|
||||
- [Fix retweet extraction](https://github.com/yt-dlp/yt-dlp/commit/cf605226521e99c89fc8dff26a319025810e63a0) ([#6422](https://github.com/yt-dlp/yt-dlp/issues/6422)) by [selfisekai](https://github.com/selfisekai)
|
||||
- xvideos
|
||||
- quickies: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/283a0b5bc511f3b350eead4488158f50c20ec526) ([#6414](https://github.com/yt-dlp/yt-dlp/issues/6414)) by [Yakabuff](https://github.com/Yakabuff)
|
||||
|
||||
#### Misc. changes
|
||||
- build
|
||||
- [Fix publishing to PyPI and homebrew](https://github.com/yt-dlp/yt-dlp/commit/55676fe498345a389a2539d8baaba958d6d61c3e) by [bashonly](https://github.com/bashonly)
|
||||
- [Only archive if `vars.ARCHIVE_REPO` is set](https://github.com/yt-dlp/yt-dlp/commit/08ff6d59f97b5f5f0128f6bf6fbef56fd836cc52) by [Grub4K](https://github.com/Grub4K)
|
||||
- cleanup
|
||||
- Miscellaneous: [392389b](https://github.com/yt-dlp/yt-dlp/commit/392389b7df7b818f794b231f14dc396d4875fbad) by [pukkandan](https://github.com/pukkandan)
|
||||
- devscripts
|
||||
- `make_changelog`: [Stop at `Release ...` commit](https://github.com/yt-dlp/yt-dlp/commit/7accdd9845fe7ce9d0aa5a9d16faaa489c1294eb) by [pukkandan](https://github.com/pukkandan)
|
||||
|
||||
### 2023.03.03
|
||||
|
||||
#### Important changes
|
||||
- **A new release type has been added!**
|
||||
* [`nightly`](https://github.com/yt-dlp/yt-dlp/releases/tag/nightly) builds will be made after each push, containing the latest fixes (but also possibly bugs).
|
||||
* When using `--update`/`-U`, a release binary will only update to its current channel (either `stable` or `nightly`).
|
||||
* The `--update-to` option has been added allowing the user more control over program upgrades (or downgrades).
|
||||
* `--update-to` can change the release channel (`stable`, `nightly`) and also upgrade or downgrade to specific tags.
|
||||
* **Usage**: `--update-to CHANNEL`, `--update-to TAG`, `--update-to CHANNEL@TAG`
|
||||
- **YouTube throttling fixes!**
|
||||
|
||||
#### Core changes
|
||||
- [Add option `--break-match-filters`](https://github.com/yt-dlp/yt-dlp/commit/fe2ce85aff0aa03735fc0152bb8cb9c3d4ef0753) by [pukkandan](https://github.com/pukkandan)
|
||||
- [Fix `--break-on-existing` with `--lazy-playlist`](https://github.com/yt-dlp/yt-dlp/commit/d21056f4cf0a1623daa107f9181074f5725ac436) by [pukkandan](https://github.com/pukkandan)
|
||||
- dependencies
|
||||
- [Simplify `Cryptodome`](https://github.com/yt-dlp/yt-dlp/commit/65f6e807804d2af5e00f2aecd72bfc43af19324a) by [pukkandan](https://github.com/pukkandan)
|
||||
- jsinterp
|
||||
- [Handle `Date` at epoch 0](https://github.com/yt-dlp/yt-dlp/commit/9acf1ee25f7ad3920ede574a9de95b8c18626af4) by [pukkandan](https://github.com/pukkandan)
|
||||
- plugins
|
||||
- [Don't look in `.egg` directories](https://github.com/yt-dlp/yt-dlp/commit/b059188383eee4fa336ef728dda3ff4bb7335625) by [pukkandan](https://github.com/pukkandan)
|
||||
- update
|
||||
- [Add option `--update-to`, including to nightly](https://github.com/yt-dlp/yt-dlp/commit/77df20f14cc9ed41dfe3a1fe2d77fd27f5365a94) ([#6220](https://github.com/yt-dlp/yt-dlp/issues/6220)) by [bashonly](https://github.com/bashonly), [Grub4K](https://github.com/Grub4K), [pukkandan](https://github.com/pukkandan)
|
||||
- utils
|
||||
- `LenientJSONDecoder`: [Parse unclosed objects](https://github.com/yt-dlp/yt-dlp/commit/cc09083636ce21e58ff74f45eac2dbda507462b0) by [pukkandan](https://github.com/pukkandan)
|
||||
- `Popen`: [Shim undocumented `text_mode` property](https://github.com/yt-dlp/yt-dlp/commit/da8e2912b165005f76779a115a071cd6132ceedf) by [Grub4K](https://github.com/Grub4K)
|
||||
|
||||
#### Extractor changes
|
||||
- [Fix DRM detection in m3u8](https://github.com/yt-dlp/yt-dlp/commit/43a3eaf96393b712d60cbcf5c6cb1e90ed7f42f5) by [pukkandan](https://github.com/pukkandan)
|
||||
- generic
|
||||
- [Detect manifest links via extension](https://github.com/yt-dlp/yt-dlp/commit/b38cae49e6f4849c8ee2a774bdc3c1c647ae5f0e) by [bashonly](https://github.com/bashonly)
|
||||
- [Handle basic-auth when checking redirects](https://github.com/yt-dlp/yt-dlp/commit/8e9fe43cd393e69fa49b3d842aa3180c1d105b8f) by [pukkandan](https://github.com/pukkandan)
|
||||
- GoogleDrive
|
||||
- [Fix some audio](https://github.com/yt-dlp/yt-dlp/commit/4d248e29d20d983ededab0b03d4fe69dff9eb4ed) by [pukkandan](https://github.com/pukkandan)
|
||||
- iprima
|
||||
- [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/9fddc12ab022a31754e0eaa358fc4e1dfa974587) ([#6291](https://github.com/yt-dlp/yt-dlp/issues/6291)) by [std-move](https://github.com/std-move)
|
||||
- mediastream
|
||||
- [Improve WinSports support](https://github.com/yt-dlp/yt-dlp/commit/2d5a8c5db2bd4ff1c2e45e00cd890a10f8ffca9e) ([#6401](https://github.com/yt-dlp/yt-dlp/issues/6401)) by [bashonly](https://github.com/bashonly)
|
||||
- ntvru
|
||||
- [Extract HLS and DASH formats](https://github.com/yt-dlp/yt-dlp/commit/77d6d136468d0c23c8e79bc937898747804f585a) ([#6403](https://github.com/yt-dlp/yt-dlp/issues/6403)) by [bashonly](https://github.com/bashonly)
|
||||
- tencent
|
||||
- [Add more formats and info](https://github.com/yt-dlp/yt-dlp/commit/18d295c9e0f95adc179eef345b7af64d6372db78) ([#5950](https://github.com/yt-dlp/yt-dlp/issues/5950)) by [Hill-98](https://github.com/Hill-98)
|
||||
- yle_areena
|
||||
- [Extract non-Kaltura videos](https://github.com/yt-dlp/yt-dlp/commit/40d77d89027cd0e0ce31d22aec81db3e1d433900) ([#6402](https://github.com/yt-dlp/yt-dlp/issues/6402)) by [bashonly](https://github.com/bashonly)
|
||||
- youtube
|
||||
- [Construct dash formats with `range` query](https://github.com/yt-dlp/yt-dlp/commit/5038f6d713303e0967d002216e7a88652401c22a) by [pukkandan](https://github.com/pukkandan) (With fixes in [f34804b](https://github.com/yt-dlp/yt-dlp/commit/f34804b2f920f62a6e893a14a9e2a2144b14dd23) by [bashonly](https://github.com/bashonly), [coletdjnz](https://github.com/coletdjnz))
|
||||
- [Detect and break on looping comments](https://github.com/yt-dlp/yt-dlp/commit/7f51861b1820c37b157a239b1fe30628d907c034) ([#6301](https://github.com/yt-dlp/yt-dlp/issues/6301)) by [coletdjnz](https://github.com/coletdjnz)
|
||||
- [Extract channel `view_count` when `/about` tab is passed](https://github.com/yt-dlp/yt-dlp/commit/31e183557fcd1b937582f9429f29207c1261f501) by [pukkandan](https://github.com/pukkandan)
|
||||
|
||||
#### Misc. changes
|
||||
- build
|
||||
- [Add `cffi` as a dependency for `yt_dlp_linux`](https://github.com/yt-dlp/yt-dlp/commit/776d1c3f0c9b00399896dd2e40e78e9a43218109) by [bashonly](https://github.com/bashonly)
|
||||
- [Automated builds and nightly releases](https://github.com/yt-dlp/yt-dlp/commit/29cb20bd563c02671b31dd840139e93dd37150a1) ([#6220](https://github.com/yt-dlp/yt-dlp/issues/6220)) by [bashonly](https://github.com/bashonly), [Grub4K](https://github.com/Grub4K) (With fixes in [bfc861a](https://github.com/yt-dlp/yt-dlp/commit/bfc861a91ee65c9b0ac169754f512e052c6827cf) by [pukkandan](https://github.com/pukkandan))
|
||||
- [Sign SHA files and release public key](https://github.com/yt-dlp/yt-dlp/commit/12647e03d417feaa9ea6a458bea5ebd747494a53) by [Grub4K](https://github.com/Grub4K)
|
||||
- cleanup
|
||||
- [Fix `Changelog`](https://github.com/yt-dlp/yt-dlp/commit/17ca19ab60a6a13eb8a629c51442b5248b0d8394) by [pukkandan](https://github.com/pukkandan)
|
||||
- jsinterp: [Give functions names to help debugging](https://github.com/yt-dlp/yt-dlp/commit/b2e0343ba0fc5d8702e90f6ba2b71358e2677e0b) by [pukkandan](https://github.com/pukkandan)
|
||||
- Miscellaneous: [4815bbf](https://github.com/yt-dlp/yt-dlp/commit/4815bbfc41cf641e4a0650289dbff968cb3bde76), [5b28cef](https://github.com/yt-dlp/yt-dlp/commit/5b28cef72db3b531680d89c121631c73ae05354f) by [pukkandan](https://github.com/pukkandan)
|
||||
- devscripts
|
||||
- [Script to generate changelog](https://github.com/yt-dlp/yt-dlp/commit/d400e261cf029a3f20d364113b14de973be75404) ([#6220](https://github.com/yt-dlp/yt-dlp/issues/6220)) by [Grub4K](https://github.com/Grub4K) (With fixes in [9344964](https://github.com/yt-dlp/yt-dlp/commit/93449642815a6973a4b09b289982ca7e1f961b5f))
|
||||
|
||||
### 2023.02.17
|
||||
|
||||
* Merge youtube-dl: Upto [commit/2dd6c6e](https://github.com/ytdl-org/youtube-dl/commit/2dd6c6e)
|
||||
* Fix `--concat-playlist`
|
||||
@@ -50,8 +143,8 @@
|
||||
* [extractor/txxx] Add extractors by [chio0hai](https://github.com/chio0hai)
|
||||
* [extractor/vocaroo] Add extractor by [SuperSonicHub1](https://github.com/SuperSonicHub1), [qbnu](https://github.com/qbnu)
|
||||
* [extractor/wrestleuniverse] Add extractors by [Grub4K](https://github.com/Grub4K), [bashonly](https://github.com/bashonly)
|
||||
* [extractor/yappy] Add extractor by [HobbyistDev](https://github.com/HobbyistDev)
|
||||
* **[extractor/youtube] Fix `uploader_id` extraction** by [bashonly](https://github.com/bashonly)
|
||||
* [extractor/yappy] Add extractor by [HobbyistDev](https://github.com/HobbyistDev), [dirkf](https://github.com/dirkf)
|
||||
* [extractor/youtube] **Fix `uploader_id` extraction** by [bashonly](https://github.com/bashonly)
|
||||
* [extractor/youtube] Add hyperpipe instances by [Generator](https://github.com/Generator)
|
||||
* [extractor/youtube] Handle `consent.youtube`
|
||||
* [extractor/youtube] Support `/live/` URL
|
||||
@@ -101,172 +194,9 @@
|
||||
* [extractor/drtv] Fix bug in [ab4cbef](https://github.com/yt-dlp/yt-dlp/commit/ab4cbef) by [bashonly](https://github.com/bashonly)
|
||||
|
||||
|
||||
### 2023.02.17
|
||||
|
||||
#### Core changes
|
||||
### Core changes
|
||||
- [Bugfix for 39f32f1715c0dffb7626dda7307db6388bb7abaa](https://github.com/yt-dlp/yt-dlp/commit/9ebac35577e61c3d25fafc959655fa3ab04ca7ef) by [pukkandan](https://github.com/pukkandan)
|
||||
- [Bugfix for 39f32f1715c0dffb7626dda7307db6388bb7abaa](https://github.com/yt-dlp/yt-dlp/commit/c154302c588c3d4362cec4fc5545e7e5d2bcf7a3) by [pukkandan](https://github.com/pukkandan)
|
||||
- [Fix `--concat-playlist`](https://github.com/yt-dlp/yt-dlp/commit/59d7de0da545944c48a82fc2937b996d7cd8cc9c) by [pukkandan](https://github.com/pukkandan)
|
||||
- [Imply `--no-progress` when `--print`](https://github.com/yt-dlp/yt-dlp/commit/5712943b764ba819ef479524c32700228603817a) by [pukkandan](https://github.com/pukkandan)
|
||||
- [Improve default subtitle language selection](https://github.com/yt-dlp/yt-dlp/commit/376aa24b1541e2bfb23337c0ae9bafa5bb3787f1) ([#6240](https://github.com/yt-dlp/yt-dlp/issues/6240)) by [sdht0](https://github.com/sdht0)
|
||||
- [Make `title` completely non-fatal](https://github.com/yt-dlp/yt-dlp/commit/7aefd19afed357c80743405ec2ace2148cba42e3) by [pukkandan](https://github.com/pukkandan)
|
||||
- [Sanitize formats before sorting](https://github.com/yt-dlp/yt-dlp/commit/39f32f1715c0dffb7626dda7307db6388bb7abaa) by [pukkandan](https://github.com/pukkandan)
|
||||
- [Support module level `__bool__` and `property`](https://github.com/yt-dlp/yt-dlp/commit/754c84e2e416cf6609dd0e4632b4985a08d34043) by [pukkandan](https://github.com/pukkandan)
|
||||
- [Update to ytdl-commit-2dd6c6e](https://github.com/yt-dlp/yt-dlp/commit/48fde8ac4ccbaaea868f6378814dde395f649fbf) by [pukkandan](https://github.com/pukkandan)
|
||||
- [extractor/douyutv]: [Use new API](https://github.com/yt-dlp/yt-dlp/commit/f14c2333481c63c24017a41ded7d8f36726504b7) ([#6074](https://github.com/yt-dlp/yt-dlp/issues/6074)) by [hatienl0i261299](https://github.com/hatienl0i261299)
|
||||
- compat_utils
|
||||
- [Improve `passthrough_module`](https://github.com/yt-dlp/yt-dlp/commit/88426d9446758c707fb511408f2d6f56de952db4) by [pukkandan](https://github.com/pukkandan)
|
||||
- [Simplify `EnhancedModule`](https://github.com/yt-dlp/yt-dlp/commit/768a00178109508893488e53a0e720b117fbccf6) by [pukkandan](https://github.com/pukkandan)
|
||||
- dependencies
|
||||
- [Standardize `Cryptodome` imports](https://github.com/yt-dlp/yt-dlp/commit/f6a765ceb59c55aea06921880c1c87d1ff36e5de) by [pukkandan](https://github.com/pukkandan)
|
||||
- jsinterp
|
||||
- [Support `if` statements](https://github.com/yt-dlp/yt-dlp/commit/8b008d62544b82e24a0ba36c30e8e51855d93419) by [pukkandan](https://github.com/pukkandan)
|
||||
- plugins
|
||||
- [Fix zip search paths](https://github.com/yt-dlp/yt-dlp/commit/88d8928bf7630801865cf8728ae5c77234324b7b) by [pukkandan](https://github.com/pukkandan)
|
||||
- utils
|
||||
- [Don't use Content-length with encoding](https://github.com/yt-dlp/yt-dlp/commit/65e5c021e7c5f23ecbc6a982b72a02ac6cd6900d) ([#6176](https://github.com/yt-dlp/yt-dlp/issues/6176)) by [felixonmars](https://github.com/felixonmars)
|
||||
- [Fix `time_seconds` to use the provided TZ](https://github.com/yt-dlp/yt-dlp/commit/83c4970e52839ce8761ec61bd19d549aed7d7920) ([#6118](https://github.com/yt-dlp/yt-dlp/issues/6118)) by [Grub4K](https://github.com/Grub4K), [Lesmiscore](https://github.com/Lesmiscore)
|
||||
- [Fix race condition in `make_dir`](https://github.com/yt-dlp/yt-dlp/commit/b25d6cb96337d479bdcb41768356da414c3aa835) ([#6089](https://github.com/yt-dlp/yt-dlp/issues/6089)) by [aionescu](https://github.com/aionescu)
|
||||
- [Use local kernel32 for file locking on Windows](https://github.com/yt-dlp/yt-dlp/commit/37e325b92ff9d784715ac0e5d1f7d96bf5f45ad9) by [Grub4K](https://github.com/Grub4K)
|
||||
- traverse_obj
|
||||
- [Fix more bugs](https://github.com/yt-dlp/yt-dlp/commit/6839ae1f6dde4c0442619e351b3f0442312ab4f9) by [pukkandan](https://github.com/pukkandan)
|
||||
- [Fix several behavioral problems](https://github.com/yt-dlp/yt-dlp/commit/b1bde57bef878478e3503ab07190fd207914ade9) by [Grub4K](https://github.com/Grub4K)
|
||||
- [Various improvements](https://github.com/yt-dlp/yt-dlp/commit/776995bc109c5cd1aa56b684fada2ce718a386ec) by [Grub4K](https://github.com/Grub4K)
|
||||
### Extractor changes
|
||||
- [Fix `_search_nuxt_data`](https://github.com/yt-dlp/yt-dlp/commit/b23167e7542c177f32b22b29857b637dc4aede69) ([#6062](https://github.com/yt-dlp/yt-dlp/issues/6062)) by [LowSuggestion912](https://github.com/LowSuggestion912)
|
||||
- 91porn
|
||||
- [Fix title and comment extraction](https://github.com/yt-dlp/yt-dlp/commit/c085cc2def9862ac8a7619ce8ea5dcc177325719) ([#5932](https://github.com/yt-dlp/yt-dlp/issues/5932)) by [pmitchell86](https://github.com/pmitchell86)
|
||||
- abematv
|
||||
- [Cache user token whenever appropriate](https://github.com/yt-dlp/yt-dlp/commit/a4f16832213d9e29beecf685d6cd09a2f0b48c87) ([#6216](https://github.com/yt-dlp/yt-dlp/issues/6216)) by [Lesmiscore](https://github.com/Lesmiscore)
|
||||
- anchorfm
|
||||
- [Add episode extractor](https://github.com/yt-dlp/yt-dlp/commit/a4ad59ff2ded208bf33f6fe07299a3449eadccdc) ([#6092](https://github.com/yt-dlp/yt-dlp/issues/6092)) by [bashonly](https://github.com/bashonly), [HobbyistDev](https://github.com/HobbyistDev)
|
||||
- bfmtv
|
||||
- [Support `rmc` prefix](https://github.com/yt-dlp/yt-dlp/commit/20266508dd6247dd3cf0e97b9b9f14c3afc046db) ([#6025](https://github.com/yt-dlp/yt-dlp/issues/6025)) by [carusocr](https://github.com/carusocr)
|
||||
- biliintl
|
||||
- [Add intro and ending chapters](https://github.com/yt-dlp/yt-dlp/commit/0ba87dd279d3565ed93c559cf7880ad61eb83af8) ([#6018](https://github.com/yt-dlp/yt-dlp/issues/6018)) by [HobbyistDev](https://github.com/HobbyistDev)
|
||||
- boxcast
|
||||
- [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/9acca71237f42a4775008e51fe26e42f0a39c552) ([#5983](https://github.com/yt-dlp/yt-dlp/issues/5983)) by [HobbyistDev](https://github.com/HobbyistDev)
|
||||
- clyp
|
||||
- [Support `wav`](https://github.com/yt-dlp/yt-dlp/commit/cc13293c2819b5461be211a9729fd02bb1e2f476) ([#6102](https://github.com/yt-dlp/yt-dlp/issues/6102)) by [qulaz](https://github.com/qulaz)
|
||||
- crunchyroll
|
||||
- [Add intro chapter](https://github.com/yt-dlp/yt-dlp/commit/93abb7406b95793f6872d12979b91d5f336b4f43) ([#6023](https://github.com/yt-dlp/yt-dlp/issues/6023)) by [ByteDream](https://github.com/ByteDream)
|
||||
- [Better message for premium videos](https://github.com/yt-dlp/yt-dlp/commit/44699d10dc8de9c6a338f4a8e5c63506ec4d2118) by [pukkandan](https://github.com/pukkandan)
|
||||
- [Fix incorrect premium-only error](https://github.com/yt-dlp/yt-dlp/commit/c9d14bd22ab31e2a41f9f8061843668a06db583b) by [Grub4K](https://github.com/Grub4K)
|
||||
- drtv
|
||||
- [Fix bug in ab4cbef](https://github.com/yt-dlp/yt-dlp/commit/7481998b169b2a52049fc33bff82034d6563ead4) ([#6034](https://github.com/yt-dlp/yt-dlp/issues/6034)) by [bashonly](https://github.com/bashonly)
|
||||
- ebay
|
||||
- [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/da880559a6ecbbf374cc9f3378e696b55b9599af) ([#6170](https://github.com/yt-dlp/yt-dlp/issues/6170)) by [JChris246](https://github.com/JChris246)
|
||||
- embedly
|
||||
- [Embedded links may be for other extractors](https://github.com/yt-dlp/yt-dlp/commit/87ebab0615b1bf9b14b478b055e7059d630b4833) by [pukkandan](https://github.com/pukkandan)
|
||||
- freesound
|
||||
- [Workaround invalid URL in webpage](https://github.com/yt-dlp/yt-dlp/commit/9cfdbcbf3f17be51f5b6bb9bb6d880b2f3d67362) ([#6147](https://github.com/yt-dlp/yt-dlp/issues/6147)) by [rebane2001](https://github.com/rebane2001)
|
||||
- generic
|
||||
- [Avoid catastrophic backtracking in KVS regex](https://github.com/yt-dlp/yt-dlp/commit/8aa0bd5d10627ece3c1815c01d02fb8bf22847a7) by [bashonly](https://github.com/bashonly)
|
||||
- goplay
|
||||
- [Use new API](https://github.com/yt-dlp/yt-dlp/commit/d27bde98832e3b7ffb39f3cf6346011b97bb3bc3) ([#6151](https://github.com/yt-dlp/yt-dlp/issues/6151)) by [jeroenj](https://github.com/jeroenj)
|
||||
- hidive
|
||||
- [Fix subtitles and age-restriction](https://github.com/yt-dlp/yt-dlp/commit/7708df8da05c94270b43e0630e4e20f6d2d62c55) ([#5828](https://github.com/yt-dlp/yt-dlp/issues/5828)) by [chexxor](https://github.com/chexxor)
|
||||
- huya
|
||||
- [Support HD streams](https://github.com/yt-dlp/yt-dlp/commit/fbbb5508ea98ed8709847f5ecced7d70ff05e0ee) ([#6172](https://github.com/yt-dlp/yt-dlp/issues/6172)) by [felixonmars](https://github.com/felixonmars)
|
||||
- hypergryph
|
||||
- [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/31c279a2a2c2ef402a9e6dad9992b310d16439a6) ([#6094](https://github.com/yt-dlp/yt-dlp/issues/6094)) by [bashonly](https://github.com/bashonly), [HobbyistDev](https://github.com/HobbyistDev)
|
||||
- moviepilot
|
||||
- [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/c62e64cf0122e52fa2175dd1b004ca6b8e1d82af) ([#5954](https://github.com/yt-dlp/yt-dlp/issues/5954)) by [panatexxa](https://github.com/panatexxa)
|
||||
- nbc
|
||||
- [Fix XML parsing](https://github.com/yt-dlp/yt-dlp/commit/176a068cde4f2d9dfa0336168caead0b1edcb8ac) by [bashonly](https://github.com/bashonly)
|
||||
- [Fix `NBC` and `NBCStations` extractors](https://github.com/yt-dlp/yt-dlp/commit/cb73b8460c3ce6d37ab651a4e44bb23b10056154) ([#6033](https://github.com/yt-dlp/yt-dlp/issues/6033)) by [bashonly](https://github.com/bashonly)
|
||||
- nebula
|
||||
- [Remove broken cookie support](https://github.com/yt-dlp/yt-dlp/commit/d50ea3ce5abc3b0defc0e5d1e22b22ce9b01b07b) ([#5979](https://github.com/yt-dlp/yt-dlp/issues/5979)) by [hheimbuerger](https://github.com/hheimbuerger)
|
||||
- nfl
|
||||
- [Add `NFLPlus` extractors](https://github.com/yt-dlp/yt-dlp/commit/8b37c58f8b5494504acdb5ebe3f8bbd26230f725) ([#6222](https://github.com/yt-dlp/yt-dlp/issues/6222)) by [bashonly](https://github.com/bashonly)
|
||||
- niconico
|
||||
- [Add support for like history](https://github.com/yt-dlp/yt-dlp/commit/3b161265add30613bde2e46fca214fe94d09e651) ([#5705](https://github.com/yt-dlp/yt-dlp/issues/5705)) by [Matumo](https://github.com/Matumo), [pukkandan](https://github.com/pukkandan)
|
||||
- nitter
|
||||
- [Update instance list](https://github.com/yt-dlp/yt-dlp/commit/a9189510baadf0dccd2d4d363bc6f3a441128bb0) ([#6236](https://github.com/yt-dlp/yt-dlp/issues/6236)) by [OIRNOIR](https://github.com/OIRNOIR)
|
||||
- npo
|
||||
- [Fix extractor and add HD support](https://github.com/yt-dlp/yt-dlp/commit/cc2389c8ac72a514d4e002a0f6ca5a7d65c7eff0) ([#6155](https://github.com/yt-dlp/yt-dlp/issues/6155)) by [seproDev](https://github.com/seproDev)
|
||||
- nzonscreen
|
||||
- [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/d3bb187f01e1e30db05e639fc23a2e1935d777fe) ([#6208](https://github.com/yt-dlp/yt-dlp/issues/6208)) by [gregsadetsky](https://github.com/gregsadetsky), [pukkandan](https://github.com/pukkandan)
|
||||
- odkmedia
|
||||
- [Add `OnDemandChinaEpisodeIE`](https://github.com/yt-dlp/yt-dlp/commit/10fd9e6ee833c88edf6c633f864f42843a708d32) ([#6116](https://github.com/yt-dlp/yt-dlp/issues/6116)) by [HobbyistDev](https://github.com/HobbyistDev), [pukkandan](https://github.com/pukkandan)
|
||||
- pornez
|
||||
- [Handle relative URLs in iframe](https://github.com/yt-dlp/yt-dlp/commit/f7efe6dc958eb0689cb9534ff0b4e592040be8df) ([#6171](https://github.com/yt-dlp/yt-dlp/issues/6171)) by [JChris246](https://github.com/JChris246)
|
||||
- radiko
|
||||
- [Fix format sorting for Time Free](https://github.com/yt-dlp/yt-dlp/commit/203a06f8554df6db07d8f20f465ecbfe8a14e591) ([#6159](https://github.com/yt-dlp/yt-dlp/issues/6159)) by [road-master](https://github.com/road-master)
|
||||
- rcs
|
||||
- [Fix extractors](https://github.com/yt-dlp/yt-dlp/commit/c6b657867ad68af6b930ed0aa11ec5d93ee187b7) ([#5700](https://github.com/yt-dlp/yt-dlp/issues/5700)) by [nixxo](https://github.com/nixxo), [pukkandan](https://github.com/pukkandan)
|
||||
- reddit
|
||||
- [Support user posts](https://github.com/yt-dlp/yt-dlp/commit/c77df98b1a477a020a57141464d10c0f4d0fdbc9) ([#6173](https://github.com/yt-dlp/yt-dlp/issues/6173)) by [OMEGARAZER](https://github.com/OMEGARAZER)
|
||||
- rozhlas
|
||||
- [Add extractor RozhlasVltavaIE](https://github.com/yt-dlp/yt-dlp/commit/355d781bed497cbcb254bf2a2737b83fa51c84ea) ([#5951](https://github.com/yt-dlp/yt-dlp/issues/5951)) by [amra](https://github.com/amra)
|
||||
- rumble
|
||||
- [Fix format sorting](https://github.com/yt-dlp/yt-dlp/commit/acacb57c7e173b93c6e0f0c43e61b9b2912719d8) by [pukkandan](https://github.com/pukkandan)
|
||||
- servus
|
||||
- [Rewrite extractor](https://github.com/yt-dlp/yt-dlp/commit/f40e32fb1ac67be5bdbc8e32a3c235abfc4be260) ([#6036](https://github.com/yt-dlp/yt-dlp/issues/6036)) by [Ashish0804](https://github.com/Ashish0804), [FrankZ85](https://github.com/FrankZ85), [StefanLobbenmeier](https://github.com/StefanLobbenmeier)
|
||||
- slideslive
|
||||
- [Fix slides and chapters/duration](https://github.com/yt-dlp/yt-dlp/commit/5ab3534d44231f7711398bc3cfc520e2efd09f50) ([#6024](https://github.com/yt-dlp/yt-dlp/issues/6024)) by [bashonly](https://github.com/bashonly)
|
||||
- sportdeutschland
|
||||
- [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/5e1a54f63e393c218a40949012ff0de0ce63cb15) ([#6041](https://github.com/yt-dlp/yt-dlp/issues/6041)) by [FriedrichRehren](https://github.com/FriedrichRehren)
|
||||
- stripchat
|
||||
- [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/7d5f919bad07017f4b39b55725491b1e9717d47a) ([#5985](https://github.com/yt-dlp/yt-dlp/issues/5985)) by [bashonly](https://github.com/bashonly), [JChris246](https://github.com/JChris246)
|
||||
- tempo
|
||||
- [Add IVXPlayer extractor](https://github.com/yt-dlp/yt-dlp/commit/30031be974d210f451100339699ef03b0ddb5f10) ([#5837](https://github.com/yt-dlp/yt-dlp/issues/5837)) by [HobbyistDev](https://github.com/HobbyistDev)
|
||||
- tnaflix
|
||||
- [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/989f47b6315541989bb507f26b431d9586430995) ([#6086](https://github.com/yt-dlp/yt-dlp/issues/6086)) by [bashonly](https://github.com/bashonly), [oxamun](https://github.com/oxamun)
|
||||
- tvp
|
||||
- [Support `stream.tvp.pl`](https://github.com/yt-dlp/yt-dlp/commit/a31d0fa6c315b1145d682361149003d98f1e3782) ([#6139](https://github.com/yt-dlp/yt-dlp/issues/6139)) by [selfisekai](https://github.com/selfisekai)
|
||||
- twitter
|
||||
- [Fix `--no-playlist` and add media `view_count` when using GraphQL](https://github.com/yt-dlp/yt-dlp/commit/b6795fd310f1dd61dddc9fd08e52fe485bdc8a3e) ([#6211](https://github.com/yt-dlp/yt-dlp/issues/6211)) by [Grub4K](https://github.com/Grub4K)
|
||||
- [Fix graphql extraction on some tweets](https://github.com/yt-dlp/yt-dlp/commit/7543c9c99bcb116b085fdb1f41b84a0ead04c05d) ([#6075](https://github.com/yt-dlp/yt-dlp/issues/6075)) by [selfisekai](https://github.com/selfisekai)
|
||||
- txxx
|
||||
- [Add extractors](https://github.com/yt-dlp/yt-dlp/commit/389896df85ed14eaf74f72531da6c4491d6b73b0) ([#5240](https://github.com/yt-dlp/yt-dlp/issues/5240)) by [chio0hai](https://github.com/chio0hai)
|
||||
- vimeo
|
||||
- [Fix `playerConfig` extraction](https://github.com/yt-dlp/yt-dlp/commit/c0cd13fb1c71b842c3d272d0273c03542b467766) ([#6203](https://github.com/yt-dlp/yt-dlp/issues/6203)) by [bashonly](https://github.com/bashonly), [LeoniePhiline](https://github.com/LeoniePhiline)
|
||||
- viu
|
||||
- [Add `ViuOTTIndonesiaIE` extractor](https://github.com/yt-dlp/yt-dlp/commit/72671a212d7c939329cb5d34335fa089dd3acbd3) ([#6099](https://github.com/yt-dlp/yt-dlp/issues/6099)) by [HobbyistDev](https://github.com/HobbyistDev)
|
||||
- vk
|
||||
- [Fix playlists for new API](https://github.com/yt-dlp/yt-dlp/commit/a9c685453f7019bee94170f936619c6db76c964e) ([#6122](https://github.com/yt-dlp/yt-dlp/issues/6122)) by [the-marenga](https://github.com/the-marenga)
|
||||
- vlive
|
||||
- [Replace with `VLiveWebArchiveIE`](https://github.com/yt-dlp/yt-dlp/commit/b3eaab7ca2e118d4db73dcb44afd9c8717db8b67) ([#6196](https://github.com/yt-dlp/yt-dlp/issues/6196)) by [seproDev](https://github.com/seproDev)
|
||||
- vocaroo
|
||||
- [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/e4a8b1769e19755acba6d8f212208359905a3159) ([#6117](https://github.com/yt-dlp/yt-dlp/issues/6117)) by [qbnu](https://github.com/qbnu), [SuperSonicHub1](https://github.com/SuperSonicHub1)
|
||||
- wrestleuniverse
|
||||
- [Add extractors](https://github.com/yt-dlp/yt-dlp/commit/e61acb40b2cb6ef45508d72235026d458c9d5dff) ([#6158](https://github.com/yt-dlp/yt-dlp/issues/6158)) by [bashonly](https://github.com/bashonly), [Grub4K](https://github.com/Grub4K)
|
||||
- ximalaya
|
||||
- [Update album `_VALID_URL`](https://github.com/yt-dlp/yt-dlp/commit/417cdaae08fc447c9d15c53a88e2e9a027cdbf0a) ([#6110](https://github.com/yt-dlp/yt-dlp/issues/6110)) by [carusocr](https://github.com/carusocr)
|
||||
- yappy
|
||||
- [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/361630015535026712bdb67f804a15b65ff9ee7e) ([#6111](https://github.com/yt-dlp/yt-dlp/issues/6111)) by [HobbyistDev](https://github.com/HobbyistDev)
|
||||
- youtube
|
||||
- [Add hyperpipe instances](https://github.com/yt-dlp/yt-dlp/commit/78a78fa74dbc888d20f1b65e1382bf99131597d5) ([#6020](https://github.com/yt-dlp/yt-dlp/issues/6020)) by [Generator](https://github.com/Generator)
|
||||
- [Fix `uploader_id` extraction](https://github.com/yt-dlp/yt-dlp/commit/149eb0bbf34fa8fdf8d1e2aa28e17479d099e26b) by [bashonly](https://github.com/bashonly)
|
||||
- [Handle `consent.youtube`](https://github.com/yt-dlp/yt-dlp/commit/b032ff0f032512bd6fc70c9c1994d906eacc06cb) by [pukkandan](https://github.com/pukkandan)
|
||||
- [Support `/live/` URL](https://github.com/yt-dlp/yt-dlp/commit/dad2210c0cb9cf03702a9511817ee5ec646d7bc8) by [pukkandan](https://github.com/pukkandan)
|
||||
- [Update invidious and piped instances](https://github.com/yt-dlp/yt-dlp/commit/05799a48c7dec12b34c8bf951c8d2eceedda59f8) ([#6030](https://github.com/yt-dlp/yt-dlp/issues/6030)) by [rohieb](https://github.com/rohieb)
|
||||
- [`uploader_id` includes `@` with handle](https://github.com/yt-dlp/yt-dlp/commit/c61cf091a54d3aa3c611722035ccde5ecfe981bb) by [bashonly](https://github.com/bashonly)
|
||||
- zdf
|
||||
- [Use android API endpoint for UHD downloads](https://github.com/yt-dlp/yt-dlp/commit/0fe87a8730638490415d630f48e61d264d89c358) ([#6150](https://github.com/yt-dlp/yt-dlp/issues/6150)) by [seproDev](https://github.com/seproDev)
|
||||
### Downloader changes
|
||||
- hls
|
||||
- [Allow extractors to provide AES key](https://github.com/yt-dlp/yt-dlp/commit/7e68567e508168b345266c0c19812ad50a829eaa) ([#6158](https://github.com/yt-dlp/yt-dlp/issues/6158)) by [bashonly](https://github.com/bashonly), [Grub4K](https://github.com/Grub4K)
|
||||
### Postprocessor changes
|
||||
- extractaudio
|
||||
- [Handle outtmpl without ext](https://github.com/yt-dlp/yt-dlp/commit/f737fb16d8234408c85bc189ccc926fea000515b) ([#6005](https://github.com/yt-dlp/yt-dlp/issues/6005)) by [carusocr](https://github.com/carusocr)
|
||||
- pyinst
|
||||
- [Fix for pyinstaller 5.8](https://github.com/yt-dlp/yt-dlp/commit/2e269bd998c61efaf7500907d114a56e5e83e65e) by [pukkandan](https://github.com/pukkandan)
|
||||
### Misc. changes
|
||||
- build
|
||||
- [Update pyinstaller](https://github.com/yt-dlp/yt-dlp/commit/365b9006051ac7d735c20bb63c4907b758233048) by [pukkandan](https://github.com/pukkandan)
|
||||
- cleanup
|
||||
- Miscellaneous: [76c9c52](https://github.com/yt-dlp/yt-dlp/commit/76c9c523071150053df7b56956646b680b6a6e05) by [pukkandan](https://github.com/pukkandan)
|
||||
- devscripts
|
||||
- [Provide pyinstaller hooks](https://github.com/yt-dlp/yt-dlp/commit/acb1042a9ffa8769fe691beac1011d6da1fcf321) by [pukkandan](https://github.com/pukkandan)
|
||||
- pyinstaller
|
||||
- [Analyze sub-modules of `Cryptodome`](https://github.com/yt-dlp/yt-dlp/commit/b85faf6ffb700058e774e99c04304a7a9257cdd0) by [pukkandan](https://github.com/pukkandan)
|
||||
|
||||
### 2023.01.06
|
||||
|
||||
* Fix config locations by [Grub4k](https://github.com/Grub4k), [coletdjnz](https://github.com/coletdjnz), [pukkandan](https://github.com/pukkandan)
|
||||
* Fix config locations by [Grub4K](https://github.com/Grub4K), [coletdjnz](https://github.com/coletdjnz), [pukkandan](https://github.com/pukkandan)
|
||||
* [downloader/aria2c] Disable native progress
|
||||
* [utils] `mimetype2ext`: `weba` is not standard
|
||||
* [utils] `windows_enable_vt_mode`: Better error handling
|
||||
@@ -293,7 +223,7 @@
|
||||
* Add `--compat-options 2021,2022`
|
||||
* This allows devs to change defaults and make other potentially breaking changes more easily. If you need everything to work exactly as-is, put Use `--compat 2022` in your config to guard against future compat changes.
|
||||
* [downloader/aria2c] Native progress for aria2c via RPC by [Lesmiscore](https://github.com/Lesmiscore), [pukkandan](https://github.com/pukkandan)
|
||||
* Merge youtube-dl: Upto [commit/195f22f](https://github.com/ytdl-org/youtube-dl/commit/195f22f6) by [Grub4k](https://github.com/Grub4k), [pukkandan](https://github.com/pukkandan)
|
||||
* Merge youtube-dl: Upto [commit/195f22f](https://github.com/ytdl-org/youtube-dl/commit/195f22f6) by [Grub4K](https://github.com/Grub4K), [pukkandan](https://github.com/pukkandan)
|
||||
* Add pre-processor stage `video`
|
||||
* Let `--parse/replace-in-metadata` run at any post-processing stage
|
||||
* Add `--enable-file-urls` by [coletdjnz](https://github.com/coletdjnz)
|
||||
@@ -408,7 +338,7 @@
|
||||
* [extractor/udemy] Fix lectures that have no URL and detect DRM
|
||||
* [extractor/unsupported] Add more URLs
|
||||
* [extractor/urplay] Support for audio-only formats by [barsnick](https://github.com/barsnick)
|
||||
* [extractor/wistia] Improve extension detection by [Grub4k](https://github.com/Grub4k), [bashonly](https://github.com/bashonly), [pukkandan](https://github.com/pukkandan)
|
||||
* [extractor/wistia] Improve extension detection by [Grub4K](https://github.com/Grub4K), [bashonly](https://github.com/bashonly), [pukkandan](https://github.com/pukkandan)
|
||||
* [extractor/yle_areena] Support restricted videos by [docbender](https://github.com/docbender)
|
||||
* [extractor/youku] Fix extractor by [KurtBestor](https://github.com/KurtBestor)
|
||||
* [extractor/youporn] Fix metadata by [marieell](https://github.com/marieell)
|
||||
|
||||
@@ -56,6 +56,7 @@ You can also find lists of all [contributors of yt-dlp](CONTRIBUTORS) and [autho
|
||||
|
||||
## [bashonly](https://github.com/bashonly)
|
||||
|
||||
* `--update-to`, automated release, nightly builds
|
||||
* `--cookies-from-browser` support for Firefox containers
|
||||
* Added support for new websites Genius, Kick, NBCStations, Triller, VideoKen etc
|
||||
* Improved/fixed support for Anvato, Brightcove, Instagram, ParamountPlus, Reddit, SlidesLive, TikTok, Twitter, Vimeo etc
|
||||
@@ -65,5 +66,6 @@ You can also find lists of all [contributors of yt-dlp](CONTRIBUTORS) and [autho
|
||||
|
||||
[](https://ko-fi.com/Grub4K) [](https://github.com/sponsors/Grub4K)
|
||||
|
||||
* `--update-to`, automated release, nightly builds
|
||||
* Rework internals like `traverse_obj`, various core refactors and bugs fixes
|
||||
* Helped fix crunchyroll, Twitter, wrestleuniverse, wistia, slideslive etc
|
||||
|
||||
69
README.md
69
README.md
@@ -114,13 +114,15 @@ yt-dlp is a [youtube-dl](https://github.com/ytdl-org/youtube-dl) fork based on t
|
||||
|
||||
* **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-on-reject` 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-filter` 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
|
||||
|
||||
* **Plugins**: Extractors and PostProcessors can be loaded from an external file. See [plugins](#plugins) for details
|
||||
|
||||
* **Self-updater**: The releases can be updated using `yt-dlp -U`
|
||||
* **Self updater**: The releases can be updated using `yt-dlp -U`, and downgraded using `--update-to` if required
|
||||
|
||||
* **Nightly builds**: [Automated nightly builds](#update-channels) can be used with `--update-to nightly`
|
||||
|
||||
See [changelog](Changelog.md) or [commits](https://github.com/yt-dlp/yt-dlp/commits) for the full list of changes
|
||||
|
||||
@@ -130,6 +132,7 @@ 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:
|
||||
|
||||
* yt-dlp supports only [Python 3.7+](## "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)
|
||||
* 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`
|
||||
* yt-dlp stores config files in slightly different locations to youtube-dl. See [CONFIGURATION](#configuration) for a list of correct locations
|
||||
@@ -180,12 +183,25 @@ You can install yt-dlp using [the binaries](#release-files), [PIP](https://pypi.
|
||||
|
||||
|
||||
## UPDATE
|
||||
You can use `yt-dlp -U` to update if you are [using the release binaries](#release-files)
|
||||
You can use `yt-dlp -U` to update if you are using the [release binaries](#release-files)
|
||||
|
||||
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
|
||||
|
||||
<a id="update-channels"/>
|
||||
|
||||
There are currently two release channels for binaries, `stable` and `nightly`.
|
||||
`stable` is the default channel, and many of its changes have been tested by users of the nightly channel.
|
||||
The `nightly` channel has releases built after each push to the master branch, and will have the most recent fixes and additions, but also have more risk of regressions. They are available in [their own repo](https://github.com/yt-dlp/yt-dlp-nightly-builds/releases).
|
||||
|
||||
When using `--update`/`-U`, a release binary will only update to its current channel.
|
||||
This release channel can be changed by using the `--update-to` option. `--update-to` can also be used to upgrade or downgrade to specific tags from a channel.
|
||||
|
||||
Example usage:
|
||||
* `yt-dlp --update-to nightly` change to `nightly` channel and update to its latest release
|
||||
* `yt-dlp --update-to stable@2023.02.17` upgrade/downgrade to release to `stable` channel tag `2023.02.17`
|
||||
* `yt-dlp --update-to 2023.01.06` upgrade/downgrade to tag `2023.01.06` if it exists on the current channel
|
||||
|
||||
<!-- MANPAGE: BEGIN EXCLUDED SECTION -->
|
||||
## RELEASE FILES
|
||||
@@ -218,11 +234,20 @@ File|Description
|
||||
:---|:---
|
||||
[yt-dlp.tar.gz](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.tar.gz)|Source tarball
|
||||
[SHA2-512SUMS](https://github.com/yt-dlp/yt-dlp/releases/latest/download/SHA2-512SUMS)|GNU-style SHA512 sums
|
||||
[SHA2-512SUMS.sig](https://github.com/yt-dlp/yt-dlp/releases/latest/download/SHA2-512SUMS.sig)|GPG signature file for SHA512 sums
|
||||
[SHA2-256SUMS](https://github.com/yt-dlp/yt-dlp/releases/latest/download/SHA2-256SUMS)|GNU-style SHA256 sums
|
||||
[SHA2-256SUMS.sig](https://github.com/yt-dlp/yt-dlp/releases/latest/download/SHA2-256SUMS.sig)|GPG signature file for SHA256 sums
|
||||
|
||||
The public key that can be used to verify the GPG signatures is [available here](https://github.com/yt-dlp/yt-dlp/blob/master/public.key)
|
||||
Example usage:
|
||||
```
|
||||
curl -L https://github.com/yt-dlp/yt-dlp/raw/master/public.key | gpg --import
|
||||
gpg --verify SHA2-256SUMS.sig SHA2-256SUMS
|
||||
gpg --verify SHA2-512SUMS.sig SHA2-512SUMS
|
||||
```
|
||||
<!-- MANPAGE: END EXCLUDED SECTION -->
|
||||
|
||||
|
||||
**Note**: The manpages, shell completion files etc. are available in the [source tarball](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.tar.gz)
|
||||
**Note**: The manpages, shell completion files etc. are available inside the [source tarball](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.tar.gz)
|
||||
|
||||
## DEPENDENCIES
|
||||
Python versions 3.7+ (CPython and PyPy) are supported. Other versions and implementations may or may not work correctly.
|
||||
@@ -310,11 +335,15 @@ If you wish to build it anyway, install Python and py2exe, and then simply run `
|
||||
|
||||
### Related scripts
|
||||
|
||||
* **`devscripts/update-version.py [revision]`** - Update the version number based on current date
|
||||
* **`devscripts/set-variant.py variant [-M update_message]`** - Set the build variant of the executable
|
||||
* **`devscripts/update-version.py`** - Update the version number based on current date.
|
||||
* **`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_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.
|
||||
|
||||
You can also fork the project on GitHub and run your fork's [build workflow](.github/workflows/build.yml) to automatically build a full release
|
||||
Note: See their `--help` for more info.
|
||||
|
||||
### Forking the project
|
||||
If you fork the project on GitHub, you can run your fork's [build workflow](.github/workflows/build.yml) to automatically build the selected version(s) as artifacts. Alternatively, you can run the [release workflow](.github/workflows/release.yml) or enable the [nightly workflow](.github/workflows/release-nightly.yml) to create full (pre-)releases.
|
||||
|
||||
# USAGE AND OPTIONS
|
||||
|
||||
@@ -330,6 +359,11 @@ You can also fork the project on GitHub and run your fork's [build workflow](.gi
|
||||
--version Print program version and exit
|
||||
-U, --update Update this program to the latest version
|
||||
--no-update Do not check for updates (default)
|
||||
--update-to [CHANNEL]@[TAG] Upgrade/downgrade to a specific version.
|
||||
CHANNEL and TAG defaults to "stable" and
|
||||
"latest" respectively if omitted; See
|
||||
"UPDATE" for details. Supported channels:
|
||||
stable, nightly
|
||||
-i, --ignore-errors Ignore download and postprocessing errors.
|
||||
The download will be considered successful
|
||||
even if the postprocessing fails
|
||||
@@ -456,9 +490,8 @@ You can also fork the project on GitHub and run your fork's [build workflow](.gi
|
||||
--date DATE Download only videos uploaded on this date.
|
||||
The date can be "YYYYMMDD" or in the format
|
||||
[now|today|yesterday][-N[day|week|month|year]].
|
||||
E.g. "--date today-2weeks" downloads
|
||||
only videos uploaded on the same day two
|
||||
weeks ago
|
||||
E.g. "--date today-2weeks" downloads only
|
||||
videos uploaded on the same day two weeks ago
|
||||
--datebefore DATE Download only videos uploaded on or before
|
||||
this date. The date formats accepted is the
|
||||
same as --date
|
||||
@@ -485,7 +518,10 @@ You can also fork the project on GitHub and run your fork's [build workflow](.gi
|
||||
dogs" (caseless). Use "--match-filter -" to
|
||||
interactively ask whether to download each
|
||||
video
|
||||
--no-match-filter Do not use generic video filter (default)
|
||||
--no-match-filter Do not use any --match-filter (default)
|
||||
--break-match-filters FILTER Same as "--match-filters" but stops the
|
||||
download process when a video is rejected
|
||||
--no-break-match-filters Do not use any --break-match-filters (default)
|
||||
--no-playlist Download only the video, if the URL refers
|
||||
to a video and a playlist
|
||||
--yes-playlist Download the playlist, if the URL refers to
|
||||
@@ -499,11 +535,9 @@ You can also fork the project on GitHub and run your fork's [build workflow](.gi
|
||||
--max-downloads NUMBER Abort after downloading NUMBER files
|
||||
--break-on-existing Stop the download process when encountering
|
||||
a file that is in the archive
|
||||
--break-on-reject Stop the download process when encountering
|
||||
a file that has been filtered out
|
||||
--break-per-input Alters --max-downloads, --break-on-existing,
|
||||
--break-on-reject, and autonumber to reset
|
||||
per input URL
|
||||
--break-match-filter, and autonumber to
|
||||
reset per input URL
|
||||
--no-break-per-input --break-on-existing and similar options
|
||||
terminates the entire download queue
|
||||
--skip-playlist-after-errors N Number of allowed failures until the rest of
|
||||
@@ -1227,7 +1261,7 @@ To summarize, the general syntax for a field is:
|
||||
|
||||
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"/>
|
||||
|
||||
**Note**: Due to post-processing (i.e. merging etc.), the actual output filename might differ. Use `--print after_move:filepath` to get the name after all post-processing is complete.
|
||||
|
||||
@@ -2099,6 +2133,7 @@ While these options are redundant, they are still expected to be used due to the
|
||||
--reject-title REGEX --match-filter "title !~= (?i)REGEX"
|
||||
--min-views COUNT --match-filter "view_count >=? COUNT"
|
||||
--max-views COUNT --match-filter "view_count <=? COUNT"
|
||||
--break-on-reject Use --break-match-filter
|
||||
--user-agent UA --add-header "User-Agent:UA"
|
||||
--referer URL --add-header "Referer:URL"
|
||||
--playlist-start NUMBER -I NUMBER:
|
||||
|
||||
12
devscripts/changelog_override.json
Normal file
12
devscripts/changelog_override.json
Normal file
@@ -0,0 +1,12 @@
|
||||
[
|
||||
{
|
||||
"action": "add",
|
||||
"when": "776d1c3f0c9b00399896dd2e40e78e9a43218109",
|
||||
"short": "[priority] **A new release type has been added!**\n * [`nightly`](https://github.com/yt-dlp/yt-dlp/releases/tag/nightly) builds will be made after each push, containing the latest fixes (but also possibly bugs).\n * When using `--update`/`-U`, a release binary will only update to its current channel (either `stable` or `nightly`).\n * The `--update-to` option has been added allowing the user more control over program upgrades (or downgrades).\n * `--update-to` can change the release channel (`stable`, `nightly`) and also upgrade or downgrade to specific tags.\n * **Usage**: `--update-to CHANNEL`, `--update-to TAG`, `--update-to CHANNEL@TAG`"
|
||||
},
|
||||
{
|
||||
"action": "add",
|
||||
"when": "776d1c3f0c9b00399896dd2e40e78e9a43218109",
|
||||
"short": "[priority] **YouTube throttling fixes!**"
|
||||
}
|
||||
]
|
||||
96
devscripts/changelog_override.schema.json
Normal file
96
devscripts/changelog_override.schema.json
Normal file
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft/2020-12/schema",
|
||||
"type": "array",
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"enum": [
|
||||
"add"
|
||||
]
|
||||
},
|
||||
"when": {
|
||||
"type": "string",
|
||||
"pattern": "^([0-9a-f]{40}|\\d{4}\\.\\d{2}\\.\\d{2})$"
|
||||
},
|
||||
"hash": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-f]{40}$"
|
||||
},
|
||||
"short": {
|
||||
"type": "string"
|
||||
},
|
||||
"authors": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"action",
|
||||
"short"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"enum": [
|
||||
"remove"
|
||||
]
|
||||
},
|
||||
"when": {
|
||||
"type": "string",
|
||||
"pattern": "^([0-9a-f]{40}|\\d{4}\\.\\d{2}\\.\\d{2})$"
|
||||
},
|
||||
"hash": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-f]{40}$"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"action",
|
||||
"hash"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"enum": [
|
||||
"change"
|
||||
]
|
||||
},
|
||||
"when": {
|
||||
"type": "string",
|
||||
"pattern": "^([0-9a-f]{40}|\\d{4}\\.\\d{2}\\.\\d{2})$"
|
||||
},
|
||||
"hash": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-f]{40}$"
|
||||
},
|
||||
"short": {
|
||||
"type": "string"
|
||||
},
|
||||
"authors": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"action",
|
||||
"hash",
|
||||
"short",
|
||||
"authors"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
470
devscripts/make_changelog.py
Normal file
470
devscripts/make_changelog.py
Normal file
@@ -0,0 +1,470 @@
|
||||
from __future__ import annotations
|
||||
|
||||
# Allow direct execution
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
import enum
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
from devscripts.utils import read_file, run_process, write_file
|
||||
|
||||
BASE_URL = 'https://github.com'
|
||||
LOCATION_PATH = Path(__file__).parent
|
||||
HASH_LENGTH = 7
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CommitGroup(enum.Enum):
|
||||
UPSTREAM = None
|
||||
PRIORITY = 'Important'
|
||||
CORE = 'Core'
|
||||
EXTRACTOR = 'Extractor'
|
||||
DOWNLOADER = 'Downloader'
|
||||
POSTPROCESSOR = 'Postprocessor'
|
||||
MISC = 'Misc.'
|
||||
|
||||
@classmethod
|
||||
@lru_cache
|
||||
def commit_lookup(cls):
|
||||
return {
|
||||
name: group
|
||||
for group, names in {
|
||||
cls.PRIORITY: {''},
|
||||
cls.UPSTREAM: {'upstream'},
|
||||
cls.CORE: {
|
||||
'aes',
|
||||
'cache',
|
||||
'compat_utils',
|
||||
'compat',
|
||||
'cookies',
|
||||
'core',
|
||||
'dependencies',
|
||||
'jsinterp',
|
||||
'outtmpl',
|
||||
'plugins',
|
||||
'update',
|
||||
'utils',
|
||||
},
|
||||
cls.MISC: {
|
||||
'build',
|
||||
'cleanup',
|
||||
'devscripts',
|
||||
'docs',
|
||||
'misc',
|
||||
'test',
|
||||
},
|
||||
cls.EXTRACTOR: {'extractor', 'extractors'},
|
||||
cls.DOWNLOADER: {'downloader'},
|
||||
cls.POSTPROCESSOR: {'postprocessor'},
|
||||
}.items()
|
||||
for name in names
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get(cls, value):
|
||||
result = cls.commit_lookup().get(value)
|
||||
if result:
|
||||
logger.debug(f'Mapped {value!r} => {result.name}')
|
||||
return result
|
||||
|
||||
|
||||
@dataclass
|
||||
class Commit:
|
||||
hash: str | None
|
||||
short: str
|
||||
authors: list[str]
|
||||
|
||||
def __str__(self):
|
||||
result = f'{self.short!r}'
|
||||
|
||||
if self.hash:
|
||||
result += f' ({self.hash[:HASH_LENGTH]})'
|
||||
|
||||
if self.authors:
|
||||
authors = ', '.join(self.authors)
|
||||
result += f' by {authors}'
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@dataclass
|
||||
class CommitInfo:
|
||||
details: str | None
|
||||
sub_details: tuple[str, ...]
|
||||
message: str
|
||||
issues: list[str]
|
||||
commit: Commit
|
||||
fixes: list[Commit]
|
||||
|
||||
def key(self):
|
||||
return ((self.details or '').lower(), self.sub_details, self.message)
|
||||
|
||||
|
||||
class Changelog:
|
||||
MISC_RE = re.compile(r'(?:^|\b)(?:lint(?:ing)?|misc|format(?:ting)?|fixes)(?:\b|$)', re.IGNORECASE)
|
||||
|
||||
def __init__(self, groups, repo):
|
||||
self._groups = groups
|
||||
self._repo = repo
|
||||
|
||||
def __str__(self):
|
||||
return '\n'.join(self._format_groups(self._groups)).replace('\t', ' ')
|
||||
|
||||
def _format_groups(self, groups):
|
||||
for item in CommitGroup:
|
||||
group = groups[item]
|
||||
if group:
|
||||
yield self.format_module(item.value, group)
|
||||
|
||||
def format_module(self, name, group):
|
||||
result = f'\n#### {name} changes\n' if name else '\n'
|
||||
return result + '\n'.join(self._format_group(group))
|
||||
|
||||
def _format_group(self, group):
|
||||
sorted_group = sorted(group, key=CommitInfo.key)
|
||||
detail_groups = itertools.groupby(sorted_group, lambda item: (item.details or '').lower())
|
||||
for _, items in detail_groups:
|
||||
items = list(items)
|
||||
details = items[0].details
|
||||
if not details:
|
||||
indent = ''
|
||||
else:
|
||||
yield f'- {details}'
|
||||
indent = '\t'
|
||||
|
||||
if details == 'cleanup':
|
||||
items, cleanup_misc_items = self._filter_cleanup_misc_items(items)
|
||||
|
||||
sub_detail_groups = itertools.groupby(items, lambda item: tuple(map(str.lower, item.sub_details)))
|
||||
for sub_details, entries in sub_detail_groups:
|
||||
if not sub_details:
|
||||
for entry in entries:
|
||||
yield f'{indent}- {self.format_single_change(entry)}'
|
||||
continue
|
||||
|
||||
entries = list(entries)
|
||||
prefix = f'{indent}- {", ".join(entries[0].sub_details)}'
|
||||
if len(entries) == 1:
|
||||
yield f'{prefix}: {self.format_single_change(entries[0])}'
|
||||
continue
|
||||
|
||||
yield prefix
|
||||
for entry in entries:
|
||||
yield f'{indent}\t- {self.format_single_change(entry)}'
|
||||
|
||||
if details == 'cleanup' and cleanup_misc_items:
|
||||
yield from self._format_cleanup_misc_sub_group(cleanup_misc_items)
|
||||
|
||||
def _filter_cleanup_misc_items(self, items):
|
||||
cleanup_misc_items = defaultdict(list)
|
||||
non_misc_items = []
|
||||
for item in items:
|
||||
if self.MISC_RE.search(item.message):
|
||||
cleanup_misc_items[tuple(item.commit.authors)].append(item)
|
||||
else:
|
||||
non_misc_items.append(item)
|
||||
|
||||
return non_misc_items, cleanup_misc_items
|
||||
|
||||
def _format_cleanup_misc_sub_group(self, group):
|
||||
prefix = '\t- Miscellaneous'
|
||||
if len(group) == 1:
|
||||
yield f'{prefix}: {next(self._format_cleanup_misc_items(group))}'
|
||||
return
|
||||
|
||||
yield prefix
|
||||
for message in self._format_cleanup_misc_items(group):
|
||||
yield f'\t\t- {message}'
|
||||
|
||||
def _format_cleanup_misc_items(self, group):
|
||||
for authors, infos in group.items():
|
||||
message = ', '.join(
|
||||
self._format_message_link(None, info.commit.hash)
|
||||
for info in sorted(infos, key=lambda item: item.commit.hash or ''))
|
||||
yield f'{message} by {self._format_authors(authors)}'
|
||||
|
||||
def format_single_change(self, info):
|
||||
message = self._format_message_link(info.message, info.commit.hash)
|
||||
if info.issues:
|
||||
message = f'{message} ({self._format_issues(info.issues)})'
|
||||
|
||||
if info.commit.authors:
|
||||
message = f'{message} by {self._format_authors(info.commit.authors)}'
|
||||
|
||||
if info.fixes:
|
||||
fix_message = ', '.join(f'{self._format_message_link(None, fix.hash)}' for fix in info.fixes)
|
||||
|
||||
authors = sorted({author for fix in info.fixes for author in fix.authors}, key=str.casefold)
|
||||
if authors != info.commit.authors:
|
||||
fix_message = f'{fix_message} by {self._format_authors(authors)}'
|
||||
|
||||
message = f'{message} (With fixes in {fix_message})'
|
||||
|
||||
return message
|
||||
|
||||
def _format_message_link(self, message, hash):
|
||||
assert message or hash, 'Improperly defined commit message or override'
|
||||
message = message if message else hash[:HASH_LENGTH]
|
||||
return f'[{message}]({self.repo_url}/commit/{hash})' if hash else message
|
||||
|
||||
def _format_issues(self, issues):
|
||||
return ', '.join(f'[#{issue}]({self.repo_url}/issues/{issue})' for issue in issues)
|
||||
|
||||
@staticmethod
|
||||
def _format_authors(authors):
|
||||
return ', '.join(f'[{author}]({BASE_URL}/{author})' for author in authors)
|
||||
|
||||
@property
|
||||
def repo_url(self):
|
||||
return f'{BASE_URL}/{self._repo}'
|
||||
|
||||
|
||||
class CommitRange:
|
||||
COMMAND = 'git'
|
||||
COMMIT_SEPARATOR = '-----'
|
||||
|
||||
AUTHOR_INDICATOR_RE = re.compile(r'Authored by:? ', re.IGNORECASE)
|
||||
MESSAGE_RE = re.compile(r'''
|
||||
(?:\[
|
||||
(?P<prefix>[^\]\/:,]+)
|
||||
(?:/(?P<details>[^\]:,]+))?
|
||||
(?:[:,](?P<sub_details>[^\]]+))?
|
||||
\]\ )?
|
||||
(?:(?P<sub_details_alt>`?[^:`]+`?): )?
|
||||
(?P<message>.+?)
|
||||
(?:\ \((?P<issues>\#\d+(?:,\ \#\d+)*)\))?
|
||||
''', re.VERBOSE | re.DOTALL)
|
||||
EXTRACTOR_INDICATOR_RE = re.compile(r'(?:Fix|Add)\s+Extractors?', re.IGNORECASE)
|
||||
FIXES_RE = re.compile(r'(?i:Fix(?:es)?(?:\s+bugs?)?(?:\s+in|\s+for)?|Revert)\s+([\da-f]{40})')
|
||||
UPSTREAM_MERGE_RE = re.compile(r'Update to ytdl-commit-([\da-f]+)')
|
||||
|
||||
def __init__(self, start, end, default_author=None):
|
||||
self._start, self._end = start, end
|
||||
self._commits, self._fixes = self._get_commits_and_fixes(default_author)
|
||||
self._commits_added = []
|
||||
|
||||
def __iter__(self):
|
||||
return iter(itertools.chain(self._commits.values(), self._commits_added))
|
||||
|
||||
def __len__(self):
|
||||
return len(self._commits) + len(self._commits_added)
|
||||
|
||||
def __contains__(self, commit):
|
||||
if isinstance(commit, Commit):
|
||||
if not commit.hash:
|
||||
return False
|
||||
commit = commit.hash
|
||||
|
||||
return commit in self._commits
|
||||
|
||||
def _get_commits_and_fixes(self, default_author):
|
||||
result = run_process(
|
||||
self.COMMAND, 'log', f'--format=%H%n%s%n%b%n{self.COMMIT_SEPARATOR}',
|
||||
f'{self._start}..{self._end}' if self._start else self._end).stdout
|
||||
|
||||
commits = {}
|
||||
fixes = defaultdict(list)
|
||||
lines = iter(result.splitlines(False))
|
||||
for i, commit_hash in enumerate(lines):
|
||||
short = next(lines)
|
||||
skip = short.startswith('Release ') or short == '[version] update'
|
||||
|
||||
authors = [default_author] if default_author else []
|
||||
for line in iter(lambda: next(lines), self.COMMIT_SEPARATOR):
|
||||
match = self.AUTHOR_INDICATOR_RE.match(line)
|
||||
if match:
|
||||
authors = sorted(map(str.strip, line[match.end():].split(',')), key=str.casefold)
|
||||
|
||||
commit = Commit(commit_hash, short, authors)
|
||||
if skip and (self._start or not i):
|
||||
logger.debug(f'Skipped commit: {commit}')
|
||||
continue
|
||||
elif skip:
|
||||
logger.debug(f'Reached Release commit, breaking: {commit}')
|
||||
break
|
||||
|
||||
fix_match = self.FIXES_RE.search(commit.short)
|
||||
if fix_match:
|
||||
commitish = fix_match.group(1)
|
||||
fixes[commitish].append(commit)
|
||||
|
||||
commits[commit.hash] = commit
|
||||
|
||||
for commitish, fix_commits in fixes.items():
|
||||
if commitish in commits:
|
||||
hashes = ', '.join(commit.hash[:HASH_LENGTH] for commit in fix_commits)
|
||||
logger.info(f'Found fix(es) for {commitish[:HASH_LENGTH]}: {hashes}')
|
||||
for fix_commit in fix_commits:
|
||||
del commits[fix_commit.hash]
|
||||
else:
|
||||
logger.debug(f'Commit with fixes not in changes: {commitish[:HASH_LENGTH]}')
|
||||
|
||||
return commits, fixes
|
||||
|
||||
def apply_overrides(self, overrides):
|
||||
for override in overrides:
|
||||
when = override.get('when')
|
||||
if when and when not in self and when != self._start:
|
||||
logger.debug(f'Ignored {when!r}, not in commits {self._start!r}')
|
||||
continue
|
||||
|
||||
override_hash = override.get('hash')
|
||||
if override['action'] == 'add':
|
||||
commit = Commit(override.get('hash'), override['short'], override.get('authors') or [])
|
||||
logger.info(f'ADD {commit}')
|
||||
self._commits_added.append(commit)
|
||||
|
||||
elif override['action'] == 'remove':
|
||||
if override_hash in self._commits:
|
||||
logger.info(f'REMOVE {self._commits[override_hash]}')
|
||||
del self._commits[override_hash]
|
||||
|
||||
elif override['action'] == 'change':
|
||||
if override_hash not in self._commits:
|
||||
continue
|
||||
commit = Commit(override_hash, override['short'], override['authors'])
|
||||
logger.info(f'CHANGE {self._commits[commit.hash]} -> {commit}')
|
||||
self._commits[commit.hash] = commit
|
||||
|
||||
self._commits = {key: value for key, value in reversed(self._commits.items())}
|
||||
|
||||
def groups(self):
|
||||
groups = defaultdict(list)
|
||||
for commit in self:
|
||||
upstream_re = self.UPSTREAM_MERGE_RE.match(commit.short)
|
||||
if upstream_re:
|
||||
commit.short = f'[upstream] Merge up to youtube-dl {upstream_re.group(1)}'
|
||||
|
||||
match = self.MESSAGE_RE.fullmatch(commit.short)
|
||||
if not match:
|
||||
logger.error(f'Error parsing short commit message: {commit.short!r}')
|
||||
continue
|
||||
|
||||
prefix, details, sub_details, sub_details_alt, message, issues = match.groups()
|
||||
group = None
|
||||
if prefix:
|
||||
if prefix == 'priority':
|
||||
prefix, _, details = (details or '').partition('/')
|
||||
logger.debug(f'Priority: {message!r}')
|
||||
group = CommitGroup.PRIORITY
|
||||
|
||||
if not details and prefix:
|
||||
if prefix not in ('core', 'downloader', 'extractor', 'misc', 'postprocessor', 'upstream'):
|
||||
logger.debug(f'Replaced details with {prefix!r}')
|
||||
details = prefix or None
|
||||
|
||||
if details == 'common':
|
||||
details = None
|
||||
|
||||
if details:
|
||||
details = details.strip()
|
||||
|
||||
else:
|
||||
group = CommitGroup.CORE
|
||||
|
||||
sub_details = f'{sub_details or ""},{sub_details_alt or ""}'.replace(':', ',')
|
||||
sub_details = tuple(filter(None, map(str.strip, sub_details.split(','))))
|
||||
|
||||
issues = [issue.strip()[1:] for issue in issues.split(',')] if issues else []
|
||||
|
||||
if not group:
|
||||
group = CommitGroup.get(prefix.lower())
|
||||
if not group:
|
||||
if self.EXTRACTOR_INDICATOR_RE.search(commit.short):
|
||||
group = CommitGroup.EXTRACTOR
|
||||
else:
|
||||
group = CommitGroup.POSTPROCESSOR
|
||||
logger.warning(f'Failed to map {commit.short!r}, selected {group.name}')
|
||||
|
||||
commit_info = CommitInfo(
|
||||
details, sub_details, message.strip(),
|
||||
issues, commit, self._fixes[commit.hash])
|
||||
logger.debug(f'Resolved {commit.short!r} to {commit_info!r}')
|
||||
groups[group].append(commit_info)
|
||||
|
||||
return groups
|
||||
|
||||
|
||||
def get_new_contributors(contributors_path, commits):
|
||||
contributors = set()
|
||||
if contributors_path.exists():
|
||||
for line in read_file(contributors_path).splitlines():
|
||||
author, _, _ = line.strip().partition(' (')
|
||||
authors = author.split('/')
|
||||
contributors.update(map(str.casefold, authors))
|
||||
|
||||
new_contributors = set()
|
||||
for commit in commits:
|
||||
for author in commit.authors:
|
||||
author_folded = author.casefold()
|
||||
if author_folded not in contributors:
|
||||
contributors.add(author_folded)
|
||||
new_contributors.add(author)
|
||||
|
||||
return sorted(new_contributors, key=str.casefold)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Create a changelog markdown from a git commit range')
|
||||
parser.add_argument(
|
||||
'commitish', default='HEAD', nargs='?',
|
||||
help='The commitish to create the range from (default: %(default)s)')
|
||||
parser.add_argument(
|
||||
'-v', '--verbosity', action='count', default=0,
|
||||
help='increase verbosity (can be used twice)')
|
||||
parser.add_argument(
|
||||
'-c', '--contributors', action='store_true',
|
||||
help='update CONTRIBUTORS file (default: %(default)s)')
|
||||
parser.add_argument(
|
||||
'--contributors-path', type=Path, default=LOCATION_PATH.parent / 'CONTRIBUTORS',
|
||||
help='path to the CONTRIBUTORS file')
|
||||
parser.add_argument(
|
||||
'--no-override', action='store_true',
|
||||
help='skip override json in commit generation (default: %(default)s)')
|
||||
parser.add_argument(
|
||||
'--override-path', type=Path, default=LOCATION_PATH / 'changelog_override.json',
|
||||
help='path to the changelog_override.json file')
|
||||
parser.add_argument(
|
||||
'--default-author', default='pukkandan',
|
||||
help='the author to use without a author indicator (default: %(default)s)')
|
||||
parser.add_argument(
|
||||
'--repo', default='yt-dlp/yt-dlp',
|
||||
help='the github repository to use for the operations (default: %(default)s)')
|
||||
args = parser.parse_args()
|
||||
|
||||
logging.basicConfig(
|
||||
datefmt='%Y-%m-%d %H-%M-%S', format='{asctime} | {levelname:<8} | {message}',
|
||||
level=logging.WARNING - 10 * args.verbosity, style='{', stream=sys.stderr)
|
||||
|
||||
commits = CommitRange(None, args.commitish, args.default_author)
|
||||
|
||||
if not args.no_override:
|
||||
if args.override_path.exists():
|
||||
overrides = json.loads(read_file(args.override_path))
|
||||
commits.apply_overrides(overrides)
|
||||
else:
|
||||
logger.warning(f'File {args.override_path.as_posix()} does not exist')
|
||||
|
||||
logger.info(f'Loaded {len(commits)} commits')
|
||||
|
||||
new_contributors = get_new_contributors(args.contributors_path, commits)
|
||||
if new_contributors:
|
||||
if args.contributors:
|
||||
write_file(args.contributors_path, '\n'.join(new_contributors) + '\n', mode='a')
|
||||
logger.info(f'New contributors: {", ".join(new_contributors)}')
|
||||
|
||||
print(Changelog(commits.groups(), args.repo))
|
||||
@@ -24,6 +24,8 @@ VERBOSE_TMPL = '''
|
||||
options:
|
||||
- label: Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
|
||||
required: true
|
||||
- label: "If using API, add `'verbose': True` to `YoutubeDL` params instead"
|
||||
required: false
|
||||
- label: Copy the WHOLE output (starting with `[debug] Command-line config`) and insert it below
|
||||
required: true
|
||||
- type: textarea
|
||||
|
||||
@@ -45,33 +45,43 @@ switch_col_width = len(re.search(r'(?m)^\s{5,}', options).group())
|
||||
delim = f'\n{" " * switch_col_width}'
|
||||
|
||||
PATCHES = (
|
||||
( # Standardize update message
|
||||
( # Standardize `--update` message
|
||||
r'(?m)^( -U, --update\s+).+(\n \s.+)*$',
|
||||
r'\1Update this program to the latest version',
|
||||
),
|
||||
( # Headings
|
||||
( # Headings
|
||||
r'(?m)^ (\w.+\n)( (?=\w))?',
|
||||
r'## \1'
|
||||
),
|
||||
( # Do not split URLs
|
||||
( # Fixup `--date` formatting
|
||||
rf'(?m)( --date DATE.+({delim}[^\[]+)*)\[.+({delim}.+)*$',
|
||||
(rf'\1[now|today|yesterday][-N[day|week|month|year]].{delim}'
|
||||
f'E.g. "--date today-2weeks" downloads only{delim}'
|
||||
'videos uploaded on the same day two weeks ago'),
|
||||
),
|
||||
( # Do not split URLs
|
||||
rf'({delim[:-1]})? (?P<label>\[\S+\] )?(?P<url>https?({delim})?:({delim})?/({delim})?/(({delim})?\S+)+)\s',
|
||||
lambda mobj: ''.join((delim, mobj.group('label') or '', re.sub(r'\s+', '', mobj.group('url')), '\n'))
|
||||
),
|
||||
( # Do not split "words"
|
||||
( # Do not split "words"
|
||||
rf'(?m)({delim}\S+)+$',
|
||||
lambda mobj: ''.join((delim, mobj.group(0).replace(delim, '')))
|
||||
),
|
||||
( # Allow overshooting last line
|
||||
( # Allow overshooting last line
|
||||
rf'(?m)^(?P<prev>.+)${delim}(?P<current>.+)$(?!{delim})',
|
||||
lambda mobj: (mobj.group().replace(delim, ' ')
|
||||
if len(mobj.group()) - len(delim) + 1 <= max_width + ALLOWED_OVERSHOOT
|
||||
else mobj.group())
|
||||
),
|
||||
( # Avoid newline when a space is available b/w switch and description
|
||||
( # Avoid newline when a space is available b/w switch and description
|
||||
DISABLE_PATCH, # This creates issues with prepare_manpage
|
||||
r'(?m)^(\s{4}-.{%d})(%s)' % (switch_col_width - 6, delim),
|
||||
r'\1 '
|
||||
),
|
||||
( # Replace brackets with a Markdown link
|
||||
r'SponsorBlock API \((http.+)\)',
|
||||
r'[SponsorBlock API](\1)'
|
||||
),
|
||||
)
|
||||
|
||||
readme = read_file(README_FILE)
|
||||
|
||||
@@ -7,16 +7,17 @@ import sys
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
|
||||
import argparse
|
||||
import contextlib
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
from devscripts.utils import read_version, write_file
|
||||
from devscripts.utils import read_version, run_process, write_file
|
||||
|
||||
|
||||
def get_new_version(revision):
|
||||
version = datetime.utcnow().strftime('%Y.%m.%d')
|
||||
def get_new_version(version, revision):
|
||||
if not version:
|
||||
version = datetime.utcnow().strftime('%Y.%m.%d')
|
||||
|
||||
if revision:
|
||||
assert revision.isdigit(), 'Revision must be a number'
|
||||
@@ -30,27 +31,41 @@ def get_new_version(revision):
|
||||
|
||||
def get_git_head():
|
||||
with contextlib.suppress(Exception):
|
||||
sp = subprocess.Popen(['git', 'rev-parse', '--short', 'HEAD'], stdout=subprocess.PIPE)
|
||||
return sp.communicate()[0].decode().strip() or None
|
||||
return run_process('git', 'rev-parse', 'HEAD').stdout.strip()
|
||||
|
||||
|
||||
VERSION = get_new_version((sys.argv + [''])[1])
|
||||
GIT_HEAD = get_git_head()
|
||||
|
||||
VERSION_FILE = f'''\
|
||||
VERSION_TEMPLATE = '''\
|
||||
# Autogenerated by devscripts/update-version.py
|
||||
|
||||
__version__ = {VERSION!r}
|
||||
__version__ = {version!r}
|
||||
|
||||
RELEASE_GIT_HEAD = {GIT_HEAD!r}
|
||||
RELEASE_GIT_HEAD = {git_head!r}
|
||||
|
||||
VARIANT = None
|
||||
|
||||
UPDATE_HINT = None
|
||||
|
||||
CHANNEL = {channel!r}
|
||||
'''
|
||||
|
||||
write_file('yt_dlp/version.py', VERSION_FILE)
|
||||
github_output = os.getenv('GITHUB_OUTPUT')
|
||||
if github_output:
|
||||
write_file(github_output, f'ytdlp_version={VERSION}\n', 'a')
|
||||
print(f'\nVersion = {VERSION}, Git HEAD = {GIT_HEAD}')
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description='Update the version.py file')
|
||||
parser.add_argument(
|
||||
'-c', '--channel', choices=['stable', 'nightly'], default='stable',
|
||||
help='Select update channel (default: %(default)s)')
|
||||
parser.add_argument(
|
||||
'-o', '--output', default='yt_dlp/version.py',
|
||||
help='The output file to write to (default: %(default)s)')
|
||||
parser.add_argument(
|
||||
'version', nargs='?', default=None,
|
||||
help='A version or revision to use instead of generating one')
|
||||
args = parser.parse_args()
|
||||
|
||||
git_head = get_git_head()
|
||||
version = (
|
||||
args.version if args.version and '.' in args.version
|
||||
else get_new_version(None, args.version))
|
||||
write_file(args.output, VERSION_TEMPLATE.format(
|
||||
version=version, git_head=git_head, channel=args.channel))
|
||||
|
||||
print(f'version={version} ({args.channel}), head={git_head}')
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import argparse
|
||||
import functools
|
||||
import subprocess
|
||||
|
||||
|
||||
def read_file(fname):
|
||||
@@ -12,8 +13,8 @@ def write_file(fname, content, mode='w'):
|
||||
return f.write(content)
|
||||
|
||||
|
||||
# Get the version without importing the package
|
||||
def read_version(fname='yt_dlp/version.py'):
|
||||
"""Get the version without importing the package"""
|
||||
exec(compile(read_file(fname), fname, 'exec'))
|
||||
return locals()['__version__']
|
||||
|
||||
@@ -33,3 +34,13 @@ def get_filename_args(has_infile=False, default_outfile=None):
|
||||
|
||||
def compose_functions(*functions):
|
||||
return lambda x: functools.reduce(lambda y, f: f(y), functions, x)
|
||||
|
||||
|
||||
def run_process(*args, **kwargs):
|
||||
kwargs.setdefault('text', True)
|
||||
kwargs.setdefault('check', True)
|
||||
kwargs.setdefault('capture_output', True)
|
||||
if kwargs['text']:
|
||||
kwargs.setdefault('encoding', 'utf-8')
|
||||
kwargs.setdefault('errors', 'replace')
|
||||
return subprocess.run(args, **kwargs)
|
||||
|
||||
29
public.key
Normal file
29
public.key
Normal file
@@ -0,0 +1,29 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQINBGP78C4BEAD0rF9zjGPAt0thlt5C1ebzccAVX7Nb1v+eqQjk+WEZdTETVCg3
|
||||
WAM5ngArlHdm/fZqzUgO+pAYrB60GKeg7ffUDf+S0XFKEZdeRLYeAaqqKhSibVal
|
||||
DjvOBOztu3W607HLETQAqA7wTPuIt2WqmpL60NIcyr27LxqmgdN3mNvZ2iLO+bP0
|
||||
nKR/C+PgE9H4ytywDa12zMx6PmZCnVOOOu6XZEFmdUxxdQ9fFDqd9LcBKY2LDOcS
|
||||
Yo1saY0YWiZWHtzVoZu1kOzjnS5Fjq/yBHJLImDH7pNxHm7s/PnaurpmQFtDFruk
|
||||
t+2lhDnpKUmGr/I/3IHqH/X+9nPoS4uiqQ5HpblB8BK+4WfpaiEg75LnvuOPfZIP
|
||||
KYyXa/0A7QojMwgOrD88ozT+VCkKkkJ+ijXZ7gHNjmcBaUdKK7fDIEOYI63Lyc6Q
|
||||
WkGQTigFffSUXWHDCO9aXNhP3ejqFWgGMtCUsrbkcJkWuWY7q5ARy/05HbSM3K4D
|
||||
U9eqtnxmiV1WQ8nXuI9JgJQRvh5PTkny5LtxqzcmqvWO9TjHBbrs14BPEO9fcXxK
|
||||
L/CFBbzXDSvvAgArdqqlMoncQ/yicTlfL6qzJ8EKFiqW14QMTdAn6SuuZTodXCTi
|
||||
InwoT7WjjuFPKKdvfH1GP4bnqdzTnzLxCSDIEtfyfPsIX+9GI7Jkk/zZjQARAQAB
|
||||
tDdTaW1vbiBTYXdpY2tpICh5dC1kbHAgc2lnbmluZyBrZXkpIDxjb250YWN0QGdy
|
||||
dWI0ay54eXo+iQJOBBMBCgA4FiEErAy75oSNaoc0ZK9OV89lkztadYEFAmP78C4C
|
||||
GwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQV89lkztadYEVqQ//cW7TxhXg
|
||||
7Xbh2EZQzXml0egn6j8QaV9KzGragMiShrlvTO2zXfLXqyizrFP4AspgjSn/4NrI
|
||||
8mluom+Yi+qr7DXT4BjQqIM9y3AjwZPdywe912Lxcw52NNoPZCm24I9T7ySc8lmR
|
||||
FQvZC0w4H/VTNj/2lgJ1dwMflpwvNRiWa5YzcFGlCUeDIPskLx9++AJE+xwU3LYm
|
||||
jQQsPBqpHHiTBEJzMLl+rfd9Fg4N+QNzpFkTDW3EPerLuvJniSBBwZthqxeAtw4M
|
||||
UiAXh6JvCc2hJkKCoygRfM281MeolvmsGNyQm+axlB0vyldiPP6BnaRgZlx+l6MU
|
||||
cPqgHblb7RW5j9lfr6OYL7SceBIHNv0CFrt1OnkGo/tVMwcs8LH3Ae4a7UJlIceL
|
||||
V54aRxSsZU7w4iX+PB79BWkEsQzwKrUuJVOeL4UDwWajp75OFaUqbS/slDDVXvK5
|
||||
OIeuth3mA/adjdvgjPxhRQjA3l69rRWIJDrqBSHldmRsnX6cvXTDy8wSXZgy51lP
|
||||
m4IVLHnCy9m4SaGGoAsfTZS0cC9FgjUIyTyrq9M67wOMpUxnuB0aRZgJE1DsI23E
|
||||
qdvcSNVlO+39xM/KPWUEh6b83wMn88QeW+DCVGWACQq5N3YdPnAJa50617fGbY6I
|
||||
gXIoRHXkDqe23PZ/jURYCv0sjVtjPoVC+bg=
|
||||
=bJkn
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@@ -28,14 +28,14 @@
|
||||
- **abcnews:video**
|
||||
- **abcotvs**: ABC Owned Television Stations
|
||||
- **abcotvs:clips**
|
||||
- **AbemaTV**: [<abbr title="netrc machine"><em>abematv</em></abbr>]
|
||||
- **AbemaTV**: [*abematv*](## "netrc machine")
|
||||
- **AbemaTVTitle**
|
||||
- **AcademicEarth:Course**
|
||||
- **acast**
|
||||
- **acast:channel**
|
||||
- **AcFunBangumi**
|
||||
- **AcFunVideo**
|
||||
- **ADN**: [<abbr title="netrc machine"><em>animationdigitalnetwork</em></abbr>] Animation Digital Network
|
||||
- **ADN**: [*animationdigitalnetwork*](## "netrc machine") Animation Digital Network
|
||||
- **AdobeConnect**
|
||||
- **adobetv**
|
||||
- **adobetv:channel**
|
||||
@@ -47,8 +47,8 @@
|
||||
- **aenetworks:collection**
|
||||
- **aenetworks:show**
|
||||
- **AeonCo**
|
||||
- **afreecatv**: [<abbr title="netrc machine"><em>afreecatv</em></abbr>] afreecatv.com
|
||||
- **afreecatv:live**: [<abbr title="netrc machine"><em>afreecatv</em></abbr>] afreecatv.com
|
||||
- **afreecatv**: [*afreecatv*](## "netrc machine") afreecatv.com
|
||||
- **afreecatv:live**: [*afreecatv*](## "netrc machine") afreecatv.com
|
||||
- **afreecatv:user**
|
||||
- **AirMozilla**
|
||||
- **AirTV**
|
||||
@@ -59,8 +59,8 @@
|
||||
- **AlphaPorno**
|
||||
- **Alsace20TV**
|
||||
- **Alsace20TVEmbed**
|
||||
- **Alura**: [<abbr title="netrc machine"><em>alura</em></abbr>]
|
||||
- **AluraCourse**: [<abbr title="netrc machine"><em>aluracourse</em></abbr>]
|
||||
- **Alura**: [*alura*](## "netrc machine")
|
||||
- **AluraCourse**: [*aluracourse*](## "netrc machine")
|
||||
- **Amara**
|
||||
- **AmazonMiniTV**
|
||||
- **amazonminitv:season**: Amazon MiniTV Season, "minitv:season:" prefix
|
||||
@@ -100,7 +100,7 @@
|
||||
- **ArteTVPlaylist**
|
||||
- **AsianCrush**
|
||||
- **AsianCrushPlaylist**
|
||||
- **AtresPlayer**: [<abbr title="netrc machine"><em>atresplayer</em></abbr>]
|
||||
- **AtresPlayer**: [*atresplayer*](## "netrc machine")
|
||||
- **AtScaleConfEvent**
|
||||
- **ATTTechChannel**
|
||||
- **ATVAt**
|
||||
@@ -128,15 +128,15 @@
|
||||
- **Bandcamp:user**
|
||||
- **Bandcamp:weekly**
|
||||
- **BannedVideo**
|
||||
- **bbc**: [<abbr title="netrc machine"><em>bbc</em></abbr>] BBC
|
||||
- **bbc.co.uk**: [<abbr title="netrc machine"><em>bbc</em></abbr>] BBC iPlayer
|
||||
- **bbc**: [*bbc*](## "netrc machine") BBC
|
||||
- **bbc.co.uk**: [*bbc*](## "netrc machine") BBC iPlayer
|
||||
- **bbc.co.uk:article**: BBC articles
|
||||
- **bbc.co.uk:iplayer:episodes**
|
||||
- **bbc.co.uk:iplayer:group**
|
||||
- **bbc.co.uk:playlist**
|
||||
- **BBVTV**: [<abbr title="netrc machine"><em>bbvtv</em></abbr>]
|
||||
- **BBVTVLive**: [<abbr title="netrc machine"><em>bbvtv</em></abbr>]
|
||||
- **BBVTVRecordings**: [<abbr title="netrc machine"><em>bbvtv</em></abbr>]
|
||||
- **BBVTV**: [*bbvtv*](## "netrc machine")
|
||||
- **BBVTVLive**: [*bbvtv*](## "netrc machine")
|
||||
- **BBVTVRecordings**: [*bbvtv*](## "netrc machine")
|
||||
- **BeatBumpPlaylist**
|
||||
- **BeatBumpVideo**
|
||||
- **Beatport**
|
||||
@@ -165,8 +165,8 @@
|
||||
- **BilibiliSpaceAudio**
|
||||
- **BilibiliSpacePlaylist**
|
||||
- **BilibiliSpaceVideo**
|
||||
- **BiliIntl**: [<abbr title="netrc machine"><em>biliintl</em></abbr>]
|
||||
- **biliIntl:series**: [<abbr title="netrc machine"><em>biliintl</em></abbr>]
|
||||
- **BiliIntl**: [*biliintl*](## "netrc machine")
|
||||
- **biliIntl:series**: [*biliintl*](## "netrc machine")
|
||||
- **BiliLive**
|
||||
- **BioBioChileTV**
|
||||
- **Biography**
|
||||
@@ -232,7 +232,7 @@
|
||||
- **cbssports:embed**
|
||||
- **CCMA**
|
||||
- **CCTV**: 央视网
|
||||
- **CDA**: [<abbr title="netrc machine"><em>cdapl</em></abbr>]
|
||||
- **CDA**: [*cdapl*](## "netrc machine")
|
||||
- **Cellebrite**
|
||||
- **CeskaTelevize**
|
||||
- **CGTN**
|
||||
@@ -286,8 +286,8 @@
|
||||
- **CrooksAndLiars**
|
||||
- **CrowdBunker**
|
||||
- **CrowdBunkerChannel**
|
||||
- **crunchyroll**: [<abbr title="netrc machine"><em>crunchyroll</em></abbr>]
|
||||
- **crunchyroll:playlist**: [<abbr title="netrc machine"><em>crunchyroll</em></abbr>]
|
||||
- **crunchyroll**: [*crunchyroll*](## "netrc machine")
|
||||
- **crunchyroll:playlist**: [*crunchyroll*](## "netrc machine")
|
||||
- **CSpan**: C-SPAN
|
||||
- **CSpanCongress**
|
||||
- **CtsNews**: 華視新聞
|
||||
@@ -295,18 +295,18 @@
|
||||
- **CTVNews**
|
||||
- **cu.ntv.co.jp**: Nippon Television Network
|
||||
- **CultureUnplugged**
|
||||
- **curiositystream**: [<abbr title="netrc machine"><em>curiositystream</em></abbr>]
|
||||
- **curiositystream:collections**: [<abbr title="netrc machine"><em>curiositystream</em></abbr>]
|
||||
- **curiositystream:series**: [<abbr title="netrc machine"><em>curiositystream</em></abbr>]
|
||||
- **curiositystream**: [*curiositystream*](## "netrc machine")
|
||||
- **curiositystream:collections**: [*curiositystream*](## "netrc machine")
|
||||
- **curiositystream:series**: [*curiositystream*](## "netrc machine")
|
||||
- **CWTV**
|
||||
- **Cybrary**: [<abbr title="netrc machine"><em>cybrary</em></abbr>]
|
||||
- **CybraryCourse**: [<abbr title="netrc machine"><em>cybrary</em></abbr>]
|
||||
- **Cybrary**: [*cybrary*](## "netrc machine")
|
||||
- **CybraryCourse**: [*cybrary*](## "netrc machine")
|
||||
- **Daftsex**
|
||||
- **DagelijkseKost**: dagelijksekost.een.be
|
||||
- **DailyMail**
|
||||
- **dailymotion**: [<abbr title="netrc machine"><em>dailymotion</em></abbr>]
|
||||
- **dailymotion:playlist**: [<abbr title="netrc machine"><em>dailymotion</em></abbr>]
|
||||
- **dailymotion:user**: [<abbr title="netrc machine"><em>dailymotion</em></abbr>]
|
||||
- **dailymotion**: [*dailymotion*](## "netrc machine")
|
||||
- **dailymotion:playlist**: [*dailymotion*](## "netrc machine")
|
||||
- **dailymotion:user**: [*dailymotion*](## "netrc machine")
|
||||
- **DailyWire**
|
||||
- **DailyWirePodcast**
|
||||
- **damtomo:record**
|
||||
@@ -328,7 +328,7 @@
|
||||
- **DeuxMNews**
|
||||
- **DHM**: Filmarchiv - Deutsches Historisches Museum
|
||||
- **Digg**
|
||||
- **DigitalConcertHall**: [<abbr title="netrc machine"><em>digitalconcerthall</em></abbr>] DigitalConcertHall extractor
|
||||
- **DigitalConcertHall**: [*digitalconcerthall*](## "netrc machine") DigitalConcertHall extractor
|
||||
- **DigitallySpeaking**
|
||||
- **Digiteka**
|
||||
- **Discovery**
|
||||
@@ -351,7 +351,7 @@
|
||||
- **DRBonanza**
|
||||
- **Drooble**
|
||||
- **Dropbox**
|
||||
- **Dropout**: [<abbr title="netrc machine"><em>dropout</em></abbr>]
|
||||
- **Dropout**: [*dropout*](## "netrc machine")
|
||||
- **DropoutSeason**
|
||||
- **DrTuber**
|
||||
- **drtv**
|
||||
@@ -373,9 +373,9 @@
|
||||
- **egghead:lesson**: egghead.io lesson
|
||||
- **ehftv**
|
||||
- **eHow**
|
||||
- **EinsUndEinsTV**: [<abbr title="netrc machine"><em>1und1tv</em></abbr>]
|
||||
- **EinsUndEinsTVLive**: [<abbr title="netrc machine"><em>1und1tv</em></abbr>]
|
||||
- **EinsUndEinsTVRecordings**: [<abbr title="netrc machine"><em>1und1tv</em></abbr>]
|
||||
- **EinsUndEinsTV**: [*1und1tv*](## "netrc machine")
|
||||
- **EinsUndEinsTVLive**: [*1und1tv*](## "netrc machine")
|
||||
- **EinsUndEinsTVRecordings**: [*1und1tv*](## "netrc machine")
|
||||
- **Einthusan**
|
||||
- **eitb.tv**
|
||||
- **EllenTube**
|
||||
@@ -390,7 +390,7 @@
|
||||
- **EpiconSeries**
|
||||
- **Epoch**
|
||||
- **Eporner**
|
||||
- **EroProfile**: [<abbr title="netrc machine"><em>eroprofile</em></abbr>]
|
||||
- **EroProfile**: [*eroprofile*](## "netrc machine")
|
||||
- **EroProfile:album**
|
||||
- **ertflix**: ERTFLIX videos
|
||||
- **ertflix:codename**: ERTFLIX videos by codename
|
||||
@@ -405,20 +405,20 @@
|
||||
- **EuropeanTour**
|
||||
- **Eurosport**
|
||||
- **EUScreen**
|
||||
- **EWETV**: [<abbr title="netrc machine"><em>ewetv</em></abbr>]
|
||||
- **EWETVLive**: [<abbr title="netrc machine"><em>ewetv</em></abbr>]
|
||||
- **EWETVRecordings**: [<abbr title="netrc machine"><em>ewetv</em></abbr>]
|
||||
- **EWETV**: [*ewetv*](## "netrc machine")
|
||||
- **EWETVLive**: [*ewetv*](## "netrc machine")
|
||||
- **EWETVRecordings**: [*ewetv*](## "netrc machine")
|
||||
- **ExpoTV**
|
||||
- **Expressen**
|
||||
- **ExtremeTube**
|
||||
- **EyedoTV**
|
||||
- **facebook**: [<abbr title="netrc machine"><em>facebook</em></abbr>]
|
||||
- **facebook**: [*facebook*](## "netrc machine")
|
||||
- **facebook:reel**
|
||||
- **FacebookPluginsVideo**
|
||||
- **fancode:live**: [<abbr title="netrc machine"><em>fancode</em></abbr>]
|
||||
- **fancode:vod**: [<abbr title="netrc machine"><em>fancode</em></abbr>]
|
||||
- **fancode:live**: [*fancode*](## "netrc machine")
|
||||
- **fancode:vod**: [*fancode*](## "netrc machine")
|
||||
- **faz.net**
|
||||
- **fc2**: [<abbr title="netrc machine"><em>fc2</em></abbr>]
|
||||
- **fc2**: [*fc2*](## "netrc machine")
|
||||
- **fc2:embed**
|
||||
- **fc2:live**
|
||||
- **Fczenit**
|
||||
@@ -452,20 +452,20 @@
|
||||
- **freespeech.org**
|
||||
- **freetv:series**
|
||||
- **FreeTvMovies**
|
||||
- **FrontendMasters**: [<abbr title="netrc machine"><em>frontendmasters</em></abbr>]
|
||||
- **FrontendMastersCourse**: [<abbr title="netrc machine"><em>frontendmasters</em></abbr>]
|
||||
- **FrontendMastersLesson**: [<abbr title="netrc machine"><em>frontendmasters</em></abbr>]
|
||||
- **FrontendMasters**: [*frontendmasters*](## "netrc machine")
|
||||
- **FrontendMastersCourse**: [*frontendmasters*](## "netrc machine")
|
||||
- **FrontendMastersLesson**: [*frontendmasters*](## "netrc machine")
|
||||
- **FujiTVFODPlus7**
|
||||
- **Funimation**: [<abbr title="netrc machine"><em>funimation</em></abbr>]
|
||||
- **funimation:page**: [<abbr title="netrc machine"><em>funimation</em></abbr>]
|
||||
- **funimation:show**: [<abbr title="netrc machine"><em>funimation</em></abbr>]
|
||||
- **Funimation**: [*funimation*](## "netrc machine")
|
||||
- **funimation:page**: [*funimation*](## "netrc machine")
|
||||
- **funimation:show**: [*funimation*](## "netrc machine")
|
||||
- **Funk**
|
||||
- **Fusion**
|
||||
- **Fux**
|
||||
- **FuyinTV**
|
||||
- **Gab**
|
||||
- **GabTV**
|
||||
- **Gaia**: [<abbr title="netrc machine"><em>gaia</em></abbr>]
|
||||
- **Gaia**: [*gaia*](## "netrc machine")
|
||||
- **GameInformer**
|
||||
- **GameJolt**
|
||||
- **GameJoltCommunity**
|
||||
@@ -477,9 +477,9 @@
|
||||
- **GameStar**
|
||||
- **Gaskrank**
|
||||
- **Gazeta**
|
||||
- **GDCVault**: [<abbr title="netrc machine"><em>gdcvault</em></abbr>]
|
||||
- **GDCVault**: [*gdcvault*](## "netrc machine")
|
||||
- **GediDigital**
|
||||
- **gem.cbc.ca**: [<abbr title="netrc machine"><em>cbcgem</em></abbr>]
|
||||
- **gem.cbc.ca**: [*cbcgem*](## "netrc machine")
|
||||
- **gem.cbc.ca:live**
|
||||
- **gem.cbc.ca:playlist**
|
||||
- **Genius**
|
||||
@@ -489,11 +489,11 @@
|
||||
- **Gfycat**
|
||||
- **GiantBomb**
|
||||
- **Giga**
|
||||
- **GlattvisionTV**: [<abbr title="netrc machine"><em>glattvisiontv</em></abbr>]
|
||||
- **GlattvisionTVLive**: [<abbr title="netrc machine"><em>glattvisiontv</em></abbr>]
|
||||
- **GlattvisionTVRecordings**: [<abbr title="netrc machine"><em>glattvisiontv</em></abbr>]
|
||||
- **GlattvisionTV**: [*glattvisiontv*](## "netrc machine")
|
||||
- **GlattvisionTVLive**: [*glattvisiontv*](## "netrc machine")
|
||||
- **GlattvisionTVRecordings**: [*glattvisiontv*](## "netrc machine")
|
||||
- **Glide**: Glide mobile video messages (glide.me)
|
||||
- **Globo**: [<abbr title="netrc machine"><em>globo</em></abbr>]
|
||||
- **Globo**: [*globo*](## "netrc machine")
|
||||
- **GloboArticle**
|
||||
- **glomex**: Glomex videos
|
||||
- **glomex:embed**: Glomex embedded videos
|
||||
@@ -507,7 +507,7 @@
|
||||
- **google:podcasts:feed**
|
||||
- **GoogleDrive**
|
||||
- **GoogleDrive:Folder**
|
||||
- **GoPlay**: [<abbr title="netrc machine"><em>goplay</em></abbr>]
|
||||
- **GoPlay**: [*goplay*](## "netrc machine")
|
||||
- **GoPro**
|
||||
- **Goshgay**
|
||||
- **GoToStage**
|
||||
@@ -527,7 +527,7 @@
|
||||
- **hgtv.com:show**
|
||||
- **HGTVDe**
|
||||
- **HGTVUsa**
|
||||
- **HiDive**: [<abbr title="netrc machine"><em>hidive</em></abbr>]
|
||||
- **HiDive**: [*hidive*](## "netrc machine")
|
||||
- **HistoricFilms**
|
||||
- **history:player**
|
||||
- **history:topic**: History.com Topic
|
||||
@@ -544,8 +544,8 @@
|
||||
- **Howcast**
|
||||
- **HowStuffWorks**
|
||||
- **hrfernsehen**
|
||||
- **HRTi**: [<abbr title="netrc machine"><em>hrti</em></abbr>]
|
||||
- **HRTiPlaylist**: [<abbr title="netrc machine"><em>hrti</em></abbr>]
|
||||
- **HRTi**: [*hrti*](## "netrc machine")
|
||||
- **HRTiPlaylist**: [*hrti*](## "netrc machine")
|
||||
- **HSEProduct**
|
||||
- **HSEShow**
|
||||
- **html5**
|
||||
@@ -575,19 +575,19 @@
|
||||
- **Inc**
|
||||
- **IndavideoEmbed**
|
||||
- **InfoQ**
|
||||
- **Instagram**: [<abbr title="netrc machine"><em>instagram</em></abbr>]
|
||||
- **instagram:story**: [<abbr title="netrc machine"><em>instagram</em></abbr>]
|
||||
- **instagram:tag**: [<abbr title="netrc machine"><em>instagram</em></abbr>] Instagram hashtag search URLs
|
||||
- **instagram:user**: [<abbr title="netrc machine"><em>instagram</em></abbr>] Instagram user profile
|
||||
- **Instagram**: [*instagram*](## "netrc machine")
|
||||
- **instagram:story**: [*instagram*](## "netrc machine")
|
||||
- **instagram:tag**: [*instagram*](## "netrc machine") Instagram hashtag search URLs
|
||||
- **instagram:user**: [*instagram*](## "netrc machine") Instagram user profile
|
||||
- **InstagramIOS**: IOS instagram:// URL
|
||||
- **Internazionale**
|
||||
- **InternetVideoArchive**
|
||||
- **InvestigationDiscovery**
|
||||
- **IPrima**: [<abbr title="netrc machine"><em>iprima</em></abbr>]
|
||||
- **IPrima**: [*iprima*](## "netrc machine")
|
||||
- **IPrimaCNN**
|
||||
- **iq.com**: International version of iQiyi
|
||||
- **iq.com:album**
|
||||
- **iqiyi**: [<abbr title="netrc machine"><em>iqiyi</em></abbr>] 爱奇艺
|
||||
- **iqiyi**: [*iqiyi*](## "netrc machine") 爱奇艺
|
||||
- **IslamChannel**
|
||||
- **IslamChannelSeries**
|
||||
- **IsraelNationalNews**
|
||||
@@ -660,9 +660,11 @@
|
||||
- **LcpPlay**
|
||||
- **Le**: 乐视网
|
||||
- **Lecture2Go**
|
||||
- **Lecturio**: [<abbr title="netrc machine"><em>lecturio</em></abbr>]
|
||||
- **LecturioCourse**: [<abbr title="netrc machine"><em>lecturio</em></abbr>]
|
||||
- **LecturioDeCourse**: [<abbr title="netrc machine"><em>lecturio</em></abbr>]
|
||||
- **Lecturio**: [*lecturio*](## "netrc machine")
|
||||
- **LecturioCourse**: [*lecturio*](## "netrc machine")
|
||||
- **LecturioDeCourse**: [*lecturio*](## "netrc machine")
|
||||
- **LeFigaroVideoEmbed**
|
||||
- **LeFigaroVideoSection**
|
||||
- **LEGO**
|
||||
- **Lemonde**
|
||||
- **Lenta**
|
||||
@@ -678,10 +680,10 @@
|
||||
- **limelight:channel_list**
|
||||
- **LineLive**
|
||||
- **LineLiveChannel**
|
||||
- **LinkedIn**: [<abbr title="netrc machine"><em>linkedin</em></abbr>]
|
||||
- **linkedin:learning**: [<abbr title="netrc machine"><em>linkedin</em></abbr>]
|
||||
- **linkedin:learning:course**: [<abbr title="netrc machine"><em>linkedin</em></abbr>]
|
||||
- **LinuxAcademy**: [<abbr title="netrc machine"><em>linuxacademy</em></abbr>]
|
||||
- **LinkedIn**: [*linkedin*](## "netrc machine")
|
||||
- **linkedin:learning**: [*linkedin*](## "netrc machine")
|
||||
- **linkedin:learning:course**: [*linkedin*](## "netrc machine")
|
||||
- **LinuxAcademy**: [*linuxacademy*](## "netrc machine")
|
||||
- **Liputan6**
|
||||
- **ListenNotes**
|
||||
- **LiTV**
|
||||
@@ -696,8 +698,9 @@
|
||||
- **LoveHomePorn**
|
||||
- **LRTStream**
|
||||
- **LRTVOD**
|
||||
- **lynda**: [<abbr title="netrc machine"><em>lynda</em></abbr>] lynda.com videos
|
||||
- **lynda:course**: [<abbr title="netrc machine"><em>lynda</em></abbr>] lynda.com online courses
|
||||
- **Lumni**
|
||||
- **lynda**: [*lynda*](## "netrc machine") lynda.com videos
|
||||
- **lynda:course**: [*lynda*](## "netrc machine") lynda.com online courses
|
||||
- **m6**
|
||||
- **MagentaMusik360**
|
||||
- **mailru**: Видео@Mail.Ru
|
||||
@@ -767,13 +770,13 @@
|
||||
- **mixcloud:user**
|
||||
- **MLB**
|
||||
- **MLBArticle**
|
||||
- **MLBTV**: [<abbr title="netrc machine"><em>mlb</em></abbr>]
|
||||
- **MLBTV**: [*mlb*](## "netrc machine")
|
||||
- **MLBVideo**
|
||||
- **MLSSoccer**
|
||||
- **Mnet**
|
||||
- **MNetTV**: [<abbr title="netrc machine"><em>mnettv</em></abbr>]
|
||||
- **MNetTVLive**: [<abbr title="netrc machine"><em>mnettv</em></abbr>]
|
||||
- **MNetTVRecordings**: [<abbr title="netrc machine"><em>mnettv</em></abbr>]
|
||||
- **MNetTV**: [*mnettv*](## "netrc machine")
|
||||
- **MNetTVLive**: [*mnettv*](## "netrc machine")
|
||||
- **MNetTVRecordings**: [*mnettv*](## "netrc machine")
|
||||
- **MochaVideo**
|
||||
- **MoeVideo**: LetitBit video services: moevideo.net, playreplay.net and videochart.net
|
||||
- **Mofosex**
|
||||
@@ -852,9 +855,9 @@
|
||||
- **ndr:embed**
|
||||
- **ndr:embed:base**
|
||||
- **NDTV**
|
||||
- **Nebula**: [<abbr title="netrc machine"><em>watchnebula</em></abbr>]
|
||||
- **nebula:channel**: [<abbr title="netrc machine"><em>watchnebula</em></abbr>]
|
||||
- **nebula:subscriptions**: [<abbr title="netrc machine"><em>watchnebula</em></abbr>]
|
||||
- **Nebula**: [*watchnebula*](## "netrc machine")
|
||||
- **nebula:channel**: [*watchnebula*](## "netrc machine")
|
||||
- **nebula:subscriptions**: [*watchnebula*](## "netrc machine")
|
||||
- **NerdCubedFeed**
|
||||
- **netease:album**: 网易云音乐 - 专辑
|
||||
- **netease:djradio**: 网易云音乐 - 电台
|
||||
@@ -863,9 +866,9 @@
|
||||
- **netease:program**: 网易云音乐 - 电台节目
|
||||
- **netease:singer**: 网易云音乐 - 歌手
|
||||
- **netease:song**: 网易云音乐
|
||||
- **NetPlusTV**: [<abbr title="netrc machine"><em>netplus</em></abbr>]
|
||||
- **NetPlusTVLive**: [<abbr title="netrc machine"><em>netplus</em></abbr>]
|
||||
- **NetPlusTVRecordings**: [<abbr title="netrc machine"><em>netplus</em></abbr>]
|
||||
- **NetPlusTV**: [*netplus*](## "netrc machine")
|
||||
- **NetPlusTVLive**: [*netplus*](## "netrc machine")
|
||||
- **NetPlusTVRecordings**: [*netplus*](## "netrc machine")
|
||||
- **Netverse**
|
||||
- **NetversePlaylist**
|
||||
- **NetverseSearch**: "netsearch:" prefix
|
||||
@@ -898,7 +901,7 @@
|
||||
- **nickelodeon:br**
|
||||
- **nickelodeonru**
|
||||
- **nicknight**
|
||||
- **niconico**: [<abbr title="netrc machine"><em>niconico</em></abbr>] ニコニコ動画
|
||||
- **niconico**: [*niconico*](## "netrc machine") ニコニコ動画
|
||||
- **niconico:history**: NicoNico user history or likes. Requires cookies.
|
||||
- **niconico:playlist**
|
||||
- **niconico:series**
|
||||
@@ -911,7 +914,7 @@
|
||||
- **Nitter**
|
||||
- **njoy**: N-JOY
|
||||
- **njoy:embed**
|
||||
- **NJPWWorld**: [<abbr title="netrc machine"><em>njpwworld</em></abbr>] 新日本プロレスワールド
|
||||
- **NJPWWorld**: [*njpwworld*](## "netrc machine") 新日本プロレスワールド
|
||||
- **NobelPrize**
|
||||
- **NoicePodcast**
|
||||
- **NonkTube**
|
||||
@@ -980,11 +983,11 @@
|
||||
- **orf:iptv**: iptv.ORF.at
|
||||
- **orf:radio**
|
||||
- **orf:tvthek**: ORF TVthek
|
||||
- **OsnatelTV**: [<abbr title="netrc machine"><em>osnateltv</em></abbr>]
|
||||
- **OsnatelTVLive**: [<abbr title="netrc machine"><em>osnateltv</em></abbr>]
|
||||
- **OsnatelTVRecordings**: [<abbr title="netrc machine"><em>osnateltv</em></abbr>]
|
||||
- **OsnatelTV**: [*osnateltv*](## "netrc machine")
|
||||
- **OsnatelTVLive**: [*osnateltv*](## "netrc machine")
|
||||
- **OsnatelTVRecordings**: [*osnateltv*](## "netrc machine")
|
||||
- **OutsideTV**
|
||||
- **PacktPub**: [<abbr title="netrc machine"><em>packtpub</em></abbr>]
|
||||
- **PacktPub**: [*packtpub*](## "netrc machine")
|
||||
- **PacktPubCourse**
|
||||
- **PalcoMP3:artist**
|
||||
- **PalcoMP3:song**
|
||||
@@ -1007,7 +1010,7 @@
|
||||
- **peer.tv**
|
||||
- **PeerTube**
|
||||
- **PeerTube:Playlist**
|
||||
- **peloton**: [<abbr title="netrc machine"><em>peloton</em></abbr>]
|
||||
- **peloton**: [*peloton*](## "netrc machine")
|
||||
- **peloton:live**: Peloton Live
|
||||
- **People**
|
||||
- **PerformGroup**
|
||||
@@ -1016,7 +1019,7 @@
|
||||
- **PhilharmonieDeParis**: Philharmonie de Paris
|
||||
- **phoenix.de**
|
||||
- **Photobucket**
|
||||
- **Piapro**: [<abbr title="netrc machine"><em>piapro</em></abbr>]
|
||||
- **Piapro**: [*piapro*](## "netrc machine")
|
||||
- **Picarto**
|
||||
- **PicartoVod**
|
||||
- **Piksel**
|
||||
@@ -1027,11 +1030,11 @@
|
||||
- **pixiv:sketch:user**
|
||||
- **Pladform**
|
||||
- **PlanetMarathi**
|
||||
- **Platzi**: [<abbr title="netrc machine"><em>platzi</em></abbr>]
|
||||
- **PlatziCourse**: [<abbr title="netrc machine"><em>platzi</em></abbr>]
|
||||
- **Platzi**: [*platzi*](## "netrc machine")
|
||||
- **PlatziCourse**: [*platzi*](## "netrc machine")
|
||||
- **play.fm**
|
||||
- **player.sky.it**
|
||||
- **PlayPlusTV**: [<abbr title="netrc machine"><em>playplustv</em></abbr>]
|
||||
- **PlayPlusTV**: [*playplustv*](## "netrc machine")
|
||||
- **PlayStuff**
|
||||
- **PlaysTV**
|
||||
- **PlaySuisse**
|
||||
@@ -1039,7 +1042,7 @@
|
||||
- **Playvid**
|
||||
- **PlayVids**
|
||||
- **Playwire**
|
||||
- **pluralsight**: [<abbr title="netrc machine"><em>pluralsight</em></abbr>]
|
||||
- **pluralsight**: [*pluralsight*](## "netrc machine")
|
||||
- **pluralsight:course**
|
||||
- **PlutoTV**
|
||||
- **PodbayFM**
|
||||
@@ -1048,8 +1051,8 @@
|
||||
- **podomatic**
|
||||
- **Pokemon**
|
||||
- **PokemonWatch**
|
||||
- **PokerGo**: [<abbr title="netrc machine"><em>pokergo</em></abbr>]
|
||||
- **PokerGoCollection**: [<abbr title="netrc machine"><em>pokergo</em></abbr>]
|
||||
- **PokerGo**: [*pokergo*](## "netrc machine")
|
||||
- **PokerGoCollection**: [*pokergo*](## "netrc machine")
|
||||
- **PolsatGo**
|
||||
- **PolskieRadio**
|
||||
- **polskieradio:audition**
|
||||
@@ -1066,11 +1069,11 @@
|
||||
- **Pornez**
|
||||
- **PornFlip**
|
||||
- **PornHd**
|
||||
- **PornHub**: [<abbr title="netrc machine"><em>pornhub</em></abbr>] PornHub and Thumbzilla
|
||||
- **PornHubPagedVideoList**: [<abbr title="netrc machine"><em>pornhub</em></abbr>]
|
||||
- **PornHubPlaylist**: [<abbr title="netrc machine"><em>pornhub</em></abbr>]
|
||||
- **PornHubUser**: [<abbr title="netrc machine"><em>pornhub</em></abbr>]
|
||||
- **PornHubUserVideosUpload**: [<abbr title="netrc machine"><em>pornhub</em></abbr>]
|
||||
- **PornHub**: [*pornhub*](## "netrc machine") PornHub and Thumbzilla
|
||||
- **PornHubPagedVideoList**: [*pornhub*](## "netrc machine")
|
||||
- **PornHubPlaylist**: [*pornhub*](## "netrc machine")
|
||||
- **PornHubUser**: [*pornhub*](## "netrc machine")
|
||||
- **PornHubUserVideosUpload**: [*pornhub*](## "netrc machine")
|
||||
- **Pornotube**
|
||||
- **PornoVoisines**
|
||||
- **PornoXO**
|
||||
@@ -1098,9 +1101,9 @@
|
||||
- **qqmusic:playlist**: QQ音乐 - 歌单
|
||||
- **qqmusic:singer**: QQ音乐 - 歌手
|
||||
- **qqmusic:toplist**: QQ音乐 - 排行榜
|
||||
- **QuantumTV**: [<abbr title="netrc machine"><em>quantumtv</em></abbr>]
|
||||
- **QuantumTVLive**: [<abbr title="netrc machine"><em>quantumtv</em></abbr>]
|
||||
- **QuantumTVRecordings**: [<abbr title="netrc machine"><em>quantumtv</em></abbr>]
|
||||
- **QuantumTV**: [*quantumtv*](## "netrc machine")
|
||||
- **QuantumTVLive**: [*quantumtv*](## "netrc machine")
|
||||
- **QuantumTVRecordings**: [*quantumtv*](## "netrc machine")
|
||||
- **Qub**
|
||||
- **R7**
|
||||
- **R7Article**
|
||||
@@ -1157,16 +1160,16 @@
|
||||
- **RICE**
|
||||
- **RMCDecouverte**
|
||||
- **RockstarGames**
|
||||
- **Rokfin**: [<abbr title="netrc machine"><em>rokfin</em></abbr>]
|
||||
- **Rokfin**: [*rokfin*](## "netrc machine")
|
||||
- **rokfin:channel**: Rokfin Channels
|
||||
- **rokfin:search**: Rokfin Search; "rkfnsearch:" prefix
|
||||
- **rokfin:stack**: Rokfin Stacks
|
||||
- **RoosterTeeth**: [<abbr title="netrc machine"><em>roosterteeth</em></abbr>]
|
||||
- **RoosterTeethSeries**: [<abbr title="netrc machine"><em>roosterteeth</em></abbr>]
|
||||
- **RoosterTeeth**: [*roosterteeth*](## "netrc machine")
|
||||
- **RoosterTeethSeries**: [*roosterteeth*](## "netrc machine")
|
||||
- **RottenTomatoes**
|
||||
- **Rozhlas**
|
||||
- **RozhlasVltava**
|
||||
- **RTBF**: [<abbr title="netrc machine"><em>rtbf</em></abbr>]
|
||||
- **RTBF**: [*rtbf*](## "netrc machine")
|
||||
- **RTDocumentry**
|
||||
- **RTDocumentryPlaylist**
|
||||
- **rte**: Raidió Teilifís Éireann TV
|
||||
@@ -1208,16 +1211,16 @@
|
||||
- **Ruutu**
|
||||
- **Ruv**
|
||||
- **ruv.is:spila**
|
||||
- **safari**: [<abbr title="netrc machine"><em>safari</em></abbr>] safaribooksonline.com online video
|
||||
- **safari:api**: [<abbr title="netrc machine"><em>safari</em></abbr>]
|
||||
- **safari:course**: [<abbr title="netrc machine"><em>safari</em></abbr>] safaribooksonline.com online courses
|
||||
- **safari**: [*safari*](## "netrc machine") safaribooksonline.com online video
|
||||
- **safari:api**: [*safari*](## "netrc machine")
|
||||
- **safari:course**: [*safari*](## "netrc machine") safaribooksonline.com online courses
|
||||
- **Saitosan**
|
||||
- **SAKTV**: [<abbr title="netrc machine"><em>saktv</em></abbr>]
|
||||
- **SAKTVLive**: [<abbr title="netrc machine"><em>saktv</em></abbr>]
|
||||
- **SAKTVRecordings**: [<abbr title="netrc machine"><em>saktv</em></abbr>]
|
||||
- **SaltTV**: [<abbr title="netrc machine"><em>salttv</em></abbr>]
|
||||
- **SaltTVLive**: [<abbr title="netrc machine"><em>salttv</em></abbr>]
|
||||
- **SaltTVRecordings**: [<abbr title="netrc machine"><em>salttv</em></abbr>]
|
||||
- **SAKTV**: [*saktv*](## "netrc machine")
|
||||
- **SAKTVLive**: [*saktv*](## "netrc machine")
|
||||
- **SAKTVRecordings**: [*saktv*](## "netrc machine")
|
||||
- **SaltTV**: [*salttv*](## "netrc machine")
|
||||
- **SaltTVLive**: [*salttv*](## "netrc machine")
|
||||
- **SaltTVRecordings**: [*salttv*](## "netrc machine")
|
||||
- **SampleFocus**
|
||||
- **Sangiin**: 参議院インターネット審議中継 (archive)
|
||||
- **Sapo**: SAPO Vídeos
|
||||
@@ -1233,8 +1236,8 @@
|
||||
- **ScrippsNetworks**
|
||||
- **scrippsnetworks:watch**
|
||||
- **Scrolller**
|
||||
- **SCTE**: [<abbr title="netrc machine"><em>scte</em></abbr>]
|
||||
- **SCTECourse**: [<abbr title="netrc machine"><em>scte</em></abbr>]
|
||||
- **SCTE**: [*scte*](## "netrc machine")
|
||||
- **SCTECourse**: [*scte*](## "netrc machine")
|
||||
- **Seeker**
|
||||
- **SenateGov**
|
||||
- **SenateISVP**
|
||||
@@ -1243,7 +1246,7 @@
|
||||
- **Sexu**
|
||||
- **SeznamZpravy**
|
||||
- **SeznamZpravyArticle**
|
||||
- **Shahid**: [<abbr title="netrc machine"><em>shahid</em></abbr>]
|
||||
- **Shahid**: [*shahid*](## "netrc machine")
|
||||
- **ShahidShow**
|
||||
- **Shared**: shared.sx
|
||||
- **ShareVideosEmbed**
|
||||
@@ -1273,16 +1276,16 @@
|
||||
- **Smotrim**
|
||||
- **Snotr**
|
||||
- **Sohu**
|
||||
- **SonyLIV**: [<abbr title="netrc machine"><em>sonyliv</em></abbr>]
|
||||
- **SonyLIV**: [*sonyliv*](## "netrc machine")
|
||||
- **SonyLIVSeries**
|
||||
- **soundcloud**: [<abbr title="netrc machine"><em>soundcloud</em></abbr>]
|
||||
- **soundcloud:playlist**: [<abbr title="netrc machine"><em>soundcloud</em></abbr>]
|
||||
- **soundcloud:related**: [<abbr title="netrc machine"><em>soundcloud</em></abbr>]
|
||||
- **soundcloud:search**: [<abbr title="netrc machine"><em>soundcloud</em></abbr>] Soundcloud search; "scsearch:" prefix
|
||||
- **soundcloud:set**: [<abbr title="netrc machine"><em>soundcloud</em></abbr>]
|
||||
- **soundcloud:trackstation**: [<abbr title="netrc machine"><em>soundcloud</em></abbr>]
|
||||
- **soundcloud:user**: [<abbr title="netrc machine"><em>soundcloud</em></abbr>]
|
||||
- **soundcloud:user:permalink**: [<abbr title="netrc machine"><em>soundcloud</em></abbr>]
|
||||
- **soundcloud**: [*soundcloud*](## "netrc machine")
|
||||
- **soundcloud:playlist**: [*soundcloud*](## "netrc machine")
|
||||
- **soundcloud:related**: [*soundcloud*](## "netrc machine")
|
||||
- **soundcloud:search**: [*soundcloud*](## "netrc machine") Soundcloud search; "scsearch:" prefix
|
||||
- **soundcloud:set**: [*soundcloud*](## "netrc machine")
|
||||
- **soundcloud:trackstation**: [*soundcloud*](## "netrc machine")
|
||||
- **soundcloud:user**: [*soundcloud*](## "netrc machine")
|
||||
- **soundcloud:user:permalink**: [*soundcloud*](## "netrc machine")
|
||||
- **SoundcloudEmbed**
|
||||
- **soundgasm**
|
||||
- **soundgasm:profile**
|
||||
@@ -1349,13 +1352,13 @@
|
||||
- **Tass**
|
||||
- **TBS**
|
||||
- **TDSLifeway**
|
||||
- **Teachable**: [<abbr title="netrc machine"><em>teachable</em></abbr>]
|
||||
- **TeachableCourse**: [<abbr title="netrc machine"><em>teachable</em></abbr>]
|
||||
- **Teachable**: [*teachable*](## "netrc machine")
|
||||
- **TeachableCourse**: [*teachable*](## "netrc machine")
|
||||
- **teachertube**: teachertube.com videos
|
||||
- **teachertube:user:collection**: teachertube.com user and collection videos
|
||||
- **TeachingChannel**
|
||||
- **Teamcoco**
|
||||
- **TeamTreeHouse**: [<abbr title="netrc machine"><em>teamtreehouse</em></abbr>]
|
||||
- **TeamTreeHouse**: [*teamtreehouse*](## "netrc machine")
|
||||
- **TechTalks**
|
||||
- **techtv.mit.edu**
|
||||
- **TedEmbed**
|
||||
@@ -1365,6 +1368,7 @@
|
||||
- **Tele13**
|
||||
- **Tele5**
|
||||
- **TeleBruxelles**
|
||||
- **TelecaribePlay**
|
||||
- **Telecinco**: telecinco.es, cuatro.com and mediaset.es
|
||||
- **Telegraaf**
|
||||
- **telegram:embed**
|
||||
@@ -1378,8 +1382,8 @@
|
||||
- **TeleTask**
|
||||
- **Telewebion**
|
||||
- **Tempo**
|
||||
- **TennisTV**: [<abbr title="netrc machine"><em>tennistv</em></abbr>]
|
||||
- **TenPlay**: [<abbr title="netrc machine"><em>10play</em></abbr>]
|
||||
- **TennisTV**: [*tennistv*](## "netrc machine")
|
||||
- **TenPlay**: [*10play*](## "netrc machine")
|
||||
- **TF1**
|
||||
- **TFO**
|
||||
- **TheHoleTv**
|
||||
@@ -1417,13 +1421,13 @@
|
||||
- **tokfm:audition**
|
||||
- **tokfm:podcast**
|
||||
- **ToonGoggles**
|
||||
- **tou.tv**: [<abbr title="netrc machine"><em>toutv</em></abbr>]
|
||||
- **tou.tv**: [*toutv*](## "netrc machine")
|
||||
- **Toypics**: Toypics video
|
||||
- **ToypicsUser**: Toypics user profile
|
||||
- **TrailerAddict**: (**Currently broken**)
|
||||
- **TravelChannel**
|
||||
- **Triller**: [<abbr title="netrc machine"><em>triller</em></abbr>]
|
||||
- **TrillerUser**: [<abbr title="netrc machine"><em>triller</em></abbr>]
|
||||
- **Triller**: [*triller*](## "netrc machine")
|
||||
- **TrillerUser**: [*triller*](## "netrc machine")
|
||||
- **Trilulilu**
|
||||
- **Trovo**
|
||||
- **TrovoChannelClip**: All Clips of a trovo.live channel; "trovoclip:" prefix
|
||||
@@ -1435,15 +1439,14 @@
|
||||
- **Truth**
|
||||
- **TruTV**
|
||||
- **Tube8**
|
||||
- **TubeTuGraz**: [<abbr title="netrc machine"><em>tubetugraz</em></abbr>] tube.tugraz.at
|
||||
- **TubeTuGrazSeries**: [<abbr title="netrc machine"><em>tubetugraz</em></abbr>]
|
||||
- **TubiTv**: [<abbr title="netrc machine"><em>tubitv</em></abbr>]
|
||||
- **TubeTuGraz**: [*tubetugraz*](## "netrc machine") tube.tugraz.at
|
||||
- **TubeTuGrazSeries**: [*tubetugraz*](## "netrc machine")
|
||||
- **TubiTv**: [*tubitv*](## "netrc machine")
|
||||
- **TubiTvShow**
|
||||
- **Tumblr**: [<abbr title="netrc machine"><em>tumblr</em></abbr>]
|
||||
- **tunein:clip**
|
||||
- **tunein:program**
|
||||
- **tunein:station**
|
||||
- **tunein:topic**
|
||||
- **Tumblr**: [*tumblr*](## "netrc machine")
|
||||
- **TuneInPodcast**
|
||||
- **TuneInPodcastEpisode**
|
||||
- **TuneInStation**
|
||||
- **TunePk**
|
||||
- **Turbo**
|
||||
- **tv.dfb.de**
|
||||
@@ -1489,13 +1492,13 @@
|
||||
- **TwitCasting**
|
||||
- **TwitCastingLive**
|
||||
- **TwitCastingUser**
|
||||
- **twitch:clips**: [<abbr title="netrc machine"><em>twitch</em></abbr>]
|
||||
- **twitch:stream**: [<abbr title="netrc machine"><em>twitch</em></abbr>]
|
||||
- **twitch:vod**: [<abbr title="netrc machine"><em>twitch</em></abbr>]
|
||||
- **TwitchCollection**: [<abbr title="netrc machine"><em>twitch</em></abbr>]
|
||||
- **TwitchVideos**: [<abbr title="netrc machine"><em>twitch</em></abbr>]
|
||||
- **TwitchVideosClips**: [<abbr title="netrc machine"><em>twitch</em></abbr>]
|
||||
- **TwitchVideosCollections**: [<abbr title="netrc machine"><em>twitch</em></abbr>]
|
||||
- **twitch:clips**: [*twitch*](## "netrc machine")
|
||||
- **twitch:stream**: [*twitch*](## "netrc machine")
|
||||
- **twitch:vod**: [*twitch*](## "netrc machine")
|
||||
- **TwitchCollection**: [*twitch*](## "netrc machine")
|
||||
- **TwitchVideos**: [*twitch*](## "netrc machine")
|
||||
- **TwitchVideosClips**: [*twitch*](## "netrc machine")
|
||||
- **TwitchVideosCollections**: [*twitch*](## "netrc machine")
|
||||
- **twitter**
|
||||
- **twitter:amplify**
|
||||
- **twitter:broadcast**
|
||||
@@ -1503,11 +1506,11 @@
|
||||
- **twitter:shortener**
|
||||
- **twitter:spaces**
|
||||
- **Txxx**
|
||||
- **udemy**: [<abbr title="netrc machine"><em>udemy</em></abbr>]
|
||||
- **udemy:course**: [<abbr title="netrc machine"><em>udemy</em></abbr>]
|
||||
- **udemy**: [*udemy*](## "netrc machine")
|
||||
- **udemy:course**: [*udemy*](## "netrc machine")
|
||||
- **UDNEmbed**: 聯合影音
|
||||
- **UFCArabia**: [<abbr title="netrc machine"><em>ufcarabia</em></abbr>]
|
||||
- **UFCTV**: [<abbr title="netrc machine"><em>ufctv</em></abbr>]
|
||||
- **UFCArabia**: [*ufcarabia*](## "netrc machine")
|
||||
- **UFCTV**: [*ufctv*](## "netrc machine")
|
||||
- **ukcolumn**
|
||||
- **UKTVPlay**
|
||||
- **umg:de**: Universal Music Deutschland
|
||||
@@ -1537,7 +1540,7 @@
|
||||
- **VevoPlaylist**
|
||||
- **VGTV**: VGTV, BTTV, FTV, Aftenposten and Aftonbladet
|
||||
- **vh1.com**
|
||||
- **vhx:embed**: [<abbr title="netrc machine"><em>vimeo</em></abbr>]
|
||||
- **vhx:embed**: [*vimeo*](## "netrc machine")
|
||||
- **Viafree**
|
||||
- **vice**
|
||||
- **vice:article**
|
||||
@@ -1560,25 +1563,25 @@
|
||||
- **videomore:season**
|
||||
- **videomore:video**
|
||||
- **VideoPress**
|
||||
- **Vidio**: [<abbr title="netrc machine"><em>vidio</em></abbr>]
|
||||
- **VidioLive**: [<abbr title="netrc machine"><em>vidio</em></abbr>]
|
||||
- **VidioPremier**: [<abbr title="netrc machine"><em>vidio</em></abbr>]
|
||||
- **Vidio**: [*vidio*](## "netrc machine")
|
||||
- **VidioLive**: [*vidio*](## "netrc machine")
|
||||
- **VidioPremier**: [*vidio*](## "netrc machine")
|
||||
- **VidLii**
|
||||
- **viewlift**
|
||||
- **viewlift:embed**
|
||||
- **Viidea**
|
||||
- **viki**: [<abbr title="netrc machine"><em>viki</em></abbr>]
|
||||
- **viki:channel**: [<abbr title="netrc machine"><em>viki</em></abbr>]
|
||||
- **vimeo**: [<abbr title="netrc machine"><em>vimeo</em></abbr>]
|
||||
- **vimeo:album**: [<abbr title="netrc machine"><em>vimeo</em></abbr>]
|
||||
- **vimeo:channel**: [<abbr title="netrc machine"><em>vimeo</em></abbr>]
|
||||
- **vimeo:group**: [<abbr title="netrc machine"><em>vimeo</em></abbr>]
|
||||
- **vimeo:likes**: [<abbr title="netrc machine"><em>vimeo</em></abbr>] Vimeo user likes
|
||||
- **vimeo:ondemand**: [<abbr title="netrc machine"><em>vimeo</em></abbr>]
|
||||
- **vimeo:pro**: [<abbr title="netrc machine"><em>vimeo</em></abbr>]
|
||||
- **vimeo:review**: [<abbr title="netrc machine"><em>vimeo</em></abbr>] Review pages on vimeo
|
||||
- **vimeo:user**: [<abbr title="netrc machine"><em>vimeo</em></abbr>]
|
||||
- **vimeo:watchlater**: [<abbr title="netrc machine"><em>vimeo</em></abbr>] Vimeo watch later list, ":vimeowatchlater" keyword (requires authentication)
|
||||
- **viki**: [*viki*](## "netrc machine")
|
||||
- **viki:channel**: [*viki*](## "netrc machine")
|
||||
- **vimeo**: [*vimeo*](## "netrc machine")
|
||||
- **vimeo:album**: [*vimeo*](## "netrc machine")
|
||||
- **vimeo:channel**: [*vimeo*](## "netrc machine")
|
||||
- **vimeo:group**: [*vimeo*](## "netrc machine")
|
||||
- **vimeo:likes**: [*vimeo*](## "netrc machine") Vimeo user likes
|
||||
- **vimeo:ondemand**: [*vimeo*](## "netrc machine")
|
||||
- **vimeo:pro**: [*vimeo*](## "netrc machine")
|
||||
- **vimeo:review**: [*vimeo*](## "netrc machine") Review pages on vimeo
|
||||
- **vimeo:user**: [*vimeo*](## "netrc machine")
|
||||
- **vimeo:watchlater**: [*vimeo*](## "netrc machine") Vimeo watch later list, ":vimeowatchlater" keyword (requires authentication)
|
||||
- **Vimm:recording**
|
||||
- **Vimm:stream**
|
||||
- **ViMP**
|
||||
@@ -1588,13 +1591,13 @@
|
||||
- **vine:user**
|
||||
- **Viqeo**
|
||||
- **Viu**
|
||||
- **viu:ott**: [<abbr title="netrc machine"><em>viu</em></abbr>]
|
||||
- **viu:ott**: [*viu*](## "netrc machine")
|
||||
- **viu:playlist**
|
||||
- **ViuOTTIndonesia**
|
||||
- **Vivo**: vivo.sx
|
||||
- **vk**: [<abbr title="netrc machine"><em>vk</em></abbr>] VK
|
||||
- **vk:uservideos**: [<abbr title="netrc machine"><em>vk</em></abbr>] VK - User's Videos
|
||||
- **vk:wallpost**: [<abbr title="netrc machine"><em>vk</em></abbr>]
|
||||
- **vk**: [*vk*](## "netrc machine") VK
|
||||
- **vk:uservideos**: [*vk*](## "netrc machine") VK - User's Videos
|
||||
- **vk:wallpost**: [*vk*](## "netrc machine")
|
||||
- **vm.tiktok**
|
||||
- **Vocaroo**
|
||||
- **Vodlocker**
|
||||
@@ -1613,14 +1616,14 @@
|
||||
- **vqq:video**
|
||||
- **Vrak**
|
||||
- **VRT**: VRT NWS, Flanders News, Flandern Info and Sporza
|
||||
- **VrtNU**: [<abbr title="netrc machine"><em>vrtnu</em></abbr>] VrtNU.be
|
||||
- **vrv**: [<abbr title="netrc machine"><em>vrv</em></abbr>]
|
||||
- **VrtNU**: [*vrtnu*](## "netrc machine") VrtNU.be
|
||||
- **vrv**: [*vrv*](## "netrc machine")
|
||||
- **vrv:series**
|
||||
- **VShare**
|
||||
- **VTM**
|
||||
- **VTXTV**: [<abbr title="netrc machine"><em>vtxtv</em></abbr>]
|
||||
- **VTXTVLive**: [<abbr title="netrc machine"><em>vtxtv</em></abbr>]
|
||||
- **VTXTVRecordings**: [<abbr title="netrc machine"><em>vtxtv</em></abbr>]
|
||||
- **VTXTV**: [*vtxtv*](## "netrc machine")
|
||||
- **VTXTVLive**: [*vtxtv*](## "netrc machine")
|
||||
- **VTXTVRecordings**: [*vtxtv*](## "netrc machine")
|
||||
- **VuClip**
|
||||
- **Vupload**
|
||||
- **VVVVID**
|
||||
@@ -1629,9 +1632,9 @@
|
||||
- **Vzaar**
|
||||
- **Wakanim**
|
||||
- **Walla**
|
||||
- **WalyTV**: [<abbr title="netrc machine"><em>walytv</em></abbr>]
|
||||
- **WalyTVLive**: [<abbr title="netrc machine"><em>walytv</em></abbr>]
|
||||
- **WalyTVRecordings**: [<abbr title="netrc machine"><em>walytv</em></abbr>]
|
||||
- **WalyTV**: [*walytv*](## "netrc machine")
|
||||
- **WalyTVLive**: [*walytv*](## "netrc machine")
|
||||
- **WalyTVRecordings**: [*walytv*](## "netrc machine")
|
||||
- **wasdtv:clip**
|
||||
- **wasdtv:record**
|
||||
- **wasdtv:stream**
|
||||
@@ -1695,6 +1698,7 @@
|
||||
- **XTubeUser**: XTube user profile
|
||||
- **Xuite**: 隨意窩Xuite影音
|
||||
- **XVideos**
|
||||
- **xvideos:quickies**
|
||||
- **XXXYMovies**
|
||||
- **Yahoo**: Yahoo screen and movies
|
||||
- **yahoo:gyao**
|
||||
@@ -1743,13 +1747,13 @@
|
||||
- **YoutubeLivestreamEmbed**: YouTube livestream embeds
|
||||
- **YoutubeYtBe**: youtu.be
|
||||
- **Zapiks**
|
||||
- **Zattoo**: [<abbr title="netrc machine"><em>zattoo</em></abbr>]
|
||||
- **ZattooLive**: [<abbr title="netrc machine"><em>zattoo</em></abbr>]
|
||||
- **ZattooMovies**: [<abbr title="netrc machine"><em>zattoo</em></abbr>]
|
||||
- **ZattooRecordings**: [<abbr title="netrc machine"><em>zattoo</em></abbr>]
|
||||
- **Zattoo**: [*zattoo*](## "netrc machine")
|
||||
- **ZattooLive**: [*zattoo*](## "netrc machine")
|
||||
- **ZattooMovies**: [*zattoo*](## "netrc machine")
|
||||
- **ZattooRecordings**: [*zattoo*](## "netrc machine")
|
||||
- **ZDF**
|
||||
- **ZDFChannel**
|
||||
- **Zee5**: [<abbr title="netrc machine"><em>zee5</em></abbr>]
|
||||
- **Zee5**: [*zee5*](## "netrc machine")
|
||||
- **zee5:series**
|
||||
- **ZeeNews**
|
||||
- **ZenYandex**
|
||||
|
||||
@@ -48,7 +48,7 @@ class TestAES(unittest.TestCase):
|
||||
data = b'\x97\x92+\xe5\x0b\xc3\x18\x91ky9m&\xb3\xb5@\xe6\x27\xc2\x96.\xc8u\x88\xab9-[\x9e|\xf1\xcd'
|
||||
decrypted = intlist_to_bytes(aes_cbc_decrypt(bytes_to_intlist(data), self.key, self.iv))
|
||||
self.assertEqual(decrypted.rstrip(b'\x08'), self.secret_msg)
|
||||
if Cryptodome:
|
||||
if Cryptodome.AES:
|
||||
decrypted = aes_cbc_decrypt_bytes(data, intlist_to_bytes(self.key), intlist_to_bytes(self.iv))
|
||||
self.assertEqual(decrypted.rstrip(b'\x08'), self.secret_msg)
|
||||
|
||||
@@ -78,7 +78,7 @@ class TestAES(unittest.TestCase):
|
||||
decrypted = intlist_to_bytes(aes_gcm_decrypt_and_verify(
|
||||
bytes_to_intlist(data), self.key, bytes_to_intlist(authentication_tag), self.iv[:12]))
|
||||
self.assertEqual(decrypted.rstrip(b'\x08'), self.secret_msg)
|
||||
if Cryptodome:
|
||||
if Cryptodome.AES:
|
||||
decrypted = aes_gcm_decrypt_and_verify_bytes(
|
||||
data, intlist_to_bytes(self.key), authentication_tag, intlist_to_bytes(self.iv[:12]))
|
||||
self.assertEqual(decrypted.rstrip(b'\x08'), self.secret_msg)
|
||||
|
||||
@@ -66,6 +66,10 @@ _SIG_TESTS = [
|
||||
]
|
||||
|
||||
_NSIG_TESTS = [
|
||||
(
|
||||
'https://www.youtube.com/s/player/7862ca1f/player_ias.vflset/en_US/base.js',
|
||||
'X_LCxVDjAavgE5t', 'yxJ1dM6iz5ogUg',
|
||||
),
|
||||
(
|
||||
'https://www.youtube.com/s/player/9216d1f7/player_ias.vflset/en_US/base.js',
|
||||
'SLp9F5bwjAdhE9F-', 'gWnb9IK2DJ8Q1w',
|
||||
|
||||
@@ -150,7 +150,7 @@ from .utils import (
|
||||
write_json_file,
|
||||
write_string,
|
||||
)
|
||||
from .version import RELEASE_GIT_HEAD, VARIANT, __version__
|
||||
from .version import CHANNEL, RELEASE_GIT_HEAD, VARIANT, __version__
|
||||
|
||||
if compat_os_name == 'nt':
|
||||
import ctypes
|
||||
@@ -300,8 +300,6 @@ class YoutubeDL:
|
||||
Videos already present in the file are not downloaded again.
|
||||
break_on_existing: Stop the download process after attempting to download a
|
||||
file that is in the archive.
|
||||
break_on_reject: Stop the download process when encountering a video that
|
||||
has been filtered out.
|
||||
break_per_url: Whether break_on_reject and break_on_existing
|
||||
should act on each input URL as opposed to for the entire queue
|
||||
cookiefile: File name or text stream from where cookies should be read and dumped to
|
||||
@@ -414,6 +412,8 @@ class YoutubeDL:
|
||||
- If it returns None, the video is downloaded.
|
||||
- If it returns utils.NO_DEFAULT, the user is interactively
|
||||
asked whether to download the video.
|
||||
- Raise utils.DownloadCancelled(msg) to abort remaining
|
||||
downloads when a video is rejected.
|
||||
match_filter_func in utils.py is one example for this.
|
||||
no_color: Do not emit color codes in output.
|
||||
geo_bypass: Bypass geographic restriction via faking X-Forwarded-For
|
||||
@@ -483,6 +483,9 @@ class YoutubeDL:
|
||||
|
||||
The following options are deprecated and may be removed in the future:
|
||||
|
||||
break_on_reject: Stop the download process when encountering a video that
|
||||
has been filtered out.
|
||||
- `raise DownloadCancelled(msg)` in match_filter instead
|
||||
force_generic_extractor: Force downloader to use the generic extractor
|
||||
- Use allowed_extractors = ['generic', 'default']
|
||||
playliststart: - Use playlist_items
|
||||
@@ -614,7 +617,7 @@ class YoutubeDL:
|
||||
'\n You will no longer receive updates on this version')
|
||||
if current_version < MIN_SUPPORTED:
|
||||
msg = 'Python version %d.%d is no longer supported'
|
||||
self.deprecation_warning(
|
||||
self.deprecated_feature(
|
||||
f'{msg}! Please update to Python %d.%d or above' % (*current_version, *MIN_RECOMMENDED))
|
||||
|
||||
if self.params.get('allow_unplayable_formats'):
|
||||
@@ -1407,31 +1410,44 @@ class YoutubeDL:
|
||||
return 'Skipping "%s" because it is age restricted' % video_title
|
||||
|
||||
match_filter = self.params.get('match_filter')
|
||||
if match_filter is not None:
|
||||
if match_filter is None:
|
||||
return None
|
||||
|
||||
cancelled = None
|
||||
try:
|
||||
try:
|
||||
ret = match_filter(info_dict, incomplete=incomplete)
|
||||
except TypeError:
|
||||
# For backward compatibility
|
||||
ret = None if incomplete else match_filter(info_dict)
|
||||
if ret is NO_DEFAULT:
|
||||
while True:
|
||||
filename = self._format_screen(self.prepare_filename(info_dict), self.Styles.FILENAME)
|
||||
reply = input(self._format_screen(
|
||||
f'Download "{filename}"? (Y/n): ', self.Styles.EMPHASIS)).lower().strip()
|
||||
if reply in {'y', ''}:
|
||||
return None
|
||||
elif reply == 'n':
|
||||
return f'Skipping {video_title}'
|
||||
elif ret is not None:
|
||||
return ret
|
||||
return None
|
||||
except DownloadCancelled as err:
|
||||
if err.msg is not NO_DEFAULT:
|
||||
raise
|
||||
ret, cancelled = err.msg, err
|
||||
|
||||
if ret is NO_DEFAULT:
|
||||
while True:
|
||||
filename = self._format_screen(self.prepare_filename(info_dict), self.Styles.FILENAME)
|
||||
reply = input(self._format_screen(
|
||||
f'Download "{filename}"? (Y/n): ', self.Styles.EMPHASIS)).lower().strip()
|
||||
if reply in {'y', ''}:
|
||||
return None
|
||||
elif reply == 'n':
|
||||
if cancelled:
|
||||
raise type(cancelled)(f'Skipping {video_title}')
|
||||
return f'Skipping {video_title}'
|
||||
return ret
|
||||
|
||||
if self.in_download_archive(info_dict):
|
||||
reason = '%s has already been recorded in the archive' % video_title
|
||||
break_opt, break_err = 'break_on_existing', ExistingVideoReached
|
||||
else:
|
||||
reason = check_filter()
|
||||
break_opt, break_err = 'break_on_reject', RejectedVideoReached
|
||||
try:
|
||||
reason = check_filter()
|
||||
except DownloadCancelled as e:
|
||||
reason, break_opt, break_err = e.msg, 'match_filter', type(e)
|
||||
else:
|
||||
break_opt, break_err = 'break_on_reject', RejectedVideoReached
|
||||
if reason is not None:
|
||||
if not silent:
|
||||
self.to_screen('[download] ' + reason)
|
||||
@@ -3768,8 +3784,8 @@ class YoutubeDL:
|
||||
klass = type(self)
|
||||
write_debug(join_nonempty(
|
||||
f'{"yt-dlp" if REPOSITORY == "yt-dlp/yt-dlp" else REPOSITORY} version',
|
||||
__version__,
|
||||
f'[{RELEASE_GIT_HEAD}]' if RELEASE_GIT_HEAD else '',
|
||||
f'{CHANNEL}@{__version__}',
|
||||
f'[{RELEASE_GIT_HEAD[:9]}]' if RELEASE_GIT_HEAD else '',
|
||||
'' if source == 'unknown' else f'({source})',
|
||||
'' if _IN_CLI else 'API' if klass == YoutubeDL else f'API:{self.__module__}.{klass.__qualname__}',
|
||||
delim=' '))
|
||||
|
||||
@@ -403,7 +403,7 @@ def validate_options(opts):
|
||||
except Exception:
|
||||
raise ValueError('unsupported geo-bypass country or ip-block')
|
||||
|
||||
opts.match_filter = match_filter_func(opts.match_filter)
|
||||
opts.match_filter = match_filter_func(opts.match_filter, opts.breaking_match_filter)
|
||||
|
||||
if opts.download_archive is not None:
|
||||
opts.download_archive = expand_path(opts.download_archive)
|
||||
@@ -931,7 +931,7 @@ def _real_main(argv=None):
|
||||
if opts.rm_cachedir:
|
||||
ydl.cache.remove()
|
||||
|
||||
updater = Updater(ydl)
|
||||
updater = Updater(ydl, opts.update_self if isinstance(opts.update_self, str) else None)
|
||||
if opts.update_self and updater.update() and actual_use:
|
||||
if updater.cmd:
|
||||
return updater.restart()
|
||||
|
||||
@@ -1,30 +1,8 @@
|
||||
import ast
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from PyInstaller.utils.hooks import collect_submodules
|
||||
|
||||
|
||||
def find_attribute_accesses(node, name, path=()):
|
||||
if isinstance(node, ast.Attribute):
|
||||
path = [*path, node.attr]
|
||||
if isinstance(node.value, ast.Name) and node.value.id == name:
|
||||
yield path[::-1]
|
||||
for child in ast.iter_child_nodes(node):
|
||||
yield from find_attribute_accesses(child, name, path)
|
||||
|
||||
|
||||
def collect_used_submodules(name, level):
|
||||
for dirpath, _, filenames in os.walk(Path(__file__).parent.parent):
|
||||
for filename in filenames:
|
||||
if not filename.endswith('.py'):
|
||||
continue
|
||||
with open(Path(dirpath) / filename, encoding='utf8') as f:
|
||||
for submodule in find_attribute_accesses(ast.parse(f.read()), name):
|
||||
yield '.'.join(submodule[:level])
|
||||
|
||||
|
||||
def pycryptodome_module():
|
||||
try:
|
||||
import Cryptodome # noqa: F401
|
||||
@@ -41,12 +19,8 @@ def pycryptodome_module():
|
||||
|
||||
def get_hidden_imports():
|
||||
yield 'yt_dlp.compat._legacy'
|
||||
yield pycryptodome_module()
|
||||
yield from collect_submodules('websockets')
|
||||
|
||||
crypto = pycryptodome_module()
|
||||
for sm in set(collect_used_submodules('Cryptodome', 2)):
|
||||
yield f'{crypto}.{sm}'
|
||||
|
||||
# These are auto-detected, but explicitly add them just in case
|
||||
yield from ('mutagen', 'brotli', 'certifi')
|
||||
|
||||
|
||||
@@ -5,14 +5,14 @@ from .compat import compat_ord
|
||||
from .dependencies import Cryptodome
|
||||
from .utils import bytes_to_intlist, intlist_to_bytes
|
||||
|
||||
if Cryptodome:
|
||||
if Cryptodome.AES:
|
||||
def aes_cbc_decrypt_bytes(data, key, iv):
|
||||
""" Decrypt bytes with AES-CBC using pycryptodome """
|
||||
return Cryptodome.Cipher.AES.new(key, Cryptodome.Cipher.AES.MODE_CBC, iv).decrypt(data)
|
||||
return Cryptodome.AES.new(key, Cryptodome.AES.MODE_CBC, iv).decrypt(data)
|
||||
|
||||
def aes_gcm_decrypt_and_verify_bytes(data, key, tag, nonce):
|
||||
""" Decrypt bytes with AES-GCM using pycryptodome """
|
||||
return Cryptodome.Cipher.AES.new(key, Cryptodome.Cipher.AES.MODE_GCM, nonce).decrypt_and_verify(data, tag)
|
||||
return Cryptodome.AES.new(key, Cryptodome.AES.MODE_GCM, nonce).decrypt_and_verify(data, tag)
|
||||
|
||||
else:
|
||||
def aes_cbc_decrypt_bytes(data, key, iv):
|
||||
|
||||
@@ -32,9 +32,9 @@ from re import match as compat_Match # noqa: F401
|
||||
|
||||
from . import compat_expanduser, compat_HTMLParseError, compat_realpath
|
||||
from .compat_utils import passthrough_module
|
||||
from ..dependencies import Cryptodome_AES as compat_pycrypto_AES # noqa: F401
|
||||
from ..dependencies import brotli as compat_brotli # noqa: F401
|
||||
from ..dependencies import websockets as compat_websockets # noqa: F401
|
||||
from ..dependencies.Cryptodome import AES as compat_pycrypto_AES # noqa: F401
|
||||
|
||||
passthrough_module(__name__, '...utils', ('WINDOWS_VT_MODE', 'windows_enable_vt_mode'))
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ def passthrough_module(parent, child, allowed_attributes=(..., ), *, callback=la
|
||||
"""Passthrough parent module into a child module, creating the parent if necessary"""
|
||||
def __getattr__(attr):
|
||||
if _is_package(parent):
|
||||
with contextlib.suppress(ImportError):
|
||||
with contextlib.suppress(ModuleNotFoundError):
|
||||
return importlib.import_module(f'.{attr}', parent.__name__)
|
||||
|
||||
ret = from_child(attr)
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import types
|
||||
|
||||
from ..compat import functools
|
||||
from ..compat.compat_utils import passthrough_module
|
||||
|
||||
try:
|
||||
import Cryptodome as _parent
|
||||
except ImportError:
|
||||
@@ -12,19 +9,28 @@ except ImportError:
|
||||
_parent = types.ModuleType('no_Cryptodome')
|
||||
__bool__ = lambda: False
|
||||
|
||||
passthrough_module(__name__, _parent, (..., '__version__'))
|
||||
del passthrough_module
|
||||
__version__ = ''
|
||||
AES = PKCS1_v1_5 = Blowfish = PKCS1_OAEP = SHA1 = CMAC = RSA = None
|
||||
try:
|
||||
if _parent.__name__ == 'Cryptodome':
|
||||
from Cryptodome import __version__
|
||||
from Cryptodome.Cipher import AES, PKCS1_OAEP, Blowfish, PKCS1_v1_5
|
||||
from Cryptodome.Hash import CMAC, SHA1
|
||||
from Cryptodome.PublicKey import RSA
|
||||
elif _parent.__name__ == 'Crypto':
|
||||
from Crypto import __version__
|
||||
from Crypto.Cipher import AES, PKCS1_OAEP, Blowfish, PKCS1_v1_5 # noqa: F401
|
||||
from Crypto.Hash import CMAC, SHA1 # noqa: F401
|
||||
from Crypto.PublicKey import RSA # noqa: F401
|
||||
except ImportError:
|
||||
__version__ = f'broken {__version__}'.strip()
|
||||
|
||||
|
||||
@property
|
||||
@functools.cache
|
||||
def _yt_dlp__identifier():
|
||||
if _parent.__name__ == 'Crypto':
|
||||
from Crypto.Cipher import AES
|
||||
try:
|
||||
# In pycrypto, mode defaults to ECB. See:
|
||||
# https://www.pycryptodome.org/en/latest/src/vs_pycrypto.html#:~:text=not%20have%20ECB%20as%20default%20mode
|
||||
AES.new(b'abcdefghijklmnop')
|
||||
except TypeError:
|
||||
return 'pycrypto'
|
||||
return _parent.__name__
|
||||
_yt_dlp__identifier = _parent.__name__
|
||||
if AES and _yt_dlp__identifier == 'Crypto':
|
||||
try:
|
||||
# In pycrypto, mode defaults to ECB. See:
|
||||
# https://www.pycryptodome.org/en/latest/src/vs_pycrypto.html#:~:text=not%20have%20ECB%20as%20default%20mode
|
||||
AES.new(b'abcdefghijklmnop')
|
||||
except TypeError:
|
||||
_yt_dlp__identifier = 'pycrypto'
|
||||
|
||||
@@ -73,7 +73,7 @@ available_dependencies = {k: v for k, v in all_dependencies.items() if v}
|
||||
|
||||
|
||||
# Deprecated
|
||||
Cryptodome_AES = Cryptodome.Cipher.AES if Cryptodome else None
|
||||
Cryptodome_AES = Cryptodome.AES
|
||||
|
||||
|
||||
__all__ = [
|
||||
|
||||
@@ -466,7 +466,8 @@ class FragmentFD(FileDownloader):
|
||||
for retry in RetryManager(self.params.get('fragment_retries'), error_callback):
|
||||
try:
|
||||
ctx['fragment_count'] = fragment.get('fragment_count')
|
||||
if not self._download_fragment(ctx, fragment['url'], info_dict, headers):
|
||||
if not self._download_fragment(
|
||||
ctx, fragment['url'], info_dict, headers, info_dict.get('request_data')):
|
||||
return
|
||||
except (urllib.error.HTTPError, http.client.IncompleteRead) as err:
|
||||
retry.error = err
|
||||
@@ -496,7 +497,7 @@ class FragmentFD(FileDownloader):
|
||||
download_fragment(fragment, ctx_copy)
|
||||
return fragment, fragment['frag_index'], ctx_copy.get('fragment_filename_sanitized')
|
||||
|
||||
self.report_warning('The download speed shown is only of one thread. This is a known issue and patches are welcome')
|
||||
self.report_warning('The download speed shown is only of one thread. This is a known issue')
|
||||
with tpe or concurrent.futures.ThreadPoolExecutor(max_workers) as pool:
|
||||
try:
|
||||
for fragment, frag_index, frag_filename in pool.map(_download_fragment, fragments):
|
||||
|
||||
@@ -70,7 +70,7 @@ class HlsFD(FragmentFD):
|
||||
can_download, message = self.can_download(s, info_dict, self.params.get('allow_unplayable_formats')), None
|
||||
if can_download:
|
||||
has_ffmpeg = FFmpegFD.available()
|
||||
no_crypto = not Cryptodome and '#EXT-X-KEY:METHOD=AES-128' in s
|
||||
no_crypto = not Cryptodome.AES and '#EXT-X-KEY:METHOD=AES-128' in s
|
||||
if no_crypto and has_ffmpeg:
|
||||
can_download, message = False, 'The stream has AES-128 encryption and pycryptodomex is not available'
|
||||
elif no_crypto:
|
||||
|
||||
@@ -914,6 +914,10 @@ from .leeco import (
|
||||
LePlaylistIE,
|
||||
LetvCloudIE,
|
||||
)
|
||||
from .lefigaro import (
|
||||
LeFigaroVideoEmbedIE,
|
||||
LeFigaroVideoSectionIE,
|
||||
)
|
||||
from .lego import LEGOIE
|
||||
from .lemonde import LemondeIE
|
||||
from .lenta import LentaIE
|
||||
@@ -962,6 +966,9 @@ from .lrt import (
|
||||
LRTVODIE,
|
||||
LRTStreamIE
|
||||
)
|
||||
from .lumni import (
|
||||
LumniIE
|
||||
)
|
||||
from .lynda import (
|
||||
LyndaIE,
|
||||
LyndaCourseIE
|
||||
@@ -1851,6 +1858,7 @@ from .ted import (
|
||||
from .tele5 import Tele5IE
|
||||
from .tele13 import Tele13IE
|
||||
from .telebruxelles import TeleBruxellesIE
|
||||
from .telecaribe import TelecaribePlayIE
|
||||
from .telecinco import TelecincoIE
|
||||
from .telegraaf import TelegraafIE
|
||||
from .telegram import TelegramEmbedIE
|
||||
@@ -1963,10 +1971,9 @@ from .tubitv import (
|
||||
)
|
||||
from .tumblr import TumblrIE
|
||||
from .tunein import (
|
||||
TuneInClipIE,
|
||||
TuneInStationIE,
|
||||
TuneInProgramIE,
|
||||
TuneInTopicIE,
|
||||
TuneInPodcastIE,
|
||||
TuneInPodcastEpisodeIE,
|
||||
TuneInShortenerIE,
|
||||
)
|
||||
from .tunepk import TunePkIE
|
||||
@@ -2315,7 +2322,10 @@ from .xnxx import XNXXIE
|
||||
from .xstream import XstreamIE
|
||||
from .xtube import XTubeUserIE, XTubeIE
|
||||
from .xuite import XuiteIE
|
||||
from .xvideos import XVideosIE
|
||||
from .xvideos import (
|
||||
XVideosIE,
|
||||
XVideosQuickiesIE
|
||||
)
|
||||
from .xxxymovies import XXXYMoviesIE
|
||||
from .yahoo import (
|
||||
YahooIE,
|
||||
|
||||
@@ -81,7 +81,7 @@ class BilibiliBaseIE(InfoExtractor):
|
||||
f'{line["content"]}\n\n')
|
||||
return srt_data
|
||||
|
||||
def _get_subtitles(self, video_id, initial_state, cid):
|
||||
def _get_subtitles(self, video_id, aid, cid):
|
||||
subtitles = {
|
||||
'danmaku': [{
|
||||
'ext': 'xml',
|
||||
@@ -89,7 +89,8 @@ class BilibiliBaseIE(InfoExtractor):
|
||||
}]
|
||||
}
|
||||
|
||||
for s in traverse_obj(initial_state, ('videoData', 'subtitle', 'list')) or []:
|
||||
video_info_json = self._download_json(f'https://api.bilibili.com/x/player/v2?aid={aid}&cid={cid}', video_id)
|
||||
for s in traverse_obj(video_info_json, ('data', 'subtitle', 'subtitles', ...)):
|
||||
subtitles.setdefault(s['lan'], []).append({
|
||||
'ext': 'srt',
|
||||
'data': self.json2srt(self._download_json(s['subtitle_url'], video_id))
|
||||
@@ -331,7 +332,7 @@ class BiliBiliIE(BilibiliBaseIE):
|
||||
'timestamp': traverse_obj(initial_state, ('videoData', 'pubdate')),
|
||||
'duration': float_or_none(play_info.get('timelength'), scale=1000),
|
||||
'chapters': self._get_chapters(aid, cid),
|
||||
'subtitles': self.extract_subtitles(video_id, initial_state, cid),
|
||||
'subtitles': self.extract_subtitles(video_id, aid, cid),
|
||||
'__post_extractor': self.extract_comments(aid),
|
||||
'http_headers': {'Referer': url},
|
||||
}
|
||||
@@ -894,15 +895,15 @@ class BiliIntlBaseIE(InfoExtractor):
|
||||
}
|
||||
|
||||
def _perform_login(self, username, password):
|
||||
if not Cryptodome:
|
||||
if not Cryptodome.RSA:
|
||||
raise ExtractorError('pycryptodomex not found. Please install', expected=True)
|
||||
|
||||
key_data = self._download_json(
|
||||
'https://passport.bilibili.tv/x/intl/passport-login/web/key?lang=en-US', None,
|
||||
note='Downloading login key', errnote='Unable to download login key')['data']
|
||||
|
||||
public_key = Cryptodome.PublicKey.RSA.importKey(key_data['key'])
|
||||
password_hash = Cryptodome.Cipher.PKCS1_v1_5.new(public_key).encrypt((key_data['hash'] + password).encode('utf-8'))
|
||||
public_key = Cryptodome.RSA.importKey(key_data['key'])
|
||||
password_hash = Cryptodome.PKCS1_v1_5.new(public_key).encrypt((key_data['hash'] + password).encode('utf-8'))
|
||||
login_post = self._download_json(
|
||||
'https://passport.bilibili.tv/x/intl/passport-login/web/login/password?lang=en-US', None, data=urlencode_postdata({
|
||||
'username': username,
|
||||
|
||||
@@ -132,6 +132,7 @@ class InfoExtractor:
|
||||
is parsed from a string (in case of
|
||||
fragmented media)
|
||||
for MSS - URL of the ISM manifest.
|
||||
* request_data Data to send in POST request to the URL
|
||||
* manifest_url
|
||||
The URL of the manifest file in case of
|
||||
fragmented media:
|
||||
@@ -2063,6 +2064,7 @@ class InfoExtractor:
|
||||
'protocol': entry_protocol,
|
||||
'preference': preference,
|
||||
'quality': quality,
|
||||
'has_drm': has_drm,
|
||||
'vcodec': 'none' if media_type == 'AUDIO' else None,
|
||||
} for idx in _extract_m3u8_playlist_indices(manifest_url))
|
||||
|
||||
@@ -2122,6 +2124,7 @@ class InfoExtractor:
|
||||
'protocol': entry_protocol,
|
||||
'preference': preference,
|
||||
'quality': quality,
|
||||
'has_drm': has_drm,
|
||||
}
|
||||
resolution = last_stream_inf.get('RESOLUTION')
|
||||
if resolution:
|
||||
@@ -3524,7 +3527,7 @@ class InfoExtractor:
|
||||
desc = ''
|
||||
if cls._NETRC_MACHINE:
|
||||
if markdown:
|
||||
desc += f' [<abbr title="netrc machine"><em>{cls._NETRC_MACHINE}</em></abbr>]'
|
||||
desc += f' [*{cls._NETRC_MACHINE}*](## "netrc machine")'
|
||||
else:
|
||||
desc += f' [{cls._NETRC_MACHINE}]'
|
||||
if cls.IE_DESC is False:
|
||||
@@ -3646,6 +3649,38 @@ class InfoExtractor:
|
||||
or urllib.parse.unquote(os.path.splitext(url_basename(url))[0])
|
||||
or default)
|
||||
|
||||
def _extract_chapters_helper(self, chapter_list, start_function, title_function, duration, strict=True):
|
||||
if not duration:
|
||||
return
|
||||
chapter_list = [{
|
||||
'start_time': start_function(chapter),
|
||||
'title': title_function(chapter),
|
||||
} for chapter in chapter_list or []]
|
||||
if not strict:
|
||||
chapter_list.sort(key=lambda c: c['start_time'] or 0)
|
||||
|
||||
chapters = [{'start_time': 0}]
|
||||
for idx, chapter in enumerate(chapter_list):
|
||||
if chapter['start_time'] is None:
|
||||
self.report_warning(f'Incomplete chapter {idx}')
|
||||
elif chapters[-1]['start_time'] <= chapter['start_time'] <= duration:
|
||||
chapters.append(chapter)
|
||||
elif chapter not in chapters:
|
||||
self.report_warning(
|
||||
f'Invalid start time ({chapter["start_time"]} < {chapters[-1]["start_time"]}) for chapter "{chapter["title"]}"')
|
||||
return chapters[1:]
|
||||
|
||||
def _extract_chapters_from_description(self, description, duration):
|
||||
duration_re = r'(?:\d+:)?\d{1,2}:\d{2}'
|
||||
sep_re = r'(?m)^\s*(%s)\b\W*\s(%s)\s*$'
|
||||
return self._extract_chapters_helper(
|
||||
re.findall(sep_re % (duration_re, r'.+?'), description or ''),
|
||||
start_function=lambda x: parse_duration(x[0]), title_function=lambda x: x[1],
|
||||
duration=duration, strict=False) or self._extract_chapters_helper(
|
||||
re.findall(sep_re % (r'.+?', duration_re), description or ''),
|
||||
start_function=lambda x: parse_duration(x[1]), title_function=lambda x: x[0],
|
||||
duration=duration, strict=False)
|
||||
|
||||
@staticmethod
|
||||
def _availability(is_private=None, needs_premium=None, needs_subscription=None, needs_auth=None, is_unlisted=None):
|
||||
all_known = all(map(
|
||||
|
||||
@@ -240,7 +240,7 @@ class FiveThirtyEightIE(InfoExtractor):
|
||||
|
||||
|
||||
class ESPNCricInfoIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://(?:www\.)?espncricinfo\.com/video/[^#$&?/]+-(?P<id>\d+)'
|
||||
_VALID_URL = r'https?://(?:www\.)?espncricinfo\.com/(?:cricket-)?videos?/[^#$&?/]+-(?P<id>\d+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.espncricinfo.com/video/finch-chasing-comes-with-risks-despite-world-cup-trend-1289135',
|
||||
'info_dict': {
|
||||
@@ -252,6 +252,17 @@ class ESPNCricInfoIE(InfoExtractor):
|
||||
'duration': 96,
|
||||
},
|
||||
'params': {'skip_download': True}
|
||||
}, {
|
||||
'url': 'https://www.espncricinfo.com/cricket-videos/daryl-mitchell-mitchell-santner-is-one-of-the-best-white-ball-spinners-india-vs-new-zealand-1356225',
|
||||
'info_dict': {
|
||||
'id': '1356225',
|
||||
'ext': 'mp4',
|
||||
'description': '"Santner has done it for a long time for New Zealand - we\'re lucky to have him"',
|
||||
'upload_date': '20230128',
|
||||
'title': 'Mitchell: \'Santner is one of the best white-ball spinners at the moment\'',
|
||||
'duration': 87,
|
||||
},
|
||||
'params': {'skip_download': 'm3u8'},
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
|
||||
@@ -15,6 +15,7 @@ from ..utils import (
|
||||
UnsupportedError,
|
||||
determine_ext,
|
||||
dict_get,
|
||||
extract_basic_auth,
|
||||
format_field,
|
||||
int_or_none,
|
||||
is_html,
|
||||
@@ -2372,9 +2373,8 @@ class GenericIE(InfoExtractor):
|
||||
**smuggled_data.get('http_headers', {})
|
||||
})
|
||||
new_url = full_response.geturl()
|
||||
if new_url == urllib.parse.urlparse(url)._replace(scheme='https').geturl():
|
||||
url = new_url
|
||||
elif url != new_url:
|
||||
url = urllib.parse.urlparse(url)._replace(scheme=urllib.parse.urlparse(new_url).scheme).geturl()
|
||||
if new_url != extract_basic_auth(url)[0]:
|
||||
self.report_following_redirect(new_url)
|
||||
if force_videoid:
|
||||
new_url = smuggle_url(new_url, {'force_videoid': force_videoid})
|
||||
@@ -2393,14 +2393,15 @@ class GenericIE(InfoExtractor):
|
||||
self.report_detected('direct video link')
|
||||
headers = smuggled_data.get('http_headers', {})
|
||||
format_id = str(m.group('format_id'))
|
||||
ext = determine_ext(url)
|
||||
subtitles = {}
|
||||
if format_id.endswith('mpegurl'):
|
||||
if format_id.endswith('mpegurl') or ext == 'm3u8':
|
||||
formats, subtitles = self._extract_m3u8_formats_and_subtitles(url, video_id, 'mp4', headers=headers)
|
||||
info_dict.update(self._fragment_query(url))
|
||||
elif format_id.endswith('mpd') or format_id.endswith('dash+xml'):
|
||||
elif format_id.endswith('mpd') or format_id.endswith('dash+xml') or ext == 'mpd':
|
||||
formats, subtitles = self._extract_mpd_formats_and_subtitles(url, video_id, headers=headers)
|
||||
info_dict.update(self._fragment_query(url))
|
||||
elif format_id == 'f4m':
|
||||
elif format_id == 'f4m' or ext == 'f4m':
|
||||
formats = self._extract_f4m_formats(url, video_id, headers=headers)
|
||||
else:
|
||||
formats = [{
|
||||
|
||||
@@ -3,8 +3,8 @@ import re
|
||||
from .common import InfoExtractor
|
||||
from ..compat import compat_parse_qs
|
||||
from ..utils import (
|
||||
determine_ext,
|
||||
ExtractorError,
|
||||
determine_ext,
|
||||
get_element_by_class,
|
||||
int_or_none,
|
||||
lowercase_escape,
|
||||
@@ -163,15 +163,13 @@ class GoogleDriveIE(InfoExtractor):
|
||||
video_id = self._match_id(url)
|
||||
video_info = compat_parse_qs(self._download_webpage(
|
||||
'https://drive.google.com/get_video_info',
|
||||
video_id, query={'docid': video_id}))
|
||||
video_id, 'Downloading video webpage', query={'docid': video_id}))
|
||||
|
||||
def get_value(key):
|
||||
return try_get(video_info, lambda x: x[key][0])
|
||||
|
||||
reason = get_value('reason')
|
||||
title = get_value('title')
|
||||
if not title and reason:
|
||||
raise ExtractorError(reason, expected=True)
|
||||
|
||||
formats = []
|
||||
fmt_stream_map = (get_value('fmt_stream_map') or '').split(',')
|
||||
@@ -216,6 +214,11 @@ class GoogleDriveIE(InfoExtractor):
|
||||
urlh = request_source_file(source_url, 'source')
|
||||
if urlh:
|
||||
def add_source_format(urlh):
|
||||
nonlocal title
|
||||
if not title:
|
||||
title = self._search_regex(
|
||||
r'\bfilename="([^"]+)"', urlh.headers.get('Content-Disposition'),
|
||||
'title', default=None)
|
||||
formats.append({
|
||||
# Use redirect URLs as download URLs in order to calculate
|
||||
# correct cookies in _calc_cookies.
|
||||
@@ -251,7 +254,10 @@ class GoogleDriveIE(InfoExtractor):
|
||||
or 'unable to extract confirmation code')
|
||||
|
||||
if not formats and reason:
|
||||
self.raise_no_formats(reason, expected=True)
|
||||
if title:
|
||||
self.raise_no_formats(reason, expected=True)
|
||||
else:
|
||||
raise ExtractorError(reason, expected=True)
|
||||
|
||||
hl = get_value('hl')
|
||||
subtitles_id = None
|
||||
|
||||
@@ -7,7 +7,8 @@ from ..utils import (
|
||||
js_to_json,
|
||||
urlencode_postdata,
|
||||
ExtractorError,
|
||||
parse_qs
|
||||
parse_qs,
|
||||
traverse_obj
|
||||
)
|
||||
|
||||
|
||||
@@ -15,8 +16,7 @@ class IPrimaIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://(?!cnn)(?:[^/]+)\.iprima\.cz/(?:[^/]+/)*(?P<id>[^/?#&]+)'
|
||||
_GEO_BYPASS = False
|
||||
_NETRC_MACHINE = 'iprima'
|
||||
_LOGIN_URL = 'https://auth.iprima.cz/oauth2/login'
|
||||
_TOKEN_URL = 'https://auth.iprima.cz/oauth2/token'
|
||||
_AUTH_ROOT = 'https://auth.iprima.cz'
|
||||
access_token = None
|
||||
|
||||
_TESTS = [{
|
||||
@@ -67,7 +67,7 @@ class IPrimaIE(InfoExtractor):
|
||||
return
|
||||
|
||||
login_page = self._download_webpage(
|
||||
self._LOGIN_URL, None, note='Downloading login page',
|
||||
f'{self._AUTH_ROOT}/oauth2/login', None, note='Downloading login page',
|
||||
errnote='Downloading login page failed')
|
||||
|
||||
login_form = self._hidden_inputs(login_page)
|
||||
@@ -76,11 +76,20 @@ class IPrimaIE(InfoExtractor):
|
||||
'_email': username,
|
||||
'_password': password})
|
||||
|
||||
_, login_handle = self._download_webpage_handle(
|
||||
self._LOGIN_URL, None, data=urlencode_postdata(login_form),
|
||||
profile_select_html, login_handle = self._download_webpage_handle(
|
||||
f'{self._AUTH_ROOT}/oauth2/login', None, data=urlencode_postdata(login_form),
|
||||
note='Logging in')
|
||||
|
||||
code = parse_qs(login_handle.geturl()).get('code')[0]
|
||||
# a profile may need to be selected first, even when there is only a single one
|
||||
if '/profile-select' in login_handle.geturl():
|
||||
profile_id = self._search_regex(
|
||||
r'data-identifier\s*=\s*["\']?(\w+)', profile_select_html, 'profile id')
|
||||
|
||||
login_handle = self._request_webpage(
|
||||
f'{self._AUTH_ROOT}/user/profile-select-perform/{profile_id}', None,
|
||||
query={'continueUrl': '/user/login?redirect_uri=/user/'}, note='Selecting profile')
|
||||
|
||||
code = traverse_obj(login_handle.geturl(), ({parse_qs}, 'code', 0))
|
||||
if not code:
|
||||
raise ExtractorError('Login failed', expected=True)
|
||||
|
||||
@@ -89,10 +98,10 @@ class IPrimaIE(InfoExtractor):
|
||||
'client_id': 'prima_sso',
|
||||
'grant_type': 'authorization_code',
|
||||
'code': code,
|
||||
'redirect_uri': 'https://auth.iprima.cz/sso/auth-check'}
|
||||
'redirect_uri': f'{self._AUTH_ROOT}/sso/auth-check'}
|
||||
|
||||
token_data = self._download_json(
|
||||
self._TOKEN_URL, None,
|
||||
f'{self._AUTH_ROOT}/oauth2/token', None,
|
||||
note='Downloading token', errnote='Downloading token failed',
|
||||
data=urlencode_postdata(token_request_data))
|
||||
|
||||
@@ -115,14 +124,22 @@ class IPrimaIE(InfoExtractor):
|
||||
|
||||
webpage = self._download_webpage(url, video_id)
|
||||
|
||||
title = self._html_search_meta(
|
||||
title = self._html_extract_title(webpage) or self._html_search_meta(
|
||||
['og:title', 'twitter:title'],
|
||||
webpage, 'title', default=None)
|
||||
|
||||
video_id = self._search_regex((
|
||||
r'productId\s*=\s*([\'"])(?P<id>p\d+)\1',
|
||||
r'pproduct_id\s*=\s*([\'"])(?P<id>p\d+)\1'),
|
||||
webpage, 'real id', group='id')
|
||||
r'pproduct_id\s*=\s*([\'"])(?P<id>p\d+)\1',
|
||||
), webpage, 'real id', group='id', default=None)
|
||||
|
||||
if not video_id:
|
||||
nuxt_data = self._search_nuxt_data(webpage, video_id, traverse='data')
|
||||
video_id = traverse_obj(
|
||||
nuxt_data, (..., 'content', 'additionals', 'videoPlayId', {str}), get_all=False)
|
||||
|
||||
if not video_id:
|
||||
self.raise_no_formats('Unable to extract video ID from webpage')
|
||||
|
||||
metadata = self._download_json(
|
||||
f'https://api.play-backend.iprima.cz/api/v1//products/id-{video_id}/play',
|
||||
|
||||
@@ -91,7 +91,7 @@ class IviIE(InfoExtractor):
|
||||
for site in (353, 183):
|
||||
content_data = (data % site).encode()
|
||||
if site == 353:
|
||||
if not Cryptodome:
|
||||
if not Cryptodome.CMAC:
|
||||
continue
|
||||
|
||||
timestamp = (self._download_json(
|
||||
@@ -105,8 +105,8 @@ class IviIE(InfoExtractor):
|
||||
|
||||
query = {
|
||||
'ts': timestamp,
|
||||
'sign': Cryptodome.Hash.CMAC.new(self._LIGHT_KEY, timestamp.encode() + content_data,
|
||||
Cryptodome.Cipher.Blowfish).hexdigest(),
|
||||
'sign': Cryptodome.CMAC.new(self._LIGHT_KEY, timestamp.encode() + content_data,
|
||||
Cryptodome.Blowfish).hexdigest(),
|
||||
}
|
||||
else:
|
||||
query = {}
|
||||
@@ -126,7 +126,7 @@ class IviIE(InfoExtractor):
|
||||
extractor_msg = 'Video %s does not exist'
|
||||
elif site == 353:
|
||||
continue
|
||||
elif not Cryptodome:
|
||||
elif not Cryptodome.CMAC:
|
||||
raise ExtractorError('pycryptodomex not found. Please install', expected=True)
|
||||
elif message:
|
||||
extractor_msg += ': ' + message
|
||||
|
||||
135
yt_dlp/extractor/lefigaro.py
Normal file
135
yt_dlp/extractor/lefigaro.py
Normal file
@@ -0,0 +1,135 @@
|
||||
import json
|
||||
import math
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
InAdvancePagedList,
|
||||
traverse_obj,
|
||||
)
|
||||
|
||||
|
||||
class LeFigaroVideoEmbedIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://video\.lefigaro\.fr/embed/[^?#]+/(?P<id>[\w-]+)'
|
||||
|
||||
_TESTS = [{
|
||||
'url': 'https://video.lefigaro.fr/embed/figaro/video/les-francais-ne-veulent-ils-plus-travailler-suivez-en-direct-le-club-le-figaro-idees/',
|
||||
'md5': 'e94de44cd80818084352fcf8de1ce82c',
|
||||
'info_dict': {
|
||||
'id': 'g9j7Eovo',
|
||||
'title': 'Les Français ne veulent-ils plus travailler ? Retrouvez Le Club Le Figaro Idées',
|
||||
'description': 'md5:862b8813148ba4bf10763a65a69dfe41',
|
||||
'upload_date': '20230216',
|
||||
'timestamp': 1676581615,
|
||||
'duration': 3076,
|
||||
'thumbnail': r're:^https?://[^?#]+\.(?:jpeg|jpg)',
|
||||
'ext': 'mp4',
|
||||
},
|
||||
}, {
|
||||
'url': 'https://video.lefigaro.fr/embed/figaro/video/intelligence-artificielle-faut-il-sen-mefier/',
|
||||
'md5': '0b3f10332b812034b3a3eda1ef877c5f',
|
||||
'info_dict': {
|
||||
'id': 'LeAgybyc',
|
||||
'title': 'Intelligence artificielle : faut-il s’en méfier ?',
|
||||
'description': 'md5:249d136e3e5934a67c8cb704f8abf4d2',
|
||||
'upload_date': '20230124',
|
||||
'timestamp': 1674584477,
|
||||
'duration': 860,
|
||||
'thumbnail': r're:^https?://[^?#]+\.(?:jpeg|jpg)',
|
||||
'ext': 'mp4',
|
||||
},
|
||||
}]
|
||||
|
||||
_WEBPAGE_TESTS = [{
|
||||
'url': 'https://video.lefigaro.fr/figaro/video/suivez-en-direct-le-club-le-figaro-international-avec-philippe-gelie-9/',
|
||||
'md5': '3972ddf2d5f8b98699f191687258e2f9',
|
||||
'info_dict': {
|
||||
'id': 'QChnbPYA',
|
||||
'title': 'Où en est le couple franco-allemand ? Retrouvez Le Club Le Figaro International',
|
||||
'description': 'md5:6f47235b7e7c93b366fd8ebfa10572ac',
|
||||
'upload_date': '20230123',
|
||||
'timestamp': 1674503575,
|
||||
'duration': 3153,
|
||||
'thumbnail': r're:^https?://[^?#]+\.(?:jpeg|jpg)',
|
||||
'age_limit': 0,
|
||||
'ext': 'mp4',
|
||||
},
|
||||
}, {
|
||||
'url': 'https://video.lefigaro.fr/figaro/video/la-philosophe-nathalie-sarthou-lajus-est-linvitee-du-figaro-live/',
|
||||
'md5': '3ac0a0769546ee6be41ab52caea5d9a9',
|
||||
'info_dict': {
|
||||
'id': 'QJzqoNbf',
|
||||
'title': 'La philosophe Nathalie Sarthou-Lajus est l’invitée du Figaro Live',
|
||||
'description': 'md5:c586793bb72e726c83aa257f99a8c8c4',
|
||||
'upload_date': '20230217',
|
||||
'timestamp': 1676661986,
|
||||
'duration': 1558,
|
||||
'thumbnail': r're:^https?://[^?#]+\.(?:jpeg|jpg)',
|
||||
'age_limit': 0,
|
||||
'ext': 'mp4',
|
||||
},
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
display_id = self._match_id(url)
|
||||
webpage = self._download_webpage(url, display_id)
|
||||
|
||||
player_data = self._search_nextjs_data(webpage, display_id)['props']['pageProps']['pageData']['playerData']
|
||||
|
||||
return self.url_result(
|
||||
f'jwplatform:{player_data["videoId"]}', title=player_data.get('title'),
|
||||
description=player_data.get('description'), thumbnail=player_data.get('poster'))
|
||||
|
||||
|
||||
class LeFigaroVideoSectionIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://video\.lefigaro\.fr/figaro/(?P<id>[\w-]+)/?(?:[#?]|$)'
|
||||
|
||||
_TESTS = [{
|
||||
'url': 'https://video.lefigaro.fr/figaro/le-club-le-figaro-idees/',
|
||||
'info_dict': {
|
||||
'id': 'le-club-le-figaro-idees',
|
||||
'title': 'Le Club Le Figaro Idées',
|
||||
},
|
||||
'playlist_mincount': 14,
|
||||
}, {
|
||||
'url': 'https://video.lefigaro.fr/figaro/factu/',
|
||||
'info_dict': {
|
||||
'id': 'factu',
|
||||
'title': 'Factu',
|
||||
},
|
||||
'playlist_mincount': 519,
|
||||
}]
|
||||
|
||||
_PAGE_SIZE = 20
|
||||
|
||||
def _get_api_response(self, display_id, page_num, note=None):
|
||||
return self._download_json(
|
||||
'https://api-graphql.lefigaro.fr/graphql', display_id, note=note,
|
||||
query={
|
||||
'id': 'flive-website_UpdateListPage_1fb260f996bca2d78960805ac382544186b3225f5bedb43ad08b9b8abef79af6',
|
||||
'variables': json.dumps({
|
||||
'slug': display_id,
|
||||
'videosLimit': self._PAGE_SIZE,
|
||||
'sort': 'DESC',
|
||||
'order': 'PUBLISHED_AT',
|
||||
'page': page_num,
|
||||
}).encode(),
|
||||
})
|
||||
|
||||
def _real_extract(self, url):
|
||||
display_id = self._match_id(url)
|
||||
initial_response = self._get_api_response(display_id, page_num=1)['data']['playlist']
|
||||
|
||||
def page_func(page_num):
|
||||
api_response = self._get_api_response(display_id, page_num + 1, note=f'Downloading page {page_num + 1}')
|
||||
|
||||
return [self.url_result(
|
||||
video['embedUrl'], LeFigaroVideoEmbedIE, **traverse_obj(video, {
|
||||
'title': 'name',
|
||||
'description': 'description',
|
||||
'thumbnail': 'thumbnailUrl',
|
||||
})) for video in api_response['data']['playlist']['jsonLd'][0]['itemListElement']]
|
||||
|
||||
entries = InAdvancePagedList(
|
||||
page_func, math.ceil(initial_response['videoCount'] / self._PAGE_SIZE), self._PAGE_SIZE)
|
||||
|
||||
return self.playlist_result(entries, playlist_id=display_id, playlist_title=initial_response.get('title'))
|
||||
24
yt_dlp/extractor/lumni.py
Normal file
24
yt_dlp/extractor/lumni.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from .common import InfoExtractor
|
||||
from .francetv import FranceTVIE
|
||||
|
||||
|
||||
class LumniIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://(?:www\.)?lumni\.fr/video/(?P<id>[\w-]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.lumni.fr/video/l-homme-et-son-environnement-dans-la-revolution-industrielle',
|
||||
'md5': '960e8240c4f2c7a20854503a71e52f5e',
|
||||
'info_dict': {
|
||||
'id': 'd2b9a4e5-a526-495b-866c-ab72737e3645',
|
||||
'ext': 'mp4',
|
||||
'title': "L'homme et son environnement dans la révolution industrielle - L'ère de l'homme",
|
||||
'thumbnail': 'https://assets.webservices.francetelevisions.fr/v1/assets/images/a7/17/9f/a7179f5f-63a5-4e11-8d4d-012ab942d905.jpg',
|
||||
'duration': 230,
|
||||
}
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
display_id = self._match_id(url)
|
||||
webpage = self._download_webpage(url, display_id)
|
||||
video_id = self._html_search_regex(
|
||||
r'<div[^>]+data-factoryid\s*=\s*["\']([^"\']+)', webpage, 'video id')
|
||||
return self.url_result(f'francetv:{video_id}', FranceTVIE, video_id)
|
||||
@@ -1,7 +1,13 @@
|
||||
import re
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import clean_html, get_element_html_by_class
|
||||
from ..utils import (
|
||||
remove_end,
|
||||
str_or_none,
|
||||
strip_or_none,
|
||||
traverse_obj,
|
||||
urljoin,
|
||||
)
|
||||
|
||||
|
||||
class MediaStreamIE(InfoExtractor):
|
||||
@@ -117,39 +123,56 @@ class MediaStreamIE(InfoExtractor):
|
||||
|
||||
|
||||
class WinSportsVideoIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://www\.winsports\.co/videos/(?P<display_id>[\w-]+)-(?P<id>\d+)'
|
||||
_VALID_URL = r'https?://www\.winsports\.co/videos/(?P<id>[\w-]+)'
|
||||
|
||||
_TESTS = [{
|
||||
'url': 'https://www.winsports.co/videos/siempre-castellanos-gran-atajada-del-portero-cardenal-para-evitar-la-caida-de-su-arco-60536',
|
||||
'info_dict': {
|
||||
'id': '62dc8357162c4b0821fcfb3c',
|
||||
'display_id': 'siempre-castellanos-gran-atajada-del-portero-cardenal-para-evitar-la-caida-de-su-arco',
|
||||
'display_id': 'siempre-castellanos-gran-atajada-del-portero-cardenal-para-evitar-la-caida-de-su-arco-60536',
|
||||
'title': '¡Siempre Castellanos! Gran atajada del portero \'cardenal\' para evitar la caída de su arco',
|
||||
'description': 'md5:eb811b2b2882bdc59431732c06b905f2',
|
||||
'thumbnail': r're:^https?://[^?#]+62dc8357162c4b0821fcfb3c',
|
||||
'ext': 'mp4',
|
||||
},
|
||||
'params': {'skip_download': 'm3u8'},
|
||||
}, {
|
||||
'url': 'https://www.winsports.co/videos/observa-aqui-los-goles-del-empate-entre-tolima-y-nacional-60548',
|
||||
'info_dict': {
|
||||
'id': '62dcb875ef12a5526790b552',
|
||||
'display_id': 'observa-aqui-los-goles-del-empate-entre-tolima-y-nacional',
|
||||
'display_id': 'observa-aqui-los-goles-del-empate-entre-tolima-y-nacional-60548',
|
||||
'title': 'Observa aquí los goles del empate entre Tolima y Nacional',
|
||||
'description': 'md5:b19402ba6e46558b93fd24b873eea9c9',
|
||||
'thumbnail': r're:^https?://[^?#]+62dcb875ef12a5526790b552',
|
||||
'ext': 'mp4',
|
||||
},
|
||||
'params': {'skip_download': 'm3u8'},
|
||||
}, {
|
||||
'url': 'https://www.winsports.co/videos/equidad-vuelve-defender-su-arco-de-remates-de-junior',
|
||||
'info_dict': {
|
||||
'id': '63fa7eca72f1741ad3a4d515',
|
||||
'display_id': 'equidad-vuelve-defender-su-arco-de-remates-de-junior',
|
||||
'title': '⚽ Equidad vuelve a defender su arco de remates de Junior',
|
||||
'description': 'Remate de Sierra',
|
||||
'thumbnail': r're:^https?://[^?#]+63fa7eca72f1741ad3a4d515',
|
||||
'ext': 'mp4',
|
||||
},
|
||||
'params': {'skip_download': 'm3u8'},
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
display_id, video_id = self._match_valid_url(url).group('display_id', 'id')
|
||||
display_id = self._match_id(url)
|
||||
webpage = self._download_webpage(url, display_id)
|
||||
|
||||
json_ld = self._search_json_ld(webpage, display_id, expected_type='VideoObject', default={})
|
||||
media_setting_json = self._search_json(
|
||||
r'<script\s*[^>]+data-drupal-selector="drupal-settings-json">', webpage, 'drupal-setting-json', display_id)
|
||||
|
||||
mediastream_id = media_setting_json['settings']['mediastream_formatter'][video_id]['mediastream_id']
|
||||
mediastream_id = traverse_obj(
|
||||
media_setting_json, ('settings', 'mediastream_formatter', ..., 'mediastream_id', {str_or_none}),
|
||||
get_all=False) or json_ld.get('url')
|
||||
if not mediastream_id:
|
||||
self.raise_no_formats('No MediaStream embed found in webpage')
|
||||
|
||||
return self.url_result(
|
||||
f'https://mdstrm.com/embed/{mediastream_id}', MediaStreamIE, video_id, url_transparent=True,
|
||||
display_id=display_id, video_title=clean_html(get_element_html_by_class('title-news', webpage)))
|
||||
urljoin('https://mdstrm.com/embed/', mediastream_id), MediaStreamIE, display_id, url_transparent=True,
|
||||
display_id=display_id, video_title=strip_or_none(remove_end(json_ld.get('title'), '| Win Sports')))
|
||||
|
||||
@@ -21,6 +21,7 @@ class NTVRuIE(InfoExtractor):
|
||||
'description': 'Командующий Черноморским флотом провел переговоры в штабе ВМС Украины',
|
||||
'thumbnail': r're:^http://.*\.jpg',
|
||||
'duration': 136,
|
||||
'view_count': int,
|
||||
},
|
||||
}, {
|
||||
'url': 'http://www.ntv.ru/video/novosti/750370/',
|
||||
@@ -32,6 +33,7 @@ class NTVRuIE(InfoExtractor):
|
||||
'description': 'Родные пассажиров пропавшего Boeing не верят в трагический исход',
|
||||
'thumbnail': r're:^http://.*\.jpg',
|
||||
'duration': 172,
|
||||
'view_count': int,
|
||||
},
|
||||
}, {
|
||||
'url': 'http://www.ntv.ru/peredacha/segodnya/m23700/o232416',
|
||||
@@ -43,6 +45,7 @@ class NTVRuIE(InfoExtractor):
|
||||
'description': '«Сегодня». 21 марта 2014 года. 16:00',
|
||||
'thumbnail': r're:^http://.*\.jpg',
|
||||
'duration': 1496,
|
||||
'view_count': int,
|
||||
},
|
||||
}, {
|
||||
'url': 'https://www.ntv.ru/kino/Koma_film/m70281/o336036/video/',
|
||||
@@ -54,6 +57,7 @@ class NTVRuIE(InfoExtractor):
|
||||
'description': 'Остросюжетный фильм «Кома»',
|
||||
'thumbnail': r're:^http://.*\.jpg',
|
||||
'duration': 5592,
|
||||
'view_count': int,
|
||||
},
|
||||
}, {
|
||||
'url': 'http://www.ntv.ru/serial/Delo_vrachey/m31760/o233916/',
|
||||
@@ -65,6 +69,7 @@ class NTVRuIE(InfoExtractor):
|
||||
'description': '«Дело врачей»: «Деревце жизни»',
|
||||
'thumbnail': r're:^http://.*\.jpg',
|
||||
'duration': 2590,
|
||||
'view_count': int,
|
||||
},
|
||||
}, {
|
||||
# Schemeless file URL
|
||||
@@ -115,6 +120,14 @@ class NTVRuIE(InfoExtractor):
|
||||
'url': file_,
|
||||
'filesize': int_or_none(xpath_text(video, './%ssize' % format_id)),
|
||||
})
|
||||
hls_manifest = xpath_text(video, './playback/hls')
|
||||
if hls_manifest:
|
||||
formats.extend(self._extract_m3u8_formats(
|
||||
hls_manifest, video_id, m3u8_id='hls', fatal=False))
|
||||
dash_manifest = xpath_text(video, './playback/dash')
|
||||
if dash_manifest:
|
||||
formats.extend(self._extract_mpd_formats(
|
||||
dash_manifest, video_id, mpd_id='dash', fatal=False))
|
||||
|
||||
return {
|
||||
'id': xpath_text(video, './id'),
|
||||
|
||||
@@ -18,7 +18,7 @@ class PrankCastIE(InfoExtractor):
|
||||
'cast': ['Devonanustart', 'Phonelosers'],
|
||||
'description': '',
|
||||
'categories': ['prank'],
|
||||
'tags': ['prank call', 'prank'],
|
||||
'tags': ['prank call', 'prank', 'live show'],
|
||||
'upload_date': '20220825'
|
||||
}
|
||||
}, {
|
||||
@@ -35,7 +35,7 @@ class PrankCastIE(InfoExtractor):
|
||||
'cast': ['phonelosers'],
|
||||
'description': '',
|
||||
'categories': ['prank'],
|
||||
'tags': ['prank call', 'prank'],
|
||||
'tags': ['prank call', 'prank', 'live show'],
|
||||
'upload_date': '20221006'
|
||||
}
|
||||
}]
|
||||
@@ -62,5 +62,5 @@ class PrankCastIE(InfoExtractor):
|
||||
'cast': list(filter(None, [uploader] + traverse_obj(guests_json, (..., 'name')))),
|
||||
'description': json_info.get('broadcast_description'),
|
||||
'categories': [json_info.get('broadcast_category')],
|
||||
'tags': self._parse_json(json_info.get('broadcast_tags') or '{}', video_id)
|
||||
'tags': try_call(lambda: json_info['broadcast_tags'].split(','))
|
||||
}
|
||||
|
||||
@@ -25,8 +25,7 @@ class RutubeBaseIE(InfoExtractor):
|
||||
video_id, 'Downloading video JSON',
|
||||
'Unable to download video JSON', query=query)
|
||||
|
||||
@staticmethod
|
||||
def _extract_info(video, video_id=None, require_title=True):
|
||||
def _extract_info(self, video, video_id=None, require_title=True):
|
||||
title = video['title'] if require_title else video.get('title')
|
||||
|
||||
age_limit = video.get('is_adult')
|
||||
@@ -35,13 +34,15 @@ class RutubeBaseIE(InfoExtractor):
|
||||
|
||||
uploader_id = try_get(video, lambda x: x['author']['id'])
|
||||
category = try_get(video, lambda x: x['category']['name'])
|
||||
description = video.get('description')
|
||||
duration = int_or_none(video.get('duration'))
|
||||
|
||||
return {
|
||||
'id': video.get('id') or video_id if video_id else video['id'],
|
||||
'title': title,
|
||||
'description': video.get('description'),
|
||||
'description': description,
|
||||
'thumbnail': video.get('thumbnail_url'),
|
||||
'duration': int_or_none(video.get('duration')),
|
||||
'duration': duration,
|
||||
'uploader': try_get(video, lambda x: x['author']['name']),
|
||||
'uploader_id': compat_str(uploader_id) if uploader_id else None,
|
||||
'timestamp': unified_timestamp(video.get('created_ts')),
|
||||
@@ -50,6 +51,7 @@ class RutubeBaseIE(InfoExtractor):
|
||||
'view_count': int_or_none(video.get('hits')),
|
||||
'comment_count': int_or_none(video.get('comments_count')),
|
||||
'is_live': bool_or_none(video.get('is_livestream')),
|
||||
'chapters': self._extract_chapters_from_description(description, duration),
|
||||
}
|
||||
|
||||
def _download_and_extract_info(self, video_id, query=None):
|
||||
@@ -111,8 +113,9 @@ class RutubeIE(RutubeBaseIE):
|
||||
'view_count': int,
|
||||
'thumbnail': 'http://pic.rutubelist.ru/video/d2/a0/d2a0aec998494a396deafc7ba2c82add.jpg',
|
||||
'category': ['Новости и СМИ'],
|
||||
|
||||
'chapters': [],
|
||||
},
|
||||
'expected_warnings': ['Unable to download f4m'],
|
||||
}, {
|
||||
'url': 'http://rutube.ru/play/embed/a10e53b86e8f349080f718582ce4c661',
|
||||
'only_matching': True,
|
||||
@@ -142,7 +145,28 @@ class RutubeIE(RutubeBaseIE):
|
||||
'view_count': int,
|
||||
'thumbnail': 'http://pic.rutubelist.ru/video/f2/d4/f2d42b54be0a6e69c1c22539e3152156.jpg',
|
||||
'category': ['Видеоигры'],
|
||||
'chapters': [],
|
||||
},
|
||||
'expected_warnings': ['Unable to download f4m'],
|
||||
}, {
|
||||
'url': 'https://rutube.ru/video/c65b465ad0c98c89f3b25cb03dcc87c6/',
|
||||
'info_dict': {
|
||||
'id': 'c65b465ad0c98c89f3b25cb03dcc87c6',
|
||||
'ext': 'mp4',
|
||||
'chapters': 'count:4',
|
||||
'category': ['Бизнес и предпринимательство'],
|
||||
'description': 'md5:252feac1305257d8c1bab215cedde75d',
|
||||
'thumbnail': 'http://pic.rutubelist.ru/video/71/8f/718f27425ea9706073eb80883dd3787b.png',
|
||||
'duration': 782,
|
||||
'age_limit': 0,
|
||||
'uploader_id': '23491359',
|
||||
'timestamp': 1677153329,
|
||||
'view_count': int,
|
||||
'upload_date': '20230223',
|
||||
'title': 'Бизнес с нуля: найм сотрудников. Интервью с директором строительной компании',
|
||||
'uploader': 'Стас Быков',
|
||||
},
|
||||
'expected_warnings': ['Unable to download f4m'],
|
||||
}]
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
from .common import InfoExtractor
|
||||
|
||||
from ..utils import (
|
||||
format_field,
|
||||
join_nonempty,
|
||||
strip_or_none,
|
||||
traverse_obj,
|
||||
unified_timestamp,
|
||||
strip_or_none
|
||||
)
|
||||
|
||||
|
||||
@@ -13,98 +12,131 @@ class SportDeutschlandIE(InfoExtractor):
|
||||
_TESTS = [{
|
||||
'url': 'https://sportdeutschland.tv/blauweissbuchholztanzsport/buchholzer-formationswochenende-2023-samstag-1-bundesliga-landesliga',
|
||||
'info_dict': {
|
||||
'id': '983758e9-5829-454d-a3cf-eb27bccc3c94',
|
||||
'id': '9839a5c7-0dbb-48a8-ab63-3b408adc7b54',
|
||||
'ext': 'mp4',
|
||||
'title': 'Buchholzer Formationswochenende 2023 - Samstag - 1. Bundesliga / Landesliga',
|
||||
'display_id': 'blauweissbuchholztanzsport/buchholzer-formationswochenende-2023-samstag-1-bundesliga-landesliga',
|
||||
'description': 'md5:a288c794a5ee69e200d8f12982f81a87',
|
||||
'live_status': 'was_live',
|
||||
'channel': 'Blau-Weiss Buchholz Tanzsport',
|
||||
'channel_url': 'https://sportdeutschland.tv/blauweissbuchholztanzsport',
|
||||
'channel_id': '93ec33c9-48be-43b6-b404-e016b64fdfa3',
|
||||
'display_id': '9839a5c7-0dbb-48a8-ab63-3b408adc7b54',
|
||||
'duration': 32447,
|
||||
'upload_date': '20230114',
|
||||
'timestamp': 1673730018.0,
|
||||
'timestamp': 1673733618,
|
||||
}
|
||||
}, {
|
||||
'url': 'https://sportdeutschland.tv/deutscherbadmintonverband/bwf-tour-1-runde-feld-1-yonex-gainward-german-open-2022-0',
|
||||
'info_dict': {
|
||||
'id': '95b97d9a-04f6-4880-9039-182985c33943',
|
||||
'id': '95c80c52-6b9a-4ae9-9197-984145adfced',
|
||||
'ext': 'mp4',
|
||||
'title': 'BWF Tour: 1. Runde Feld 1 - YONEX GAINWARD German Open 2022',
|
||||
'display_id': 'deutscherbadmintonverband/bwf-tour-1-runde-feld-1-yonex-gainward-german-open-2022-0',
|
||||
'description': 'md5:2afb5996ceb9ac0b2ac81f563d3a883e',
|
||||
'live_status': 'was_live',
|
||||
'channel': 'Deutscher Badminton Verband',
|
||||
'channel_url': 'https://sportdeutschland.tv/deutscherbadmintonverband',
|
||||
'channel_id': '93ca5866-2551-49fc-8424-6db35af58920',
|
||||
'display_id': '95c80c52-6b9a-4ae9-9197-984145adfced',
|
||||
'duration': 41097,
|
||||
'upload_date': '20220309',
|
||||
'timestamp': 1646860727.0,
|
||||
}
|
||||
}, {
|
||||
'url': 'https://sportdeutschland.tv/ggcbremen/formationswochenende-latein-2023',
|
||||
'info_dict': {
|
||||
'id': '9889785e-55b0-4d97-a72a-ce9a9f157cce',
|
||||
'title': 'Formationswochenende Latein 2023 - Samstag',
|
||||
'display_id': 'ggcbremen/formationswochenende-latein-2023',
|
||||
'description': 'md5:6e4060d40ff6a8f8eeb471b51a8f08b2',
|
||||
'live_status': 'was_live',
|
||||
'channel': 'Grün-Gold-Club Bremen e.V.',
|
||||
'channel_id': '9888f04e-bb46-4c7f-be47-df960a4167bb',
|
||||
'channel_url': 'https://sportdeutschland.tv/ggcbremen',
|
||||
},
|
||||
'playlist_count': 3,
|
||||
'playlist': [{
|
||||
'info_dict': {
|
||||
'id': '988e1fea-9d44-4fab-8c72-3085fb667547',
|
||||
'ext': 'mp4',
|
||||
'channel_url': 'https://sportdeutschland.tv/ggcbremen',
|
||||
'channel_id': '9888f04e-bb46-4c7f-be47-df960a4167bb',
|
||||
'channel': 'Grün-Gold-Club Bremen e.V.',
|
||||
'duration': 86,
|
||||
'title': 'Formationswochenende Latein 2023 - Samstag Part 1',
|
||||
'upload_date': '20230225',
|
||||
'timestamp': 1677349909,
|
||||
'live_status': 'was_live',
|
||||
}
|
||||
}]
|
||||
}, {
|
||||
'url': 'https://sportdeutschland.tv/dtb/gymnastik-international-tag-1',
|
||||
'info_dict': {
|
||||
'id': '95d71b8a-370a-4b87-ad16-94680da18528',
|
||||
'ext': 'mp4',
|
||||
'title': r're:Gymnastik International - Tag 1 .+',
|
||||
'display_id': 'dtb/gymnastik-international-tag-1',
|
||||
'channel_id': '936ecef1-2f4a-4e08-be2f-68073cb7ecab',
|
||||
'channel': 'Deutscher Turner-Bund',
|
||||
'channel_url': 'https://sportdeutschland.tv/dtb',
|
||||
'description': 'md5:07a885dde5838a6f0796ee21dc3b0c52',
|
||||
'live_status': 'is_live',
|
||||
},
|
||||
'skip': 'live',
|
||||
}]
|
||||
|
||||
def _process_video(self, asset_id, video):
|
||||
is_live = video['type'] == 'mux_live'
|
||||
token = self._download_json(
|
||||
f'https://api.sportdeutschland.tv/api/frontend/asset-token/{asset_id}',
|
||||
video['id'], query={'type': video['type'], 'playback_id': video['src']})['token']
|
||||
formats, subtitles = self._extract_m3u8_formats_and_subtitles(
|
||||
f'https://stream.mux.com/{video["src"]}.m3u8?token={token}', video['id'], live=is_live)
|
||||
|
||||
return {
|
||||
'is_live': is_live,
|
||||
'formats': formats,
|
||||
'subtitles': subtitles,
|
||||
**traverse_obj(video, {
|
||||
'id': 'id',
|
||||
'duration': ('duration', {lambda x: float(x) > 0 and float(x)}),
|
||||
'timestamp': ('created_at', {unified_timestamp})
|
||||
}),
|
||||
}
|
||||
|
||||
def _real_extract(self, url):
|
||||
display_id = self._match_id(url)
|
||||
meta = self._download_json(
|
||||
'https://api.sportdeutschland.tv/api/stateless/frontend/assets/' + display_id,
|
||||
f'https://api.sportdeutschland.tv/api/stateless/frontend/assets/{display_id}',
|
||||
display_id, query={'access_token': 'true'})
|
||||
|
||||
asset_id = traverse_obj(meta, 'id', 'uuid')
|
||||
|
||||
info = {
|
||||
'id': asset_id,
|
||||
'channel_url': format_field(meta, ('profile', 'slug'), 'https://sportdeutschland.tv/%s'),
|
||||
'display_id': display_id,
|
||||
**traverse_obj(meta, {
|
||||
'id': (('id', 'uuid'), ),
|
||||
'title': (('title', 'name'), {strip_or_none}),
|
||||
'description': 'description',
|
||||
'channel': ('profile', 'name'),
|
||||
'channel_id': ('profile', 'id'),
|
||||
'is_live': 'currently_live',
|
||||
'was_live': 'was_live'
|
||||
'was_live': 'was_live',
|
||||
'channel_url': ('profile', 'slug', {lambda x: f'https://sportdeutschland.tv/{x}'}),
|
||||
}, get_all=False)
|
||||
}
|
||||
|
||||
videos = meta.get('videos') or []
|
||||
parts = traverse_obj(meta, (('livestream', ('videos', ...)), ))
|
||||
entries = [{
|
||||
'title': join_nonempty(info.get('title'), f'Part {i}', delim=' '),
|
||||
**traverse_obj(info, {'channel': 'channel', 'channel_id': 'channel_id',
|
||||
'channel_url': 'channel_url', 'was_live': 'was_live'}),
|
||||
**self._process_video(info['id'], video),
|
||||
} for i, video in enumerate(parts, 1)]
|
||||
|
||||
if len(videos) > 1:
|
||||
info.update({
|
||||
'_type': 'multi_video',
|
||||
'entries': self.processVideoOrStream(asset_id, video)
|
||||
} for video in enumerate(videos) if video.get('formats'))
|
||||
|
||||
elif len(videos) == 1:
|
||||
info.update(
|
||||
self.processVideoOrStream(asset_id, videos[0])
|
||||
)
|
||||
|
||||
livestream = meta.get('livestream')
|
||||
|
||||
if livestream is not None:
|
||||
info.update(
|
||||
self.processVideoOrStream(asset_id, livestream)
|
||||
)
|
||||
|
||||
return info
|
||||
|
||||
def process_video_or_stream(self, asset_id, video):
|
||||
video_id = video['id']
|
||||
video_src = video['src']
|
||||
video_type = video['type']
|
||||
|
||||
token = self._download_json(
|
||||
f'https://api.sportdeutschland.tv/api/frontend/asset-token/{asset_id}',
|
||||
video_id, query={'type': video_type, 'playback_id': video_src})['token']
|
||||
formats = self._extract_m3u8_formats(f'https://stream.mux.com/{video_src}.m3u8?token={token}', video_id)
|
||||
|
||||
video_data = {
|
||||
'display_id': video_id,
|
||||
'formats': formats,
|
||||
return {
|
||||
'_type': 'multi_video',
|
||||
**info,
|
||||
'entries': entries,
|
||||
} if len(entries) > 1 else {
|
||||
**info,
|
||||
**entries[0],
|
||||
'title': info.get('title'),
|
||||
}
|
||||
if video_type == 'mux_vod':
|
||||
video_data.update({
|
||||
'duration': video.get('duration'),
|
||||
'timestamp': unified_timestamp(video.get('created_at'))
|
||||
})
|
||||
|
||||
return video_data
|
||||
|
||||
77
yt_dlp/extractor/telecaribe.py
Normal file
77
yt_dlp/extractor/telecaribe.py
Normal file
@@ -0,0 +1,77 @@
|
||||
import re
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import traverse_obj
|
||||
|
||||
|
||||
class TelecaribePlayIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://(?:www\.)?play\.telecaribe\.co/(?P<id>[\w-]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.play.telecaribe.co/breicok',
|
||||
'info_dict': {
|
||||
'id': 'breicok',
|
||||
'title': 'Breicok',
|
||||
},
|
||||
'playlist_count': 7,
|
||||
}, {
|
||||
'url': 'https://www.play.telecaribe.co/si-fue-gol-de-yepes',
|
||||
'info_dict': {
|
||||
'id': 'si-fue-gol-de-yepes',
|
||||
'title': 'Sí Fue Gol de Yepes',
|
||||
},
|
||||
'playlist_count': 6,
|
||||
}, {
|
||||
'url': 'https://www.play.telecaribe.co/ciudad-futura',
|
||||
'info_dict': {
|
||||
'id': 'ciudad-futura',
|
||||
'title': 'Ciudad Futura',
|
||||
},
|
||||
'playlist_count': 10,
|
||||
}, {
|
||||
'url': 'https://www.play.telecaribe.co/live',
|
||||
'info_dict': {
|
||||
'id': 'live',
|
||||
'title': r're:^Señal en vivo',
|
||||
'live_status': 'is_live',
|
||||
'ext': 'mp4',
|
||||
},
|
||||
'params': {
|
||||
'skip_download': 'Livestream',
|
||||
}
|
||||
}]
|
||||
|
||||
def _download_player_webpage(self, webpage, display_id):
|
||||
page_id = self._search_regex(
|
||||
(r'window.firstPageId\s*=\s*["\']([^"\']+)', r'<div[^>]+id\s*=\s*"pageBackground_([^"]+)'),
|
||||
webpage, 'page_id')
|
||||
|
||||
props = self._download_json(self._search_regex(
|
||||
rf'<link[^>]+href\s*=\s*"([^"]+)"[^>]+id\s*=\s*"features_{page_id}"',
|
||||
webpage, 'json_props_url'), display_id)['props']['render']['compProps']
|
||||
|
||||
return self._download_webpage(traverse_obj(props, (..., 'url'))[-1], display_id)
|
||||
|
||||
def _get_clean_title(self, title):
|
||||
return re.sub(r'\s*\|\s*Telecaribe\s*VOD', '', title or '').strip() or None
|
||||
|
||||
def _real_extract(self, url):
|
||||
display_id = self._match_id(url)
|
||||
webpage = self._download_webpage(url, display_id)
|
||||
player = self._download_player_webpage(webpage, display_id)
|
||||
|
||||
if display_id != 'live':
|
||||
return self.playlist_from_matches(
|
||||
re.findall(r'<a[^>]+href\s*=\s*"([^"]+\.mp4)', player), display_id,
|
||||
self._get_clean_title(self._og_search_title(webpage)))
|
||||
|
||||
formats, subtitles = self._extract_m3u8_formats_and_subtitles(
|
||||
self._search_regex(r'(?:let|const|var)\s+source\s*=\s*["\']([^"\']+)', player, 'm3u8 url'),
|
||||
display_id, 'mp4')
|
||||
|
||||
return {
|
||||
'id': display_id,
|
||||
'title': self._get_clean_title(self._og_search_title(webpage)),
|
||||
'formats': formats,
|
||||
'subtitles': subtitles,
|
||||
'is_live': True,
|
||||
}
|
||||
@@ -8,6 +8,7 @@ from .common import InfoExtractor
|
||||
from ..aes import aes_cbc_encrypt_bytes
|
||||
from ..utils import (
|
||||
ExtractorError,
|
||||
float_or_none,
|
||||
determine_ext,
|
||||
int_or_none,
|
||||
js_to_json,
|
||||
@@ -19,6 +20,16 @@ from ..utils import (
|
||||
class TencentBaseIE(InfoExtractor):
|
||||
"""Subclasses must set _API_URL, _APP_VERSION, _PLATFORM, _HOST, _REFERER"""
|
||||
|
||||
def _check_api_response(self, api_response):
|
||||
msg = api_response.get('msg')
|
||||
if api_response.get('code') != '0.0' and msg is not None:
|
||||
if msg in (
|
||||
'您所在区域暂无此内容版权(如设置VPN请关闭后重试)',
|
||||
'This content is not available in your area due to copyright restrictions. Please choose other videos.'
|
||||
):
|
||||
self.raise_geo_restricted()
|
||||
raise ExtractorError(f'Tencent said: {msg}')
|
||||
|
||||
def _get_ckey(self, video_id, url, guid):
|
||||
ua = self.get_param('http_headers')['User-Agent']
|
||||
|
||||
@@ -47,6 +58,11 @@ class TencentBaseIE(InfoExtractor):
|
||||
'sphttps': '1', # Enable HTTPS
|
||||
'otype': 'json',
|
||||
'spwm': '1',
|
||||
'hevclv': '28', # Enable HEVC
|
||||
'drm': '40', # Enable DRM
|
||||
# For HDR
|
||||
'spvideo': '4',
|
||||
'spsfrhdr': '100',
|
||||
# For SHD
|
||||
'host': self._HOST,
|
||||
'referer': self._REFERER,
|
||||
@@ -63,7 +79,6 @@ class TencentBaseIE(InfoExtractor):
|
||||
|
||||
def _extract_video_formats_and_subtitles(self, api_response, video_id):
|
||||
video_response = api_response['vl']['vi'][0]
|
||||
video_width, video_height = video_response.get('vw'), video_response.get('vh')
|
||||
|
||||
formats, subtitles = [], {}
|
||||
for video_format in video_response['ul']['ui']:
|
||||
@@ -71,47 +86,61 @@ class TencentBaseIE(InfoExtractor):
|
||||
fmts, subs = self._extract_m3u8_formats_and_subtitles(
|
||||
video_format['url'] + traverse_obj(video_format, ('hls', 'pt'), default=''),
|
||||
video_id, 'mp4', fatal=False)
|
||||
for f in fmts:
|
||||
f.update({'width': video_width, 'height': video_height})
|
||||
|
||||
formats.extend(fmts)
|
||||
self._merge_subtitles(subs, target=subtitles)
|
||||
else:
|
||||
formats.append({
|
||||
'url': f'{video_format["url"]}{video_response["fn"]}?vkey={video_response["fvkey"]}',
|
||||
'width': video_width,
|
||||
'height': video_height,
|
||||
'ext': 'mp4',
|
||||
})
|
||||
|
||||
identifier = video_response.get('br')
|
||||
format_response = traverse_obj(
|
||||
api_response, ('fl', 'fi', lambda _, v: v['br'] == identifier),
|
||||
expected_type=dict, get_all=False) or {}
|
||||
common_info = {
|
||||
'width': video_response.get('vw'),
|
||||
'height': video_response.get('vh'),
|
||||
'abr': float_or_none(format_response.get('audiobandwidth'), scale=1000),
|
||||
'vbr': float_or_none(format_response.get('bandwidth'), scale=1000),
|
||||
'fps': format_response.get('vfps'),
|
||||
'format': format_response.get('sname'),
|
||||
'format_id': format_response.get('name'),
|
||||
'format_note': format_response.get('resolution'),
|
||||
'dynamic_range': {'hdr10': 'hdr10'}.get(format_response.get('name'), 'sdr'),
|
||||
'has_drm': format_response.get('drm', 0) != 0,
|
||||
}
|
||||
for f in formats:
|
||||
f.update(common_info)
|
||||
|
||||
return formats, subtitles
|
||||
|
||||
def _extract_video_native_subtitles(self, api_response, subtitles_format):
|
||||
def _extract_video_native_subtitles(self, api_response):
|
||||
subtitles = {}
|
||||
for subtitle in traverse_obj(api_response, ('sfl', 'fi')) or ():
|
||||
subtitles.setdefault(subtitle['lang'].lower(), []).append({
|
||||
'url': subtitle['url'],
|
||||
'ext': subtitles_format,
|
||||
'ext': 'srt' if subtitle.get('captionType') == 1 else 'vtt',
|
||||
'protocol': 'm3u8_native' if determine_ext(subtitle['url']) == 'm3u8' else 'http',
|
||||
})
|
||||
|
||||
return subtitles
|
||||
|
||||
def _extract_all_video_formats_and_subtitles(self, url, video_id, series_id):
|
||||
api_responses = [self._get_video_api_response(url, video_id, series_id, 'srt', 'hls', 'hd')]
|
||||
self._check_api_response(api_responses[0])
|
||||
qualities = traverse_obj(api_responses, (0, 'fl', 'fi', ..., 'name')) or ('shd', 'fhd')
|
||||
for q in qualities:
|
||||
if q not in ('ld', 'sd', 'hd'):
|
||||
api_responses.append(self._get_video_api_response(
|
||||
url, video_id, series_id, 'vtt', 'hls', q))
|
||||
self._check_api_response(api_responses[-1])
|
||||
|
||||
formats, subtitles = [], {}
|
||||
for video_format, subtitle_format, video_quality in (
|
||||
# '': 480p, 'shd': 720p, 'fhd': 1080p
|
||||
('mp4', 'srt', ''), ('hls', 'vtt', 'shd'), ('hls', 'vtt', 'fhd')):
|
||||
api_response = self._get_video_api_response(
|
||||
url, video_id, series_id, subtitle_format, video_format, video_quality)
|
||||
|
||||
if api_response.get('em') != 0 and api_response.get('exem') != 0:
|
||||
if '您所在区域暂无此内容版权' in api_response.get('msg'):
|
||||
self.raise_geo_restricted()
|
||||
raise ExtractorError(f'Tencent said: {api_response.get("msg")}')
|
||||
|
||||
for api_response in api_responses:
|
||||
fmts, subs = self._extract_video_formats_and_subtitles(api_response, video_id)
|
||||
native_subtitles = self._extract_video_native_subtitles(api_response, subtitle_format)
|
||||
native_subtitles = self._extract_video_native_subtitles(api_response)
|
||||
|
||||
formats.extend(fmts)
|
||||
self._merge_subtitles(subs, native_subtitles, target=subtitles)
|
||||
@@ -120,7 +149,7 @@ class TencentBaseIE(InfoExtractor):
|
||||
|
||||
def _get_clean_title(self, title):
|
||||
return re.sub(
|
||||
r'\s*[_\-]\s*(?:Watch online|腾讯视频|(?:高清)?1080P在线观看平台).*?$',
|
||||
r'\s*[_\-]\s*(?:Watch online|Watch HD Video Online|WeTV|腾讯视频|(?:高清)?1080P在线观看平台).*?$',
|
||||
'', title or '').strip() or None
|
||||
|
||||
|
||||
@@ -147,27 +176,29 @@ class VQQVideoIE(VQQBaseIE):
|
||||
|
||||
_TESTS = [{
|
||||
'url': 'https://v.qq.com/x/page/q326831cny0.html',
|
||||
'md5': '826ef93682df09e3deac4a6e6e8cdb6e',
|
||||
'md5': '84568b3722e15e9cd023b5594558c4a7',
|
||||
'info_dict': {
|
||||
'id': 'q326831cny0',
|
||||
'ext': 'mp4',
|
||||
'title': '我是选手:雷霆裂阵,终极时刻',
|
||||
'description': 'md5:e7ed70be89244017dac2a835a10aeb1e',
|
||||
'thumbnail': r're:^https?://[^?#]+q326831cny0',
|
||||
'format_id': r're:^shd',
|
||||
},
|
||||
}, {
|
||||
'url': 'https://v.qq.com/x/page/o3013za7cse.html',
|
||||
'md5': 'b91cbbeada22ef8cc4b06df53e36fa21',
|
||||
'md5': 'cc431c4f9114a55643893c2c8ebf5592',
|
||||
'info_dict': {
|
||||
'id': 'o3013za7cse',
|
||||
'ext': 'mp4',
|
||||
'title': '欧阳娜娜VLOG',
|
||||
'description': 'md5:29fe847497a98e04a8c3826e499edd2e',
|
||||
'thumbnail': r're:^https?://[^?#]+o3013za7cse',
|
||||
'format_id': r're:^shd',
|
||||
},
|
||||
}, {
|
||||
'url': 'https://v.qq.com/x/cover/7ce5noezvafma27/a00269ix3l8.html',
|
||||
'md5': '71459c5375c617c265a22f083facce67',
|
||||
'md5': '87968df6238a65d2478f19c25adf850b',
|
||||
'info_dict': {
|
||||
'id': 'a00269ix3l8',
|
||||
'ext': 'mp4',
|
||||
@@ -175,10 +206,11 @@ class VQQVideoIE(VQQBaseIE):
|
||||
'description': 'md5:8cae3534327315b3872fbef5e51b5c5b',
|
||||
'thumbnail': r're:^https?://[^?#]+7ce5noezvafma27',
|
||||
'series': '鸡毛飞上天',
|
||||
'format_id': r're:^shd',
|
||||
},
|
||||
}, {
|
||||
'url': 'https://v.qq.com/x/cover/mzc00200p29k31e/s0043cwsgj0.html',
|
||||
'md5': '96b9fd4a189fdd4078c111f21d7ac1bc',
|
||||
'md5': 'fadd10bf88aec3420f06f19ee1d24c5b',
|
||||
'info_dict': {
|
||||
'id': 's0043cwsgj0',
|
||||
'ext': 'mp4',
|
||||
@@ -186,6 +218,7 @@ class VQQVideoIE(VQQBaseIE):
|
||||
'description': 'md5:1d8c3a0b8729ae3827fa5b2d3ebd5213',
|
||||
'thumbnail': r're:^https?://[^?#]+s0043cwsgj0',
|
||||
'series': '青年理工工作者生活研究所',
|
||||
'format_id': r're:^shd',
|
||||
},
|
||||
}, {
|
||||
# Geo-restricted to China
|
||||
@@ -319,6 +352,7 @@ class WeTvEpisodeIE(WeTvBaseIE):
|
||||
'episode': 'Episode 1',
|
||||
'episode_number': 1,
|
||||
'duration': 2835,
|
||||
'format_id': r're:^shd',
|
||||
},
|
||||
}, {
|
||||
'url': 'https://wetv.vip/en/play/u37kgfnfzs73kiu/p0039b9nvik',
|
||||
@@ -333,6 +367,7 @@ class WeTvEpisodeIE(WeTvBaseIE):
|
||||
'episode': 'Episode 1',
|
||||
'episode_number': 1,
|
||||
'duration': 2454,
|
||||
'format_id': r're:^shd',
|
||||
},
|
||||
}, {
|
||||
'url': 'https://wetv.vip/en/play/lcxgwod5hapghvw-WeTV-PICK-A-BOO/i0042y00lxp-Zhao-Lusi-Describes-The-First-Experiences-She-Had-In-Who-Rules-The-World-%7C-WeTV-PICK-A-BOO',
|
||||
@@ -342,11 +377,12 @@ class WeTvEpisodeIE(WeTvBaseIE):
|
||||
'ext': 'mp4',
|
||||
'title': 'md5:f7a0857dbe5fbbe2e7ad630b92b54e6a',
|
||||
'description': 'md5:76260cb9cdc0ef76826d7ca9d92fadfa',
|
||||
'thumbnail': r're:^https?://[^?#]+lcxgwod5hapghvw',
|
||||
'thumbnail': r're:^https?://[^?#]+i0042y00lxp',
|
||||
'series': 'WeTV PICK-A-BOO',
|
||||
'episode': 'Episode 0',
|
||||
'episode_number': 0,
|
||||
'duration': 442,
|
||||
'format_id': r're:^shd',
|
||||
},
|
||||
}]
|
||||
|
||||
@@ -406,6 +442,7 @@ class IflixEpisodeIE(IflixBaseIE):
|
||||
'episode': 'Episode 1',
|
||||
'episode_number': 1,
|
||||
'duration': 2639,
|
||||
'format_id': r're:^shd',
|
||||
},
|
||||
}, {
|
||||
'url': 'https://www.iflix.com/en/play/fvvrcc3ra9lbtt1-Take-My-Brother-Away/i0029sd3gm1-EP1%EF%BC%9ATake-My-Brother-Away',
|
||||
@@ -420,6 +457,7 @@ class IflixEpisodeIE(IflixBaseIE):
|
||||
'episode': 'Episode 1',
|
||||
'episode_number': 1,
|
||||
'duration': 228,
|
||||
'format_id': r're:^shd',
|
||||
},
|
||||
}]
|
||||
|
||||
|
||||
@@ -21,17 +21,36 @@ class TubeTuGrazBaseIE(InfoExtractor):
|
||||
if not urlh:
|
||||
return
|
||||
|
||||
urlh = self._request_webpage(
|
||||
content, urlh = self._download_webpage_handle(
|
||||
urlh.geturl(), None, fatal=False, headers={'referer': urlh.geturl()},
|
||||
note='logging in', errnote='unable to log in', data=urlencode_postdata({
|
||||
note='logging in', errnote='unable to log in',
|
||||
data=urlencode_postdata({
|
||||
'lang': 'de',
|
||||
'_eventId_proceed': '',
|
||||
'j_username': username,
|
||||
'j_password': password
|
||||
}))
|
||||
if not urlh or urlh.geturl() == 'https://tube.tugraz.at/paella/ui/index.html':
|
||||
return
|
||||
|
||||
if urlh and urlh.geturl() != 'https://tube.tugraz.at/paella/ui/index.html':
|
||||
if not self._html_search_regex(
|
||||
r'<p\b[^>]*>(Bitte geben Sie einen OTP-Wert ein:)</p>',
|
||||
content, 'TFA prompt', default=None):
|
||||
self.report_warning('unable to login: incorrect password')
|
||||
return
|
||||
|
||||
content, urlh = self._download_webpage_handle(
|
||||
urlh.geturl(), None, fatal=False, headers={'referer': urlh.geturl()},
|
||||
note='logging in with TFA', errnote='unable to log in with TFA',
|
||||
data=urlencode_postdata({
|
||||
'lang': 'de',
|
||||
'_eventId_proceed': '',
|
||||
'j_tokenNumber': self._get_tfa_info(),
|
||||
}))
|
||||
if not urlh or urlh.geturl() == 'https://tube.tugraz.at/paella/ui/index.html':
|
||||
return
|
||||
|
||||
self.report_warning('unable to login: incorrect TFA code')
|
||||
|
||||
def _extract_episode(self, episode_info):
|
||||
id = episode_info.get('id')
|
||||
|
||||
@@ -1,149 +1,201 @@
|
||||
import re
|
||||
import urllib.parse
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import ExtractorError
|
||||
from ..compat import compat_urlparse
|
||||
from ..utils import (
|
||||
OnDemandPagedList,
|
||||
determine_ext,
|
||||
parse_iso8601,
|
||||
traverse_obj,
|
||||
)
|
||||
|
||||
|
||||
class TuneInBaseIE(InfoExtractor):
|
||||
_API_BASE_URL = 'http://tunein.com/tuner/tune/'
|
||||
_VALID_URL_BASE = r'https?://(?:www\.)?tunein\.com'
|
||||
|
||||
def _real_extract(self, url):
|
||||
content_id = self._match_id(url)
|
||||
|
||||
content_info = self._download_json(
|
||||
self._API_BASE_URL + self._API_URL_QUERY % content_id,
|
||||
content_id, note='Downloading JSON metadata')
|
||||
|
||||
title = content_info['Title']
|
||||
thumbnail = content_info.get('Logo')
|
||||
location = content_info.get('Location')
|
||||
streams_url = content_info.get('StreamUrl')
|
||||
if not streams_url:
|
||||
raise ExtractorError('No downloadable streams found', expected=True)
|
||||
if not streams_url.startswith('http://'):
|
||||
streams_url = compat_urlparse.urljoin(url, streams_url)
|
||||
def _extract_metadata(self, webpage, content_id):
|
||||
return self._search_json(r'window.INITIAL_STATE=', webpage, 'hydration', content_id, fatal=False)
|
||||
|
||||
def _extract_formats_and_subtitles(self, content_id):
|
||||
streams = self._download_json(
|
||||
streams_url, content_id, note='Downloading stream data',
|
||||
transform_source=lambda s: re.sub(r'^\s*\((.*)\);\s*$', r'\1', s))['Streams']
|
||||
f'https://opml.radiotime.com/Tune.ashx?render=json&formats=mp3,aac,ogg,flash,hls&id={content_id}',
|
||||
content_id)['body']
|
||||
|
||||
is_live = None
|
||||
formats = []
|
||||
formats, subtitles = [], {}
|
||||
for stream in streams:
|
||||
if stream.get('Type') == 'Live':
|
||||
is_live = True
|
||||
reliability = stream.get('Reliability')
|
||||
format_note = (
|
||||
'Reliability: %d%%' % reliability
|
||||
if reliability is not None else None)
|
||||
formats.append({
|
||||
'preference': (
|
||||
0 if reliability is None or reliability > 90
|
||||
else 1),
|
||||
'abr': stream.get('Bandwidth'),
|
||||
'ext': stream.get('MediaType').lower(),
|
||||
'acodec': stream.get('MediaType'),
|
||||
'vcodec': 'none',
|
||||
'url': stream.get('Url'),
|
||||
'source_preference': reliability,
|
||||
'format_note': format_note,
|
||||
})
|
||||
if stream.get('media_type') == 'hls':
|
||||
fmts, subs = self._extract_m3u8_formats_and_subtitles(stream['url'], content_id, fatal=False)
|
||||
formats.extend(fmts)
|
||||
self._merge_subtitles(subs, target=subtitles)
|
||||
elif determine_ext(stream['url']) == 'pls':
|
||||
playlist_content = self._download_webpage(stream['url'], content_id)
|
||||
formats.append({
|
||||
'url': self._search_regex(r'File1=(.*)', playlist_content, 'url', fatal=False),
|
||||
'abr': stream.get('bitrate'),
|
||||
'ext': stream.get('media_type'),
|
||||
})
|
||||
else:
|
||||
formats.append({
|
||||
'url': stream['url'],
|
||||
'abr': stream.get('bitrate'),
|
||||
'ext': stream.get('media_type'),
|
||||
})
|
||||
|
||||
return {
|
||||
'id': content_id,
|
||||
'title': title,
|
||||
'formats': formats,
|
||||
'thumbnail': thumbnail,
|
||||
'location': location,
|
||||
'is_live': is_live,
|
||||
}
|
||||
|
||||
|
||||
class TuneInClipIE(TuneInBaseIE):
|
||||
IE_NAME = 'tunein:clip'
|
||||
_VALID_URL = r'https?://(?:www\.)?tunein\.com/station/.*?audioClipId\=(?P<id>\d+)'
|
||||
_API_URL_QUERY = '?tuneType=AudioClip&audioclipId=%s'
|
||||
|
||||
_TESTS = [{
|
||||
'url': 'http://tunein.com/station/?stationId=246119&audioClipId=816',
|
||||
'md5': '99f00d772db70efc804385c6b47f4e77',
|
||||
'info_dict': {
|
||||
'id': '816',
|
||||
'title': '32m',
|
||||
'ext': 'mp3',
|
||||
},
|
||||
}]
|
||||
return formats, subtitles
|
||||
|
||||
|
||||
class TuneInStationIE(TuneInBaseIE):
|
||||
IE_NAME = 'tunein:station'
|
||||
_VALID_URL = r'https?://(?:www\.)?tunein\.com/(?:radio/.*?-s|station/.*?StationId=|embed/player/s)(?P<id>\d+)'
|
||||
_EMBED_REGEX = [r'<iframe[^>]+src=["\'](?P<url>(?:https?://)?tunein\.com/embed/player/[pst]\d+)']
|
||||
_API_URL_QUERY = '?tuneType=Station&stationId=%s'
|
||||
|
||||
@classmethod
|
||||
def suitable(cls, url):
|
||||
return False if TuneInClipIE.suitable(url) else super(TuneInStationIE, cls).suitable(url)
|
||||
_VALID_URL = TuneInBaseIE._VALID_URL_BASE + r'(?:/radio/[^?#]+-|/embed/player/)(?P<id>s\d+)'
|
||||
_EMBED_REGEX = [r'<iframe[^>]+src=["\'](?P<url>(?:https?://)?tunein\.com/embed/player/s\d+)']
|
||||
|
||||
_TESTS = [{
|
||||
'url': 'http://tunein.com/radio/Jazz24-885-s34682/',
|
||||
'url': 'https://tunein.com/radio/Jazz24-885-s34682/',
|
||||
'info_dict': {
|
||||
'id': '34682',
|
||||
'title': 'Jazz 24 on 88.5 Jazz24 - KPLU-HD2',
|
||||
'id': 's34682',
|
||||
'title': 're:^Jazz24',
|
||||
'description': 'md5:d6d0b89063fd68d529fa7058ee98619b',
|
||||
'thumbnail': 're:^https?://[^?&]+/s34682',
|
||||
'location': 'Seattle-Tacoma, US',
|
||||
'ext': 'mp3',
|
||||
'location': 'Tacoma, WA',
|
||||
'live_status': 'is_live',
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True, # live stream
|
||||
'skip_download': True,
|
||||
},
|
||||
}, {
|
||||
'url': 'http://tunein.com/embed/player/s6404/',
|
||||
'url': 'https://tunein.com/embed/player/s6404/',
|
||||
'only_matching': True,
|
||||
}]
|
||||
|
||||
|
||||
class TuneInProgramIE(TuneInBaseIE):
|
||||
IE_NAME = 'tunein:program'
|
||||
_VALID_URL = r'https?://(?:www\.)?tunein\.com/(?:radio/.*?-p|program/.*?ProgramId=|embed/player/p)(?P<id>\d+)'
|
||||
_API_URL_QUERY = '?tuneType=Program&programId=%s'
|
||||
|
||||
_TESTS = [{
|
||||
'url': 'http://tunein.com/radio/Jazz-24-p2506/',
|
||||
}, {
|
||||
'url': 'https://tunein.com/radio/BBC-Radio-1-988-s24939/',
|
||||
'info_dict': {
|
||||
'id': '2506',
|
||||
'title': 'Jazz 24 on 91.3 WUKY-HD3',
|
||||
'id': 's24939',
|
||||
'title': 're:^BBC Radio 1',
|
||||
'description': 'md5:f3f75f7423398d87119043c26e7bfb84',
|
||||
'thumbnail': 're:^https?://[^?&]+/s24939',
|
||||
'location': 'London, UK',
|
||||
'ext': 'mp3',
|
||||
'location': 'Lexington, KY',
|
||||
'live_status': 'is_live',
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True, # live stream
|
||||
'skip_download': True,
|
||||
},
|
||||
}, {
|
||||
'url': 'http://tunein.com/embed/player/p191660/',
|
||||
'only_matching': True,
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
station_id = self._match_id(url)
|
||||
|
||||
class TuneInTopicIE(TuneInBaseIE):
|
||||
IE_NAME = 'tunein:topic'
|
||||
_VALID_URL = r'https?://(?:www\.)?tunein\.com/(?:topic/.*?TopicId=|embed/player/t)(?P<id>\d+)'
|
||||
_API_URL_QUERY = '?tuneType=Topic&topicId=%s'
|
||||
webpage = self._download_webpage(url, station_id)
|
||||
metadata = self._extract_metadata(webpage, station_id)
|
||||
|
||||
formats, subtitles = self._extract_formats_and_subtitles(station_id)
|
||||
return {
|
||||
'id': station_id,
|
||||
'title': traverse_obj(metadata, ('profiles', station_id, 'title')),
|
||||
'description': traverse_obj(metadata, ('profiles', station_id, 'description')),
|
||||
'thumbnail': traverse_obj(metadata, ('profiles', station_id, 'image')),
|
||||
'timestamp': parse_iso8601(
|
||||
traverse_obj(metadata, ('profiles', station_id, 'actions', 'play', 'publishTime'))),
|
||||
'location': traverse_obj(
|
||||
metadata, ('profiles', station_id, 'metadata', 'properties', 'location', 'displayName'),
|
||||
('profiles', station_id, 'properties', 'location', 'displayName')),
|
||||
'formats': formats,
|
||||
'subtitles': subtitles,
|
||||
'is_live': traverse_obj(metadata, ('profiles', station_id, 'actions', 'play', 'isLive')),
|
||||
}
|
||||
|
||||
|
||||
class TuneInPodcastIE(TuneInBaseIE):
|
||||
_VALID_URL = TuneInBaseIE._VALID_URL_BASE + r'/(?:podcasts/[^?#]+-|embed/player/)(?P<id>p\d+)/?(?:#|$)'
|
||||
_EMBED_REGEX = [r'<iframe[^>]+src=["\'](?P<url>(?:https?://)?tunein\.com/embed/player/p\d+)']
|
||||
|
||||
_TESTS = [{
|
||||
'url': 'http://tunein.com/topic/?TopicId=101830576',
|
||||
'md5': 'c31a39e6f988d188252eae7af0ef09c9',
|
||||
'url': 'https://tunein.com/podcasts/Technology-Podcasts/Artificial-Intelligence-p1153019',
|
||||
'info_dict': {
|
||||
'id': '101830576',
|
||||
'title': 'Votez pour moi du 29 octobre 2015 (29/10/15)',
|
||||
'ext': 'mp3',
|
||||
'location': 'Belgium',
|
||||
'id': 'p1153019',
|
||||
'title': 'Lex Fridman Podcast',
|
||||
'description': 'md5:bedc4e5f1c94f7dec6e4317b5654b00d',
|
||||
},
|
||||
'playlist_mincount': 200,
|
||||
}, {
|
||||
'url': 'http://tunein.com/embed/player/t101830576/',
|
||||
'only_matching': True,
|
||||
'url': 'https://tunein.com/embed/player/p191660/',
|
||||
'only_matching': True
|
||||
}, {
|
||||
'url': 'https://tunein.com/podcasts/World-News/BBC-News-p14/',
|
||||
'info_dict': {
|
||||
'id': 'p14',
|
||||
'title': 'BBC News',
|
||||
'description': 'md5:1218e575eeaff75f48ed978261fa2068',
|
||||
},
|
||||
'playlist_mincount': 200,
|
||||
}]
|
||||
|
||||
_PAGE_SIZE = 30
|
||||
|
||||
def _real_extract(self, url):
|
||||
podcast_id = self._match_id(url)
|
||||
|
||||
webpage = self._download_webpage(url, podcast_id, fatal=False)
|
||||
metadata = self._extract_metadata(webpage, podcast_id)
|
||||
|
||||
def page_func(page_num):
|
||||
api_response = self._download_json(
|
||||
f'https://api.tunein.com/profiles/{podcast_id}/contents', podcast_id,
|
||||
note=f'Downloading page {page_num + 1}', query={
|
||||
'filter': 't:free',
|
||||
'offset': page_num * self._PAGE_SIZE,
|
||||
'limit': self._PAGE_SIZE,
|
||||
})
|
||||
|
||||
return [
|
||||
self.url_result(
|
||||
f'https://tunein.com/podcasts/{podcast_id}?topicId={episode["GuideId"][1:]}',
|
||||
TuneInPodcastEpisodeIE, title=episode.get('Title'))
|
||||
for episode in api_response['Items']]
|
||||
|
||||
entries = OnDemandPagedList(page_func, self._PAGE_SIZE)
|
||||
return self.playlist_result(
|
||||
entries, playlist_id=podcast_id, title=traverse_obj(metadata, ('profiles', podcast_id, 'title')),
|
||||
description=traverse_obj(metadata, ('profiles', podcast_id, 'description')))
|
||||
|
||||
|
||||
class TuneInPodcastEpisodeIE(TuneInBaseIE):
|
||||
_VALID_URL = TuneInBaseIE._VALID_URL_BASE + r'/podcasts/(?:[^?&]+-)?(?P<podcast_id>p\d+)/?\?topicId=(?P<id>\w\d+)'
|
||||
|
||||
_TESTS = [{
|
||||
'url': 'https://tunein.com/podcasts/Technology-Podcasts/Artificial-Intelligence-p1153019/?topicId=236404354',
|
||||
'info_dict': {
|
||||
'id': 't236404354',
|
||||
'title': '#351 \u2013 MrBeast: Future of YouTube, Twitter, TikTok, and Instagram',
|
||||
'description': 'md5:e1734db6f525e472c0c290d124a2ad77',
|
||||
'thumbnail': 're:^https?://[^?&]+/p1153019',
|
||||
'timestamp': 1673458571,
|
||||
'upload_date': '20230111',
|
||||
'series_id': 'p1153019',
|
||||
'series': 'Lex Fridman Podcast',
|
||||
'ext': 'mp3',
|
||||
},
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
podcast_id, episode_id = self._match_valid_url(url).group('podcast_id', 'id')
|
||||
episode_id = f't{episode_id}'
|
||||
|
||||
webpage = self._download_webpage(url, episode_id)
|
||||
metadata = self._extract_metadata(webpage, episode_id)
|
||||
|
||||
formats, subtitles = self._extract_formats_and_subtitles(episode_id)
|
||||
return {
|
||||
'id': episode_id,
|
||||
'title': traverse_obj(metadata, ('profiles', episode_id, 'title')),
|
||||
'description': traverse_obj(metadata, ('profiles', episode_id, 'description')),
|
||||
'thumbnail': traverse_obj(metadata, ('profiles', episode_id, 'image')),
|
||||
'timestamp': parse_iso8601(
|
||||
traverse_obj(metadata, ('profiles', episode_id, 'actions', 'play', 'publishTime'))),
|
||||
'series_id': podcast_id,
|
||||
'series': traverse_obj(metadata, ('profiles', podcast_id, 'title')),
|
||||
'formats': formats,
|
||||
'subtitles': subtitles,
|
||||
}
|
||||
|
||||
|
||||
class TuneInShortenerIE(InfoExtractor):
|
||||
IE_NAME = 'tunein:shortener'
|
||||
@@ -154,10 +206,13 @@ class TuneInShortenerIE(InfoExtractor):
|
||||
# test redirection
|
||||
'url': 'http://tun.in/ser7s',
|
||||
'info_dict': {
|
||||
'id': '34682',
|
||||
'title': 'Jazz 24 on 88.5 Jazz24 - KPLU-HD2',
|
||||
'id': 's34682',
|
||||
'title': 're:^Jazz24',
|
||||
'description': 'md5:d6d0b89063fd68d529fa7058ee98619b',
|
||||
'thumbnail': 're:^https?://[^?&]+/s34682',
|
||||
'location': 'Seattle-Tacoma, US',
|
||||
'ext': 'mp3',
|
||||
'location': 'Tacoma, WA',
|
||||
'live_status': 'is_live',
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True, # live stream
|
||||
@@ -169,6 +224,11 @@ class TuneInShortenerIE(InfoExtractor):
|
||||
# The server doesn't support HEAD requests
|
||||
urlh = self._request_webpage(
|
||||
url, redirect_id, note='Downloading redirect page')
|
||||
|
||||
url = urlh.geturl()
|
||||
url_parsed = urllib.parse.urlparse(url)
|
||||
if url_parsed.port == 443:
|
||||
url = url_parsed._replace(netloc=url_parsed.hostname).geturl()
|
||||
|
||||
self.to_screen('Following redirect: %s' % url)
|
||||
return self.url_result(url)
|
||||
|
||||
@@ -48,12 +48,12 @@ class TwitchBaseIE(InfoExtractor):
|
||||
'CollectionSideBar': '27111f1b382effad0b6def325caef1909c733fe6a4fbabf54f8d491ef2cf2f14',
|
||||
'FilterableVideoTower_Videos': 'a937f1d22e269e39a03b509f65a7490f9fc247d7f83d6ac1421523e3b68042cb',
|
||||
'ClipsCards__User': 'b73ad2bfaecfd30a9e6c28fada15bd97032c83ec77a0440766a56fe0bd632777',
|
||||
'ChannelCollectionsContent': '07e3691a1bad77a36aba590c351180439a40baefc1c275356f40fc7082419a84',
|
||||
'StreamMetadata': '1c719a40e481453e5c48d9bb585d971b8b372f8ebb105b17076722264dfa5b3e',
|
||||
'ChannelCollectionsContent': '447aec6a0cc1e8d0a8d7732d47eb0762c336a2294fdb009e9c9d854e49d484b9',
|
||||
'StreamMetadata': 'a647c2a13599e5991e175155f798ca7f1ecddde73f7f341f39009c14dbf59962',
|
||||
'ComscoreStreamingQuery': 'e1edae8122517d013405f237ffcc124515dc6ded82480a88daef69c83b53ac01',
|
||||
'VideoAccessToken_Clip': '36b89d2507fce29e5ca551df756d27c1cfe079e2609642b4390aa4c35796eb11',
|
||||
'VideoPreviewOverlay': '3006e77e51b128d838fa4e835723ca4dc9a05c5efd4466c1085215c6e437e65c',
|
||||
'VideoMetadata': '226edb3e692509f727fd56821f5653c05740242c82b0388883e0c0e75dcbf687',
|
||||
'VideoMetadata': '49b5b8f268cdeb259d75b58dcb0c1a748e3b575003448a2333dc5cdafd49adad',
|
||||
'VideoPlayer_ChapterSelectButtonVideo': '8d2793384aac3773beab5e59bd5d6f585aedb923d292800119e03d40cd0f9b41',
|
||||
'VideoPlayer_VODSeekbarPreviewVideo': '07e99e4d56c5a7c67117a154777b0baf85a5ffefa393b213f4bc712ccaf85dd6',
|
||||
}
|
||||
@@ -380,13 +380,14 @@ class TwitchVodIE(TwitchBaseIE):
|
||||
}],
|
||||
'Downloading stream metadata GraphQL')
|
||||
|
||||
video = traverse_obj(data, (0, 'data', 'video'))
|
||||
video['moments'] = traverse_obj(data, (1, 'data', 'video', 'moments', 'edges', ..., 'node'))
|
||||
video['storyboard'] = traverse_obj(data, (2, 'data', 'video', 'seekPreviewsURL'), expected_type=url_or_none)
|
||||
|
||||
video = traverse_obj(data, (..., 'data', 'video'), get_all=False)
|
||||
if video is None:
|
||||
raise ExtractorError(
|
||||
'Video %s does not exist' % item_id, expected=True)
|
||||
raise ExtractorError(f'Video {item_id} does not exist', expected=True)
|
||||
|
||||
video['moments'] = traverse_obj(data, (..., 'data', 'video', 'moments', 'edges', ..., 'node'))
|
||||
video['storyboard'] = traverse_obj(
|
||||
data, (..., 'data', 'video', 'seekPreviewsURL', {url_or_none}), get_all=False)
|
||||
|
||||
return video
|
||||
|
||||
def _extract_info(self, info):
|
||||
@@ -854,6 +855,13 @@ class TwitchVideosCollectionsIE(TwitchPlaylistBaseIE):
|
||||
'title': 'spamfish - Collections',
|
||||
},
|
||||
'playlist_mincount': 3,
|
||||
}, {
|
||||
'url': 'https://www.twitch.tv/monstercat/videos?filter=collections',
|
||||
'info_dict': {
|
||||
'id': 'monstercat',
|
||||
'title': 'monstercat - Collections',
|
||||
},
|
||||
'playlist_mincount': 13,
|
||||
}]
|
||||
|
||||
_OPERATION_NAME = 'ChannelCollectionsContent'
|
||||
@@ -922,6 +930,7 @@ class TwitchStreamIE(TwitchBaseIE):
|
||||
# m3u8 download
|
||||
'skip_download': True,
|
||||
},
|
||||
'skip': 'User does not exist',
|
||||
}, {
|
||||
'url': 'http://www.twitch.tv/miracle_doto#profile-0',
|
||||
'only_matching': True,
|
||||
@@ -934,6 +943,25 @@ class TwitchStreamIE(TwitchBaseIE):
|
||||
}, {
|
||||
'url': 'https://m.twitch.tv/food',
|
||||
'only_matching': True,
|
||||
}, {
|
||||
'url': 'https://www.twitch.tv/monstercat',
|
||||
'info_dict': {
|
||||
'id': '40500071752',
|
||||
'display_id': 'monstercat',
|
||||
'title': 're:Monstercat',
|
||||
'description': 'md5:0945ad625e615bc8f0469396537d87d9',
|
||||
'is_live': True,
|
||||
'timestamp': 1677107190,
|
||||
'upload_date': '20230222',
|
||||
'uploader': 'Monstercat',
|
||||
'uploader_id': 'monstercat',
|
||||
'live_status': 'is_live',
|
||||
'thumbnail': 're:https://.*.jpg',
|
||||
'ext': 'mp4',
|
||||
},
|
||||
'params': {
|
||||
'skip_download': 'Livestream',
|
||||
},
|
||||
}]
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -838,6 +838,28 @@ class TwitterIE(TwitterBaseIE):
|
||||
'timestamp': 1670306984.0,
|
||||
},
|
||||
'params': {'extractor_args': {'twitter': {'force_graphql': ['']}}},
|
||||
}, {
|
||||
# url to retweet id
|
||||
'url': 'https://twitter.com/liberdalau/status/1623739803874349067',
|
||||
'info_dict': {
|
||||
'id': '1623274794488659969',
|
||||
'display_id': '1623739803874349067',
|
||||
'ext': 'mp4',
|
||||
'title': 'Johnny Bullets - Me after going viral to over 30million people: Whoopsie-daisy',
|
||||
'description': 'md5:e873616a4a8fe0f93e71872678a672f3',
|
||||
'uploader': 'Johnny Bullets',
|
||||
'uploader_id': 'Johnnybull3ts',
|
||||
'uploader_url': 'https://twitter.com/Johnnybull3ts',
|
||||
'age_limit': 0,
|
||||
'tags': [],
|
||||
'duration': 8.033,
|
||||
'timestamp': 1675853859.0,
|
||||
'upload_date': '20230208',
|
||||
'thumbnail': r're:https://pbs\.twimg\.com/ext_tw_video_thumb/.+',
|
||||
'like_count': int,
|
||||
'repost_count': int,
|
||||
'comment_count': int,
|
||||
},
|
||||
}, {
|
||||
# onion route
|
||||
'url': 'https://twitter3e4tixl4xyajtrzo62zg5vztmjuricljdp2c5kshju4avyoid.onion/TwitterBlue/status/1484226494708662273',
|
||||
@@ -949,13 +971,13 @@ class TwitterIE(TwitterBaseIE):
|
||||
status = self._graphql_to_legacy(result, twid)
|
||||
|
||||
else:
|
||||
status = self._call_api(f'statuses/show/{twid}.json', twid, {
|
||||
status = traverse_obj(self._call_api(f'statuses/show/{twid}.json', twid, {
|
||||
'cards_platform': 'Web-12',
|
||||
'include_cards': 1,
|
||||
'include_reply_count': 1,
|
||||
'include_user_entities': 0,
|
||||
'tweet_mode': 'extended',
|
||||
})
|
||||
}), 'retweeted_status', None)
|
||||
|
||||
title = description = status['full_text'].replace('\n', ' ')
|
||||
# strip 'https -_t.co_BJYgOjSeGA' junk from filenames
|
||||
|
||||
@@ -50,10 +50,10 @@ class WrestleUniverseBaseIE(InfoExtractor):
|
||||
data=data, headers=headers, query=query, fatal=fatal)
|
||||
|
||||
def _call_encrypted_api(self, video_id, param='', msg='API', data={}, query={}, fatal=True):
|
||||
if not Cryptodome:
|
||||
if not Cryptodome.RSA:
|
||||
raise ExtractorError('pycryptodomex not found. Please install', expected=True)
|
||||
private_key = Cryptodome.PublicKey.RSA.generate(2048)
|
||||
cipher = Cryptodome.Cipher.PKCS1_OAEP.new(private_key, hashAlgo=Cryptodome.Hash.SHA1)
|
||||
private_key = Cryptodome.RSA.generate(2048)
|
||||
cipher = Cryptodome.PKCS1_OAEP.new(private_key, hashAlgo=Cryptodome.SHA1)
|
||||
|
||||
def decrypt(data):
|
||||
if not data:
|
||||
|
||||
@@ -157,3 +157,24 @@ class XVideosIE(InfoExtractor):
|
||||
'thumbnails': thumbnails,
|
||||
'age_limit': 18,
|
||||
}
|
||||
|
||||
|
||||
class XVideosQuickiesIE(InfoExtractor):
|
||||
IE_NAME = 'xvideos:quickies'
|
||||
_VALID_URL = r'https?://(?P<domain>(?:[^/]+\.)?xvideos2?\.com)/amateur-channels/[^#]+#quickies/a/(?P<id>\d+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://www.xvideos.com/amateur-channels/wifeluna#quickies/a/47258683',
|
||||
'md5': '16e322a93282667f1963915568f782c1',
|
||||
'info_dict': {
|
||||
'id': '47258683',
|
||||
'ext': 'mp4',
|
||||
'title': 'Verification video',
|
||||
'age_limit': 18,
|
||||
'duration': 16,
|
||||
'thumbnail': r're:^https://cdn.*-pic.xvideos-cdn.com/.+\.jpg',
|
||||
}
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
domain, id_ = self._match_valid_url(url).group('domain', 'id')
|
||||
return self.url_result(f'https://{domain}/video{id_}/_', XVideosIE, id_)
|
||||
|
||||
@@ -61,7 +61,22 @@ class YleAreenaIE(InfoExtractor):
|
||||
'age_limit': 0,
|
||||
'webpage_url': 'https://areena.yle.fi/1-2158940'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
'url': 'https://areena.yle.fi/1-64829589',
|
||||
'info_dict': {
|
||||
'id': '1-64829589',
|
||||
'ext': 'mp4',
|
||||
'title': 'HKO & Mälkki & Tanner',
|
||||
'description': 'md5:b4f1b1af2c6569b33f75179a86eea156',
|
||||
'series': 'Helsingin kaupunginorkesterin konsertteja',
|
||||
'thumbnail': r're:^https?://.+\.jpg$',
|
||||
'release_date': '20230120',
|
||||
},
|
||||
'params': {
|
||||
'skip_download': 'm3u8',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
def _real_extract(self, url):
|
||||
@@ -91,12 +106,22 @@ class YleAreenaIE(InfoExtractor):
|
||||
'name': sub.get('kind'),
|
||||
})
|
||||
|
||||
kaltura_id = traverse_obj(video_data, ('data', 'ongoing_ondemand', 'kaltura', 'id'), expected_type=str)
|
||||
if kaltura_id:
|
||||
info_dict = {
|
||||
'_type': 'url_transparent',
|
||||
'url': smuggle_url(f'kaltura:1955031:{kaltura_id}', {'source_url': url}),
|
||||
'ie_key': KalturaIE.ie_key(),
|
||||
}
|
||||
else:
|
||||
info_dict = {
|
||||
'id': video_id,
|
||||
'formats': self._extract_m3u8_formats(
|
||||
video_data['data']['ongoing_ondemand']['manifest_url'], video_id, 'mp4', m3u8_id='hls'),
|
||||
}
|
||||
|
||||
return {
|
||||
'_type': 'url_transparent',
|
||||
'url': smuggle_url(
|
||||
f'kaltura:1955031:{video_data["data"]["ongoing_ondemand"]["kaltura"]["id"]}',
|
||||
{'source_url': url}),
|
||||
'ie_key': KalturaIE.ie_key(),
|
||||
**info_dict,
|
||||
'title': (traverse_obj(video_data, ('data', 'ongoing_ondemand', 'title', 'fin'), expected_type=str)
|
||||
or episode or info.get('title')),
|
||||
'description': description,
|
||||
|
||||
@@ -956,7 +956,7 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
|
||||
|
||||
@staticmethod
|
||||
def is_music_url(url):
|
||||
return re.match(r'https?://music\.youtube\.com/', url) is not None
|
||||
return re.match(r'(https?://)?music\.youtube\.com/', url) is not None
|
||||
|
||||
def _extract_video(self, renderer):
|
||||
video_id = renderer.get('videoId')
|
||||
@@ -3205,11 +3205,11 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'decoratedPlayerBarRenderer', 'playerBar', 'chapteredPlayerBarRenderer', 'chapters'
|
||||
), expected_type=list)
|
||||
|
||||
return self._extract_chapters(
|
||||
return self._extract_chapters_helper(
|
||||
chapter_list,
|
||||
chapter_time=lambda chapter: float_or_none(
|
||||
start_function=lambda chapter: float_or_none(
|
||||
traverse_obj(chapter, ('chapterRenderer', 'timeRangeStartMillis')), scale=1000),
|
||||
chapter_title=lambda chapter: traverse_obj(
|
||||
title_function=lambda chapter: traverse_obj(
|
||||
chapter, ('chapterRenderer', 'title', 'simpleText'), expected_type=str),
|
||||
duration=duration)
|
||||
|
||||
@@ -3222,42 +3222,10 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
chapter_title = lambda chapter: self._get_text(chapter, 'title')
|
||||
|
||||
return next(filter(None, (
|
||||
self._extract_chapters(traverse_obj(contents, (..., 'macroMarkersListItemRenderer')),
|
||||
chapter_time, chapter_title, duration)
|
||||
self._extract_chapters_helper(traverse_obj(contents, (..., 'macroMarkersListItemRenderer')),
|
||||
chapter_time, chapter_title, duration)
|
||||
for contents in content_list)), [])
|
||||
|
||||
def _extract_chapters_from_description(self, description, duration):
|
||||
duration_re = r'(?:\d+:)?\d{1,2}:\d{2}'
|
||||
sep_re = r'(?m)^\s*(%s)\b\W*\s(%s)\s*$'
|
||||
return self._extract_chapters(
|
||||
re.findall(sep_re % (duration_re, r'.+?'), description or ''),
|
||||
chapter_time=lambda x: parse_duration(x[0]), chapter_title=lambda x: x[1],
|
||||
duration=duration, strict=False) or self._extract_chapters(
|
||||
re.findall(sep_re % (r'.+?', duration_re), description or ''),
|
||||
chapter_time=lambda x: parse_duration(x[1]), chapter_title=lambda x: x[0],
|
||||
duration=duration, strict=False)
|
||||
|
||||
def _extract_chapters(self, chapter_list, chapter_time, chapter_title, duration, strict=True):
|
||||
if not duration:
|
||||
return
|
||||
chapter_list = [{
|
||||
'start_time': chapter_time(chapter),
|
||||
'title': chapter_title(chapter),
|
||||
} for chapter in chapter_list or []]
|
||||
if not strict:
|
||||
chapter_list.sort(key=lambda c: c['start_time'] or 0)
|
||||
|
||||
chapters = [{'start_time': 0}]
|
||||
for idx, chapter in enumerate(chapter_list):
|
||||
if chapter['start_time'] is None:
|
||||
self.report_warning(f'Incomplete chapter {idx}')
|
||||
elif chapters[-1]['start_time'] <= chapter['start_time'] <= duration:
|
||||
chapters.append(chapter)
|
||||
elif chapter not in chapters:
|
||||
self.report_warning(
|
||||
f'Invalid start time ({chapter["start_time"]} < {chapters[-1]["start_time"]}) for chapter "{chapter["title"]}"')
|
||||
return chapters[1:]
|
||||
|
||||
def _extract_comment(self, comment_renderer, parent=None):
|
||||
comment_id = comment_renderer.get('commentId')
|
||||
if not comment_id:
|
||||
@@ -3341,6 +3309,13 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
comment = self._extract_comment(comment_renderer, parent)
|
||||
if not comment:
|
||||
continue
|
||||
# Sometimes YouTube may break and give us infinite looping comments.
|
||||
# See: https://github.com/yt-dlp/yt-dlp/issues/6290
|
||||
if comment['id'] in tracker['seen_comment_ids']:
|
||||
self.report_warning('Detected YouTube comments looping. Stopping comment extraction as we probably cannot get any more.')
|
||||
yield
|
||||
else:
|
||||
tracker['seen_comment_ids'].add(comment['id'])
|
||||
|
||||
tracker['running_total'] += 1
|
||||
tracker['total_reply_comments' if parent else 'total_parent_comments'] += 1
|
||||
@@ -3365,7 +3340,8 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
est_total=0,
|
||||
current_page_thread=0,
|
||||
total_parent_comments=0,
|
||||
total_reply_comments=0)
|
||||
total_reply_comments=0,
|
||||
seen_comment_ids=set())
|
||||
|
||||
# TODO: Deprecated
|
||||
# YouTube comments have a max depth of 2
|
||||
@@ -3741,10 +3717,10 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'filesize': int_or_none(fmt.get('contentLength')),
|
||||
'format_id': f'{itag}{"-drc" if fmt.get("isDrc") else ""}',
|
||||
'format_note': join_nonempty(
|
||||
'%s%s' % (audio_track.get('displayName') or '',
|
||||
' (default)' if language_preference > 0 else ''),
|
||||
join_nonempty(audio_track.get('displayName'),
|
||||
language_preference > 0 and ' (default)', delim=''),
|
||||
fmt.get('qualityLabel') or quality.replace('audio_quality_', ''),
|
||||
'DRC' if fmt.get('isDrc') else None,
|
||||
fmt.get('isDrc') and 'DRC',
|
||||
try_get(fmt, lambda x: x['projectionType'].replace('RECTANGULAR', '').lower()),
|
||||
try_get(fmt, lambda x: x['spatialAudioType'].replace('SPATIAL_AUDIO_TYPE_', '').lower()),
|
||||
throttled and 'THROTTLED', is_damaged and 'DAMAGED', delim=', '),
|
||||
@@ -3776,10 +3752,18 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
if no_video:
|
||||
dct['abr'] = tbr
|
||||
if no_audio or no_video:
|
||||
dct['downloader_options'] = {
|
||||
# Youtube throttles chunks >~10M
|
||||
'http_chunk_size': 10485760,
|
||||
}
|
||||
CHUNK_SIZE = 10 << 20
|
||||
dct.update({
|
||||
'protocol': 'http_dash_segments',
|
||||
'fragments': [{
|
||||
'url': update_url_query(dct['url'], {
|
||||
'range': f'{range_start}-{min(range_start + CHUNK_SIZE - 1, dct["filesize"])}'
|
||||
})
|
||||
} for range_start in range(0, dct['filesize'], CHUNK_SIZE)]
|
||||
} if dct['filesize'] else {
|
||||
'downloader_options': {'http_chunk_size': CHUNK_SIZE} # No longer useful?
|
||||
})
|
||||
|
||||
if dct.get('ext'):
|
||||
dct['container'] = dct['ext'] + '_dash'
|
||||
|
||||
@@ -4897,6 +4881,10 @@ class YoutubeTabBaseInfoExtractor(YoutubeBaseInfoExtractor):
|
||||
info['view_count'] = self._get_count(playlist_stats, 1)
|
||||
if info['view_count'] is None: # 0 is allowed
|
||||
info['view_count'] = self._get_count(playlist_header_renderer, 'viewCountText')
|
||||
if info['view_count'] is None:
|
||||
info['view_count'] = self._get_count(data, (
|
||||
'contents', 'twoColumnBrowseResultsRenderer', 'tabs', ..., 'tabRenderer', 'content', 'sectionListRenderer',
|
||||
'contents', ..., 'itemSectionRenderer', 'contents', ..., 'channelAboutFullMetadataRenderer', 'viewCountText'))
|
||||
|
||||
info['playlist_count'] = self._get_count(playlist_stats, 0)
|
||||
if info['playlist_count'] is None: # 0 is allowed
|
||||
@@ -6116,6 +6104,23 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
|
||||
}
|
||||
}],
|
||||
'params': {'extract_flat': True},
|
||||
}, {
|
||||
'url': 'https://www.youtube.com/@3blue1brown/about',
|
||||
'info_dict': {
|
||||
'id': 'UCYO_jab_esuFRV4b17AJtAw',
|
||||
'tags': ['Mathematics'],
|
||||
'title': '3Blue1Brown - About',
|
||||
'uploader_url': 'https://www.youtube.com/channel/UCYO_jab_esuFRV4b17AJtAw',
|
||||
'channel_follower_count': int,
|
||||
'channel_id': 'UCYO_jab_esuFRV4b17AJtAw',
|
||||
'uploader_id': 'UCYO_jab_esuFRV4b17AJtAw',
|
||||
'channel': '3Blue1Brown',
|
||||
'uploader': '3Blue1Brown',
|
||||
'view_count': int,
|
||||
'channel_url': 'https://www.youtube.com/channel/UCYO_jab_esuFRV4b17AJtAw',
|
||||
'description': 'md5:e1384e8a133307dd10edee76e875d62f',
|
||||
},
|
||||
'playlist_count': 0,
|
||||
}]
|
||||
|
||||
@classmethod
|
||||
@@ -6182,6 +6187,8 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
|
||||
original_tab_id, display_id = tab[1:], f'{item_id}{tab}'
|
||||
if is_channel and not tab and 'no-youtube-channel-redirect' not in compat_opts:
|
||||
url = f'{pre}/videos{post}'
|
||||
if smuggled_data.get('is_music_url'):
|
||||
self.report_warning(f'YouTube Music is not directly supported. Redirecting to {url}')
|
||||
|
||||
# Handle both video/playlist URLs
|
||||
qs = parse_qs(url)
|
||||
|
||||
@@ -9,6 +9,7 @@ import re
|
||||
from .utils import (
|
||||
NO_DEFAULT,
|
||||
ExtractorError,
|
||||
function_with_repr,
|
||||
js_to_json,
|
||||
remove_quotes,
|
||||
truncate_string,
|
||||
@@ -184,7 +185,8 @@ class Debugger:
|
||||
cls.write('=> Raises:', e, '<-|', stmt, level=allow_recursion)
|
||||
raise
|
||||
if cls.ENABLED and stmt.strip():
|
||||
cls.write(['->', '=>'][should_ret], repr(ret), '<-|', stmt, level=allow_recursion)
|
||||
if should_ret or not repr(ret) == stmt:
|
||||
cls.write(['->', '=>'][should_ret], repr(ret), '<-|', stmt, level=allow_recursion)
|
||||
return ret, should_ret
|
||||
return interpret_statement
|
||||
|
||||
@@ -205,8 +207,6 @@ class JSInterpreter:
|
||||
'y': 4096, # Perform a "sticky" search that matches starting at the current position in the target string
|
||||
}
|
||||
|
||||
_EXC_NAME = '__yt_dlp_exception__'
|
||||
|
||||
def __init__(self, code, objects=None):
|
||||
self.code, self._functions = code, {}
|
||||
self._objects = {} if objects is None else objects
|
||||
@@ -220,6 +220,8 @@ class JSInterpreter:
|
||||
def _named_object(self, namespace, obj):
|
||||
self.__named_object_counter += 1
|
||||
name = f'__yt_dlp_jsinterp_obj{self.__named_object_counter}'
|
||||
if callable(obj) and not isinstance(obj, function_with_repr):
|
||||
obj = function_with_repr(obj, f'F<{self.__named_object_counter}>')
|
||||
namespace[name] = obj
|
||||
return name
|
||||
|
||||
@@ -355,11 +357,11 @@ class JSInterpreter:
|
||||
obj = expr[4:]
|
||||
if obj.startswith('Date('):
|
||||
left, right = self._separate_at_paren(obj[4:])
|
||||
expr = unified_timestamp(
|
||||
date = unified_timestamp(
|
||||
self.interpret_expression(left, local_vars, allow_recursion), False)
|
||||
if not expr:
|
||||
if date is None:
|
||||
raise self.Exception(f'Failed to parse date {left!r}', expr)
|
||||
expr = self._dump(int(expr * 1000), local_vars) + right
|
||||
expr = self._dump(int(date * 1000), local_vars) + right
|
||||
else:
|
||||
raise self.Exception(f'Unsupported object {obj}', expr)
|
||||
|
||||
@@ -784,7 +786,8 @@ class JSInterpreter:
|
||||
fields)
|
||||
for f in fields_m:
|
||||
argnames = f.group('args').split(',')
|
||||
obj[remove_quotes(f.group('key'))] = self.build_function(argnames, f.group('code'))
|
||||
name = remove_quotes(f.group('key'))
|
||||
obj[name] = function_with_repr(self.build_function(argnames, f.group('code')), f'F<{name}>')
|
||||
|
||||
return obj
|
||||
|
||||
@@ -806,7 +809,9 @@ class JSInterpreter:
|
||||
return [x.strip() for x in func_m.group('args').split(',')], code
|
||||
|
||||
def extract_function(self, funcname):
|
||||
return self.extract_function_from_code(*self.extract_function_code(funcname))
|
||||
return function_with_repr(
|
||||
self.extract_function_from_code(*self.extract_function_code(funcname)),
|
||||
f'F<{funcname}>')
|
||||
|
||||
def extract_function_from_code(self, argnames, code, *global_stack):
|
||||
local_vars = {}
|
||||
|
||||
@@ -20,7 +20,7 @@ from .postprocessor import (
|
||||
SponsorBlockPP,
|
||||
)
|
||||
from .postprocessor.modify_chapters import DEFAULT_SPONSORBLOCK_CHAPTER_TITLE
|
||||
from .update import detect_variant, is_non_updateable
|
||||
from .update import UPDATE_SOURCES, detect_variant, is_non_updateable
|
||||
from .utils import (
|
||||
OUTTMPL_TYPES,
|
||||
POSTPROCESS_WHEN,
|
||||
@@ -36,7 +36,7 @@ from .utils import (
|
||||
remove_end,
|
||||
write_string,
|
||||
)
|
||||
from .version import __version__
|
||||
from .version import CHANNEL, __version__
|
||||
|
||||
|
||||
def parseOpts(overrideArguments=None, ignore_config_files='if_override'):
|
||||
@@ -326,11 +326,18 @@ def create_parser():
|
||||
action='store_true', dest='update_self',
|
||||
help=format_field(
|
||||
is_non_updateable(), None, 'Check if updates are available. %s',
|
||||
default='Update this program to the latest version'))
|
||||
default=f'Update this program to the latest {CHANNEL} version'))
|
||||
general.add_option(
|
||||
'--no-update',
|
||||
action='store_false', dest='update_self',
|
||||
help='Do not check for updates (default)')
|
||||
general.add_option(
|
||||
'--update-to',
|
||||
action='store', dest='update_self', metavar='[CHANNEL]@[TAG]',
|
||||
help=(
|
||||
'Upgrade/downgrade to a specific version. CHANNEL and TAG defaults to '
|
||||
f'"{CHANNEL}" and "latest" respectively if omitted; See "UPDATE" for details. '
|
||||
f'Supported channels: {", ".join(UPDATE_SOURCES)}'))
|
||||
general.add_option(
|
||||
'-i', '--ignore-errors',
|
||||
action='store_true', dest='ignoreerrors',
|
||||
@@ -606,8 +613,16 @@ def create_parser():
|
||||
'Use "--match-filter -" to interactively ask whether to download each video'))
|
||||
selection.add_option(
|
||||
'--no-match-filter',
|
||||
metavar='FILTER', dest='match_filter', action='store_const', const=None,
|
||||
help='Do not use generic video filter (default)')
|
||||
dest='match_filter', action='store_const', const=None,
|
||||
help='Do not use any --match-filter (default)')
|
||||
selection.add_option(
|
||||
'--break-match-filters',
|
||||
metavar='FILTER', dest='breaking_match_filter', action='append',
|
||||
help='Same as "--match-filters" but stops the download process when a video is rejected')
|
||||
selection.add_option(
|
||||
'--no-break-match-filters',
|
||||
dest='breaking_match_filter', action='store_const', const=None,
|
||||
help='Do not use any --break-match-filters (default)')
|
||||
selection.add_option(
|
||||
'--no-playlist',
|
||||
action='store_true', dest='noplaylist', default=False,
|
||||
@@ -639,11 +654,11 @@ def create_parser():
|
||||
selection.add_option(
|
||||
'--break-on-reject',
|
||||
action='store_true', dest='break_on_reject', default=False,
|
||||
help='Stop the download process when encountering a file that has been filtered out')
|
||||
help=optparse.SUPPRESS_HELP)
|
||||
selection.add_option(
|
||||
'--break-per-input',
|
||||
action='store_true', dest='break_per_url', default=False,
|
||||
help='Alters --max-downloads, --break-on-existing, --break-on-reject, and autonumber to reset per input URL')
|
||||
help='Alters --max-downloads, --break-on-existing, --break-match-filter, and autonumber to reset per input URL')
|
||||
selection.add_option(
|
||||
'--no-break-per-input',
|
||||
action='store_false', dest='break_per_url',
|
||||
|
||||
@@ -88,7 +88,7 @@ class PluginFinder(importlib.abc.MetaPathFinder):
|
||||
candidate = path / parts
|
||||
if candidate.is_dir():
|
||||
yield candidate
|
||||
elif path.suffix in ('.zip', '.egg', '.whl'):
|
||||
elif path.suffix in ('.zip', '.egg', '.whl') and path.is_file():
|
||||
if parts in dirs_in_zip(path):
|
||||
yield candidate
|
||||
|
||||
|
||||
151
yt_dlp/update.py
151
yt_dlp/update.py
@@ -7,6 +7,7 @@ import platform
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.error
|
||||
from zipimport import zipimporter
|
||||
|
||||
from .compat import functools # isort: split
|
||||
@@ -16,15 +17,26 @@ from .utils import (
|
||||
cached_method,
|
||||
deprecation_warning,
|
||||
remove_end,
|
||||
remove_start,
|
||||
sanitized_Request,
|
||||
shell_quote,
|
||||
system_identifier,
|
||||
traverse_obj,
|
||||
version_tuple,
|
||||
)
|
||||
from .version import UPDATE_HINT, VARIANT, __version__
|
||||
from .version import CHANNEL, UPDATE_HINT, VARIANT, __version__
|
||||
|
||||
REPOSITORY = 'yt-dlp/yt-dlp'
|
||||
API_URL = f'https://api.github.com/repos/{REPOSITORY}/releases'
|
||||
UPDATE_SOURCES = {
|
||||
'stable': 'yt-dlp/yt-dlp',
|
||||
'nightly': 'yt-dlp/yt-dlp-nightly-builds',
|
||||
}
|
||||
REPOSITORY = UPDATE_SOURCES['stable']
|
||||
|
||||
_VERSION_RE = re.compile(r'(\d+\.)*\d+')
|
||||
|
||||
API_BASE_URL = 'https://api.github.com/repos'
|
||||
|
||||
# Backwards compatibility variables for the current channel
|
||||
API_URL = f'{API_BASE_URL}/{REPOSITORY}/releases'
|
||||
|
||||
|
||||
@functools.cache
|
||||
@@ -110,49 +122,99 @@ def _sha256_file(path):
|
||||
|
||||
|
||||
class Updater:
|
||||
def __init__(self, ydl):
|
||||
_exact = True
|
||||
|
||||
def __init__(self, ydl, target=None):
|
||||
self.ydl = ydl
|
||||
|
||||
self.target_channel, sep, self.target_tag = (target or CHANNEL).rpartition('@')
|
||||
if not sep and self.target_tag in UPDATE_SOURCES: # stable => stable@latest
|
||||
self.target_channel, self.target_tag = self.target_tag, None
|
||||
elif not self.target_channel:
|
||||
self.target_channel = CHANNEL
|
||||
|
||||
if not self.target_tag:
|
||||
self.target_tag, self._exact = 'latest', False
|
||||
elif self.target_tag != 'latest':
|
||||
self.target_tag = f'tags/{self.target_tag}'
|
||||
|
||||
@property
|
||||
def _target_repo(self):
|
||||
try:
|
||||
return UPDATE_SOURCES[self.target_channel]
|
||||
except KeyError:
|
||||
return self._report_error(
|
||||
f'Invalid update channel {self.target_channel!r} requested. '
|
||||
f'Valid channels are {", ".join(UPDATE_SOURCES)}', True)
|
||||
|
||||
def _version_compare(self, a, b, channel=CHANNEL):
|
||||
if channel != self.target_channel:
|
||||
return False
|
||||
|
||||
if _VERSION_RE.fullmatch(f'{a}.{b}'):
|
||||
a, b = version_tuple(a), version_tuple(b)
|
||||
return a == b if self._exact else a >= b
|
||||
return a == b
|
||||
|
||||
@functools.cached_property
|
||||
def _tag(self):
|
||||
if version_tuple(__version__) >= version_tuple(self.latest_version):
|
||||
return 'latest'
|
||||
if self._version_compare(self.current_version, self.latest_version):
|
||||
return self.target_tag
|
||||
|
||||
identifier = f'{detect_variant()} {system_identifier()}'
|
||||
identifier = f'{detect_variant()} {self.target_channel} {system_identifier()}'
|
||||
for line in self._download('_update_spec', 'latest').decode().splitlines():
|
||||
if not line.startswith('lock '):
|
||||
continue
|
||||
_, tag, pattern = line.split(' ', 2)
|
||||
if re.match(pattern, identifier):
|
||||
return f'tags/{tag}'
|
||||
return 'latest'
|
||||
if not self._exact:
|
||||
return f'tags/{tag}'
|
||||
elif self.target_tag == 'latest' or not self._version_compare(
|
||||
tag, self.target_tag[5:], channel=self.target_channel):
|
||||
self._report_error(
|
||||
f'yt-dlp cannot be updated above {tag} since you are on an older Python version', True)
|
||||
return f'tags/{self.current_version}'
|
||||
return self.target_tag
|
||||
|
||||
@cached_method
|
||||
def _get_version_info(self, tag):
|
||||
self.ydl.write_debug(f'Fetching release info: {API_URL}/{tag}')
|
||||
return json.loads(self.ydl.urlopen(f'{API_URL}/{tag}').read().decode())
|
||||
url = f'{API_BASE_URL}/{self._target_repo}/releases/{tag}'
|
||||
self.ydl.write_debug(f'Fetching release info: {url}')
|
||||
return json.loads(self.ydl.urlopen(sanitized_Request(url, headers={
|
||||
'Accept': 'application/vnd.github+json',
|
||||
'User-Agent': 'yt-dlp',
|
||||
'X-GitHub-Api-Version': '2022-11-28',
|
||||
})).read().decode())
|
||||
|
||||
@property
|
||||
def current_version(self):
|
||||
"""Current version"""
|
||||
return __version__
|
||||
|
||||
@staticmethod
|
||||
def _label(channel, tag):
|
||||
"""Label for a given channel and tag"""
|
||||
return f'{channel}@{remove_start(tag, "tags/")}'
|
||||
|
||||
def _get_actual_tag(self, tag):
|
||||
if tag.startswith('tags/'):
|
||||
return tag[5:]
|
||||
return self._get_version_info(tag)['tag_name']
|
||||
|
||||
@property
|
||||
def new_version(self):
|
||||
"""Version of the latest release we can update to"""
|
||||
if self._tag.startswith('tags/'):
|
||||
return self._tag[5:]
|
||||
return self._get_version_info(self._tag)['tag_name']
|
||||
return self._get_actual_tag(self._tag)
|
||||
|
||||
@property
|
||||
def latest_version(self):
|
||||
"""Version of the latest release"""
|
||||
return self._get_version_info('latest')['tag_name']
|
||||
"""Version of the target release"""
|
||||
return self._get_actual_tag(self.target_tag)
|
||||
|
||||
@property
|
||||
def has_update(self):
|
||||
"""Whether there is an update available"""
|
||||
return version_tuple(__version__) < version_tuple(self.new_version)
|
||||
return not self._version_compare(self.current_version, self.new_version)
|
||||
|
||||
@functools.cached_property
|
||||
def filename(self):
|
||||
@@ -160,10 +222,8 @@ class Updater:
|
||||
return compat_realpath(_get_variant_and_executable_path()[1])
|
||||
|
||||
def _download(self, name, tag):
|
||||
url = traverse_obj(self._get_version_info(tag), (
|
||||
'assets', lambda _, v: v['name'] == name, 'browser_download_url'), get_all=False)
|
||||
if not url:
|
||||
raise Exception('Unable to find download URL')
|
||||
slug = 'latest/download' if tag == 'latest' else f'download/{tag[5:]}'
|
||||
url = f'https://github.com/{self._target_repo}/releases/{slug}/{name}'
|
||||
self.ydl.write_debug(f'Downloading {name} from {url}')
|
||||
return self.ydl.urlopen(url).read()
|
||||
|
||||
@@ -186,24 +246,32 @@ class Updater:
|
||||
self._report_error(f'Unable to write to {file}; Try running as administrator', True)
|
||||
|
||||
def _report_network_error(self, action, delim=';'):
|
||||
self._report_error(f'Unable to {action}{delim} Visit https://github.com/{REPOSITORY}/releases/latest', True)
|
||||
self._report_error(
|
||||
f'Unable to {action}{delim} visit '
|
||||
f'https://github.com/{self._target_repo}/releases/{self.target_tag.replace("tags/", "tag/")}', True)
|
||||
|
||||
def check_update(self):
|
||||
"""Report whether there is an update available"""
|
||||
if not self._target_repo:
|
||||
return False
|
||||
try:
|
||||
self.ydl.to_screen(
|
||||
f'Latest version: {self.latest_version}, Current version: {self.current_version}')
|
||||
if not self.has_update:
|
||||
if self._tag == 'latest':
|
||||
return self.ydl.to_screen(f'yt-dlp is up to date ({__version__})')
|
||||
return self.ydl.report_warning(
|
||||
'yt-dlp cannot be updated any further since you are on an older Python version')
|
||||
self.ydl.to_screen((
|
||||
f'Available version: {self._label(self.target_channel, self.latest_version)}, ' if self.target_tag == 'latest' else ''
|
||||
) + f'Current version: {self._label(CHANNEL, self.current_version)}')
|
||||
except Exception:
|
||||
return self._report_network_error('obtain version info', delim='; Please try again later or')
|
||||
|
||||
if not is_non_updateable():
|
||||
self.ydl.to_screen(f'Current Build Hash {_sha256_file(self.filename)}')
|
||||
return True
|
||||
self.ydl.to_screen(f'Current Build Hash: {_sha256_file(self.filename)}')
|
||||
|
||||
if self.has_update:
|
||||
return True
|
||||
|
||||
if self.target_tag == self._tag:
|
||||
self.ydl.to_screen(f'yt-dlp is up to date ({self._label(CHANNEL, self.current_version)})')
|
||||
elif not self._exact:
|
||||
self.ydl.report_warning('yt-dlp cannot be updated any further since you are on an older Python version')
|
||||
return False
|
||||
|
||||
def update(self):
|
||||
"""Update yt-dlp executable to the latest version"""
|
||||
@@ -212,7 +280,10 @@ class Updater:
|
||||
err = is_non_updateable()
|
||||
if err:
|
||||
return self._report_error(err, True)
|
||||
self.ydl.to_screen(f'Updating to version {self.new_version} ...')
|
||||
self.ydl.to_screen(f'Updating to {self._label(self.target_channel, self.new_version)} ...')
|
||||
if (_VERSION_RE.fullmatch(self.target_tag[5:])
|
||||
and version_tuple(self.target_tag[5:]) < (2023, 3, 2)):
|
||||
self.ydl.report_warning('You are downgrading to a version without --update-to')
|
||||
|
||||
directory = os.path.dirname(self.filename)
|
||||
if not os.access(self.filename, os.W_OK):
|
||||
@@ -232,10 +303,11 @@ class Updater:
|
||||
|
||||
try:
|
||||
newcontent = self._download(self.release_name, self._tag)
|
||||
except OSError:
|
||||
return self._report_network_error('download latest version')
|
||||
except Exception:
|
||||
return self._report_network_error('fetch updates')
|
||||
except Exception as e:
|
||||
if isinstance(e, urllib.error.HTTPError) and e.code == 404:
|
||||
return self._report_error(
|
||||
f'The requested tag {self._label(self.target_channel, self.target_tag)} does not exist', True)
|
||||
return self._report_network_error(f'fetch updates: {e}')
|
||||
|
||||
try:
|
||||
expected_hash = self.release_hash
|
||||
@@ -280,7 +352,7 @@ class Updater:
|
||||
return self._report_error(
|
||||
f'Unable to set permissions. Run: sudo chmod a+rx {compat_shlex_quote(self.filename)}')
|
||||
|
||||
self.ydl.to_screen(f'Updated yt-dlp to version {self.new_version}')
|
||||
self.ydl.to_screen(f'Updated yt-dlp to {self._label(self.target_channel, self.new_version)}')
|
||||
return True
|
||||
|
||||
@functools.cached_property
|
||||
@@ -346,3 +418,6 @@ def update_self(to_screen, verbose, opener):
|
||||
return opener.open(url)
|
||||
|
||||
return run_update(FakeYDL())
|
||||
|
||||
|
||||
__all__ = ['Updater']
|
||||
|
||||
@@ -593,21 +593,43 @@ def clean_html(html):
|
||||
|
||||
|
||||
class LenientJSONDecoder(json.JSONDecoder):
|
||||
def __init__(self, *args, transform_source=None, ignore_extra=False, **kwargs):
|
||||
# TODO: Write tests
|
||||
def __init__(self, *args, transform_source=None, ignore_extra=False, close_objects=0, **kwargs):
|
||||
self.transform_source, self.ignore_extra = transform_source, ignore_extra
|
||||
self._close_attempts = 2 * close_objects
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def _close_object(err):
|
||||
doc = err.doc[:err.pos]
|
||||
# We need to add comma first to get the correct error message
|
||||
if err.msg.startswith('Expecting \',\''):
|
||||
return doc + ','
|
||||
elif not doc.endswith(','):
|
||||
return
|
||||
|
||||
if err.msg.startswith('Expecting property name'):
|
||||
return doc[:-1] + '}'
|
||||
elif err.msg.startswith('Expecting value'):
|
||||
return doc[:-1] + ']'
|
||||
|
||||
def decode(self, s):
|
||||
if self.transform_source:
|
||||
s = self.transform_source(s)
|
||||
try:
|
||||
if self.ignore_extra:
|
||||
return self.raw_decode(s.lstrip())[0]
|
||||
return super().decode(s)
|
||||
except json.JSONDecodeError as e:
|
||||
if e.pos is not None:
|
||||
for attempt in range(self._close_attempts + 1):
|
||||
try:
|
||||
if self.ignore_extra:
|
||||
return self.raw_decode(s.lstrip())[0]
|
||||
return super().decode(s)
|
||||
except json.JSONDecodeError as e:
|
||||
if e.pos is None:
|
||||
raise
|
||||
elif attempt < self._close_attempts:
|
||||
s = self._close_object(e)
|
||||
if s is not None:
|
||||
continue
|
||||
raise type(e)(f'{e.msg} in {s[e.pos-10:e.pos+10]!r}', s, e.pos)
|
||||
raise
|
||||
assert False, 'Too many attempts to decode JSON'
|
||||
|
||||
|
||||
def sanitize_open(filename, open_mode):
|
||||
@@ -879,6 +901,7 @@ class Popen(subprocess.Popen):
|
||||
env = os.environ.copy()
|
||||
self._fix_pyinstaller_ld_path(env)
|
||||
|
||||
self.__text_mode = kwargs.get('encoding') or kwargs.get('errors') or text or kwargs.get('universal_newlines')
|
||||
if text is True:
|
||||
kwargs['universal_newlines'] = True # For 3.6 compatibility
|
||||
kwargs.setdefault('encoding', 'utf-8')
|
||||
@@ -900,7 +923,7 @@ class Popen(subprocess.Popen):
|
||||
@classmethod
|
||||
def run(cls, *args, timeout=None, **kwargs):
|
||||
with cls(*args, **kwargs) as proc:
|
||||
default = '' if proc.text_mode else b''
|
||||
default = '' if proc.__text_mode else b''
|
||||
stdout, stderr = proc.communicate_or_kill(timeout=timeout)
|
||||
return stdout or default, stderr or default, proc.returncode
|
||||
|
||||
@@ -1207,8 +1230,8 @@ class ExistingVideoReached(DownloadCancelled):
|
||||
|
||||
|
||||
class RejectedVideoReached(DownloadCancelled):
|
||||
""" --break-on-reject triggered """
|
||||
msg = 'Encountered a video that did not match filter, stopping due to --break-on-reject'
|
||||
""" --break-match-filter triggered """
|
||||
msg = 'Encountered a video that did not match filter, stopping due to --break-match-filter'
|
||||
|
||||
|
||||
class MaxDownloadsReached(DownloadCancelled):
|
||||
@@ -3019,8 +3042,10 @@ class PlaylistEntries:
|
||||
if not entry:
|
||||
continue
|
||||
try:
|
||||
# TODO: Add auto-generated fields
|
||||
self.ydl._match_entry(entry, incomplete=True, silent=True)
|
||||
# The item may have just been added to archive. Don't break due to it
|
||||
if not self.ydl.params.get('lazy_playlist'):
|
||||
# TODO: Add auto-generated fields
|
||||
self.ydl._match_entry(entry, incomplete=True, silent=True)
|
||||
except (ExistingVideoReached, RejectedVideoReached):
|
||||
return
|
||||
|
||||
@@ -3886,16 +3911,21 @@ def match_str(filter_str, dct, incomplete=False):
|
||||
for filter_part in re.split(r'(?<!\\)&', filter_str))
|
||||
|
||||
|
||||
def match_filter_func(filters):
|
||||
if not filters:
|
||||
def match_filter_func(filters, breaking_filters=None):
|
||||
if not filters and not breaking_filters:
|
||||
return None
|
||||
filters = set(variadic(filters))
|
||||
breaking_filters = match_filter_func(breaking_filters) or (lambda _, __: None)
|
||||
filters = set(variadic(filters or []))
|
||||
|
||||
interactive = '-' in filters
|
||||
if interactive:
|
||||
filters.remove('-')
|
||||
|
||||
def _match_func(info_dict, incomplete=False):
|
||||
ret = breaking_filters(info_dict, incomplete)
|
||||
if ret is not None:
|
||||
raise RejectedVideoReached(ret)
|
||||
|
||||
if not filters or any(match_str(f, info_dict, incomplete) for f in filters):
|
||||
return NO_DEFAULT if interactive and not incomplete else None
|
||||
else:
|
||||
@@ -6034,14 +6064,16 @@ class classproperty:
|
||||
|
||||
|
||||
class function_with_repr:
|
||||
def __init__(self, func):
|
||||
def __init__(self, func, repr_=None):
|
||||
functools.update_wrapper(self, func)
|
||||
self.func = func
|
||||
self.func, self.__repr = func, repr_
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
return self.func(*args, **kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
if self.__repr:
|
||||
return self.__repr
|
||||
return f'{self.func.__module__}.{self.func.__qualname__}'
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
# Autogenerated by devscripts/update-version.py
|
||||
|
||||
__version__ = '2023.02.17'
|
||||
__version__ = '2023.03.04'
|
||||
|
||||
RELEASE_GIT_HEAD = 'a0a7c0154'
|
||||
RELEASE_GIT_HEAD = '392389b7df7b818f794b231f14dc396d4875fbad'
|
||||
|
||||
VARIANT = None
|
||||
|
||||
UPDATE_HINT = None
|
||||
|
||||
CHANNEL = 'stable'
|
||||
|
||||
Reference in New Issue
Block a user