1
0
mirror of https://github.com/yt-dlp/yt-dlp.git synced 2026-01-17 04:11:41 +00:00

Compare commits

...

32 Commits

Author SHA1 Message Date
github-actions
354d5fca7a Release 2023.03.03
Created by: Grub4K

:ci skip all :ci run dl
2023-03-03 21:41:45 +00:00
Simon Sawicki
9344964281 Fix d400e261cf
Authored by: Grub4K
2023-03-03 22:39:09 +01:00
pukkandan
bfc861a91e Fix bug in 29cb20bd56 2023-03-04 01:24:22 +05:30
pukkandan
fe2ce85aff Add option --break-match-filters
* Deprecates `--break-on-reject`

Closes #5962
2023-03-04 01:18:54 +05:30
pukkandan
d21056f4cf Fix --break-on-existing with --lazy-playlist
Closes #6399
2023-03-03 23:59:00 +05:30
pukkandan
b2e0343ba0 [cleanup, jsinterp] Give functions names to help debugging 2023-03-03 23:24:50 +05:30
pukkandan
4815bbfc41 [cleanup] Misc 2023-03-03 23:23:33 +05:30
bashonly
776d1c3f0c [build] Add cffi as a dependency for yt_dlp_linux
Closes #6394
Authored by: bashonly
2023-03-03 22:55:10 +05:30
Simon Sawicki
12647e03d4 [build] Sign SHA files and release public key
Closes #6344
Authored by: Grub4K
2023-03-03 22:55:10 +05:30
Simon Sawicki
77df20f14c [update] Add option --update-to, including to nightly (#6220)
* By default, stable will only update to stable, and nightly to nightly

Authored by: Grub4K, bashonly, pukkandan

Co-authored-by: bashonly <88596187+bashonly@users.noreply.github.com>
2023-03-03 22:55:09 +05:30
Simon Sawicki
29cb20bd56 [build] Automated builds and nightly releases (#6220)
Closes #1839
Authored by: Grub4K, bashonly

Co-authored-by: bashonly <88596187+bashonly@users.noreply.github.com>
2023-03-03 22:54:23 +05:30
Simon Sawicki
d400e261cf [devscripts] Script to generate changelog (#6220)
Authored by: Grub4K
2023-03-03 22:54:23 +05:30
pukkandan
9acf1ee25f [jsinterp] Handle Date at epoch 0
Closes #6400
2023-03-03 16:55:06 +05:30
bashonly
40d77d8902 [extractor/yle_areena] Extract non-Kaltura videos (#6402)
Closes #6066
Authored by: bashonly
2023-03-03 09:42:54 +00:00
bashonly
2d5a8c5db2 [extractor/mediastream] Improve WinSports support (#6401)
Closes #6360
Authored by: bashonly
2023-03-03 09:37:23 +00:00
bashonly
77d6d13646 [extractor/ntvru] Extract HLS and DASH formats (#6403)
Closes #5915
Authored by: bashonly
2023-03-03 09:34:56 +00:00
std-move
9fddc12ab0 [extractor/iprima] Fix extractor (#6291)
Authored by: std-move
Closes #6187
2023-03-03 00:03:33 +05:30
bashonly
b38cae49e6 [extractor/generic] Detect manifest links via extension
Authored by: bashonly
2023-03-01 06:38:02 -06:00
coletdjnz
7f51861b18 [extractor/youtube] Detect and break on looping comments (#6301)
Fixes https://github.com/yt-dlp/yt-dlp/issues/6290

Authored by: coletdjnz
2023-03-01 07:56:53 +00:00
pukkandan
5b28cef72d [cleanup] Misc 2023-02-28 23:51:06 +05:30
pukkandan
31e183557f [extractor/youtube] Extract channel view_count when /about tab is passed 2023-02-28 23:51:03 +05:30
pukkandan
f34804b2f9 [extractor/youtube] Fix 5038f6d713
* [fragment] Fix `request_data`
* [youtube] Don't use POST for now. It may be easier to break in future

Authored by: bashonly, coletdjnz
2023-02-28 23:34:43 +05:30
pukkandan
65f6e80780 [dependencies] Simplify Cryptodome
Closes #6292, closes #6272, closes #6338
2023-02-28 23:15:13 +05:30
pukkandan
b059188383 [plugins] Don't look in .egg directories
Closes #6306
2023-02-28 23:14:37 +05:30
pukkandan
5038f6d713 [extractor/youtube] Construct dash formats with range query
Closes #6369
2023-02-28 23:14:37 +05:30
pukkandan
4d248e29d2 [extractor/GoogleDrive] Fix some audio
Only those with source url, but no confirmation page
2023-02-28 23:09:20 +05:30
pukkandan
8e9fe43cd3 [extractor/generic] Handle basic-auth when checking redirects
Closes #6352
2023-02-26 10:27:46 +05:30
pukkandan
43a3eaf963 [extractor] Fix DRM detection in m3u8
Fixes https://github.com/ytdl-org/youtube-dl/issues/31693#issuecomment-1445202857
2023-02-26 10:27:46 +05:30
pukkandan
cc09083636 [utils] LenientJSONDecoder: Parse unclosed objects 2023-02-24 11:01:50 +05:30
Simon Sawicki
da8e2912b1 [utils] Popen: Shim undocumented text_mode property
Fixes #6317

Authored by: Grub4K
2023-02-23 04:18:45 +01:00
Zhong Lufan
18d295c9e0 [extractor/tencent] Add more formats and info (#5950)
Authored by: Hill-98
2023-02-17 18:41:16 +05:30
pukkandan
17ca19ab60 [cleanup] Fix Changelog 2023-02-17 18:38:10 +05:30
54 changed files with 2073 additions and 957 deletions

View File

@@ -1,5 +1,5 @@
name: Broken site name: Broken site
description: Report broken or misfunctioning site description: Report error in a supported site
labels: [triage, site-bug] labels: [triage, site-bug]
body: body:
- type: checkboxes - type: checkboxes
@@ -16,9 +16,9 @@ body:
description: | description: |
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp: Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
options: options:
- label: I'm reporting a broken site - label: I'm reporting that a **supported** site is broken
required: true 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.03** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
required: true required: true
- label: I've checked that all provided URLs are playable in a browser with the same IP and same login details - label: I've checked that all provided URLs are playable in a browser with the same IP and same login details
required: true required: true
@@ -50,6 +50,8 @@ body:
options: options:
- label: Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`) - label: Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
required: true 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 - label: Copy the WHOLE output (starting with `[debug] Command-line config`) and insert it below
required: true required: true
- type: textarea - type: textarea
@@ -62,7 +64,7 @@ body:
[debug] Command-line config: ['-vU', 'test:youtube'] [debug] Command-line config: ['-vU', 'test:youtube']
[debug] Portable config "yt-dlp.conf": ['-i'] [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] 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.03 [9d339c4] (win32_exe)
[debug] Python 3.8.10 (CPython 64bit) - Windows-10-10.0.22000-SP0 [debug] Python 3.8.10 (CPython 64bit) - Windows-10-10.0.22000-SP0
[debug] Checking exe version: ffmpeg -bsfs [debug] Checking exe version: ffmpeg -bsfs
[debug] Checking exe version: ffprobe -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] 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] Proxy map: {}
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest [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 Latest version: 2023.03.03, Current version: 2023.03.03
yt-dlp is up to date (2023.02.17) yt-dlp is up to date (2023.03.03)
<more lines> <more lines>
render: shell render: shell
validations: validations:

View File

@@ -18,7 +18,7 @@ body:
options: options:
- label: I'm reporting a new site support request - label: I'm reporting a new site support request
required: true 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.03** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
required: true required: true
- label: I've checked that all provided URLs are playable in a browser with the same IP and same login details - label: I've checked that all provided URLs are playable in a browser with the same IP and same login details
required: true required: true
@@ -62,6 +62,8 @@ body:
options: options:
- label: Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`) - label: Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
required: true 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 - label: Copy the WHOLE output (starting with `[debug] Command-line config`) and insert it below
required: true required: true
- type: textarea - type: textarea
@@ -74,7 +76,7 @@ body:
[debug] Command-line config: ['-vU', 'test:youtube'] [debug] Command-line config: ['-vU', 'test:youtube']
[debug] Portable config "yt-dlp.conf": ['-i'] [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] 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.03 [9d339c4] (win32_exe)
[debug] Python 3.8.10 (CPython 64bit) - Windows-10-10.0.22000-SP0 [debug] Python 3.8.10 (CPython 64bit) - Windows-10-10.0.22000-SP0
[debug] Checking exe version: ffmpeg -bsfs [debug] Checking exe version: ffmpeg -bsfs
[debug] Checking exe version: ffprobe -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] 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] Proxy map: {}
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest [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 Latest version: 2023.03.03, Current version: 2023.03.03
yt-dlp is up to date (2023.02.17) yt-dlp is up to date (2023.03.03)
<more lines> <more lines>
render: shell render: shell
validations: validations:

View File

@@ -18,7 +18,7 @@ body:
options: options:
- label: I'm requesting a site-specific feature - label: I'm requesting a site-specific feature
required: true 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.03** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
required: true required: true
- label: I've checked that all provided URLs are playable in a browser with the same IP and same login details - label: I've checked that all provided URLs are playable in a browser with the same IP and same login details
required: true required: true
@@ -58,6 +58,8 @@ body:
options: options:
- label: Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`) - label: Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
required: true 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 - label: Copy the WHOLE output (starting with `[debug] Command-line config`) and insert it below
required: true required: true
- type: textarea - type: textarea
@@ -70,7 +72,7 @@ body:
[debug] Command-line config: ['-vU', 'test:youtube'] [debug] Command-line config: ['-vU', 'test:youtube']
[debug] Portable config "yt-dlp.conf": ['-i'] [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] 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.03 [9d339c4] (win32_exe)
[debug] Python 3.8.10 (CPython 64bit) - Windows-10-10.0.22000-SP0 [debug] Python 3.8.10 (CPython 64bit) - Windows-10-10.0.22000-SP0
[debug] Checking exe version: ffmpeg -bsfs [debug] Checking exe version: ffmpeg -bsfs
[debug] Checking exe version: ffprobe -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] 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] Proxy map: {}
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest [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 Latest version: 2023.03.03, Current version: 2023.03.03
yt-dlp is up to date (2023.02.17) yt-dlp is up to date (2023.03.03)
<more lines> <more lines>
render: shell render: shell
validations: validations:

View File

@@ -18,7 +18,7 @@ body:
options: options:
- label: I'm reporting a bug unrelated to a specific site - label: I'm reporting a bug unrelated to a specific site
required: true 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.03** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
required: true required: true
- label: I've checked that all provided URLs are playable in a browser with the same IP and same login details - label: I've checked that all provided URLs are playable in a browser with the same IP and same login details
required: true required: true
@@ -43,6 +43,8 @@ body:
options: options:
- label: Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`) - label: Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
required: true 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 - label: Copy the WHOLE output (starting with `[debug] Command-line config`) and insert it below
required: true required: true
- type: textarea - type: textarea
@@ -55,7 +57,7 @@ body:
[debug] Command-line config: ['-vU', 'test:youtube'] [debug] Command-line config: ['-vU', 'test:youtube']
[debug] Portable config "yt-dlp.conf": ['-i'] [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] 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.03 [9d339c4] (win32_exe)
[debug] Python 3.8.10 (CPython 64bit) - Windows-10-10.0.22000-SP0 [debug] Python 3.8.10 (CPython 64bit) - Windows-10-10.0.22000-SP0
[debug] Checking exe version: ffmpeg -bsfs [debug] Checking exe version: ffmpeg -bsfs
[debug] Checking exe version: ffprobe -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] 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] Proxy map: {}
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest [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 Latest version: 2023.03.03, Current version: 2023.03.03
yt-dlp is up to date (2023.02.17) yt-dlp is up to date (2023.03.03)
<more lines> <more lines>
render: shell render: shell
validations: validations:

View File

@@ -20,7 +20,7 @@ body:
required: true required: true
- label: I've looked through the [README](https://github.com/yt-dlp/yt-dlp#readme) - label: I've looked through the [README](https://github.com/yt-dlp/yt-dlp#readme)
required: true 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.03** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
required: true 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 - 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 required: true
@@ -40,6 +40,8 @@ body:
label: Provide verbose output that clearly demonstrates the problem label: Provide verbose output that clearly demonstrates the problem
options: options:
- label: Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`) - 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 - label: Copy the WHOLE output (starting with `[debug] Command-line config`) and insert it below
- type: textarea - type: textarea
id: log id: log
@@ -51,7 +53,7 @@ body:
[debug] Command-line config: ['-vU', 'test:youtube'] [debug] Command-line config: ['-vU', 'test:youtube']
[debug] Portable config "yt-dlp.conf": ['-i'] [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] 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.03 [9d339c4] (win32_exe)
[debug] Python 3.8.10 (CPython 64bit) - Windows-10-10.0.22000-SP0 [debug] Python 3.8.10 (CPython 64bit) - Windows-10-10.0.22000-SP0
[debug] Checking exe version: ffmpeg -bsfs [debug] Checking exe version: ffmpeg -bsfs
[debug] Checking exe version: ffprobe -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] 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] Proxy map: {}
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest [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 Latest version: 2023.03.03, Current version: 2023.03.03
yt-dlp is up to date (2023.02.17) yt-dlp is up to date (2023.03.03)
<more lines> <more lines>
render: shell render: shell

View File

@@ -26,7 +26,7 @@ body:
required: true required: true
- label: I've looked through the [README](https://github.com/yt-dlp/yt-dlp#readme) - label: I've looked through the [README](https://github.com/yt-dlp/yt-dlp#readme)
required: true 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.03** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
required: true 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 - 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 required: true
@@ -46,6 +46,8 @@ body:
label: Provide verbose output that clearly demonstrates the problem label: Provide verbose output that clearly demonstrates the problem
options: options:
- label: Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`) - 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 - label: Copy the WHOLE output (starting with `[debug] Command-line config`) and insert it below
- type: textarea - type: textarea
id: log id: log
@@ -57,7 +59,7 @@ body:
[debug] Command-line config: ['-vU', 'test:youtube'] [debug] Command-line config: ['-vU', 'test:youtube']
[debug] Portable config "yt-dlp.conf": ['-i'] [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] 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.03 [9d339c4] (win32_exe)
[debug] Python 3.8.10 (CPython 64bit) - Windows-10-10.0.22000-SP0 [debug] Python 3.8.10 (CPython 64bit) - Windows-10-10.0.22000-SP0
[debug] Checking exe version: ffmpeg -bsfs [debug] Checking exe version: ffmpeg -bsfs
[debug] Checking exe version: ffprobe -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] 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] Proxy map: {}
[debug] Fetching release info: https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest [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 Latest version: 2023.03.03, Current version: 2023.03.03
yt-dlp is up to date (2023.02.17) yt-dlp is up to date (2023.03.03)
<more lines> <more lines>
render: shell render: shell

View File

@@ -1,5 +1,5 @@
name: Broken site name: Broken site
description: Report broken or misfunctioning site description: Report error in a supported site
labels: [triage, site-bug] labels: [triage, site-bug]
body: body:
%(no_skip)s %(no_skip)s
@@ -10,7 +10,7 @@ body:
description: | description: |
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp: Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
options: options:
- label: I'm reporting a broken site - label: I'm reporting that a **supported** site is broken
required: true 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) - 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 required: true

View File

@@ -1,393 +1,356 @@
name: Build name: Build Artifacts
on: workflow_dispatch 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: permissions:
contents: read contents: read
jobs: jobs:
prepare: unix:
permissions: if: inputs.unix
contents: write # for push_release
runs-on: ubuntu-latest 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: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: - uses: actions/setup-python@v4
fetch-depth: 0 with:
- uses: actions/setup-python@v4 python-version: "3.10"
with: - uses: conda-incubator/setup-miniconda@v2
python-version: '3.10' with:
- 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:
miniforge-variant: Mambaforge miniforge-variant: Mambaforge
use-mamba: true use-mamba: true
channels: conda-forge channels: conda-forge
auto-update-conda: true auto-update-conda: true
activate-environment: '' activate-environment: ""
auto-activate-base: false auto-activate-base: false
- name: Install Requirements - name: Install Requirements
run: | run: |
sudo apt-get -y install zip pandoc man sed 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 python -m pip install -U Pyinstaller -r requirements.txt
reqs=$(mktemp) reqs=$(mktemp)
echo -e 'python=3.10.*\npyinstaller' >$reqs cat > $reqs << EOF
sed 's/^brotli.*/brotli-python/' <requirements.txt >>$reqs python=3.10.*
pyinstaller
cffi
brotli-python
EOF
sed '/^brotli.*/d' requirements.txt >> $reqs
mamba create -n build --file $reqs mamba create -n build --file $reqs
- name: Prepare - name: Prepare
run: | run: |
python devscripts/update-version.py ${{ needs.prepare.outputs.version_suffix }} python devscripts/update-version.py -c ${{ inputs.channel }} ${{ inputs.version }}
python devscripts/make_lazy_extractors.py python devscripts/make_lazy_extractors.py
- name: Build Unix platform-independent binary - name: Build Unix platform-independent binary
run: | run: |
make all tar make all tar
- name: Build Unix standalone binary - name: Build Unix standalone binary
shell: bash -l {0} shell: bash -l {0}
run: | run: |
unset LD_LIBRARY_PATH # Harmful; set by setup-python unset LD_LIBRARY_PATH # Harmful; set by setup-python
conda activate build conda activate build
python pyinst.py --onedir python pyinst.py --onedir
(cd ./dist/yt-dlp_linux && zip -r ../yt-dlp_linux.zip .) (cd ./dist/yt-dlp_linux && zip -r ../yt-dlp_linux.zip .)
python pyinst.py python pyinst.py
mv ./dist/yt-dlp_linux ./yt-dlp_linux
mv ./dist/yt-dlp_linux.zip ./yt-dlp_linux.zip
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
path: | path: |
yt-dlp yt-dlp
yt-dlp.tar.gz yt-dlp.tar.gz
dist/yt-dlp_linux yt-dlp_linux
dist/yt-dlp_linux.zip yt-dlp_linux.zip
- name: Build and publish on PyPi linux_arm:
env: if: inputs.linux_arm
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:
permissions: permissions:
packages: write # for Creating cache contents: read
packages: write # for creating cache
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: prepare
strategy: strategy:
matrix: matrix:
architecture: architecture:
- armv7 - armv7
- aarch64 - aarch64
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
path: ./repo path: ./repo
- name: Virtualized Install, Prepare & Build - name: Virtualized Install, Prepare & Build
uses: yt-dlp/run-on-arch-action@v2 uses: yt-dlp/run-on-arch-action@v2
with: with:
githubToken: ${{ github.token }} # To cache image # Ref: https://github.com/uraimo/run-on-arch-action/issues/55
arch: ${{ matrix.architecture }} env: |
distro: ubuntu18.04 # Standalone executable should be built on minimum supported OS GITHUB_WORKFLOW: build
dockerRunArgs: --volume "${PWD}/repo:/repo" githubToken: ${{ github.token }} # To cache image
install: | # Installing Python 3.10 from the Deadsnakes repo raises errors arch: ${{ matrix.architecture }}
apt update distro: ubuntu18.04 # Standalone executable should be built on minimum supported OS
apt -y install zlib1g-dev python3.8 python3.8-dev python3.8-distutils python3-pip dockerRunArgs: --volume "${PWD}/repo:/repo"
python3.8 -m pip install -U pip setuptools wheel install: | # Installing Python 3.10 from the Deadsnakes repo raises errors
# Cannot access requirements.txt from the repo directory at this stage apt update
python3.8 -m pip install -U Pyinstaller mutagen pycryptodomex websockets brotli certifi 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: | run: |
cd repo cd repo
python3.8 -m pip install -U Pyinstaller -r requirements.txt # Cached version may be out of date 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/update-version.py -c ${{ inputs.channel }} ${{ inputs.version }}
python3.8 devscripts/make_lazy_extractors.py python3.8 devscripts/make_lazy_extractors.py
python3.8 pyinst.py python3.8 pyinst.py
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
path: | # run-on-arch-action designates armv7l as armv7 path: | # run-on-arch-action designates armv7l as armv7
repo/dist/yt-dlp_linux_${{ (matrix.architecture == 'armv7' && 'armv7l') || matrix.architecture }} repo/dist/yt-dlp_linux_${{ (matrix.architecture == 'armv7' && 'armv7l') || matrix.architecture }}
macos:
build_macos: if: inputs.macos
runs-on: macos-11 runs-on: macos-11
needs: prepare
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
# NB: In order to create a universal2 application, the version of python3 in /usr/bin has to be used # NB: In order to create a universal2 application, the version of python3 in /usr/bin has to be used
- name: Install Requirements - name: Install Requirements
run: | run: |
brew install coreutils brew install coreutils
/usr/bin/python3 -m pip install -U --user pip Pyinstaller -r requirements.txt /usr/bin/python3 -m pip install -U --user pip Pyinstaller -r requirements.txt
- name: Prepare - name: Prepare
run: | run: |
/usr/bin/python3 devscripts/update-version.py ${{ needs.prepare.outputs.version_suffix }} /usr/bin/python3 devscripts/update-version.py -c ${{ inputs.channel }} ${{ inputs.version }}
/usr/bin/python3 devscripts/make_lazy_extractors.py /usr/bin/python3 devscripts/make_lazy_extractors.py
- name: Build - name: Build
run: | run: |
/usr/bin/python3 pyinst.py --target-architecture universal2 --onedir /usr/bin/python3 pyinst.py --target-architecture universal2 --onedir
(cd ./dist/yt-dlp_macos && zip -r ../yt-dlp_macos.zip .) (cd ./dist/yt-dlp_macos && zip -r ../yt-dlp_macos.zip .)
/usr/bin/python3 pyinst.py --target-architecture universal2 /usr/bin/python3 pyinst.py --target-architecture universal2
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
path: | path: |
dist/yt-dlp_macos dist/yt-dlp_macos
dist/yt-dlp_macos.zip dist/yt-dlp_macos.zip
macos_legacy:
build_macos_legacy: if: inputs.macos_legacy
runs-on: macos-latest runs-on: macos-latest
needs: prepare
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Install Python - name: Install Python
# We need the official Python, because the GA ones only support newer macOS versions # We need the official Python, because the GA ones only support newer macOS versions
env: env:
PYTHON_VERSION: 3.10.5 PYTHON_VERSION: 3.10.5
MACOSX_DEPLOYMENT_TARGET: 10.9 # Used up by the Python build tools MACOSX_DEPLOYMENT_TARGET: 10.9 # Used up by the Python build tools
run: | run: |
# Hack to get the latest patch version. Uncomment if needed # Hack to get the latest patch version. Uncomment if needed
#brew install python@3.10 #brew install python@3.10
#export PYTHON_VERSION=$( $(brew --prefix)/opt/python@3.10/bin/python3 --version | cut -d ' ' -f 2 ) #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" curl https://www.python.org/ftp/python/${PYTHON_VERSION}/python-${PYTHON_VERSION}-macos11.pkg -o "python.pkg"
sudo installer -pkg python.pkg -target / sudo installer -pkg python.pkg -target /
python3 --version python3 --version
- name: Install Requirements - name: Install Requirements
run: | run: |
brew install coreutils brew install coreutils
python3 -m pip install -U --user pip Pyinstaller -r requirements.txt python3 -m pip install -U --user pip Pyinstaller -r requirements.txt
- name: Prepare - name: Prepare
run: | run: |
python3 devscripts/update-version.py ${{ needs.prepare.outputs.version_suffix }} python3 devscripts/update-version.py -c ${{ inputs.channel }} ${{ inputs.version }}
python3 devscripts/make_lazy_extractors.py python3 devscripts/make_lazy_extractors.py
- name: Build - name: Build
run: | run: |
python3 pyinst.py python3 pyinst.py
mv dist/yt-dlp_macos dist/yt-dlp_macos_legacy mv dist/yt-dlp_macos dist/yt-dlp_macos_legacy
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
path: | path: |
dist/yt-dlp_macos_legacy dist/yt-dlp_macos_legacy
windows:
build_windows: if: inputs.windows
runs-on: windows-latest runs-on: windows-latest
needs: prepare
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
with: # 3.8 is used for Win7 support with: # 3.8 is used for Win7 support
python-version: '3.8' python-version: "3.8"
- name: Install Requirements - name: Install Requirements
run: | # Custom pyinstaller built with https://github.com/yt-dlp/pyinstaller-builds run: | # Custom pyinstaller built with https://github.com/yt-dlp/pyinstaller-builds
python -m pip install -U pip setuptools wheel py2exe 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 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 - name: Prepare
run: | run: |
python devscripts/update-version.py ${{ needs.prepare.outputs.version_suffix }} python devscripts/update-version.py -c ${{ inputs.channel }} ${{ inputs.version }}
python devscripts/make_lazy_extractors.py python devscripts/make_lazy_extractors.py
- name: Build - name: Build
run: | run: |
python setup.py py2exe python setup.py py2exe
Move-Item ./dist/yt-dlp.exe ./dist/yt-dlp_min.exe Move-Item ./dist/yt-dlp.exe ./dist/yt-dlp_min.exe
python pyinst.py python pyinst.py
python pyinst.py --onedir python pyinst.py --onedir
Compress-Archive -Path ./dist/yt-dlp/* -DestinationPath ./dist/yt-dlp_win.zip Compress-Archive -Path ./dist/yt-dlp/* -DestinationPath ./dist/yt-dlp_win.zip
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
path: | path: |
dist/yt-dlp.exe dist/yt-dlp.exe
dist/yt-dlp_min.exe dist/yt-dlp_min.exe
dist/yt-dlp_win.zip dist/yt-dlp_win.zip
windows32:
build_windows32: if: inputs.windows32
runs-on: windows-latest runs-on: windows-latest
needs: prepare
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
with: # 3.7 is used for Vista support. See https://github.com/yt-dlp/yt-dlp/issues/390 with: # 3.7 is used for Vista support. See https://github.com/yt-dlp/yt-dlp/issues/390
python-version: '3.7' python-version: "3.7"
architecture: 'x86' architecture: "x86"
- name: Install Requirements - name: Install Requirements
run: | run: |
python -m pip install -U pip setuptools wheel 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 pip install -U "https://yt-dlp.github.io/Pyinstaller-Builds/i686/pyinstaller-5.8.0-py3-none-any.whl" -r requirements.txt
- name: Prepare - name: Prepare
run: | run: |
python devscripts/update-version.py ${{ needs.prepare.outputs.version_suffix }} python devscripts/update-version.py -c ${{ inputs.channel }} ${{ inputs.version }}
python devscripts/make_lazy_extractors.py python devscripts/make_lazy_extractors.py
- name: Build - name: Build
run: | run: |
python pyinst.py python pyinst.py
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
path: | path: |
dist/yt-dlp_x86.exe dist/yt-dlp_x86.exe
meta_files:
publish_release: if: inputs.meta_files && always()
permissions: needs:
contents: write # for action-gh-release - unix
- linux_arm
- macos
- macos_legacy
- windows
- windows32
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [prepare, build_unix, build_linux_arm, build_windows, build_windows32, build_macos, build_macos_legacy]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/download-artifact@v3
- uses: actions/download-artifact@v3
- name: Get Changelog - name: Make SHA2-SUMS files
run: | run: |
changelog=$(grep -oPz '(?s)(?<=### ${{ needs.prepare.outputs.ytdlp_version }}\n{2}).+?(?=\n{2,3}###)' Changelog.md) || true cd ./artifact/
echo "changelog<<EOF" >> $GITHUB_ENV sha256sum * > ../SHA2-256SUMS
echo "$changelog" >> $GITHUB_ENV sha512sum * > ../SHA2-512SUMS
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: Publish Release - name: Make Update spec
uses: yt-dlp/action-gh-release@v1 run: |
with: cat >> _update_spec << EOF
tag_name: ${{ needs.prepare.outputs.ytdlp_version }} # This file is used for regulating self-update
name: yt-dlp ${{ needs.prepare.outputs.ytdlp_version }} lock 2022.08.18.36 .+ Python 3.6
target_commitish: ${{ needs.prepare.outputs.head_sha }} EOF
body: |
#### [A description of the various files]((https://github.com/yt-dlp/yt-dlp#release-files)) are in the README
--- - name: Sign checksum files
<details open><summary><h3>Changelog</summary> env:
<p> 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 }} - name: Upload artifacts
uses: actions/upload-artifact@v3
</p> with:
</details> path: |
files: | SHA*SUMS*
SHA2-256SUMS _update_spec
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

80
.github/workflows/publish.yml vendored Normal file
View File

@@ -0,0 +1,80 @@
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 "Build ${{ inputs.version }}" \
${{ inputs.version }} \
artifact/*
- name: Prune old nightly release
if: inputs.nightly
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 }}
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
View File

@@ -0,0 +1,51 @@
name: Release (nightly)
on:
push:
branches:
- master
paths:
- "**.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 }}

127
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,127 @@
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: |
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/*
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 }}

View File

@@ -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? ### 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).

View File

@@ -405,3 +405,4 @@ road-master
rohieb rohieb
sdht0 sdht0
seproDev seproDev
Hill-98

View File

@@ -1,16 +1,70 @@
# Changelog # Changelog
<!-- <!--
# Instuctions for creating release # To create a release, dispatch the https://github.com/yt-dlp/yt-dlp/actions/workflows/release.yml workflow on master
* 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
--> -->
# 2023.02.17 ### 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) * Merge youtube-dl: Upto [commit/2dd6c6e](https://github.com/ytdl-org/youtube-dl/commit/2dd6c6e)
* Fix `--concat-playlist` * Fix `--concat-playlist`
@@ -50,8 +104,8 @@
* [extractor/txxx] Add extractors by [chio0hai](https://github.com/chio0hai) * [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/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/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/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] **Fix `uploader_id` extraction** by [bashonly](https://github.com/bashonly)
* [extractor/youtube] Add hyperpipe instances by [Generator](https://github.com/Generator) * [extractor/youtube] Add hyperpipe instances by [Generator](https://github.com/Generator)
* [extractor/youtube] Handle `consent.youtube` * [extractor/youtube] Handle `consent.youtube`
* [extractor/youtube] Support `/live/` URL * [extractor/youtube] Support `/live/` URL
@@ -101,172 +155,9 @@
* [extractor/drtv] Fix bug in [ab4cbef](https://github.com/yt-dlp/yt-dlp/commit/ab4cbef) by [bashonly](https://github.com/bashonly) * [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 ### 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 * [downloader/aria2c] Disable native progress
* [utils] `mimetype2ext`: `weba` is not standard * [utils] `mimetype2ext`: `weba` is not standard
* [utils] `windows_enable_vt_mode`: Better error handling * [utils] `windows_enable_vt_mode`: Better error handling
@@ -293,7 +184,7 @@
* Add `--compat-options 2021,2022` * 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. * 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) * [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` * Add pre-processor stage `video`
* Let `--parse/replace-in-metadata` run at any post-processing stage * Let `--parse/replace-in-metadata` run at any post-processing stage
* Add `--enable-file-urls` by [coletdjnz](https://github.com/coletdjnz) * Add `--enable-file-urls` by [coletdjnz](https://github.com/coletdjnz)
@@ -408,7 +299,7 @@
* [extractor/udemy] Fix lectures that have no URL and detect DRM * [extractor/udemy] Fix lectures that have no URL and detect DRM
* [extractor/unsupported] Add more URLs * [extractor/unsupported] Add more URLs
* [extractor/urplay] Support for audio-only formats by [barsnick](https://github.com/barsnick) * [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/yle_areena] Support restricted videos by [docbender](https://github.com/docbender)
* [extractor/youku] Fix extractor by [KurtBestor](https://github.com/KurtBestor) * [extractor/youku] Fix extractor by [KurtBestor](https://github.com/KurtBestor)
* [extractor/youporn] Fix metadata by [marieell](https://github.com/marieell) * [extractor/youporn] Fix metadata by [marieell](https://github.com/marieell)

View File

@@ -56,6 +56,7 @@ You can also find lists of all [contributors of yt-dlp](CONTRIBUTORS) and [autho
## [bashonly](https://github.com/bashonly) ## [bashonly](https://github.com/bashonly)
* `--update-to`, automated release, nightly builds
* `--cookies-from-browser` support for Firefox containers * `--cookies-from-browser` support for Firefox containers
* Added support for new websites Genius, Kick, NBCStations, Triller, VideoKen etc * 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 * 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
[![ko-fi](https://img.shields.io/badge/_-Ko--fi-red.svg?logo=kofi&labelColor=555555&style=for-the-badge)](https://ko-fi.com/Grub4K) [![gh-sponsor](https://img.shields.io/badge/_-Github-red.svg?logo=github&labelColor=555555&style=for-the-badge)](https://github.com/sponsors/Grub4K) [![ko-fi](https://img.shields.io/badge/_-Ko--fi-red.svg?logo=kofi&labelColor=555555&style=for-the-badge)](https://ko-fi.com/Grub4K) [![gh-sponsor](https://img.shields.io/badge/_-Github-red.svg?logo=github&labelColor=555555&style=for-the-badge)](https://github.com/sponsors/Grub4K)
* `--update-to`, automated release, nightly builds
* Rework internals like `traverse_obj`, various core refactors and bugs fixes * Rework internals like `traverse_obj`, various core refactors and bugs fixes
* Helped fix crunchyroll, Twitter, wrestleuniverse, wistia, slideslive etc * Helped fix crunchyroll, Twitter, wrestleuniverse, wistia, slideslive etc

View File

@@ -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` * **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 * **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 * **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 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: 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 * The options `--auto-number` (`-A`), `--title` (`-t`) and `--literal` (`-l`), no longer work. See [removed options](#Removed) for details
* `avconv` is not supported as an alternative to `ffmpeg` * `avconv` is not supported as an alternative to `ffmpeg`
* yt-dlp stores config files in slightly different locations to youtube-dl. See [CONFIGURATION](#configuration) for a list of correct locations * yt-dlp stores config files in slightly different locations to youtube-dl. See [CONFIGURATION](#configuration) for a list of correct locations
@@ -180,12 +183,26 @@ You can install yt-dlp using [the binaries](#release-files), [PIP](https://pypi.
## UPDATE ## 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 If you [installed with PIP](https://github.com/yt-dlp/yt-dlp/wiki/Installation#with-pip), simply re-run the same command that was used to install the program
For other third-party package managers, see [the wiki](https://github.com/yt-dlp/yt-dlp/wiki/Installation#third-party-package-managers) or refer their documentation For other third-party package managers, see [the wiki](https://github.com/yt-dlp/yt-dlp/wiki/Installation#third-party-package-managers) or refer their documentation
<a id="update-channels"/>
There are currently two release channels for binaries, `stable` and `nightly`.
`stable` releases are what the program will update to by default, and have had many of their changes tested by users of the master branch.
`nightly` releases are built after each push to the master branch, and will have the most recent fixes and additions, but also have the potential for bugs.
The latest `nightly` is available as a [pre-release from this repository](https://github.com/yt-dlp/yt-dlp/releases/tag/nightly), and all `nightly` releases are [archived 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 --> <!-- MANPAGE: BEGIN EXCLUDED SECTION -->
## RELEASE FILES ## RELEASE FILES
@@ -218,11 +235,20 @@ File|Description
:---|:--- :---|:---
[yt-dlp.tar.gz](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.tar.gz)|Source tarball [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](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](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 --> <!-- MANPAGE: END EXCLUDED SECTION -->
**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)
**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)
## DEPENDENCIES ## DEPENDENCIES
Python versions 3.7+ (CPython and PyPy) are supported. Other versions and implementations may or may not work correctly. Python versions 3.7+ (CPython and PyPy) are supported. Other versions and implementations may or may not work correctly.
@@ -310,11 +336,15 @@ If you wish to build it anyway, install Python and py2exe, and then simply run `
### Related scripts ### Related scripts
* **`devscripts/update-version.py [revision]`** - Update the version number based on current date * **`devscripts/update-version.py`** - Update the version number based on current date.
* **`devscripts/set-variant.py variant [-M update_message]`** - Set the build variant of the executable * **`devscripts/set-variant.py`** - Set the build variant of the executable.
* **`devscripts/make_changelog.py`** - Create a markdown changelog using short commit messages and update `CONTRIBUTORS` file.
* **`devscripts/make_lazy_extractors.py`** - Create lazy extractors. Running this before building the binaries (any variant) will improve their startup performance. Set the environment variable `YTDLP_NO_LAZY_EXTRACTORS=1` if you wish to forcefully disable lazy extractor loading. * **`devscripts/make_lazy_extractors.py`** - Create lazy extractors. Running this before building the binaries (any variant) will improve their startup performance. Set the environment variable `YTDLP_NO_LAZY_EXTRACTORS=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 # USAGE AND OPTIONS
@@ -330,6 +360,11 @@ You can also fork the project on GitHub and run your fork's [build workflow](.gi
--version Print program version and exit --version Print program version and exit
-U, --update Update this program to the latest version -U, --update Update this program to the latest version
--no-update Do not check for updates (default) --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. -i, --ignore-errors Ignore download and postprocessing errors.
The download will be considered successful The download will be considered successful
even if the postprocessing fails even if the postprocessing fails
@@ -456,9 +491,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. --date DATE Download only videos uploaded on this date.
The date can be "YYYYMMDD" or in the format The date can be "YYYYMMDD" or in the format
[now|today|yesterday][-N[day|week|month|year]]. [now|today|yesterday][-N[day|week|month|year]].
E.g. "--date today-2weeks" downloads E.g. "--date today-2weeks" downloads only
only videos uploaded on the same day two videos uploaded on the same day two weeks ago
weeks ago
--datebefore DATE Download only videos uploaded on or before --datebefore DATE Download only videos uploaded on or before
this date. The date formats accepted is the this date. The date formats accepted is the
same as --date same as --date
@@ -485,7 +519,10 @@ You can also fork the project on GitHub and run your fork's [build workflow](.gi
dogs" (caseless). Use "--match-filter -" to dogs" (caseless). Use "--match-filter -" to
interactively ask whether to download each interactively ask whether to download each
video 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 --no-playlist Download only the video, if the URL refers
to a video and a playlist to a video and a playlist
--yes-playlist Download the playlist, if the URL refers to --yes-playlist Download the playlist, if the URL refers to
@@ -499,11 +536,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 --max-downloads NUMBER Abort after downloading NUMBER files
--break-on-existing Stop the download process when encountering --break-on-existing Stop the download process when encountering
a file that is in the archive a file that is in the archive
--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-per-input Alters --max-downloads, --break-on-existing,
--break-on-reject, and autonumber to reset --break-match-filter, and autonumber to
per input URL reset per input URL
--no-break-per-input --break-on-existing and similar options --no-break-per-input --break-on-existing and similar options
terminates the entire download queue terminates the entire download queue
--skip-playlist-after-errors N Number of allowed failures until the rest of --skip-playlist-after-errors N Number of allowed failures until the rest of
@@ -1227,7 +1262,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. 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. **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 +2134,7 @@ While these options are redundant, they are still expected to be used due to the
--reject-title REGEX --match-filter "title !~= (?i)REGEX" --reject-title REGEX --match-filter "title !~= (?i)REGEX"
--min-views COUNT --match-filter "view_count >=? COUNT" --min-views COUNT --match-filter "view_count >=? COUNT"
--max-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" --user-agent UA --add-header "User-Agent:UA"
--referer URL --add-header "Referer:URL" --referer URL --add-header "Referer:URL"
--playlist-start NUMBER -I NUMBER: --playlist-start NUMBER -I NUMBER:

View File

@@ -0,0 +1,12 @@
[
{
"action": "add",
"when": "2023.02.17",
"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": "2023.02.17",
"short": "[priority] **YouTube throttling fixes!**"
}
]

View 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"
]
}
]
}
}

View File

@@ -0,0 +1,493 @@
from __future__ import annotations
import enum
import itertools
import json
import logging
import re
import subprocess
import sys
from collections import defaultdict
from dataclasses import dataclass
from functools import lru_cache
from pathlib import Path
BASE_URL = 'https://github.com'
LOCATION_PATH = Path(__file__).parent
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[:7]})'
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[:7]
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) -> None:
self._start = start
self._end = end
self._commits, self._fixes = self._get_commits_and_fixes(default_author)
self._commits_added = []
@classmethod
def from_single(cls, commitish='HEAD', default_author=None):
start_commitish = cls.get_prev_tag(commitish)
end_commitish = cls.get_next_tag(commitish)
if start_commitish == end_commitish:
start_commitish = cls.get_prev_tag(f'{commitish}~')
logger.info(f'Determined range from {commitish!r}: {start_commitish}..{end_commitish}')
return cls(start_commitish, end_commitish, default_author)
@classmethod
def get_prev_tag(cls, commitish):
command = [cls.COMMAND, 'describe', '--tags', '--abbrev=0', '--exclude=*[^0-9.]*', commitish]
return subprocess.check_output(command, text=True).strip()
@classmethod
def get_next_tag(cls, commitish):
result = subprocess.run(
[cls.COMMAND, 'describe', '--contains', '--abbrev=0', commitish],
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True)
if result.returncode:
return 'HEAD'
return result.stdout.partition('~')[0].strip()
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 _is_ancestor(self, commitish):
return bool(subprocess.call(
[self.COMMAND, 'merge-base', '--is-ancestor', commitish, self._start]))
def _get_commits_and_fixes(self, default_author):
result = subprocess.check_output([
self.COMMAND, 'log', f'--format=%H%n%s%n%b%n{self.COMMIT_SEPARATOR}',
f'{self._start}..{self._end}'], text=True)
commits = {}
fixes = defaultdict(list)
lines = iter(result.splitlines(False))
for line in lines:
commit_hash = line
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:
logger.debug(f'Skipped commit: {commit}')
continue
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[:7] for commit in fix_commits)
logger.info(f'Found fix(es) for {commitish[:7]}: {hashes}')
for fix_commit in fix_commits:
del commits[fix_commit.hash]
else:
logger.debug(f'Commit with fixes not in changes: {commitish[:7]}')
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():
with contributors_path.open() as file:
for line in filter(None, map(str.strip, file)):
author, _, _ = line.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.from_single(args.commitish, args.default_author)
if not args.no_override:
if args.override_path.exists():
with args.override_path.open() as file:
overrides = json.load(file)
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:
with args.contributors_path.open('a') as file:
file.writelines(f'{contributor}\n' for contributor in new_contributors)
logger.info(f'New contributors: {", ".join(new_contributors)}')
print(Changelog(commits.groups(), args.repo))

View File

@@ -24,6 +24,8 @@ VERBOSE_TMPL = '''
options: options:
- label: Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`) - label: Run **your** yt-dlp command with **-vU** flag added (`yt-dlp -vU <your command line>`)
required: true 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 - label: Copy the WHOLE output (starting with `[debug] Command-line config`) and insert it below
required: true required: true
- type: textarea - type: textarea

View File

@@ -45,33 +45,43 @@ switch_col_width = len(re.search(r'(?m)^\s{5,}', options).group())
delim = f'\n{" " * switch_col_width}' delim = f'\n{" " * switch_col_width}'
PATCHES = ( PATCHES = (
( # Standardize update message ( # Standardize `--update` message
r'(?m)^( -U, --update\s+).+(\n \s.+)*$', r'(?m)^( -U, --update\s+).+(\n \s.+)*$',
r'\1Update this program to the latest version', r'\1Update this program to the latest version',
), ),
( # Headings ( # Headings
r'(?m)^ (\w.+\n)( (?=\w))?', r'(?m)^ (\w.+\n)( (?=\w))?',
r'## \1' 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', 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')) 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+)+$', rf'(?m)({delim}\S+)+$',
lambda mobj: ''.join((delim, mobj.group(0).replace(delim, ''))) 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})', rf'(?m)^(?P<prev>.+)${delim}(?P<current>.+)$(?!{delim})',
lambda mobj: (mobj.group().replace(delim, ' ') lambda mobj: (mobj.group().replace(delim, ' ')
if len(mobj.group()) - len(delim) + 1 <= max_width + ALLOWED_OVERSHOOT if len(mobj.group()) - len(delim) + 1 <= max_width + ALLOWED_OVERSHOOT
else mobj.group()) 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 DISABLE_PATCH, # This creates issues with prepare_manpage
r'(?m)^(\s{4}-.{%d})(%s)' % (switch_col_width - 6, delim), r'(?m)^(\s{4}-.{%d})(%s)' % (switch_col_width - 6, delim),
r'\1 ' r'\1 '
), ),
( # Replace brackets with a Markdown link
r'SponsorBlock API \((http.+)\)',
r'[SponsorBlock API](\1)'
),
) )
readme = read_file(README_FILE) readme = read_file(README_FILE)

View File

@@ -7,6 +7,7 @@ import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import argparse
import contextlib import contextlib
import subprocess import subprocess
import sys import sys
@@ -15,8 +16,9 @@ from datetime import datetime
from devscripts.utils import read_version, write_file from devscripts.utils import read_version, write_file
def get_new_version(revision): def get_new_version(version, revision):
version = datetime.utcnow().strftime('%Y.%m.%d') if not version:
version = datetime.utcnow().strftime('%Y.%m.%d')
if revision: if revision:
assert revision.isdigit(), 'Revision must be a number' assert revision.isdigit(), 'Revision must be a number'
@@ -30,27 +32,41 @@ def get_new_version(revision):
def get_git_head(): def get_git_head():
with contextlib.suppress(Exception): with contextlib.suppress(Exception):
sp = subprocess.Popen(['git', 'rev-parse', '--short', 'HEAD'], stdout=subprocess.PIPE) return subprocess.check_output(['git', 'rev-parse', 'HEAD'], text=True).strip() or None
return sp.communicate()[0].decode().strip() or None
VERSION = get_new_version((sys.argv + [''])[1]) VERSION_TEMPLATE = '''\
GIT_HEAD = get_git_head()
VERSION_FILE = f'''\
# Autogenerated by devscripts/update-version.py # 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 VARIANT = None
UPDATE_HINT = None UPDATE_HINT = None
CHANNEL = {channel!r}
''' '''
write_file('yt_dlp/version.py', VERSION_FILE) if __name__ == '__main__':
github_output = os.getenv('GITHUB_OUTPUT') parser = argparse.ArgumentParser(description='Update the version.py file')
if github_output: parser.add_argument(
write_file(github_output, f'ytdlp_version={VERSION}\n', 'a') '-c', '--channel', choices=['stable', 'nightly'], default='stable',
print(f'\nVersion = {VERSION}, Git HEAD = {GIT_HEAD}') 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}')

29
public.key Normal file
View 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-----

View File

@@ -28,14 +28,14 @@
- **abcnews:video** - **abcnews:video**
- **abcotvs**: ABC Owned Television Stations - **abcotvs**: ABC Owned Television Stations
- **abcotvs:clips** - **abcotvs:clips**
- **AbemaTV**: [<abbr title="netrc machine"><em>abematv</em></abbr>] - **AbemaTV**: [*abematv*](## "netrc machine")
- **AbemaTVTitle** - **AbemaTVTitle**
- **AcademicEarth:Course** - **AcademicEarth:Course**
- **acast** - **acast**
- **acast:channel** - **acast:channel**
- **AcFunBangumi** - **AcFunBangumi**
- **AcFunVideo** - **AcFunVideo**
- **ADN**: [<abbr title="netrc machine"><em>animationdigitalnetwork</em></abbr>] Animation Digital Network - **ADN**: [*animationdigitalnetwork*](## "netrc machine") Animation Digital Network
- **AdobeConnect** - **AdobeConnect**
- **adobetv** - **adobetv**
- **adobetv:channel** - **adobetv:channel**
@@ -47,8 +47,8 @@
- **aenetworks:collection** - **aenetworks:collection**
- **aenetworks:show** - **aenetworks:show**
- **AeonCo** - **AeonCo**
- **afreecatv**: [<abbr title="netrc machine"><em>afreecatv</em></abbr>] afreecatv.com - **afreecatv**: [*afreecatv*](## "netrc machine") afreecatv.com
- **afreecatv:live**: [<abbr title="netrc machine"><em>afreecatv</em></abbr>] afreecatv.com - **afreecatv:live**: [*afreecatv*](## "netrc machine") afreecatv.com
- **afreecatv:user** - **afreecatv:user**
- **AirMozilla** - **AirMozilla**
- **AirTV** - **AirTV**
@@ -59,8 +59,8 @@
- **AlphaPorno** - **AlphaPorno**
- **Alsace20TV** - **Alsace20TV**
- **Alsace20TVEmbed** - **Alsace20TVEmbed**
- **Alura**: [<abbr title="netrc machine"><em>alura</em></abbr>] - **Alura**: [*alura*](## "netrc machine")
- **AluraCourse**: [<abbr title="netrc machine"><em>aluracourse</em></abbr>] - **AluraCourse**: [*aluracourse*](## "netrc machine")
- **Amara** - **Amara**
- **AmazonMiniTV** - **AmazonMiniTV**
- **amazonminitv:season**: Amazon MiniTV Season, "minitv:season:" prefix - **amazonminitv:season**: Amazon MiniTV Season, "minitv:season:" prefix
@@ -100,7 +100,7 @@
- **ArteTVPlaylist** - **ArteTVPlaylist**
- **AsianCrush** - **AsianCrush**
- **AsianCrushPlaylist** - **AsianCrushPlaylist**
- **AtresPlayer**: [<abbr title="netrc machine"><em>atresplayer</em></abbr>] - **AtresPlayer**: [*atresplayer*](## "netrc machine")
- **AtScaleConfEvent** - **AtScaleConfEvent**
- **ATTTechChannel** - **ATTTechChannel**
- **ATVAt** - **ATVAt**
@@ -128,15 +128,15 @@
- **Bandcamp:user** - **Bandcamp:user**
- **Bandcamp:weekly** - **Bandcamp:weekly**
- **BannedVideo** - **BannedVideo**
- **bbc**: [<abbr title="netrc machine"><em>bbc</em></abbr>] BBC - **bbc**: [*bbc*](## "netrc machine") BBC
- **bbc.co.uk**: [<abbr title="netrc machine"><em>bbc</em></abbr>] BBC iPlayer - **bbc.co.uk**: [*bbc*](## "netrc machine") BBC iPlayer
- **bbc.co.uk:article**: BBC articles - **bbc.co.uk:article**: BBC articles
- **bbc.co.uk:iplayer:episodes** - **bbc.co.uk:iplayer:episodes**
- **bbc.co.uk:iplayer:group** - **bbc.co.uk:iplayer:group**
- **bbc.co.uk:playlist** - **bbc.co.uk:playlist**
- **BBVTV**: [<abbr title="netrc machine"><em>bbvtv</em></abbr>] - **BBVTV**: [*bbvtv*](## "netrc machine")
- **BBVTVLive**: [<abbr title="netrc machine"><em>bbvtv</em></abbr>] - **BBVTVLive**: [*bbvtv*](## "netrc machine")
- **BBVTVRecordings**: [<abbr title="netrc machine"><em>bbvtv</em></abbr>] - **BBVTVRecordings**: [*bbvtv*](## "netrc machine")
- **BeatBumpPlaylist** - **BeatBumpPlaylist**
- **BeatBumpVideo** - **BeatBumpVideo**
- **Beatport** - **Beatport**
@@ -165,8 +165,8 @@
- **BilibiliSpaceAudio** - **BilibiliSpaceAudio**
- **BilibiliSpacePlaylist** - **BilibiliSpacePlaylist**
- **BilibiliSpaceVideo** - **BilibiliSpaceVideo**
- **BiliIntl**: [<abbr title="netrc machine"><em>biliintl</em></abbr>] - **BiliIntl**: [*biliintl*](## "netrc machine")
- **biliIntl:series**: [<abbr title="netrc machine"><em>biliintl</em></abbr>] - **biliIntl:series**: [*biliintl*](## "netrc machine")
- **BiliLive** - **BiliLive**
- **BioBioChileTV** - **BioBioChileTV**
- **Biography** - **Biography**
@@ -232,7 +232,7 @@
- **cbssports:embed** - **cbssports:embed**
- **CCMA** - **CCMA**
- **CCTV**: 央视网 - **CCTV**: 央视网
- **CDA**: [<abbr title="netrc machine"><em>cdapl</em></abbr>] - **CDA**: [*cdapl*](## "netrc machine")
- **Cellebrite** - **Cellebrite**
- **CeskaTelevize** - **CeskaTelevize**
- **CGTN** - **CGTN**
@@ -286,8 +286,8 @@
- **CrooksAndLiars** - **CrooksAndLiars**
- **CrowdBunker** - **CrowdBunker**
- **CrowdBunkerChannel** - **CrowdBunkerChannel**
- **crunchyroll**: [<abbr title="netrc machine"><em>crunchyroll</em></abbr>] - **crunchyroll**: [*crunchyroll*](## "netrc machine")
- **crunchyroll:playlist**: [<abbr title="netrc machine"><em>crunchyroll</em></abbr>] - **crunchyroll:playlist**: [*crunchyroll*](## "netrc machine")
- **CSpan**: C-SPAN - **CSpan**: C-SPAN
- **CSpanCongress** - **CSpanCongress**
- **CtsNews**: 華視新聞 - **CtsNews**: 華視新聞
@@ -295,18 +295,18 @@
- **CTVNews** - **CTVNews**
- **cu.ntv.co.jp**: Nippon Television Network - **cu.ntv.co.jp**: Nippon Television Network
- **CultureUnplugged** - **CultureUnplugged**
- **curiositystream**: [<abbr title="netrc machine"><em>curiositystream</em></abbr>] - **curiositystream**: [*curiositystream*](## "netrc machine")
- **curiositystream:collections**: [<abbr title="netrc machine"><em>curiositystream</em></abbr>] - **curiositystream:collections**: [*curiositystream*](## "netrc machine")
- **curiositystream:series**: [<abbr title="netrc machine"><em>curiositystream</em></abbr>] - **curiositystream:series**: [*curiositystream*](## "netrc machine")
- **CWTV** - **CWTV**
- **Cybrary**: [<abbr title="netrc machine"><em>cybrary</em></abbr>] - **Cybrary**: [*cybrary*](## "netrc machine")
- **CybraryCourse**: [<abbr title="netrc machine"><em>cybrary</em></abbr>] - **CybraryCourse**: [*cybrary*](## "netrc machine")
- **Daftsex** - **Daftsex**
- **DagelijkseKost**: dagelijksekost.een.be - **DagelijkseKost**: dagelijksekost.een.be
- **DailyMail** - **DailyMail**
- **dailymotion**: [<abbr title="netrc machine"><em>dailymotion</em></abbr>] - **dailymotion**: [*dailymotion*](## "netrc machine")
- **dailymotion:playlist**: [<abbr title="netrc machine"><em>dailymotion</em></abbr>] - **dailymotion:playlist**: [*dailymotion*](## "netrc machine")
- **dailymotion:user**: [<abbr title="netrc machine"><em>dailymotion</em></abbr>] - **dailymotion:user**: [*dailymotion*](## "netrc machine")
- **DailyWire** - **DailyWire**
- **DailyWirePodcast** - **DailyWirePodcast**
- **damtomo:record** - **damtomo:record**
@@ -328,7 +328,7 @@
- **DeuxMNews** - **DeuxMNews**
- **DHM**: Filmarchiv - Deutsches Historisches Museum - **DHM**: Filmarchiv - Deutsches Historisches Museum
- **Digg** - **Digg**
- **DigitalConcertHall**: [<abbr title="netrc machine"><em>digitalconcerthall</em></abbr>] DigitalConcertHall extractor - **DigitalConcertHall**: [*digitalconcerthall*](## "netrc machine") DigitalConcertHall extractor
- **DigitallySpeaking** - **DigitallySpeaking**
- **Digiteka** - **Digiteka**
- **Discovery** - **Discovery**
@@ -351,7 +351,7 @@
- **DRBonanza** - **DRBonanza**
- **Drooble** - **Drooble**
- **Dropbox** - **Dropbox**
- **Dropout**: [<abbr title="netrc machine"><em>dropout</em></abbr>] - **Dropout**: [*dropout*](## "netrc machine")
- **DropoutSeason** - **DropoutSeason**
- **DrTuber** - **DrTuber**
- **drtv** - **drtv**
@@ -373,9 +373,9 @@
- **egghead:lesson**: egghead.io lesson - **egghead:lesson**: egghead.io lesson
- **ehftv** - **ehftv**
- **eHow** - **eHow**
- **EinsUndEinsTV**: [<abbr title="netrc machine"><em>1und1tv</em></abbr>] - **EinsUndEinsTV**: [*1und1tv*](## "netrc machine")
- **EinsUndEinsTVLive**: [<abbr title="netrc machine"><em>1und1tv</em></abbr>] - **EinsUndEinsTVLive**: [*1und1tv*](## "netrc machine")
- **EinsUndEinsTVRecordings**: [<abbr title="netrc machine"><em>1und1tv</em></abbr>] - **EinsUndEinsTVRecordings**: [*1und1tv*](## "netrc machine")
- **Einthusan** - **Einthusan**
- **eitb.tv** - **eitb.tv**
- **EllenTube** - **EllenTube**
@@ -390,7 +390,7 @@
- **EpiconSeries** - **EpiconSeries**
- **Epoch** - **Epoch**
- **Eporner** - **Eporner**
- **EroProfile**: [<abbr title="netrc machine"><em>eroprofile</em></abbr>] - **EroProfile**: [*eroprofile*](## "netrc machine")
- **EroProfile:album** - **EroProfile:album**
- **ertflix**: ERTFLIX videos - **ertflix**: ERTFLIX videos
- **ertflix:codename**: ERTFLIX videos by codename - **ertflix:codename**: ERTFLIX videos by codename
@@ -405,20 +405,20 @@
- **EuropeanTour** - **EuropeanTour**
- **Eurosport** - **Eurosport**
- **EUScreen** - **EUScreen**
- **EWETV**: [<abbr title="netrc machine"><em>ewetv</em></abbr>] - **EWETV**: [*ewetv*](## "netrc machine")
- **EWETVLive**: [<abbr title="netrc machine"><em>ewetv</em></abbr>] - **EWETVLive**: [*ewetv*](## "netrc machine")
- **EWETVRecordings**: [<abbr title="netrc machine"><em>ewetv</em></abbr>] - **EWETVRecordings**: [*ewetv*](## "netrc machine")
- **ExpoTV** - **ExpoTV**
- **Expressen** - **Expressen**
- **ExtremeTube** - **ExtremeTube**
- **EyedoTV** - **EyedoTV**
- **facebook**: [<abbr title="netrc machine"><em>facebook</em></abbr>] - **facebook**: [*facebook*](## "netrc machine")
- **facebook:reel** - **facebook:reel**
- **FacebookPluginsVideo** - **FacebookPluginsVideo**
- **fancode:live**: [<abbr title="netrc machine"><em>fancode</em></abbr>] - **fancode:live**: [*fancode*](## "netrc machine")
- **fancode:vod**: [<abbr title="netrc machine"><em>fancode</em></abbr>] - **fancode:vod**: [*fancode*](## "netrc machine")
- **faz.net** - **faz.net**
- **fc2**: [<abbr title="netrc machine"><em>fc2</em></abbr>] - **fc2**: [*fc2*](## "netrc machine")
- **fc2:embed** - **fc2:embed**
- **fc2:live** - **fc2:live**
- **Fczenit** - **Fczenit**
@@ -452,20 +452,20 @@
- **freespeech.org** - **freespeech.org**
- **freetv:series** - **freetv:series**
- **FreeTvMovies** - **FreeTvMovies**
- **FrontendMasters**: [<abbr title="netrc machine"><em>frontendmasters</em></abbr>] - **FrontendMasters**: [*frontendmasters*](## "netrc machine")
- **FrontendMastersCourse**: [<abbr title="netrc machine"><em>frontendmasters</em></abbr>] - **FrontendMastersCourse**: [*frontendmasters*](## "netrc machine")
- **FrontendMastersLesson**: [<abbr title="netrc machine"><em>frontendmasters</em></abbr>] - **FrontendMastersLesson**: [*frontendmasters*](## "netrc machine")
- **FujiTVFODPlus7** - **FujiTVFODPlus7**
- **Funimation**: [<abbr title="netrc machine"><em>funimation</em></abbr>] - **Funimation**: [*funimation*](## "netrc machine")
- **funimation:page**: [<abbr title="netrc machine"><em>funimation</em></abbr>] - **funimation:page**: [*funimation*](## "netrc machine")
- **funimation:show**: [<abbr title="netrc machine"><em>funimation</em></abbr>] - **funimation:show**: [*funimation*](## "netrc machine")
- **Funk** - **Funk**
- **Fusion** - **Fusion**
- **Fux** - **Fux**
- **FuyinTV** - **FuyinTV**
- **Gab** - **Gab**
- **GabTV** - **GabTV**
- **Gaia**: [<abbr title="netrc machine"><em>gaia</em></abbr>] - **Gaia**: [*gaia*](## "netrc machine")
- **GameInformer** - **GameInformer**
- **GameJolt** - **GameJolt**
- **GameJoltCommunity** - **GameJoltCommunity**
@@ -477,9 +477,9 @@
- **GameStar** - **GameStar**
- **Gaskrank** - **Gaskrank**
- **Gazeta** - **Gazeta**
- **GDCVault**: [<abbr title="netrc machine"><em>gdcvault</em></abbr>] - **GDCVault**: [*gdcvault*](## "netrc machine")
- **GediDigital** - **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:live**
- **gem.cbc.ca:playlist** - **gem.cbc.ca:playlist**
- **Genius** - **Genius**
@@ -489,11 +489,11 @@
- **Gfycat** - **Gfycat**
- **GiantBomb** - **GiantBomb**
- **Giga** - **Giga**
- **GlattvisionTV**: [<abbr title="netrc machine"><em>glattvisiontv</em></abbr>] - **GlattvisionTV**: [*glattvisiontv*](## "netrc machine")
- **GlattvisionTVLive**: [<abbr title="netrc machine"><em>glattvisiontv</em></abbr>] - **GlattvisionTVLive**: [*glattvisiontv*](## "netrc machine")
- **GlattvisionTVRecordings**: [<abbr title="netrc machine"><em>glattvisiontv</em></abbr>] - **GlattvisionTVRecordings**: [*glattvisiontv*](## "netrc machine")
- **Glide**: Glide mobile video messages (glide.me) - **Glide**: Glide mobile video messages (glide.me)
- **Globo**: [<abbr title="netrc machine"><em>globo</em></abbr>] - **Globo**: [*globo*](## "netrc machine")
- **GloboArticle** - **GloboArticle**
- **glomex**: Glomex videos - **glomex**: Glomex videos
- **glomex:embed**: Glomex embedded videos - **glomex:embed**: Glomex embedded videos
@@ -507,7 +507,7 @@
- **google:podcasts:feed** - **google:podcasts:feed**
- **GoogleDrive** - **GoogleDrive**
- **GoogleDrive:Folder** - **GoogleDrive:Folder**
- **GoPlay**: [<abbr title="netrc machine"><em>goplay</em></abbr>] - **GoPlay**: [*goplay*](## "netrc machine")
- **GoPro** - **GoPro**
- **Goshgay** - **Goshgay**
- **GoToStage** - **GoToStage**
@@ -527,7 +527,7 @@
- **hgtv.com:show** - **hgtv.com:show**
- **HGTVDe** - **HGTVDe**
- **HGTVUsa** - **HGTVUsa**
- **HiDive**: [<abbr title="netrc machine"><em>hidive</em></abbr>] - **HiDive**: [*hidive*](## "netrc machine")
- **HistoricFilms** - **HistoricFilms**
- **history:player** - **history:player**
- **history:topic**: History.com Topic - **history:topic**: History.com Topic
@@ -544,8 +544,8 @@
- **Howcast** - **Howcast**
- **HowStuffWorks** - **HowStuffWorks**
- **hrfernsehen** - **hrfernsehen**
- **HRTi**: [<abbr title="netrc machine"><em>hrti</em></abbr>] - **HRTi**: [*hrti*](## "netrc machine")
- **HRTiPlaylist**: [<abbr title="netrc machine"><em>hrti</em></abbr>] - **HRTiPlaylist**: [*hrti*](## "netrc machine")
- **HSEProduct** - **HSEProduct**
- **HSEShow** - **HSEShow**
- **html5** - **html5**
@@ -575,19 +575,19 @@
- **Inc** - **Inc**
- **IndavideoEmbed** - **IndavideoEmbed**
- **InfoQ** - **InfoQ**
- **Instagram**: [<abbr title="netrc machine"><em>instagram</em></abbr>] - **Instagram**: [*instagram*](## "netrc machine")
- **instagram:story**: [<abbr title="netrc machine"><em>instagram</em></abbr>] - **instagram:story**: [*instagram*](## "netrc machine")
- **instagram:tag**: [<abbr title="netrc machine"><em>instagram</em></abbr>] Instagram hashtag search URLs - **instagram:tag**: [*instagram*](## "netrc machine") Instagram hashtag search URLs
- **instagram:user**: [<abbr title="netrc machine"><em>instagram</em></abbr>] Instagram user profile - **instagram:user**: [*instagram*](## "netrc machine") Instagram user profile
- **InstagramIOS**: IOS instagram:// URL - **InstagramIOS**: IOS instagram:// URL
- **Internazionale** - **Internazionale**
- **InternetVideoArchive** - **InternetVideoArchive**
- **InvestigationDiscovery** - **InvestigationDiscovery**
- **IPrima**: [<abbr title="netrc machine"><em>iprima</em></abbr>] - **IPrima**: [*iprima*](## "netrc machine")
- **IPrimaCNN** - **IPrimaCNN**
- **iq.com**: International version of iQiyi - **iq.com**: International version of iQiyi
- **iq.com:album** - **iq.com:album**
- **iqiyi**: [<abbr title="netrc machine"><em>iqiyi</em></abbr>] 爱奇艺 - **iqiyi**: [*iqiyi*](## "netrc machine") 爱奇艺
- **IslamChannel** - **IslamChannel**
- **IslamChannelSeries** - **IslamChannelSeries**
- **IsraelNationalNews** - **IsraelNationalNews**
@@ -660,9 +660,9 @@
- **LcpPlay** - **LcpPlay**
- **Le**: 乐视网 - **Le**: 乐视网
- **Lecture2Go** - **Lecture2Go**
- **Lecturio**: [<abbr title="netrc machine"><em>lecturio</em></abbr>] - **Lecturio**: [*lecturio*](## "netrc machine")
- **LecturioCourse**: [<abbr title="netrc machine"><em>lecturio</em></abbr>] - **LecturioCourse**: [*lecturio*](## "netrc machine")
- **LecturioDeCourse**: [<abbr title="netrc machine"><em>lecturio</em></abbr>] - **LecturioDeCourse**: [*lecturio*](## "netrc machine")
- **LEGO** - **LEGO**
- **Lemonde** - **Lemonde**
- **Lenta** - **Lenta**
@@ -678,10 +678,10 @@
- **limelight:channel_list** - **limelight:channel_list**
- **LineLive** - **LineLive**
- **LineLiveChannel** - **LineLiveChannel**
- **LinkedIn**: [<abbr title="netrc machine"><em>linkedin</em></abbr>] - **LinkedIn**: [*linkedin*](## "netrc machine")
- **linkedin:learning**: [<abbr title="netrc machine"><em>linkedin</em></abbr>] - **linkedin:learning**: [*linkedin*](## "netrc machine")
- **linkedin:learning:course**: [<abbr title="netrc machine"><em>linkedin</em></abbr>] - **linkedin:learning:course**: [*linkedin*](## "netrc machine")
- **LinuxAcademy**: [<abbr title="netrc machine"><em>linuxacademy</em></abbr>] - **LinuxAcademy**: [*linuxacademy*](## "netrc machine")
- **Liputan6** - **Liputan6**
- **ListenNotes** - **ListenNotes**
- **LiTV** - **LiTV**
@@ -696,8 +696,8 @@
- **LoveHomePorn** - **LoveHomePorn**
- **LRTStream** - **LRTStream**
- **LRTVOD** - **LRTVOD**
- **lynda**: [<abbr title="netrc machine"><em>lynda</em></abbr>] lynda.com videos - **lynda**: [*lynda*](## "netrc machine") lynda.com videos
- **lynda:course**: [<abbr title="netrc machine"><em>lynda</em></abbr>] lynda.com online courses - **lynda:course**: [*lynda*](## "netrc machine") lynda.com online courses
- **m6** - **m6**
- **MagentaMusik360** - **MagentaMusik360**
- **mailru**: Видео@Mail.Ru - **mailru**: Видео@Mail.Ru
@@ -767,13 +767,13 @@
- **mixcloud:user** - **mixcloud:user**
- **MLB** - **MLB**
- **MLBArticle** - **MLBArticle**
- **MLBTV**: [<abbr title="netrc machine"><em>mlb</em></abbr>] - **MLBTV**: [*mlb*](## "netrc machine")
- **MLBVideo** - **MLBVideo**
- **MLSSoccer** - **MLSSoccer**
- **Mnet** - **Mnet**
- **MNetTV**: [<abbr title="netrc machine"><em>mnettv</em></abbr>] - **MNetTV**: [*mnettv*](## "netrc machine")
- **MNetTVLive**: [<abbr title="netrc machine"><em>mnettv</em></abbr>] - **MNetTVLive**: [*mnettv*](## "netrc machine")
- **MNetTVRecordings**: [<abbr title="netrc machine"><em>mnettv</em></abbr>] - **MNetTVRecordings**: [*mnettv*](## "netrc machine")
- **MochaVideo** - **MochaVideo**
- **MoeVideo**: LetitBit video services: moevideo.net, playreplay.net and videochart.net - **MoeVideo**: LetitBit video services: moevideo.net, playreplay.net and videochart.net
- **Mofosex** - **Mofosex**
@@ -852,9 +852,9 @@
- **ndr:embed** - **ndr:embed**
- **ndr:embed:base** - **ndr:embed:base**
- **NDTV** - **NDTV**
- **Nebula**: [<abbr title="netrc machine"><em>watchnebula</em></abbr>] - **Nebula**: [*watchnebula*](## "netrc machine")
- **nebula:channel**: [<abbr title="netrc machine"><em>watchnebula</em></abbr>] - **nebula:channel**: [*watchnebula*](## "netrc machine")
- **nebula:subscriptions**: [<abbr title="netrc machine"><em>watchnebula</em></abbr>] - **nebula:subscriptions**: [*watchnebula*](## "netrc machine")
- **NerdCubedFeed** - **NerdCubedFeed**
- **netease:album**: 网易云音乐 - 专辑 - **netease:album**: 网易云音乐 - 专辑
- **netease:djradio**: 网易云音乐 - 电台 - **netease:djradio**: 网易云音乐 - 电台
@@ -863,9 +863,9 @@
- **netease:program**: 网易云音乐 - 电台节目 - **netease:program**: 网易云音乐 - 电台节目
- **netease:singer**: 网易云音乐 - 歌手 - **netease:singer**: 网易云音乐 - 歌手
- **netease:song**: 网易云音乐 - **netease:song**: 网易云音乐
- **NetPlusTV**: [<abbr title="netrc machine"><em>netplus</em></abbr>] - **NetPlusTV**: [*netplus*](## "netrc machine")
- **NetPlusTVLive**: [<abbr title="netrc machine"><em>netplus</em></abbr>] - **NetPlusTVLive**: [*netplus*](## "netrc machine")
- **NetPlusTVRecordings**: [<abbr title="netrc machine"><em>netplus</em></abbr>] - **NetPlusTVRecordings**: [*netplus*](## "netrc machine")
- **Netverse** - **Netverse**
- **NetversePlaylist** - **NetversePlaylist**
- **NetverseSearch**: "netsearch:" prefix - **NetverseSearch**: "netsearch:" prefix
@@ -898,7 +898,7 @@
- **nickelodeon:br** - **nickelodeon:br**
- **nickelodeonru** - **nickelodeonru**
- **nicknight** - **nicknight**
- **niconico**: [<abbr title="netrc machine"><em>niconico</em></abbr>] ニコニコ動画 - **niconico**: [*niconico*](## "netrc machine") ニコニコ動画
- **niconico:history**: NicoNico user history or likes. Requires cookies. - **niconico:history**: NicoNico user history or likes. Requires cookies.
- **niconico:playlist** - **niconico:playlist**
- **niconico:series** - **niconico:series**
@@ -911,7 +911,7 @@
- **Nitter** - **Nitter**
- **njoy**: N-JOY - **njoy**: N-JOY
- **njoy:embed** - **njoy:embed**
- **NJPWWorld**: [<abbr title="netrc machine"><em>njpwworld</em></abbr>] 新日本プロレスワールド - **NJPWWorld**: [*njpwworld*](## "netrc machine") 新日本プロレスワールド
- **NobelPrize** - **NobelPrize**
- **NoicePodcast** - **NoicePodcast**
- **NonkTube** - **NonkTube**
@@ -980,11 +980,11 @@
- **orf:iptv**: iptv.ORF.at - **orf:iptv**: iptv.ORF.at
- **orf:radio** - **orf:radio**
- **orf:tvthek**: ORF TVthek - **orf:tvthek**: ORF TVthek
- **OsnatelTV**: [<abbr title="netrc machine"><em>osnateltv</em></abbr>] - **OsnatelTV**: [*osnateltv*](## "netrc machine")
- **OsnatelTVLive**: [<abbr title="netrc machine"><em>osnateltv</em></abbr>] - **OsnatelTVLive**: [*osnateltv*](## "netrc machine")
- **OsnatelTVRecordings**: [<abbr title="netrc machine"><em>osnateltv</em></abbr>] - **OsnatelTVRecordings**: [*osnateltv*](## "netrc machine")
- **OutsideTV** - **OutsideTV**
- **PacktPub**: [<abbr title="netrc machine"><em>packtpub</em></abbr>] - **PacktPub**: [*packtpub*](## "netrc machine")
- **PacktPubCourse** - **PacktPubCourse**
- **PalcoMP3:artist** - **PalcoMP3:artist**
- **PalcoMP3:song** - **PalcoMP3:song**
@@ -1007,7 +1007,7 @@
- **peer.tv** - **peer.tv**
- **PeerTube** - **PeerTube**
- **PeerTube:Playlist** - **PeerTube:Playlist**
- **peloton**: [<abbr title="netrc machine"><em>peloton</em></abbr>] - **peloton**: [*peloton*](## "netrc machine")
- **peloton:live**: Peloton Live - **peloton:live**: Peloton Live
- **People** - **People**
- **PerformGroup** - **PerformGroup**
@@ -1016,7 +1016,7 @@
- **PhilharmonieDeParis**: Philharmonie de Paris - **PhilharmonieDeParis**: Philharmonie de Paris
- **phoenix.de** - **phoenix.de**
- **Photobucket** - **Photobucket**
- **Piapro**: [<abbr title="netrc machine"><em>piapro</em></abbr>] - **Piapro**: [*piapro*](## "netrc machine")
- **Picarto** - **Picarto**
- **PicartoVod** - **PicartoVod**
- **Piksel** - **Piksel**
@@ -1027,11 +1027,11 @@
- **pixiv:sketch:user** - **pixiv:sketch:user**
- **Pladform** - **Pladform**
- **PlanetMarathi** - **PlanetMarathi**
- **Platzi**: [<abbr title="netrc machine"><em>platzi</em></abbr>] - **Platzi**: [*platzi*](## "netrc machine")
- **PlatziCourse**: [<abbr title="netrc machine"><em>platzi</em></abbr>] - **PlatziCourse**: [*platzi*](## "netrc machine")
- **play.fm** - **play.fm**
- **player.sky.it** - **player.sky.it**
- **PlayPlusTV**: [<abbr title="netrc machine"><em>playplustv</em></abbr>] - **PlayPlusTV**: [*playplustv*](## "netrc machine")
- **PlayStuff** - **PlayStuff**
- **PlaysTV** - **PlaysTV**
- **PlaySuisse** - **PlaySuisse**
@@ -1039,7 +1039,7 @@
- **Playvid** - **Playvid**
- **PlayVids** - **PlayVids**
- **Playwire** - **Playwire**
- **pluralsight**: [<abbr title="netrc machine"><em>pluralsight</em></abbr>] - **pluralsight**: [*pluralsight*](## "netrc machine")
- **pluralsight:course** - **pluralsight:course**
- **PlutoTV** - **PlutoTV**
- **PodbayFM** - **PodbayFM**
@@ -1048,8 +1048,8 @@
- **podomatic** - **podomatic**
- **Pokemon** - **Pokemon**
- **PokemonWatch** - **PokemonWatch**
- **PokerGo**: [<abbr title="netrc machine"><em>pokergo</em></abbr>] - **PokerGo**: [*pokergo*](## "netrc machine")
- **PokerGoCollection**: [<abbr title="netrc machine"><em>pokergo</em></abbr>] - **PokerGoCollection**: [*pokergo*](## "netrc machine")
- **PolsatGo** - **PolsatGo**
- **PolskieRadio** - **PolskieRadio**
- **polskieradio:audition** - **polskieradio:audition**
@@ -1066,11 +1066,11 @@
- **Pornez** - **Pornez**
- **PornFlip** - **PornFlip**
- **PornHd** - **PornHd**
- **PornHub**: [<abbr title="netrc machine"><em>pornhub</em></abbr>] PornHub and Thumbzilla - **PornHub**: [*pornhub*](## "netrc machine") PornHub and Thumbzilla
- **PornHubPagedVideoList**: [<abbr title="netrc machine"><em>pornhub</em></abbr>] - **PornHubPagedVideoList**: [*pornhub*](## "netrc machine")
- **PornHubPlaylist**: [<abbr title="netrc machine"><em>pornhub</em></abbr>] - **PornHubPlaylist**: [*pornhub*](## "netrc machine")
- **PornHubUser**: [<abbr title="netrc machine"><em>pornhub</em></abbr>] - **PornHubUser**: [*pornhub*](## "netrc machine")
- **PornHubUserVideosUpload**: [<abbr title="netrc machine"><em>pornhub</em></abbr>] - **PornHubUserVideosUpload**: [*pornhub*](## "netrc machine")
- **Pornotube** - **Pornotube**
- **PornoVoisines** - **PornoVoisines**
- **PornoXO** - **PornoXO**
@@ -1098,9 +1098,9 @@
- **qqmusic:playlist**: QQ音乐 - 歌单 - **qqmusic:playlist**: QQ音乐 - 歌单
- **qqmusic:singer**: QQ音乐 - 歌手 - **qqmusic:singer**: QQ音乐 - 歌手
- **qqmusic:toplist**: QQ音乐 - 排行榜 - **qqmusic:toplist**: QQ音乐 - 排行榜
- **QuantumTV**: [<abbr title="netrc machine"><em>quantumtv</em></abbr>] - **QuantumTV**: [*quantumtv*](## "netrc machine")
- **QuantumTVLive**: [<abbr title="netrc machine"><em>quantumtv</em></abbr>] - **QuantumTVLive**: [*quantumtv*](## "netrc machine")
- **QuantumTVRecordings**: [<abbr title="netrc machine"><em>quantumtv</em></abbr>] - **QuantumTVRecordings**: [*quantumtv*](## "netrc machine")
- **Qub** - **Qub**
- **R7** - **R7**
- **R7Article** - **R7Article**
@@ -1157,16 +1157,16 @@
- **RICE** - **RICE**
- **RMCDecouverte** - **RMCDecouverte**
- **RockstarGames** - **RockstarGames**
- **Rokfin**: [<abbr title="netrc machine"><em>rokfin</em></abbr>] - **Rokfin**: [*rokfin*](## "netrc machine")
- **rokfin:channel**: Rokfin Channels - **rokfin:channel**: Rokfin Channels
- **rokfin:search**: Rokfin Search; "rkfnsearch:" prefix - **rokfin:search**: Rokfin Search; "rkfnsearch:" prefix
- **rokfin:stack**: Rokfin Stacks - **rokfin:stack**: Rokfin Stacks
- **RoosterTeeth**: [<abbr title="netrc machine"><em>roosterteeth</em></abbr>] - **RoosterTeeth**: [*roosterteeth*](## "netrc machine")
- **RoosterTeethSeries**: [<abbr title="netrc machine"><em>roosterteeth</em></abbr>] - **RoosterTeethSeries**: [*roosterteeth*](## "netrc machine")
- **RottenTomatoes** - **RottenTomatoes**
- **Rozhlas** - **Rozhlas**
- **RozhlasVltava** - **RozhlasVltava**
- **RTBF**: [<abbr title="netrc machine"><em>rtbf</em></abbr>] - **RTBF**: [*rtbf*](## "netrc machine")
- **RTDocumentry** - **RTDocumentry**
- **RTDocumentryPlaylist** - **RTDocumentryPlaylist**
- **rte**: Raidió Teilifís Éireann TV - **rte**: Raidió Teilifís Éireann TV
@@ -1208,16 +1208,16 @@
- **Ruutu** - **Ruutu**
- **Ruv** - **Ruv**
- **ruv.is:spila** - **ruv.is:spila**
- **safari**: [<abbr title="netrc machine"><em>safari</em></abbr>] safaribooksonline.com online video - **safari**: [*safari*](## "netrc machine") safaribooksonline.com online video
- **safari:api**: [<abbr title="netrc machine"><em>safari</em></abbr>] - **safari:api**: [*safari*](## "netrc machine")
- **safari:course**: [<abbr title="netrc machine"><em>safari</em></abbr>] safaribooksonline.com online courses - **safari:course**: [*safari*](## "netrc machine") safaribooksonline.com online courses
- **Saitosan** - **Saitosan**
- **SAKTV**: [<abbr title="netrc machine"><em>saktv</em></abbr>] - **SAKTV**: [*saktv*](## "netrc machine")
- **SAKTVLive**: [<abbr title="netrc machine"><em>saktv</em></abbr>] - **SAKTVLive**: [*saktv*](## "netrc machine")
- **SAKTVRecordings**: [<abbr title="netrc machine"><em>saktv</em></abbr>] - **SAKTVRecordings**: [*saktv*](## "netrc machine")
- **SaltTV**: [<abbr title="netrc machine"><em>salttv</em></abbr>] - **SaltTV**: [*salttv*](## "netrc machine")
- **SaltTVLive**: [<abbr title="netrc machine"><em>salttv</em></abbr>] - **SaltTVLive**: [*salttv*](## "netrc machine")
- **SaltTVRecordings**: [<abbr title="netrc machine"><em>salttv</em></abbr>] - **SaltTVRecordings**: [*salttv*](## "netrc machine")
- **SampleFocus** - **SampleFocus**
- **Sangiin**: 参議院インターネット審議中継 (archive) - **Sangiin**: 参議院インターネット審議中継 (archive)
- **Sapo**: SAPO Vídeos - **Sapo**: SAPO Vídeos
@@ -1233,8 +1233,8 @@
- **ScrippsNetworks** - **ScrippsNetworks**
- **scrippsnetworks:watch** - **scrippsnetworks:watch**
- **Scrolller** - **Scrolller**
- **SCTE**: [<abbr title="netrc machine"><em>scte</em></abbr>] - **SCTE**: [*scte*](## "netrc machine")
- **SCTECourse**: [<abbr title="netrc machine"><em>scte</em></abbr>] - **SCTECourse**: [*scte*](## "netrc machine")
- **Seeker** - **Seeker**
- **SenateGov** - **SenateGov**
- **SenateISVP** - **SenateISVP**
@@ -1243,7 +1243,7 @@
- **Sexu** - **Sexu**
- **SeznamZpravy** - **SeznamZpravy**
- **SeznamZpravyArticle** - **SeznamZpravyArticle**
- **Shahid**: [<abbr title="netrc machine"><em>shahid</em></abbr>] - **Shahid**: [*shahid*](## "netrc machine")
- **ShahidShow** - **ShahidShow**
- **Shared**: shared.sx - **Shared**: shared.sx
- **ShareVideosEmbed** - **ShareVideosEmbed**
@@ -1273,16 +1273,16 @@
- **Smotrim** - **Smotrim**
- **Snotr** - **Snotr**
- **Sohu** - **Sohu**
- **SonyLIV**: [<abbr title="netrc machine"><em>sonyliv</em></abbr>] - **SonyLIV**: [*sonyliv*](## "netrc machine")
- **SonyLIVSeries** - **SonyLIVSeries**
- **soundcloud**: [<abbr title="netrc machine"><em>soundcloud</em></abbr>] - **soundcloud**: [*soundcloud*](## "netrc machine")
- **soundcloud:playlist**: [<abbr title="netrc machine"><em>soundcloud</em></abbr>] - **soundcloud:playlist**: [*soundcloud*](## "netrc machine")
- **soundcloud:related**: [<abbr title="netrc machine"><em>soundcloud</em></abbr>] - **soundcloud:related**: [*soundcloud*](## "netrc machine")
- **soundcloud:search**: [<abbr title="netrc machine"><em>soundcloud</em></abbr>] Soundcloud search; "scsearch:" prefix - **soundcloud:search**: [*soundcloud*](## "netrc machine") Soundcloud search; "scsearch:" prefix
- **soundcloud:set**: [<abbr title="netrc machine"><em>soundcloud</em></abbr>] - **soundcloud:set**: [*soundcloud*](## "netrc machine")
- **soundcloud:trackstation**: [<abbr title="netrc machine"><em>soundcloud</em></abbr>] - **soundcloud:trackstation**: [*soundcloud*](## "netrc machine")
- **soundcloud:user**: [<abbr title="netrc machine"><em>soundcloud</em></abbr>] - **soundcloud:user**: [*soundcloud*](## "netrc machine")
- **soundcloud:user:permalink**: [<abbr title="netrc machine"><em>soundcloud</em></abbr>] - **soundcloud:user:permalink**: [*soundcloud*](## "netrc machine")
- **SoundcloudEmbed** - **SoundcloudEmbed**
- **soundgasm** - **soundgasm**
- **soundgasm:profile** - **soundgasm:profile**
@@ -1349,13 +1349,13 @@
- **Tass** - **Tass**
- **TBS** - **TBS**
- **TDSLifeway** - **TDSLifeway**
- **Teachable**: [<abbr title="netrc machine"><em>teachable</em></abbr>] - **Teachable**: [*teachable*](## "netrc machine")
- **TeachableCourse**: [<abbr title="netrc machine"><em>teachable</em></abbr>] - **TeachableCourse**: [*teachable*](## "netrc machine")
- **teachertube**: teachertube.com videos - **teachertube**: teachertube.com videos
- **teachertube:user:collection**: teachertube.com user and collection videos - **teachertube:user:collection**: teachertube.com user and collection videos
- **TeachingChannel** - **TeachingChannel**
- **Teamcoco** - **Teamcoco**
- **TeamTreeHouse**: [<abbr title="netrc machine"><em>teamtreehouse</em></abbr>] - **TeamTreeHouse**: [*teamtreehouse*](## "netrc machine")
- **TechTalks** - **TechTalks**
- **techtv.mit.edu** - **techtv.mit.edu**
- **TedEmbed** - **TedEmbed**
@@ -1378,8 +1378,8 @@
- **TeleTask** - **TeleTask**
- **Telewebion** - **Telewebion**
- **Tempo** - **Tempo**
- **TennisTV**: [<abbr title="netrc machine"><em>tennistv</em></abbr>] - **TennisTV**: [*tennistv*](## "netrc machine")
- **TenPlay**: [<abbr title="netrc machine"><em>10play</em></abbr>] - **TenPlay**: [*10play*](## "netrc machine")
- **TF1** - **TF1**
- **TFO** - **TFO**
- **TheHoleTv** - **TheHoleTv**
@@ -1417,13 +1417,13 @@
- **tokfm:audition** - **tokfm:audition**
- **tokfm:podcast** - **tokfm:podcast**
- **ToonGoggles** - **ToonGoggles**
- **tou.tv**: [<abbr title="netrc machine"><em>toutv</em></abbr>] - **tou.tv**: [*toutv*](## "netrc machine")
- **Toypics**: Toypics video - **Toypics**: Toypics video
- **ToypicsUser**: Toypics user profile - **ToypicsUser**: Toypics user profile
- **TrailerAddict**: (**Currently broken**) - **TrailerAddict**: (**Currently broken**)
- **TravelChannel** - **TravelChannel**
- **Triller**: [<abbr title="netrc machine"><em>triller</em></abbr>] - **Triller**: [*triller*](## "netrc machine")
- **TrillerUser**: [<abbr title="netrc machine"><em>triller</em></abbr>] - **TrillerUser**: [*triller*](## "netrc machine")
- **Trilulilu** - **Trilulilu**
- **Trovo** - **Trovo**
- **TrovoChannelClip**: All Clips of a trovo.live channel; "trovoclip:" prefix - **TrovoChannelClip**: All Clips of a trovo.live channel; "trovoclip:" prefix
@@ -1435,11 +1435,11 @@
- **Truth** - **Truth**
- **TruTV** - **TruTV**
- **Tube8** - **Tube8**
- **TubeTuGraz**: [<abbr title="netrc machine"><em>tubetugraz</em></abbr>] tube.tugraz.at - **TubeTuGraz**: [*tubetugraz*](## "netrc machine") tube.tugraz.at
- **TubeTuGrazSeries**: [<abbr title="netrc machine"><em>tubetugraz</em></abbr>] - **TubeTuGrazSeries**: [*tubetugraz*](## "netrc machine")
- **TubiTv**: [<abbr title="netrc machine"><em>tubitv</em></abbr>] - **TubiTv**: [*tubitv*](## "netrc machine")
- **TubiTvShow** - **TubiTvShow**
- **Tumblr**: [<abbr title="netrc machine"><em>tumblr</em></abbr>] - **Tumblr**: [*tumblr*](## "netrc machine")
- **tunein:clip** - **tunein:clip**
- **tunein:program** - **tunein:program**
- **tunein:station** - **tunein:station**
@@ -1489,13 +1489,13 @@
- **TwitCasting** - **TwitCasting**
- **TwitCastingLive** - **TwitCastingLive**
- **TwitCastingUser** - **TwitCastingUser**
- **twitch:clips**: [<abbr title="netrc machine"><em>twitch</em></abbr>] - **twitch:clips**: [*twitch*](## "netrc machine")
- **twitch:stream**: [<abbr title="netrc machine"><em>twitch</em></abbr>] - **twitch:stream**: [*twitch*](## "netrc machine")
- **twitch:vod**: [<abbr title="netrc machine"><em>twitch</em></abbr>] - **twitch:vod**: [*twitch*](## "netrc machine")
- **TwitchCollection**: [<abbr title="netrc machine"><em>twitch</em></abbr>] - **TwitchCollection**: [*twitch*](## "netrc machine")
- **TwitchVideos**: [<abbr title="netrc machine"><em>twitch</em></abbr>] - **TwitchVideos**: [*twitch*](## "netrc machine")
- **TwitchVideosClips**: [<abbr title="netrc machine"><em>twitch</em></abbr>] - **TwitchVideosClips**: [*twitch*](## "netrc machine")
- **TwitchVideosCollections**: [<abbr title="netrc machine"><em>twitch</em></abbr>] - **TwitchVideosCollections**: [*twitch*](## "netrc machine")
- **twitter** - **twitter**
- **twitter:amplify** - **twitter:amplify**
- **twitter:broadcast** - **twitter:broadcast**
@@ -1503,11 +1503,11 @@
- **twitter:shortener** - **twitter:shortener**
- **twitter:spaces** - **twitter:spaces**
- **Txxx** - **Txxx**
- **udemy**: [<abbr title="netrc machine"><em>udemy</em></abbr>] - **udemy**: [*udemy*](## "netrc machine")
- **udemy:course**: [<abbr title="netrc machine"><em>udemy</em></abbr>] - **udemy:course**: [*udemy*](## "netrc machine")
- **UDNEmbed**: 聯合影音 - **UDNEmbed**: 聯合影音
- **UFCArabia**: [<abbr title="netrc machine"><em>ufcarabia</em></abbr>] - **UFCArabia**: [*ufcarabia*](## "netrc machine")
- **UFCTV**: [<abbr title="netrc machine"><em>ufctv</em></abbr>] - **UFCTV**: [*ufctv*](## "netrc machine")
- **ukcolumn** - **ukcolumn**
- **UKTVPlay** - **UKTVPlay**
- **umg:de**: Universal Music Deutschland - **umg:de**: Universal Music Deutschland
@@ -1537,7 +1537,7 @@
- **VevoPlaylist** - **VevoPlaylist**
- **VGTV**: VGTV, BTTV, FTV, Aftenposten and Aftonbladet - **VGTV**: VGTV, BTTV, FTV, Aftenposten and Aftonbladet
- **vh1.com** - **vh1.com**
- **vhx:embed**: [<abbr title="netrc machine"><em>vimeo</em></abbr>] - **vhx:embed**: [*vimeo*](## "netrc machine")
- **Viafree** - **Viafree**
- **vice** - **vice**
- **vice:article** - **vice:article**
@@ -1560,25 +1560,25 @@
- **videomore:season** - **videomore:season**
- **videomore:video** - **videomore:video**
- **VideoPress** - **VideoPress**
- **Vidio**: [<abbr title="netrc machine"><em>vidio</em></abbr>] - **Vidio**: [*vidio*](## "netrc machine")
- **VidioLive**: [<abbr title="netrc machine"><em>vidio</em></abbr>] - **VidioLive**: [*vidio*](## "netrc machine")
- **VidioPremier**: [<abbr title="netrc machine"><em>vidio</em></abbr>] - **VidioPremier**: [*vidio*](## "netrc machine")
- **VidLii** - **VidLii**
- **viewlift** - **viewlift**
- **viewlift:embed** - **viewlift:embed**
- **Viidea** - **Viidea**
- **viki**: [<abbr title="netrc machine"><em>viki</em></abbr>] - **viki**: [*viki*](## "netrc machine")
- **viki:channel**: [<abbr title="netrc machine"><em>viki</em></abbr>] - **viki:channel**: [*viki*](## "netrc machine")
- **vimeo**: [<abbr title="netrc machine"><em>vimeo</em></abbr>] - **vimeo**: [*vimeo*](## "netrc machine")
- **vimeo:album**: [<abbr title="netrc machine"><em>vimeo</em></abbr>] - **vimeo:album**: [*vimeo*](## "netrc machine")
- **vimeo:channel**: [<abbr title="netrc machine"><em>vimeo</em></abbr>] - **vimeo:channel**: [*vimeo*](## "netrc machine")
- **vimeo:group**: [<abbr title="netrc machine"><em>vimeo</em></abbr>] - **vimeo:group**: [*vimeo*](## "netrc machine")
- **vimeo:likes**: [<abbr title="netrc machine"><em>vimeo</em></abbr>] Vimeo user likes - **vimeo:likes**: [*vimeo*](## "netrc machine") Vimeo user likes
- **vimeo:ondemand**: [<abbr title="netrc machine"><em>vimeo</em></abbr>] - **vimeo:ondemand**: [*vimeo*](## "netrc machine")
- **vimeo:pro**: [<abbr title="netrc machine"><em>vimeo</em></abbr>] - **vimeo:pro**: [*vimeo*](## "netrc machine")
- **vimeo:review**: [<abbr title="netrc machine"><em>vimeo</em></abbr>] Review pages on vimeo - **vimeo:review**: [*vimeo*](## "netrc machine") Review pages on vimeo
- **vimeo:user**: [<abbr title="netrc machine"><em>vimeo</em></abbr>] - **vimeo:user**: [*vimeo*](## "netrc machine")
- **vimeo:watchlater**: [<abbr title="netrc machine"><em>vimeo</em></abbr>] Vimeo watch later list, ":vimeowatchlater" keyword (requires authentication) - **vimeo:watchlater**: [*vimeo*](## "netrc machine") Vimeo watch later list, ":vimeowatchlater" keyword (requires authentication)
- **Vimm:recording** - **Vimm:recording**
- **Vimm:stream** - **Vimm:stream**
- **ViMP** - **ViMP**
@@ -1588,13 +1588,13 @@
- **vine:user** - **vine:user**
- **Viqeo** - **Viqeo**
- **Viu** - **Viu**
- **viu:ott**: [<abbr title="netrc machine"><em>viu</em></abbr>] - **viu:ott**: [*viu*](## "netrc machine")
- **viu:playlist** - **viu:playlist**
- **ViuOTTIndonesia** - **ViuOTTIndonesia**
- **Vivo**: vivo.sx - **Vivo**: vivo.sx
- **vk**: [<abbr title="netrc machine"><em>vk</em></abbr>] VK - **vk**: [*vk*](## "netrc machine") VK
- **vk:uservideos**: [<abbr title="netrc machine"><em>vk</em></abbr>] VK - User's Videos - **vk:uservideos**: [*vk*](## "netrc machine") VK - User's Videos
- **vk:wallpost**: [<abbr title="netrc machine"><em>vk</em></abbr>] - **vk:wallpost**: [*vk*](## "netrc machine")
- **vm.tiktok** - **vm.tiktok**
- **Vocaroo** - **Vocaroo**
- **Vodlocker** - **Vodlocker**
@@ -1613,14 +1613,14 @@
- **vqq:video** - **vqq:video**
- **Vrak** - **Vrak**
- **VRT**: VRT NWS, Flanders News, Flandern Info and Sporza - **VRT**: VRT NWS, Flanders News, Flandern Info and Sporza
- **VrtNU**: [<abbr title="netrc machine"><em>vrtnu</em></abbr>] VrtNU.be - **VrtNU**: [*vrtnu*](## "netrc machine") VrtNU.be
- **vrv**: [<abbr title="netrc machine"><em>vrv</em></abbr>] - **vrv**: [*vrv*](## "netrc machine")
- **vrv:series** - **vrv:series**
- **VShare** - **VShare**
- **VTM** - **VTM**
- **VTXTV**: [<abbr title="netrc machine"><em>vtxtv</em></abbr>] - **VTXTV**: [*vtxtv*](## "netrc machine")
- **VTXTVLive**: [<abbr title="netrc machine"><em>vtxtv</em></abbr>] - **VTXTVLive**: [*vtxtv*](## "netrc machine")
- **VTXTVRecordings**: [<abbr title="netrc machine"><em>vtxtv</em></abbr>] - **VTXTVRecordings**: [*vtxtv*](## "netrc machine")
- **VuClip** - **VuClip**
- **Vupload** - **Vupload**
- **VVVVID** - **VVVVID**
@@ -1629,9 +1629,9 @@
- **Vzaar** - **Vzaar**
- **Wakanim** - **Wakanim**
- **Walla** - **Walla**
- **WalyTV**: [<abbr title="netrc machine"><em>walytv</em></abbr>] - **WalyTV**: [*walytv*](## "netrc machine")
- **WalyTVLive**: [<abbr title="netrc machine"><em>walytv</em></abbr>] - **WalyTVLive**: [*walytv*](## "netrc machine")
- **WalyTVRecordings**: [<abbr title="netrc machine"><em>walytv</em></abbr>] - **WalyTVRecordings**: [*walytv*](## "netrc machine")
- **wasdtv:clip** - **wasdtv:clip**
- **wasdtv:record** - **wasdtv:record**
- **wasdtv:stream** - **wasdtv:stream**
@@ -1743,13 +1743,13 @@
- **YoutubeLivestreamEmbed**: YouTube livestream embeds - **YoutubeLivestreamEmbed**: YouTube livestream embeds
- **YoutubeYtBe**: youtu.be - **YoutubeYtBe**: youtu.be
- **Zapiks** - **Zapiks**
- **Zattoo**: [<abbr title="netrc machine"><em>zattoo</em></abbr>] - **Zattoo**: [*zattoo*](## "netrc machine")
- **ZattooLive**: [<abbr title="netrc machine"><em>zattoo</em></abbr>] - **ZattooLive**: [*zattoo*](## "netrc machine")
- **ZattooMovies**: [<abbr title="netrc machine"><em>zattoo</em></abbr>] - **ZattooMovies**: [*zattoo*](## "netrc machine")
- **ZattooRecordings**: [<abbr title="netrc machine"><em>zattoo</em></abbr>] - **ZattooRecordings**: [*zattoo*](## "netrc machine")
- **ZDF** - **ZDF**
- **ZDFChannel** - **ZDFChannel**
- **Zee5**: [<abbr title="netrc machine"><em>zee5</em></abbr>] - **Zee5**: [*zee5*](## "netrc machine")
- **zee5:series** - **zee5:series**
- **ZeeNews** - **ZeeNews**
- **ZenYandex** - **ZenYandex**

View File

@@ -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' 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)) decrypted = intlist_to_bytes(aes_cbc_decrypt(bytes_to_intlist(data), self.key, self.iv))
self.assertEqual(decrypted.rstrip(b'\x08'), self.secret_msg) 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)) 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) 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( decrypted = intlist_to_bytes(aes_gcm_decrypt_and_verify(
bytes_to_intlist(data), self.key, bytes_to_intlist(authentication_tag), self.iv[:12])) bytes_to_intlist(data), self.key, bytes_to_intlist(authentication_tag), self.iv[:12]))
self.assertEqual(decrypted.rstrip(b'\x08'), self.secret_msg) self.assertEqual(decrypted.rstrip(b'\x08'), self.secret_msg)
if Cryptodome: if Cryptodome.AES:
decrypted = aes_gcm_decrypt_and_verify_bytes( decrypted = aes_gcm_decrypt_and_verify_bytes(
data, intlist_to_bytes(self.key), authentication_tag, intlist_to_bytes(self.iv[:12])) data, intlist_to_bytes(self.key), authentication_tag, intlist_to_bytes(self.iv[:12]))
self.assertEqual(decrypted.rstrip(b'\x08'), self.secret_msg) self.assertEqual(decrypted.rstrip(b'\x08'), self.secret_msg)

View File

@@ -66,6 +66,10 @@ _SIG_TESTS = [
] ]
_NSIG_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', 'https://www.youtube.com/s/player/9216d1f7/player_ias.vflset/en_US/base.js',
'SLp9F5bwjAdhE9F-', 'gWnb9IK2DJ8Q1w', 'SLp9F5bwjAdhE9F-', 'gWnb9IK2DJ8Q1w',

View File

@@ -150,7 +150,7 @@ from .utils import (
write_json_file, write_json_file,
write_string, write_string,
) )
from .version import RELEASE_GIT_HEAD, VARIANT, __version__ from .version import CHANNEL, RELEASE_GIT_HEAD, VARIANT, __version__
if compat_os_name == 'nt': if compat_os_name == 'nt':
import ctypes import ctypes
@@ -300,8 +300,6 @@ class YoutubeDL:
Videos already present in the file are not downloaded again. Videos already present in the file are not downloaded again.
break_on_existing: Stop the download process after attempting to download a break_on_existing: Stop the download process after attempting to download a
file that is in the archive. 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 break_per_url: Whether break_on_reject and break_on_existing
should act on each input URL as opposed to for the entire queue 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 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 None, the video is downloaded.
- If it returns utils.NO_DEFAULT, the user is interactively - If it returns utils.NO_DEFAULT, the user is interactively
asked whether to download the video. 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. match_filter_func in utils.py is one example for this.
no_color: Do not emit color codes in output. no_color: Do not emit color codes in output.
geo_bypass: Bypass geographic restriction via faking X-Forwarded-For 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: 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 force_generic_extractor: Force downloader to use the generic extractor
- Use allowed_extractors = ['generic', 'default'] - Use allowed_extractors = ['generic', 'default']
playliststart: - Use playlist_items playliststart: - Use playlist_items
@@ -614,7 +617,7 @@ class YoutubeDL:
'\n You will no longer receive updates on this version') '\n You will no longer receive updates on this version')
if current_version < MIN_SUPPORTED: if current_version < MIN_SUPPORTED:
msg = 'Python version %d.%d is no longer 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)) f'{msg}! Please update to Python %d.%d or above' % (*current_version, *MIN_RECOMMENDED))
if self.params.get('allow_unplayable_formats'): if self.params.get('allow_unplayable_formats'):
@@ -1407,31 +1410,44 @@ class YoutubeDL:
return 'Skipping "%s" because it is age restricted' % video_title return 'Skipping "%s" because it is age restricted' % video_title
match_filter = self.params.get('match_filter') match_filter = self.params.get('match_filter')
if match_filter is not None: if match_filter is None:
return None
cancelled = None
try:
try: try:
ret = match_filter(info_dict, incomplete=incomplete) ret = match_filter(info_dict, incomplete=incomplete)
except TypeError: except TypeError:
# For backward compatibility # For backward compatibility
ret = None if incomplete else match_filter(info_dict) ret = None if incomplete else match_filter(info_dict)
if ret is NO_DEFAULT: except DownloadCancelled as err:
while True: if err.msg is not NO_DEFAULT:
filename = self._format_screen(self.prepare_filename(info_dict), self.Styles.FILENAME) raise
reply = input(self._format_screen( ret, cancelled = err.msg, err
f'Download "{filename}"? (Y/n): ', self.Styles.EMPHASIS)).lower().strip()
if reply in {'y', ''}: if ret is NO_DEFAULT:
return None while True:
elif reply == 'n': filename = self._format_screen(self.prepare_filename(info_dict), self.Styles.FILENAME)
return f'Skipping {video_title}' reply = input(self._format_screen(
elif ret is not None: f'Download "{filename}"? (Y/n): ', self.Styles.EMPHASIS)).lower().strip()
return ret if reply in {'y', ''}:
return None 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): if self.in_download_archive(info_dict):
reason = '%s has already been recorded in the archive' % video_title reason = '%s has already been recorded in the archive' % video_title
break_opt, break_err = 'break_on_existing', ExistingVideoReached break_opt, break_err = 'break_on_existing', ExistingVideoReached
else: else:
reason = check_filter() try:
break_opt, break_err = 'break_on_reject', RejectedVideoReached 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 reason is not None:
if not silent: if not silent:
self.to_screen('[download] ' + reason) self.to_screen('[download] ' + reason)
@@ -3768,8 +3784,8 @@ class YoutubeDL:
klass = type(self) klass = type(self)
write_debug(join_nonempty( write_debug(join_nonempty(
f'{"yt-dlp" if REPOSITORY == "yt-dlp/yt-dlp" else REPOSITORY} version', f'{"yt-dlp" if REPOSITORY == "yt-dlp/yt-dlp" else REPOSITORY} version',
__version__, __version__ + {'stable': '', 'nightly': '*'}.get(CHANNEL, f' <{CHANNEL}>'),
f'[{RELEASE_GIT_HEAD}]' if RELEASE_GIT_HEAD else '', f'[{RELEASE_GIT_HEAD[:9]}]' if RELEASE_GIT_HEAD else '',
'' if source == 'unknown' else f'({source})', '' if source == 'unknown' else f'({source})',
'' if _IN_CLI else 'API' if klass == YoutubeDL else f'API:{self.__module__}.{klass.__qualname__}', '' if _IN_CLI else 'API' if klass == YoutubeDL else f'API:{self.__module__}.{klass.__qualname__}',
delim=' ')) delim=' '))

View File

@@ -403,7 +403,7 @@ def validate_options(opts):
except Exception: except Exception:
raise ValueError('unsupported geo-bypass country or ip-block') 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: if opts.download_archive is not None:
opts.download_archive = expand_path(opts.download_archive) opts.download_archive = expand_path(opts.download_archive)
@@ -931,7 +931,7 @@ def _real_main(argv=None):
if opts.rm_cachedir: if opts.rm_cachedir:
ydl.cache.remove() 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 opts.update_self and updater.update() and actual_use:
if updater.cmd: if updater.cmd:
return updater.restart() return updater.restart()

View File

@@ -1,30 +1,8 @@
import ast
import os
import sys import sys
from pathlib import Path
from PyInstaller.utils.hooks import collect_submodules 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(): def pycryptodome_module():
try: try:
import Cryptodome # noqa: F401 import Cryptodome # noqa: F401
@@ -41,12 +19,8 @@ def pycryptodome_module():
def get_hidden_imports(): def get_hidden_imports():
yield 'yt_dlp.compat._legacy' yield 'yt_dlp.compat._legacy'
yield pycryptodome_module()
yield from collect_submodules('websockets') 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 # These are auto-detected, but explicitly add them just in case
yield from ('mutagen', 'brotli', 'certifi') yield from ('mutagen', 'brotli', 'certifi')

View File

@@ -5,14 +5,14 @@ from .compat import compat_ord
from .dependencies import Cryptodome from .dependencies import Cryptodome
from .utils import bytes_to_intlist, intlist_to_bytes from .utils import bytes_to_intlist, intlist_to_bytes
if Cryptodome: if Cryptodome.AES:
def aes_cbc_decrypt_bytes(data, key, iv): def aes_cbc_decrypt_bytes(data, key, iv):
""" Decrypt bytes with AES-CBC using pycryptodome """ """ 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): def aes_gcm_decrypt_and_verify_bytes(data, key, tag, nonce):
""" Decrypt bytes with AES-GCM using pycryptodome """ """ 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: else:
def aes_cbc_decrypt_bytes(data, key, iv): def aes_cbc_decrypt_bytes(data, key, iv):

View File

@@ -32,9 +32,9 @@ from re import match as compat_Match # noqa: F401
from . import compat_expanduser, compat_HTMLParseError, compat_realpath from . import compat_expanduser, compat_HTMLParseError, compat_realpath
from .compat_utils import passthrough_module 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 brotli as compat_brotli # noqa: F401
from ..dependencies import websockets as compat_websockets # 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')) passthrough_module(__name__, '...utils', ('WINDOWS_VT_MODE', 'windows_enable_vt_mode'))

View File

@@ -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""" """Passthrough parent module into a child module, creating the parent if necessary"""
def __getattr__(attr): def __getattr__(attr):
if _is_package(parent): if _is_package(parent):
with contextlib.suppress(ImportError): with contextlib.suppress(ModuleNotFoundError):
return importlib.import_module(f'.{attr}', parent.__name__) return importlib.import_module(f'.{attr}', parent.__name__)
ret = from_child(attr) ret = from_child(attr)

View File

@@ -1,8 +1,5 @@
import types import types
from ..compat import functools
from ..compat.compat_utils import passthrough_module
try: try:
import Cryptodome as _parent import Cryptodome as _parent
except ImportError: except ImportError:
@@ -12,19 +9,28 @@ except ImportError:
_parent = types.ModuleType('no_Cryptodome') _parent = types.ModuleType('no_Cryptodome')
__bool__ = lambda: False __bool__ = lambda: False
passthrough_module(__name__, _parent, (..., '__version__')) __version__ = ''
del passthrough_module 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 _yt_dlp__identifier = _parent.__name__
@functools.cache if AES and _yt_dlp__identifier == 'Crypto':
def _yt_dlp__identifier(): try:
if _parent.__name__ == 'Crypto': # In pycrypto, mode defaults to ECB. See:
from Crypto.Cipher import AES # https://www.pycryptodome.org/en/latest/src/vs_pycrypto.html#:~:text=not%20have%20ECB%20as%20default%20mode
try: AES.new(b'abcdefghijklmnop')
# In pycrypto, mode defaults to ECB. See: except TypeError:
# https://www.pycryptodome.org/en/latest/src/vs_pycrypto.html#:~:text=not%20have%20ECB%20as%20default%20mode _yt_dlp__identifier = 'pycrypto'
AES.new(b'abcdefghijklmnop')
except TypeError:
return 'pycrypto'
return _parent.__name__

View File

@@ -73,7 +73,7 @@ available_dependencies = {k: v for k, v in all_dependencies.items() if v}
# Deprecated # Deprecated
Cryptodome_AES = Cryptodome.Cipher.AES if Cryptodome else None Cryptodome_AES = Cryptodome.AES
__all__ = [ __all__ = [

View File

@@ -466,7 +466,8 @@ class FragmentFD(FileDownloader):
for retry in RetryManager(self.params.get('fragment_retries'), error_callback): for retry in RetryManager(self.params.get('fragment_retries'), error_callback):
try: try:
ctx['fragment_count'] = fragment.get('fragment_count') 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 return
except (urllib.error.HTTPError, http.client.IncompleteRead) as err: except (urllib.error.HTTPError, http.client.IncompleteRead) as err:
retry.error = err retry.error = err
@@ -496,7 +497,7 @@ class FragmentFD(FileDownloader):
download_fragment(fragment, ctx_copy) download_fragment(fragment, ctx_copy)
return fragment, fragment['frag_index'], ctx_copy.get('fragment_filename_sanitized') 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: with tpe or concurrent.futures.ThreadPoolExecutor(max_workers) as pool:
try: try:
for fragment, frag_index, frag_filename in pool.map(_download_fragment, fragments): for fragment, frag_index, frag_filename in pool.map(_download_fragment, fragments):

View File

@@ -70,7 +70,7 @@ class HlsFD(FragmentFD):
can_download, message = self.can_download(s, info_dict, self.params.get('allow_unplayable_formats')), None can_download, message = self.can_download(s, info_dict, self.params.get('allow_unplayable_formats')), None
if can_download: if can_download:
has_ffmpeg = FFmpegFD.available() 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: if no_crypto and has_ffmpeg:
can_download, message = False, 'The stream has AES-128 encryption and pycryptodomex is not available' can_download, message = False, 'The stream has AES-128 encryption and pycryptodomex is not available'
elif no_crypto: elif no_crypto:

View File

@@ -894,15 +894,15 @@ class BiliIntlBaseIE(InfoExtractor):
} }
def _perform_login(self, username, password): def _perform_login(self, username, password):
if not Cryptodome: if not Cryptodome.RSA:
raise ExtractorError('pycryptodomex not found. Please install', expected=True) raise ExtractorError('pycryptodomex not found. Please install', expected=True)
key_data = self._download_json( key_data = self._download_json(
'https://passport.bilibili.tv/x/intl/passport-login/web/key?lang=en-US', None, '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'] note='Downloading login key', errnote='Unable to download login key')['data']
public_key = Cryptodome.PublicKey.RSA.importKey(key_data['key']) public_key = Cryptodome.RSA.importKey(key_data['key'])
password_hash = Cryptodome.Cipher.PKCS1_v1_5.new(public_key).encrypt((key_data['hash'] + password).encode('utf-8')) password_hash = Cryptodome.PKCS1_v1_5.new(public_key).encrypt((key_data['hash'] + password).encode('utf-8'))
login_post = self._download_json( login_post = self._download_json(
'https://passport.bilibili.tv/x/intl/passport-login/web/login/password?lang=en-US', None, data=urlencode_postdata({ 'https://passport.bilibili.tv/x/intl/passport-login/web/login/password?lang=en-US', None, data=urlencode_postdata({
'username': username, 'username': username,

View File

@@ -132,6 +132,7 @@ class InfoExtractor:
is parsed from a string (in case of is parsed from a string (in case of
fragmented media) fragmented media)
for MSS - URL of the ISM manifest. for MSS - URL of the ISM manifest.
* request_data Data to send in POST request to the URL
* manifest_url * manifest_url
The URL of the manifest file in case of The URL of the manifest file in case of
fragmented media: fragmented media:
@@ -2063,6 +2064,7 @@ class InfoExtractor:
'protocol': entry_protocol, 'protocol': entry_protocol,
'preference': preference, 'preference': preference,
'quality': quality, 'quality': quality,
'has_drm': has_drm,
'vcodec': 'none' if media_type == 'AUDIO' else None, 'vcodec': 'none' if media_type == 'AUDIO' else None,
} for idx in _extract_m3u8_playlist_indices(manifest_url)) } for idx in _extract_m3u8_playlist_indices(manifest_url))
@@ -2122,6 +2124,7 @@ class InfoExtractor:
'protocol': entry_protocol, 'protocol': entry_protocol,
'preference': preference, 'preference': preference,
'quality': quality, 'quality': quality,
'has_drm': has_drm,
} }
resolution = last_stream_inf.get('RESOLUTION') resolution = last_stream_inf.get('RESOLUTION')
if resolution: if resolution:
@@ -3524,7 +3527,7 @@ class InfoExtractor:
desc = '' desc = ''
if cls._NETRC_MACHINE: if cls._NETRC_MACHINE:
if markdown: if markdown:
desc += f' [<abbr title="netrc machine"><em>{cls._NETRC_MACHINE}</em></abbr>]' desc += f' [*{cls._NETRC_MACHINE}*](## "netrc machine")'
else: else:
desc += f' [{cls._NETRC_MACHINE}]' desc += f' [{cls._NETRC_MACHINE}]'
if cls.IE_DESC is False: if cls.IE_DESC is False:

View File

@@ -15,6 +15,7 @@ from ..utils import (
UnsupportedError, UnsupportedError,
determine_ext, determine_ext,
dict_get, dict_get,
extract_basic_auth,
format_field, format_field,
int_or_none, int_or_none,
is_html, is_html,
@@ -2372,9 +2373,8 @@ class GenericIE(InfoExtractor):
**smuggled_data.get('http_headers', {}) **smuggled_data.get('http_headers', {})
}) })
new_url = full_response.geturl() new_url = full_response.geturl()
if new_url == urllib.parse.urlparse(url)._replace(scheme='https').geturl(): url = urllib.parse.urlparse(url)._replace(scheme=urllib.parse.urlparse(new_url).scheme).geturl()
url = new_url if new_url != extract_basic_auth(url)[0]:
elif url != new_url:
self.report_following_redirect(new_url) self.report_following_redirect(new_url)
if force_videoid: if force_videoid:
new_url = smuggle_url(new_url, {'force_videoid': 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') self.report_detected('direct video link')
headers = smuggled_data.get('http_headers', {}) headers = smuggled_data.get('http_headers', {})
format_id = str(m.group('format_id')) format_id = str(m.group('format_id'))
ext = determine_ext(url)
subtitles = {} 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) formats, subtitles = self._extract_m3u8_formats_and_subtitles(url, video_id, 'mp4', headers=headers)
info_dict.update(self._fragment_query(url)) 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) formats, subtitles = self._extract_mpd_formats_and_subtitles(url, video_id, headers=headers)
info_dict.update(self._fragment_query(url)) 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) formats = self._extract_f4m_formats(url, video_id, headers=headers)
else: else:
formats = [{ formats = [{

View File

@@ -3,8 +3,8 @@ import re
from .common import InfoExtractor from .common import InfoExtractor
from ..compat import compat_parse_qs from ..compat import compat_parse_qs
from ..utils import ( from ..utils import (
determine_ext,
ExtractorError, ExtractorError,
determine_ext,
get_element_by_class, get_element_by_class,
int_or_none, int_or_none,
lowercase_escape, lowercase_escape,
@@ -163,15 +163,13 @@ class GoogleDriveIE(InfoExtractor):
video_id = self._match_id(url) video_id = self._match_id(url)
video_info = compat_parse_qs(self._download_webpage( video_info = compat_parse_qs(self._download_webpage(
'https://drive.google.com/get_video_info', '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): def get_value(key):
return try_get(video_info, lambda x: x[key][0]) return try_get(video_info, lambda x: x[key][0])
reason = get_value('reason') reason = get_value('reason')
title = get_value('title') title = get_value('title')
if not title and reason:
raise ExtractorError(reason, expected=True)
formats = [] formats = []
fmt_stream_map = (get_value('fmt_stream_map') or '').split(',') fmt_stream_map = (get_value('fmt_stream_map') or '').split(',')
@@ -216,6 +214,11 @@ class GoogleDriveIE(InfoExtractor):
urlh = request_source_file(source_url, 'source') urlh = request_source_file(source_url, 'source')
if urlh: if urlh:
def add_source_format(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({ formats.append({
# Use redirect URLs as download URLs in order to calculate # Use redirect URLs as download URLs in order to calculate
# correct cookies in _calc_cookies. # correct cookies in _calc_cookies.
@@ -251,7 +254,10 @@ class GoogleDriveIE(InfoExtractor):
or 'unable to extract confirmation code') or 'unable to extract confirmation code')
if not formats and reason: 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') hl = get_value('hl')
subtitles_id = None subtitles_id = None

View File

@@ -7,7 +7,8 @@ from ..utils import (
js_to_json, js_to_json,
urlencode_postdata, urlencode_postdata,
ExtractorError, ExtractorError,
parse_qs parse_qs,
traverse_obj
) )
@@ -15,8 +16,7 @@ class IPrimaIE(InfoExtractor):
_VALID_URL = r'https?://(?!cnn)(?:[^/]+)\.iprima\.cz/(?:[^/]+/)*(?P<id>[^/?#&]+)' _VALID_URL = r'https?://(?!cnn)(?:[^/]+)\.iprima\.cz/(?:[^/]+/)*(?P<id>[^/?#&]+)'
_GEO_BYPASS = False _GEO_BYPASS = False
_NETRC_MACHINE = 'iprima' _NETRC_MACHINE = 'iprima'
_LOGIN_URL = 'https://auth.iprima.cz/oauth2/login' _AUTH_ROOT = 'https://auth.iprima.cz'
_TOKEN_URL = 'https://auth.iprima.cz/oauth2/token'
access_token = None access_token = None
_TESTS = [{ _TESTS = [{
@@ -67,7 +67,7 @@ class IPrimaIE(InfoExtractor):
return return
login_page = self._download_webpage( 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') errnote='Downloading login page failed')
login_form = self._hidden_inputs(login_page) login_form = self._hidden_inputs(login_page)
@@ -76,11 +76,20 @@ class IPrimaIE(InfoExtractor):
'_email': username, '_email': username,
'_password': password}) '_password': password})
_, login_handle = self._download_webpage_handle( profile_select_html, login_handle = self._download_webpage_handle(
self._LOGIN_URL, None, data=urlencode_postdata(login_form), f'{self._AUTH_ROOT}/oauth2/login', None, data=urlencode_postdata(login_form),
note='Logging in') 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: if not code:
raise ExtractorError('Login failed', expected=True) raise ExtractorError('Login failed', expected=True)
@@ -89,10 +98,10 @@ class IPrimaIE(InfoExtractor):
'client_id': 'prima_sso', 'client_id': 'prima_sso',
'grant_type': 'authorization_code', 'grant_type': 'authorization_code',
'code': 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( token_data = self._download_json(
self._TOKEN_URL, None, f'{self._AUTH_ROOT}/oauth2/token', None,
note='Downloading token', errnote='Downloading token failed', note='Downloading token', errnote='Downloading token failed',
data=urlencode_postdata(token_request_data)) data=urlencode_postdata(token_request_data))
@@ -115,14 +124,22 @@ class IPrimaIE(InfoExtractor):
webpage = self._download_webpage(url, video_id) 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'], ['og:title', 'twitter:title'],
webpage, 'title', default=None) webpage, 'title', default=None)
video_id = self._search_regex(( video_id = self._search_regex((
r'productId\s*=\s*([\'"])(?P<id>p\d+)\1', r'productId\s*=\s*([\'"])(?P<id>p\d+)\1',
r'pproduct_id\s*=\s*([\'"])(?P<id>p\d+)\1'), r'pproduct_id\s*=\s*([\'"])(?P<id>p\d+)\1',
webpage, 'real id', group='id') ), 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( metadata = self._download_json(
f'https://api.play-backend.iprima.cz/api/v1//products/id-{video_id}/play', f'https://api.play-backend.iprima.cz/api/v1//products/id-{video_id}/play',

View File

@@ -91,7 +91,7 @@ class IviIE(InfoExtractor):
for site in (353, 183): for site in (353, 183):
content_data = (data % site).encode() content_data = (data % site).encode()
if site == 353: if site == 353:
if not Cryptodome: if not Cryptodome.CMAC:
continue continue
timestamp = (self._download_json( timestamp = (self._download_json(
@@ -105,8 +105,8 @@ class IviIE(InfoExtractor):
query = { query = {
'ts': timestamp, 'ts': timestamp,
'sign': Cryptodome.Hash.CMAC.new(self._LIGHT_KEY, timestamp.encode() + content_data, 'sign': Cryptodome.CMAC.new(self._LIGHT_KEY, timestamp.encode() + content_data,
Cryptodome.Cipher.Blowfish).hexdigest(), Cryptodome.Blowfish).hexdigest(),
} }
else: else:
query = {} query = {}
@@ -126,7 +126,7 @@ class IviIE(InfoExtractor):
extractor_msg = 'Video %s does not exist' extractor_msg = 'Video %s does not exist'
elif site == 353: elif site == 353:
continue continue
elif not Cryptodome: elif not Cryptodome.CMAC:
raise ExtractorError('pycryptodomex not found. Please install', expected=True) raise ExtractorError('pycryptodomex not found. Please install', expected=True)
elif message: elif message:
extractor_msg += ': ' + message extractor_msg += ': ' + message

View File

@@ -1,7 +1,13 @@
import re import re
from .common import InfoExtractor 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): class MediaStreamIE(InfoExtractor):
@@ -117,39 +123,56 @@ class MediaStreamIE(InfoExtractor):
class WinSportsVideoIE(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 = [{ _TESTS = [{
'url': 'https://www.winsports.co/videos/siempre-castellanos-gran-atajada-del-portero-cardenal-para-evitar-la-caida-de-su-arco-60536', 'url': 'https://www.winsports.co/videos/siempre-castellanos-gran-atajada-del-portero-cardenal-para-evitar-la-caida-de-su-arco-60536',
'info_dict': { 'info_dict': {
'id': '62dc8357162c4b0821fcfb3c', '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', 'title': '¡Siempre Castellanos! Gran atajada del portero \'cardenal\' para evitar la caída de su arco',
'description': 'md5:eb811b2b2882bdc59431732c06b905f2', 'description': 'md5:eb811b2b2882bdc59431732c06b905f2',
'thumbnail': r're:^https?://[^?#]+62dc8357162c4b0821fcfb3c', 'thumbnail': r're:^https?://[^?#]+62dc8357162c4b0821fcfb3c',
'ext': 'mp4', 'ext': 'mp4',
}, },
'params': {'skip_download': 'm3u8'},
}, { }, {
'url': 'https://www.winsports.co/videos/observa-aqui-los-goles-del-empate-entre-tolima-y-nacional-60548', 'url': 'https://www.winsports.co/videos/observa-aqui-los-goles-del-empate-entre-tolima-y-nacional-60548',
'info_dict': { 'info_dict': {
'id': '62dcb875ef12a5526790b552', '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', 'title': 'Observa aquí los goles del empate entre Tolima y Nacional',
'description': 'md5:b19402ba6e46558b93fd24b873eea9c9', 'description': 'md5:b19402ba6e46558b93fd24b873eea9c9',
'thumbnail': r're:^https?://[^?#]+62dcb875ef12a5526790b552', 'thumbnail': r're:^https?://[^?#]+62dcb875ef12a5526790b552',
'ext': 'mp4', '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): 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) 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( media_setting_json = self._search_json(
r'<script\s*[^>]+data-drupal-selector="drupal-settings-json">', webpage, 'drupal-setting-json', display_id) 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( return self.url_result(
f'https://mdstrm.com/embed/{mediastream_id}', MediaStreamIE, video_id, url_transparent=True, urljoin('https://mdstrm.com/embed/', mediastream_id), MediaStreamIE, display_id, url_transparent=True,
display_id=display_id, video_title=clean_html(get_element_html_by_class('title-news', webpage))) display_id=display_id, video_title=strip_or_none(remove_end(json_ld.get('title'), '| Win Sports')))

View File

@@ -21,6 +21,7 @@ class NTVRuIE(InfoExtractor):
'description': 'Командующий Черноморским флотом провел переговоры в штабе ВМС Украины', 'description': 'Командующий Черноморским флотом провел переговоры в штабе ВМС Украины',
'thumbnail': r're:^http://.*\.jpg', 'thumbnail': r're:^http://.*\.jpg',
'duration': 136, 'duration': 136,
'view_count': int,
}, },
}, { }, {
'url': 'http://www.ntv.ru/video/novosti/750370/', 'url': 'http://www.ntv.ru/video/novosti/750370/',
@@ -32,6 +33,7 @@ class NTVRuIE(InfoExtractor):
'description': 'Родные пассажиров пропавшего Boeing не верят в трагический исход', 'description': 'Родные пассажиров пропавшего Boeing не верят в трагический исход',
'thumbnail': r're:^http://.*\.jpg', 'thumbnail': r're:^http://.*\.jpg',
'duration': 172, 'duration': 172,
'view_count': int,
}, },
}, { }, {
'url': 'http://www.ntv.ru/peredacha/segodnya/m23700/o232416', 'url': 'http://www.ntv.ru/peredacha/segodnya/m23700/o232416',
@@ -43,6 +45,7 @@ class NTVRuIE(InfoExtractor):
'description': '«Сегодня». 21 марта 2014 года. 16:00', 'description': '«Сегодня». 21 марта 2014 года. 16:00',
'thumbnail': r're:^http://.*\.jpg', 'thumbnail': r're:^http://.*\.jpg',
'duration': 1496, 'duration': 1496,
'view_count': int,
}, },
}, { }, {
'url': 'https://www.ntv.ru/kino/Koma_film/m70281/o336036/video/', 'url': 'https://www.ntv.ru/kino/Koma_film/m70281/o336036/video/',
@@ -54,6 +57,7 @@ class NTVRuIE(InfoExtractor):
'description': 'Остросюжетный фильм «Кома»', 'description': 'Остросюжетный фильм «Кома»',
'thumbnail': r're:^http://.*\.jpg', 'thumbnail': r're:^http://.*\.jpg',
'duration': 5592, 'duration': 5592,
'view_count': int,
}, },
}, { }, {
'url': 'http://www.ntv.ru/serial/Delo_vrachey/m31760/o233916/', 'url': 'http://www.ntv.ru/serial/Delo_vrachey/m31760/o233916/',
@@ -65,6 +69,7 @@ class NTVRuIE(InfoExtractor):
'description': '«Дело врачей»: «Деревце жизни»', 'description': '«Дело врачей»: «Деревце жизни»',
'thumbnail': r're:^http://.*\.jpg', 'thumbnail': r're:^http://.*\.jpg',
'duration': 2590, 'duration': 2590,
'view_count': int,
}, },
}, { }, {
# Schemeless file URL # Schemeless file URL
@@ -115,6 +120,14 @@ class NTVRuIE(InfoExtractor):
'url': file_, 'url': file_,
'filesize': int_or_none(xpath_text(video, './%ssize' % format_id)), '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 { return {
'id': xpath_text(video, './id'), 'id': xpath_text(video, './id'),

View File

@@ -8,6 +8,7 @@ from .common import InfoExtractor
from ..aes import aes_cbc_encrypt_bytes from ..aes import aes_cbc_encrypt_bytes
from ..utils import ( from ..utils import (
ExtractorError, ExtractorError,
float_or_none,
determine_ext, determine_ext,
int_or_none, int_or_none,
js_to_json, js_to_json,
@@ -19,6 +20,16 @@ from ..utils import (
class TencentBaseIE(InfoExtractor): class TencentBaseIE(InfoExtractor):
"""Subclasses must set _API_URL, _APP_VERSION, _PLATFORM, _HOST, _REFERER""" """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): def _get_ckey(self, video_id, url, guid):
ua = self.get_param('http_headers')['User-Agent'] ua = self.get_param('http_headers')['User-Agent']
@@ -47,6 +58,11 @@ class TencentBaseIE(InfoExtractor):
'sphttps': '1', # Enable HTTPS 'sphttps': '1', # Enable HTTPS
'otype': 'json', 'otype': 'json',
'spwm': '1', 'spwm': '1',
'hevclv': '28', # Enable HEVC
'drm': '40', # Enable DRM
# For HDR
'spvideo': '4',
'spsfrhdr': '100',
# For SHD # For SHD
'host': self._HOST, 'host': self._HOST,
'referer': self._REFERER, 'referer': self._REFERER,
@@ -63,7 +79,6 @@ class TencentBaseIE(InfoExtractor):
def _extract_video_formats_and_subtitles(self, api_response, video_id): def _extract_video_formats_and_subtitles(self, api_response, video_id):
video_response = api_response['vl']['vi'][0] video_response = api_response['vl']['vi'][0]
video_width, video_height = video_response.get('vw'), video_response.get('vh')
formats, subtitles = [], {} formats, subtitles = [], {}
for video_format in video_response['ul']['ui']: for video_format in video_response['ul']['ui']:
@@ -71,47 +86,61 @@ class TencentBaseIE(InfoExtractor):
fmts, subs = self._extract_m3u8_formats_and_subtitles( fmts, subs = self._extract_m3u8_formats_and_subtitles(
video_format['url'] + traverse_obj(video_format, ('hls', 'pt'), default=''), video_format['url'] + traverse_obj(video_format, ('hls', 'pt'), default=''),
video_id, 'mp4', fatal=False) video_id, 'mp4', fatal=False)
for f in fmts:
f.update({'width': video_width, 'height': video_height})
formats.extend(fmts) formats.extend(fmts)
self._merge_subtitles(subs, target=subtitles) self._merge_subtitles(subs, target=subtitles)
else: else:
formats.append({ formats.append({
'url': f'{video_format["url"]}{video_response["fn"]}?vkey={video_response["fvkey"]}', 'url': f'{video_format["url"]}{video_response["fn"]}?vkey={video_response["fvkey"]}',
'width': video_width,
'height': video_height,
'ext': 'mp4', '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 return formats, subtitles
def _extract_video_native_subtitles(self, api_response, subtitles_format): def _extract_video_native_subtitles(self, api_response):
subtitles = {} subtitles = {}
for subtitle in traverse_obj(api_response, ('sfl', 'fi')) or (): for subtitle in traverse_obj(api_response, ('sfl', 'fi')) or ():
subtitles.setdefault(subtitle['lang'].lower(), []).append({ subtitles.setdefault(subtitle['lang'].lower(), []).append({
'url': subtitle['url'], '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', 'protocol': 'm3u8_native' if determine_ext(subtitle['url']) == 'm3u8' else 'http',
}) })
return subtitles return subtitles
def _extract_all_video_formats_and_subtitles(self, url, video_id, series_id): 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 = [], {} formats, subtitles = [], {}
for video_format, subtitle_format, video_quality in ( for api_response in api_responses:
# '': 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")}')
fmts, subs = self._extract_video_formats_and_subtitles(api_response, video_id) 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) formats.extend(fmts)
self._merge_subtitles(subs, native_subtitles, target=subtitles) self._merge_subtitles(subs, native_subtitles, target=subtitles)
@@ -120,7 +149,7 @@ class TencentBaseIE(InfoExtractor):
def _get_clean_title(self, title): def _get_clean_title(self, title):
return re.sub( 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 '', title or '').strip() or None
@@ -147,27 +176,29 @@ class VQQVideoIE(VQQBaseIE):
_TESTS = [{ _TESTS = [{
'url': 'https://v.qq.com/x/page/q326831cny0.html', 'url': 'https://v.qq.com/x/page/q326831cny0.html',
'md5': '826ef93682df09e3deac4a6e6e8cdb6e', 'md5': '84568b3722e15e9cd023b5594558c4a7',
'info_dict': { 'info_dict': {
'id': 'q326831cny0', 'id': 'q326831cny0',
'ext': 'mp4', 'ext': 'mp4',
'title': '我是选手:雷霆裂阵,终极时刻', 'title': '我是选手:雷霆裂阵,终极时刻',
'description': 'md5:e7ed70be89244017dac2a835a10aeb1e', 'description': 'md5:e7ed70be89244017dac2a835a10aeb1e',
'thumbnail': r're:^https?://[^?#]+q326831cny0', 'thumbnail': r're:^https?://[^?#]+q326831cny0',
'format_id': r're:^shd',
}, },
}, { }, {
'url': 'https://v.qq.com/x/page/o3013za7cse.html', 'url': 'https://v.qq.com/x/page/o3013za7cse.html',
'md5': 'b91cbbeada22ef8cc4b06df53e36fa21', 'md5': 'cc431c4f9114a55643893c2c8ebf5592',
'info_dict': { 'info_dict': {
'id': 'o3013za7cse', 'id': 'o3013za7cse',
'ext': 'mp4', 'ext': 'mp4',
'title': '欧阳娜娜VLOG', 'title': '欧阳娜娜VLOG',
'description': 'md5:29fe847497a98e04a8c3826e499edd2e', 'description': 'md5:29fe847497a98e04a8c3826e499edd2e',
'thumbnail': r're:^https?://[^?#]+o3013za7cse', 'thumbnail': r're:^https?://[^?#]+o3013za7cse',
'format_id': r're:^shd',
}, },
}, { }, {
'url': 'https://v.qq.com/x/cover/7ce5noezvafma27/a00269ix3l8.html', 'url': 'https://v.qq.com/x/cover/7ce5noezvafma27/a00269ix3l8.html',
'md5': '71459c5375c617c265a22f083facce67', 'md5': '87968df6238a65d2478f19c25adf850b',
'info_dict': { 'info_dict': {
'id': 'a00269ix3l8', 'id': 'a00269ix3l8',
'ext': 'mp4', 'ext': 'mp4',
@@ -175,10 +206,11 @@ class VQQVideoIE(VQQBaseIE):
'description': 'md5:8cae3534327315b3872fbef5e51b5c5b', 'description': 'md5:8cae3534327315b3872fbef5e51b5c5b',
'thumbnail': r're:^https?://[^?#]+7ce5noezvafma27', 'thumbnail': r're:^https?://[^?#]+7ce5noezvafma27',
'series': '鸡毛飞上天', 'series': '鸡毛飞上天',
'format_id': r're:^shd',
}, },
}, { }, {
'url': 'https://v.qq.com/x/cover/mzc00200p29k31e/s0043cwsgj0.html', 'url': 'https://v.qq.com/x/cover/mzc00200p29k31e/s0043cwsgj0.html',
'md5': '96b9fd4a189fdd4078c111f21d7ac1bc', 'md5': 'fadd10bf88aec3420f06f19ee1d24c5b',
'info_dict': { 'info_dict': {
'id': 's0043cwsgj0', 'id': 's0043cwsgj0',
'ext': 'mp4', 'ext': 'mp4',
@@ -186,6 +218,7 @@ class VQQVideoIE(VQQBaseIE):
'description': 'md5:1d8c3a0b8729ae3827fa5b2d3ebd5213', 'description': 'md5:1d8c3a0b8729ae3827fa5b2d3ebd5213',
'thumbnail': r're:^https?://[^?#]+s0043cwsgj0', 'thumbnail': r're:^https?://[^?#]+s0043cwsgj0',
'series': '青年理工工作者生活研究所', 'series': '青年理工工作者生活研究所',
'format_id': r're:^shd',
}, },
}, { }, {
# Geo-restricted to China # Geo-restricted to China
@@ -319,6 +352,7 @@ class WeTvEpisodeIE(WeTvBaseIE):
'episode': 'Episode 1', 'episode': 'Episode 1',
'episode_number': 1, 'episode_number': 1,
'duration': 2835, 'duration': 2835,
'format_id': r're:^shd',
}, },
}, { }, {
'url': 'https://wetv.vip/en/play/u37kgfnfzs73kiu/p0039b9nvik', 'url': 'https://wetv.vip/en/play/u37kgfnfzs73kiu/p0039b9nvik',
@@ -333,6 +367,7 @@ class WeTvEpisodeIE(WeTvBaseIE):
'episode': 'Episode 1', 'episode': 'Episode 1',
'episode_number': 1, 'episode_number': 1,
'duration': 2454, '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', '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', 'ext': 'mp4',
'title': 'md5:f7a0857dbe5fbbe2e7ad630b92b54e6a', 'title': 'md5:f7a0857dbe5fbbe2e7ad630b92b54e6a',
'description': 'md5:76260cb9cdc0ef76826d7ca9d92fadfa', 'description': 'md5:76260cb9cdc0ef76826d7ca9d92fadfa',
'thumbnail': r're:^https?://[^?#]+lcxgwod5hapghvw', 'thumbnail': r're:^https?://[^?#]+i0042y00lxp',
'series': 'WeTV PICK-A-BOO', 'series': 'WeTV PICK-A-BOO',
'episode': 'Episode 0', 'episode': 'Episode 0',
'episode_number': 0, 'episode_number': 0,
'duration': 442, 'duration': 442,
'format_id': r're:^shd',
}, },
}] }]
@@ -406,6 +442,7 @@ class IflixEpisodeIE(IflixBaseIE):
'episode': 'Episode 1', 'episode': 'Episode 1',
'episode_number': 1, 'episode_number': 1,
'duration': 2639, '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', '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': 'Episode 1',
'episode_number': 1, 'episode_number': 1,
'duration': 228, 'duration': 228,
'format_id': r're:^shd',
}, },
}] }]

View File

@@ -50,10 +50,10 @@ class WrestleUniverseBaseIE(InfoExtractor):
data=data, headers=headers, query=query, fatal=fatal) data=data, headers=headers, query=query, fatal=fatal)
def _call_encrypted_api(self, video_id, param='', msg='API', data={}, query={}, fatal=True): 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) raise ExtractorError('pycryptodomex not found. Please install', expected=True)
private_key = Cryptodome.PublicKey.RSA.generate(2048) private_key = Cryptodome.RSA.generate(2048)
cipher = Cryptodome.Cipher.PKCS1_OAEP.new(private_key, hashAlgo=Cryptodome.Hash.SHA1) cipher = Cryptodome.PKCS1_OAEP.new(private_key, hashAlgo=Cryptodome.SHA1)
def decrypt(data): def decrypt(data):
if not data: if not data:

View File

@@ -61,7 +61,22 @@ class YleAreenaIE(InfoExtractor):
'age_limit': 0, 'age_limit': 0,
'webpage_url': 'https://areena.yle.fi/1-2158940' '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): def _real_extract(self, url):
@@ -91,12 +106,22 @@ class YleAreenaIE(InfoExtractor):
'name': sub.get('kind'), '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 { return {
'_type': 'url_transparent', **info_dict,
'url': smuggle_url(
f'kaltura:1955031:{video_data["data"]["ongoing_ondemand"]["kaltura"]["id"]}',
{'source_url': url}),
'ie_key': KalturaIE.ie_key(),
'title': (traverse_obj(video_data, ('data', 'ongoing_ondemand', 'title', 'fin'), expected_type=str) 'title': (traverse_obj(video_data, ('data', 'ongoing_ondemand', 'title', 'fin'), expected_type=str)
or episode or info.get('title')), or episode or info.get('title')),
'description': description, 'description': description,

View File

@@ -956,7 +956,7 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
@staticmethod @staticmethod
def is_music_url(url): 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): def _extract_video(self, renderer):
video_id = renderer.get('videoId') video_id = renderer.get('videoId')
@@ -3341,6 +3341,13 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
comment = self._extract_comment(comment_renderer, parent) comment = self._extract_comment(comment_renderer, parent)
if not comment: if not comment:
continue 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['running_total'] += 1
tracker['total_reply_comments' if parent else 'total_parent_comments'] += 1 tracker['total_reply_comments' if parent else 'total_parent_comments'] += 1
@@ -3365,7 +3372,8 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
est_total=0, est_total=0,
current_page_thread=0, current_page_thread=0,
total_parent_comments=0, total_parent_comments=0,
total_reply_comments=0) total_reply_comments=0,
seen_comment_ids=set())
# TODO: Deprecated # TODO: Deprecated
# YouTube comments have a max depth of 2 # YouTube comments have a max depth of 2
@@ -3776,10 +3784,18 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
if no_video: if no_video:
dct['abr'] = tbr dct['abr'] = tbr
if no_audio or no_video: if no_audio or no_video:
dct['downloader_options'] = { CHUNK_SIZE = 10 << 20
# Youtube throttles chunks >~10M dct.update({
'http_chunk_size': 10485760, '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'): if dct.get('ext'):
dct['container'] = dct['ext'] + '_dash' dct['container'] = dct['ext'] + '_dash'
@@ -4897,6 +4913,10 @@ class YoutubeTabBaseInfoExtractor(YoutubeBaseInfoExtractor):
info['view_count'] = self._get_count(playlist_stats, 1) info['view_count'] = self._get_count(playlist_stats, 1)
if info['view_count'] is None: # 0 is allowed if info['view_count'] is None: # 0 is allowed
info['view_count'] = self._get_count(playlist_header_renderer, 'viewCountText') 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) info['playlist_count'] = self._get_count(playlist_stats, 0)
if info['playlist_count'] is None: # 0 is allowed if info['playlist_count'] is None: # 0 is allowed
@@ -6116,6 +6136,23 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
} }
}], }],
'params': {'extract_flat': True}, '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 @classmethod
@@ -6182,6 +6219,8 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
original_tab_id, display_id = tab[1:], f'{item_id}{tab}' 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: if is_channel and not tab and 'no-youtube-channel-redirect' not in compat_opts:
url = f'{pre}/videos{post}' 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 # Handle both video/playlist URLs
qs = parse_qs(url) qs = parse_qs(url)

View File

@@ -9,6 +9,7 @@ import re
from .utils import ( from .utils import (
NO_DEFAULT, NO_DEFAULT,
ExtractorError, ExtractorError,
function_with_repr,
js_to_json, js_to_json,
remove_quotes, remove_quotes,
truncate_string, truncate_string,
@@ -184,7 +185,8 @@ class Debugger:
cls.write('=> Raises:', e, '<-|', stmt, level=allow_recursion) cls.write('=> Raises:', e, '<-|', stmt, level=allow_recursion)
raise raise
if cls.ENABLED and stmt.strip(): 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 ret, should_ret
return interpret_statement 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 '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): def __init__(self, code, objects=None):
self.code, self._functions = code, {} self.code, self._functions = code, {}
self._objects = {} if objects is None else objects self._objects = {} if objects is None else objects
@@ -220,6 +220,8 @@ class JSInterpreter:
def _named_object(self, namespace, obj): def _named_object(self, namespace, obj):
self.__named_object_counter += 1 self.__named_object_counter += 1
name = f'__yt_dlp_jsinterp_obj{self.__named_object_counter}' 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 namespace[name] = obj
return name return name
@@ -355,11 +357,11 @@ class JSInterpreter:
obj = expr[4:] obj = expr[4:]
if obj.startswith('Date('): if obj.startswith('Date('):
left, right = self._separate_at_paren(obj[4:]) left, right = self._separate_at_paren(obj[4:])
expr = unified_timestamp( date = unified_timestamp(
self.interpret_expression(left, local_vars, allow_recursion), False) 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) 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: else:
raise self.Exception(f'Unsupported object {obj}', expr) raise self.Exception(f'Unsupported object {obj}', expr)
@@ -784,7 +786,8 @@ class JSInterpreter:
fields) fields)
for f in fields_m: for f in fields_m:
argnames = f.group('args').split(',') 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 return obj
@@ -806,7 +809,9 @@ class JSInterpreter:
return [x.strip() for x in func_m.group('args').split(',')], code return [x.strip() for x in func_m.group('args').split(',')], code
def extract_function(self, funcname): 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): def extract_function_from_code(self, argnames, code, *global_stack):
local_vars = {} local_vars = {}

View File

@@ -20,7 +20,7 @@ from .postprocessor import (
SponsorBlockPP, SponsorBlockPP,
) )
from .postprocessor.modify_chapters import DEFAULT_SPONSORBLOCK_CHAPTER_TITLE 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 ( from .utils import (
OUTTMPL_TYPES, OUTTMPL_TYPES,
POSTPROCESS_WHEN, POSTPROCESS_WHEN,
@@ -36,7 +36,7 @@ from .utils import (
remove_end, remove_end,
write_string, write_string,
) )
from .version import __version__ from .version import CHANNEL, __version__
def parseOpts(overrideArguments=None, ignore_config_files='if_override'): def parseOpts(overrideArguments=None, ignore_config_files='if_override'):
@@ -326,11 +326,18 @@ def create_parser():
action='store_true', dest='update_self', action='store_true', dest='update_self',
help=format_field( help=format_field(
is_non_updateable(), None, 'Check if updates are available. %s', 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( general.add_option(
'--no-update', '--no-update',
action='store_false', dest='update_self', action='store_false', dest='update_self',
help='Do not check for updates (default)') 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( general.add_option(
'-i', '--ignore-errors', '-i', '--ignore-errors',
action='store_true', dest='ignoreerrors', action='store_true', dest='ignoreerrors',
@@ -606,8 +613,16 @@ def create_parser():
'Use "--match-filter -" to interactively ask whether to download each video')) 'Use "--match-filter -" to interactively ask whether to download each video'))
selection.add_option( selection.add_option(
'--no-match-filter', '--no-match-filter',
metavar='FILTER', dest='match_filter', action='store_const', const=None, dest='match_filter', action='store_const', const=None,
help='Do not use generic video filter (default)') 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( selection.add_option(
'--no-playlist', '--no-playlist',
action='store_true', dest='noplaylist', default=False, action='store_true', dest='noplaylist', default=False,
@@ -639,11 +654,11 @@ def create_parser():
selection.add_option( selection.add_option(
'--break-on-reject', '--break-on-reject',
action='store_true', dest='break_on_reject', default=False, 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( selection.add_option(
'--break-per-input', '--break-per-input',
action='store_true', dest='break_per_url', default=False, action='store_true', dest='break_per_url', default=False,
help='Alters --max-downloads, --break-on-existing, --break-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( selection.add_option(
'--no-break-per-input', '--no-break-per-input',
action='store_false', dest='break_per_url', action='store_false', dest='break_per_url',

View File

@@ -88,7 +88,7 @@ class PluginFinder(importlib.abc.MetaPathFinder):
candidate = path / parts candidate = path / parts
if candidate.is_dir(): if candidate.is_dir():
yield candidate 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): if parts in dirs_in_zip(path):
yield candidate yield candidate

View File

@@ -7,6 +7,7 @@ import platform
import re import re
import subprocess import subprocess
import sys import sys
import urllib.error
from zipimport import zipimporter from zipimport import zipimporter
from .compat import functools # isort: split from .compat import functools # isort: split
@@ -16,15 +17,26 @@ from .utils import (
cached_method, cached_method,
deprecation_warning, deprecation_warning,
remove_end, remove_end,
remove_start,
sanitized_Request,
shell_quote, shell_quote,
system_identifier, system_identifier,
traverse_obj,
version_tuple, version_tuple,
) )
from .version import UPDATE_HINT, VARIANT, __version__ from .version import CHANNEL, UPDATE_HINT, VARIANT, __version__
REPOSITORY = 'yt-dlp/yt-dlp' UPDATE_SOURCES = {
API_URL = f'https://api.github.com/repos/{REPOSITORY}/releases' 'stable': 'yt-dlp/yt-dlp',
'nightly': 'yt-dlp/yt-dlp-nightly-builds',
}
_VERSION_RE = re.compile(r'(\d+\.)*\d+')
API_BASE_URL = 'https://api.github.com/repos'
# Backwards compatibility variables for the current channel
REPOSITORY = UPDATE_SOURCES[CHANNEL]
API_URL = f'{API_BASE_URL}/{REPOSITORY}/releases'
@functools.cache @functools.cache
@@ -110,49 +122,99 @@ def _sha256_file(path):
class Updater: class Updater:
def __init__(self, ydl): _exact = True
def __init__(self, ydl, target=None):
self.ydl = ydl 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 @functools.cached_property
def _tag(self): def _tag(self):
if version_tuple(__version__) >= version_tuple(self.latest_version): if self._version_compare(self.current_version, self.latest_version):
return 'latest' 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(): for line in self._download('_update_spec', 'latest').decode().splitlines():
if not line.startswith('lock '): if not line.startswith('lock '):
continue continue
_, tag, pattern = line.split(' ', 2) _, tag, pattern = line.split(' ', 2)
if re.match(pattern, identifier): if re.match(pattern, identifier):
return f'tags/{tag}' if not self._exact:
return 'latest' 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 @cached_method
def _get_version_info(self, tag): def _get_version_info(self, tag):
self.ydl.write_debug(f'Fetching release info: {API_URL}/{tag}') url = f'{API_BASE_URL}/{self._target_repo}/releases/{tag}'
return json.loads(self.ydl.urlopen(f'{API_URL}/{tag}').read().decode()) 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 @property
def current_version(self): def current_version(self):
"""Current version""" """Current version"""
return __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 @property
def new_version(self): def new_version(self):
"""Version of the latest release we can update to""" """Version of the latest release we can update to"""
if self._tag.startswith('tags/'): return self._get_actual_tag(self._tag)
return self._tag[5:]
return self._get_version_info(self._tag)['tag_name']
@property @property
def latest_version(self): def latest_version(self):
"""Version of the latest release""" """Version of the target release"""
return self._get_version_info('latest')['tag_name'] return self._get_actual_tag(self.target_tag)
@property @property
def has_update(self): def has_update(self):
"""Whether there is an update available""" """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 @functools.cached_property
def filename(self): def filename(self):
@@ -160,10 +222,8 @@ class Updater:
return compat_realpath(_get_variant_and_executable_path()[1]) return compat_realpath(_get_variant_and_executable_path()[1])
def _download(self, name, tag): def _download(self, name, tag):
url = traverse_obj(self._get_version_info(tag), ( slug = 'latest/download' if tag == 'latest' else f'download/{tag[5:]}'
'assets', lambda _, v: v['name'] == name, 'browser_download_url'), get_all=False) url = f'https://github.com/{self._target_repo}/releases/{slug}/{name}'
if not url:
raise Exception('Unable to find download URL')
self.ydl.write_debug(f'Downloading {name} from {url}') self.ydl.write_debug(f'Downloading {name} from {url}')
return self.ydl.urlopen(url).read() 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) self._report_error(f'Unable to write to {file}; Try running as administrator', True)
def _report_network_error(self, action, delim=';'): 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): def check_update(self):
"""Report whether there is an update available""" """Report whether there is an update available"""
if not self._target_repo:
return False
try: try:
self.ydl.to_screen( self.ydl.to_screen((
f'Latest version: {self.latest_version}, Current version: {self.current_version}') f'Available version: {self._label(self.target_channel, self.latest_version)}, ' if self.target_tag == 'latest' else ''
if not self.has_update: ) + f'Current version: {self._label(CHANNEL, self.current_version)}')
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')
except Exception: except Exception:
return self._report_network_error('obtain version info', delim='; Please try again later or') return self._report_network_error('obtain version info', delim='; Please try again later or')
if not is_non_updateable(): if not is_non_updateable():
self.ydl.to_screen(f'Current Build Hash {_sha256_file(self.filename)}') self.ydl.to_screen(f'Current Build Hash: {_sha256_file(self.filename)}')
return True
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): def update(self):
"""Update yt-dlp executable to the latest version""" """Update yt-dlp executable to the latest version"""
@@ -212,7 +280,10 @@ class Updater:
err = is_non_updateable() err = is_non_updateable()
if err: if err:
return self._report_error(err, True) 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) directory = os.path.dirname(self.filename)
if not os.access(self.filename, os.W_OK): if not os.access(self.filename, os.W_OK):
@@ -232,10 +303,11 @@ class Updater:
try: try:
newcontent = self._download(self.release_name, self._tag) newcontent = self._download(self.release_name, self._tag)
except OSError: except Exception as e:
return self._report_network_error('download latest version') if isinstance(e, urllib.error.HTTPError) and e.code == 404:
except Exception: return self._report_error(
return self._report_network_error('fetch updates') 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: try:
expected_hash = self.release_hash expected_hash = self.release_hash
@@ -280,7 +352,7 @@ class Updater:
return self._report_error( return self._report_error(
f'Unable to set permissions. Run: sudo chmod a+rx {compat_shlex_quote(self.filename)}') 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 return True
@functools.cached_property @functools.cached_property
@@ -346,3 +418,6 @@ def update_self(to_screen, verbose, opener):
return opener.open(url) return opener.open(url)
return run_update(FakeYDL()) return run_update(FakeYDL())
__all__ = ['Updater']

View File

@@ -593,21 +593,43 @@ def clean_html(html):
class LenientJSONDecoder(json.JSONDecoder): 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.transform_source, self.ignore_extra = transform_source, ignore_extra
self._close_attempts = 2 * close_objects
super().__init__(*args, **kwargs) 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): def decode(self, s):
if self.transform_source: if self.transform_source:
s = self.transform_source(s) s = self.transform_source(s)
try: for attempt in range(self._close_attempts + 1):
if self.ignore_extra: try:
return self.raw_decode(s.lstrip())[0] if self.ignore_extra:
return super().decode(s) return self.raw_decode(s.lstrip())[0]
except json.JSONDecodeError as e: return super().decode(s)
if e.pos is not None: 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 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): def sanitize_open(filename, open_mode):
@@ -879,6 +901,7 @@ class Popen(subprocess.Popen):
env = os.environ.copy() env = os.environ.copy()
self._fix_pyinstaller_ld_path(env) 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: if text is True:
kwargs['universal_newlines'] = True # For 3.6 compatibility kwargs['universal_newlines'] = True # For 3.6 compatibility
kwargs.setdefault('encoding', 'utf-8') kwargs.setdefault('encoding', 'utf-8')
@@ -900,7 +923,7 @@ class Popen(subprocess.Popen):
@classmethod @classmethod
def run(cls, *args, timeout=None, **kwargs): def run(cls, *args, timeout=None, **kwargs):
with cls(*args, **kwargs) as proc: 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) stdout, stderr = proc.communicate_or_kill(timeout=timeout)
return stdout or default, stderr or default, proc.returncode return stdout or default, stderr or default, proc.returncode
@@ -1207,8 +1230,8 @@ class ExistingVideoReached(DownloadCancelled):
class RejectedVideoReached(DownloadCancelled): class RejectedVideoReached(DownloadCancelled):
""" --break-on-reject triggered """ """ --break-match-filter triggered """
msg = 'Encountered a video that did not match filter, stopping due to --break-on-reject' msg = 'Encountered a video that did not match filter, stopping due to --break-match-filter'
class MaxDownloadsReached(DownloadCancelled): class MaxDownloadsReached(DownloadCancelled):
@@ -3019,8 +3042,10 @@ class PlaylistEntries:
if not entry: if not entry:
continue continue
try: try:
# TODO: Add auto-generated fields # The item may have just been added to archive. Don't break due to it
self.ydl._match_entry(entry, incomplete=True, silent=True) 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): except (ExistingVideoReached, RejectedVideoReached):
return return
@@ -3886,16 +3911,21 @@ def match_str(filter_str, dct, incomplete=False):
for filter_part in re.split(r'(?<!\\)&', filter_str)) for filter_part in re.split(r'(?<!\\)&', filter_str))
def match_filter_func(filters): def match_filter_func(filters, breaking_filters=None):
if not filters: if not filters and not breaking_filters:
return None return None
filters = set(variadic(filters)) breaking_filters = match_filter_func(breaking_filters) or (lambda _, __: None)
filters = set(variadic(filters or []))
interactive = '-' in filters interactive = '-' in filters
if interactive: if interactive:
filters.remove('-') filters.remove('-')
def _match_func(info_dict, incomplete=False): 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): 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 return NO_DEFAULT if interactive and not incomplete else None
else: else:
@@ -6034,14 +6064,16 @@ class classproperty:
class function_with_repr: class function_with_repr:
def __init__(self, func): def __init__(self, func, repr_=None):
functools.update_wrapper(self, func) functools.update_wrapper(self, func)
self.func = func self.func, self.__repr = func, repr_
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs) return self.func(*args, **kwargs)
def __repr__(self): def __repr__(self):
if self.__repr:
return self.__repr
return f'{self.func.__module__}.{self.func.__qualname__}' return f'{self.func.__module__}.{self.func.__qualname__}'

View File

@@ -1,9 +1,11 @@
# Autogenerated by devscripts/update-version.py # Autogenerated by devscripts/update-version.py
__version__ = '2023.02.17' __version__ = '2023.03.03'
RELEASE_GIT_HEAD = 'a0a7c0154' RELEASE_GIT_HEAD = '93449642815a6973a4b09b289982ca7e1f961b5f'
VARIANT = None VARIANT = None
UPDATE_HINT = None UPDATE_HINT = None
CHANNEL = 'stable'