mirror of
https://github.com/yt-dlp/yt-dlp
synced 2025-12-18 23:25:42 +07:00
Compare commits
21 Commits
2023.01.02
...
2023.01.06
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7287ab92f6 | ||
|
|
6becd2508c | ||
|
|
edfc7725b1 | ||
|
|
b382c1fc6a | ||
|
|
8a6b167723 | ||
|
|
253ac4ba6a | ||
|
|
84e0e33a19 | ||
|
|
ab4cbeff00 | ||
|
|
773c272d66 | ||
|
|
c3366fdfd0 | ||
|
|
5be214abed | ||
|
|
d37422f1db | ||
|
|
933ed882e9 | ||
|
|
a1d9aca338 | ||
|
|
91d54e9b99 | ||
|
|
76c3ceccfb | ||
|
|
ad68b16a1e | ||
|
|
f079514957 | ||
|
|
e9df3d42c4 | ||
|
|
d80ca5deaa | ||
|
|
1a3cd8ec35 |
8
.github/ISSUE_TEMPLATE/1_broken_site.yml
vendored
8
.github/ISSUE_TEMPLATE/1_broken_site.yml
vendored
@@ -18,7 +18,7 @@ body:
|
|||||||
options:
|
options:
|
||||||
- label: I'm reporting a broken site
|
- label: I'm reporting a broken site
|
||||||
required: true
|
required: true
|
||||||
- label: I've verified that I'm running yt-dlp version **2023.01.02** ([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.01.06** ([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,7 +62,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.01.02 [9d339c4] (win32_exe)
|
[debug] yt-dlp version 2023.01.06 [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 +70,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.01.02, Current version: 2023.01.02
|
Latest version: 2023.01.06, Current version: 2023.01.06
|
||||||
yt-dlp is up to date (2023.01.02)
|
yt-dlp is up to date (2023.01.06)
|
||||||
<more lines>
|
<more lines>
|
||||||
render: shell
|
render: shell
|
||||||
validations:
|
validations:
|
||||||
|
|||||||
@@ -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.01.02** ([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.01.06** ([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
|
||||||
@@ -74,7 +74,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.01.02 [9d339c4] (win32_exe)
|
[debug] yt-dlp version 2023.01.06 [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 +82,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.01.02, Current version: 2023.01.02
|
Latest version: 2023.01.06, Current version: 2023.01.06
|
||||||
yt-dlp is up to date (2023.01.02)
|
yt-dlp is up to date (2023.01.06)
|
||||||
<more lines>
|
<more lines>
|
||||||
render: shell
|
render: shell
|
||||||
validations:
|
validations:
|
||||||
|
|||||||
@@ -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.01.02** ([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.01.06** ([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
|
||||||
@@ -70,7 +70,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.01.02 [9d339c4] (win32_exe)
|
[debug] yt-dlp version 2023.01.06 [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 +78,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.01.02, Current version: 2023.01.02
|
Latest version: 2023.01.06, Current version: 2023.01.06
|
||||||
yt-dlp is up to date (2023.01.02)
|
yt-dlp is up to date (2023.01.06)
|
||||||
<more lines>
|
<more lines>
|
||||||
render: shell
|
render: shell
|
||||||
validations:
|
validations:
|
||||||
|
|||||||
8
.github/ISSUE_TEMPLATE/4_bug_report.yml
vendored
8
.github/ISSUE_TEMPLATE/4_bug_report.yml
vendored
@@ -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.01.02** ([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.01.06** ([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
|
||||||
@@ -55,7 +55,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.01.02 [9d339c4] (win32_exe)
|
[debug] yt-dlp version 2023.01.06 [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 +63,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.01.02, Current version: 2023.01.02
|
Latest version: 2023.01.06, Current version: 2023.01.06
|
||||||
yt-dlp is up to date (2023.01.02)
|
yt-dlp is up to date (2023.01.06)
|
||||||
<more lines>
|
<more lines>
|
||||||
render: shell
|
render: shell
|
||||||
validations:
|
validations:
|
||||||
|
|||||||
8
.github/ISSUE_TEMPLATE/5_feature_request.yml
vendored
8
.github/ISSUE_TEMPLATE/5_feature_request.yml
vendored
@@ -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.01.02** ([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.01.06** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
|
||||||
required: true
|
required: true
|
||||||
- label: I've searched 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 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
|
||||||
@@ -51,7 +51,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.01.02 [9d339c4] (win32_exe)
|
[debug] yt-dlp version 2023.01.06 [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 +59,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.01.02, Current version: 2023.01.02
|
Latest version: 2023.01.06, Current version: 2023.01.06
|
||||||
yt-dlp is up to date (2023.01.02)
|
yt-dlp is up to date (2023.01.06)
|
||||||
<more lines>
|
<more lines>
|
||||||
render: shell
|
render: shell
|
||||||
|
|||||||
8
.github/ISSUE_TEMPLATE/6_question.yml
vendored
8
.github/ISSUE_TEMPLATE/6_question.yml
vendored
@@ -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.01.02** ([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.01.06** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
|
||||||
required: true
|
required: true
|
||||||
- label: I've searched 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 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
|
||||||
@@ -57,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.01.02 [9d339c4] (win32_exe)
|
[debug] yt-dlp version 2023.01.06 [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 +65,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.01.02, Current version: 2023.01.02
|
Latest version: 2023.01.06, Current version: 2023.01.06
|
||||||
yt-dlp is up to date (2023.01.02)
|
yt-dlp is up to date (2023.01.06)
|
||||||
<more lines>
|
<more lines>
|
||||||
render: shell
|
render: shell
|
||||||
|
|||||||
@@ -375,3 +375,9 @@ Spicadox
|
|||||||
barsnick
|
barsnick
|
||||||
docbender
|
docbender
|
||||||
KurtBestor
|
KurtBestor
|
||||||
|
Chrissi2812
|
||||||
|
FrederikNS
|
||||||
|
gschizas
|
||||||
|
JC-Chung
|
||||||
|
mzhou
|
||||||
|
OndrejBakan
|
||||||
|
|||||||
24
Changelog.md
24
Changelog.md
@@ -11,7 +11,29 @@ # Instuctions for creating release
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
|
|
||||||
## 2023.01.02
|
### 2023.01.06
|
||||||
|
|
||||||
|
* Fix config locations by [Grub4k](https://github.com/Grub4k), [coletdjnz](https://github.com/coletdjnz), [pukkandan](https://github.com/pukkandan)
|
||||||
|
* [downloader/aria2c] Disable native progress
|
||||||
|
* [utils] `mimetype2ext`: `weba` is not standard
|
||||||
|
* [utils] `windows_enable_vt_mode`: Better error handling
|
||||||
|
* [build] Add minimal `pyproject.toml`
|
||||||
|
* [update] Fix updater file removal on windows by [Grub4K](https://github.com/Grub4K)
|
||||||
|
* [cleanup] Misc fixes and cleanup
|
||||||
|
* [extractor/aitube] Add extractor by [HobbyistDev](https://github.com/HobbyistDev)
|
||||||
|
* [extractor/drtv] Add series extractors by [FrederikNS](https://github.com/FrederikNS)
|
||||||
|
* [extractor/volejtv] Add extractor by [HobbyistDev](https://github.com/HobbyistDev)
|
||||||
|
* [extractor/xanimu] Add extractor by [JChris246](https://github.com/JChris246)
|
||||||
|
* [extractor/youtube] Retry manifest refresh for live-from-start by [mzhou](https://github.com/mzhou)
|
||||||
|
* [extractor/biliintl] Add `/media` to `VALID_URL` by [HobbyistDev](https://github.com/HobbyistDev)
|
||||||
|
* [extractor/biliIntl] Add fallback to `video_data` by [HobbyistDev](https://github.com/HobbyistDev)
|
||||||
|
* [extractor/crunchyroll:show] Add `language` to entries by [Chrissi2812](https://github.com/Chrissi2812)
|
||||||
|
* [extractor/joj] Fix extractor by [OndrejBakan](https://github.com/OndrejBakan), [pukkandan](https://github.com/pukkandan)
|
||||||
|
* [extractor/nbc] Update graphql query by [jacobtruman](https://github.com/jacobtruman)
|
||||||
|
* [extractor/reddit] Add subreddit as `channel_id` by [gschizas](https://github.com/gschizas)
|
||||||
|
* [extractor/tiktok] Add `TikTokLive` extractor by [JC-Chung](https://github.com/JC-Chung)
|
||||||
|
|
||||||
|
### 2023.01.02
|
||||||
|
|
||||||
* **Improve plugin architecture** by [Grub4K](https://github.com/Grub4K), [coletdjnz](https://github.com/coletdjnz), [flashdagger](https://github.com/flashdagger), [pukkandan](https://github.com/pukkandan)
|
* **Improve plugin architecture** by [Grub4K](https://github.com/Grub4K), [coletdjnz](https://github.com/coletdjnz), [flashdagger](https://github.com/flashdagger), [pukkandan](https://github.com/pukkandan)
|
||||||
* Plugins can be loaded in any distribution of yt-dlp (binary, pip, source, etc.) and can be distributed and installed as packages. See [the readme](https://github.com/yt-dlp/yt-dlp/tree/05997b6e98e638d97d409c65bb5eb86da68f3b64#plugins) for more information
|
* Plugins can be loaded in any distribution of yt-dlp (binary, pip, source, etc.) and can be distributed and installed as packages. See [the readme](https://github.com/yt-dlp/yt-dlp/tree/05997b6e98e638d97d409c65bb5eb86da68f3b64#plugins) for more information
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ ## [Ashish0804](https://github.com/Ashish0804) <sub><sup>[Inactive]</sup></sub>
|
|||||||
* Improved/fixed support for HiDive, HotStar, Hungama, LBRY, LinkedInLearning, Mxplayer, SonyLiv, TV2, Vimeo, VLive etc
|
* Improved/fixed support for HiDive, HotStar, Hungama, LBRY, LinkedInLearning, Mxplayer, SonyLiv, TV2, Vimeo, VLive etc
|
||||||
|
|
||||||
|
|
||||||
## [Lesmiscore](https://github.com/Lesmiscore) <sup><sub>(nao20010128nao)</sup></sub>
|
## [Lesmiscore](https://github.com/Lesmiscore) <sub><sup>(nao20010128nao)</sup></sub>
|
||||||
|
|
||||||
**Bitcoin**: bc1qfd02r007cutfdjwjmyy9w23rjvtls6ncve7r3s
|
**Bitcoin**: bc1qfd02r007cutfdjwjmyy9w23rjvtls6ncve7r3s
|
||||||
**Monacoin**: mona1q3tf7dzvshrhfe3md379xtvt2n22duhglv5dskr
|
**Monacoin**: mona1q3tf7dzvshrhfe3md379xtvt2n22duhglv5dskr
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ ### Differences in default behavior
|
|||||||
* When `--embed-subs` and `--write-subs` are used together, the subtitles are written to disk and also embedded in the media file. You can use just `--embed-subs` to embed the subs and automatically delete the separate file. See [#630 (comment)](https://github.com/yt-dlp/yt-dlp/issues/630#issuecomment-893659460) for more info. `--compat-options no-keep-subs` can be used to revert this
|
* When `--embed-subs` and `--write-subs` are used together, the subtitles are written to disk and also embedded in the media file. You can use just `--embed-subs` to embed the subs and automatically delete the separate file. See [#630 (comment)](https://github.com/yt-dlp/yt-dlp/issues/630#issuecomment-893659460) for more info. `--compat-options no-keep-subs` can be used to revert this
|
||||||
* `certifi` will be used for SSL root certificates, if installed. If you want to use system certificates (e.g. self-signed), use `--compat-options no-certifi`
|
* `certifi` will be used for SSL root certificates, if installed. If you want to use system certificates (e.g. self-signed), use `--compat-options no-certifi`
|
||||||
* yt-dlp's sanitization of invalid characters in filenames is different/smarter than in youtube-dl. You can use `--compat-options filename-sanitization` to revert to youtube-dl's behavior
|
* yt-dlp's sanitization of invalid characters in filenames is different/smarter than in youtube-dl. You can use `--compat-options filename-sanitization` to revert to youtube-dl's behavior
|
||||||
* yt-dlp tries to parse the external downloader outputs into the standard progress output if possible (Currently implemented: `aria2c`). You can use `--compat-options no-external-downloader-progress` to get the downloader output as-is
|
* yt-dlp tries to parse the external downloader outputs into the standard progress output if possible (Currently implemented: [~~aria2c~~](https://github.com/yt-dlp/yt-dlp/issues/5931)). You can use `--compat-options no-external-downloader-progress` to get the downloader output as-is
|
||||||
|
|
||||||
For ease of use, a few more compat options are available:
|
For ease of use, a few more compat options are available:
|
||||||
|
|
||||||
@@ -1119,9 +1119,10 @@ # CONFIGURATION
|
|||||||
* `yt-dlp.conf` in the home path given by `-P`
|
* `yt-dlp.conf` in the home path given by `-P`
|
||||||
* If `-P` is not given, the current directory is searched
|
* If `-P` is not given, the current directory is searched
|
||||||
1. **User Configuration**:
|
1. **User Configuration**:
|
||||||
|
* `${XDG_CONFIG_HOME}/yt-dlp.conf`
|
||||||
* `${XDG_CONFIG_HOME}/yt-dlp/config` (recommended on Linux/macOS)
|
* `${XDG_CONFIG_HOME}/yt-dlp/config` (recommended on Linux/macOS)
|
||||||
* `${XDG_CONFIG_HOME}/yt-dlp/config.txt`
|
* `${XDG_CONFIG_HOME}/yt-dlp/config.txt`
|
||||||
* `${XDG_CONFIG_HOME}/yt-dlp.conf`
|
* `${APPDATA}/yt-dlp.conf`
|
||||||
* `${APPDATA}/yt-dlp/config` (recommended on Windows)
|
* `${APPDATA}/yt-dlp/config` (recommended on Windows)
|
||||||
* `${APPDATA}/yt-dlp/config.txt`
|
* `${APPDATA}/yt-dlp/config.txt`
|
||||||
* `~/yt-dlp.conf`
|
* `~/yt-dlp.conf`
|
||||||
@@ -1836,6 +1837,7 @@ ## Installing Plugins
|
|||||||
* `${XDG_CONFIG_HOME}/yt-dlp/plugins/<package name>/yt_dlp_plugins/` (recommended on Linux/macOS)
|
* `${XDG_CONFIG_HOME}/yt-dlp/plugins/<package name>/yt_dlp_plugins/` (recommended on Linux/macOS)
|
||||||
* `${XDG_CONFIG_HOME}/yt-dlp-plugins/<package name>/yt_dlp_plugins/`
|
* `${XDG_CONFIG_HOME}/yt-dlp-plugins/<package name>/yt_dlp_plugins/`
|
||||||
* `${APPDATA}/yt-dlp/plugins/<package name>/yt_dlp_plugins/` (recommended on Windows)
|
* `${APPDATA}/yt-dlp/plugins/<package name>/yt_dlp_plugins/` (recommended on Windows)
|
||||||
|
* `${APPDATA}/yt-dlp-plugins/<package name>/yt_dlp_plugins/`
|
||||||
* `~/.yt-dlp/plugins/<package name>/yt_dlp_plugins/`
|
* `~/.yt-dlp/plugins/<package name>/yt_dlp_plugins/`
|
||||||
* `~/yt-dlp-plugins/<package name>/yt_dlp_plugins/`
|
* `~/yt-dlp-plugins/<package name>/yt_dlp_plugins/`
|
||||||
* **System Plugins**
|
* **System Plugins**
|
||||||
@@ -1863,7 +1865,7 @@ ## Developing Plugins
|
|||||||
|
|
||||||
All public classes with a name ending in `IE`/`PP` are imported from each file for extractors and postprocessors repectively. This respects underscore prefix (e.g. `_MyBasePluginIE` is private) and `__all__`. Modules can similarly be excluded by prefixing the module name with an underscore (e.g. `_myplugin.py`).
|
All public classes with a name ending in `IE`/`PP` are imported from each file for extractors and postprocessors repectively. This respects underscore prefix (e.g. `_MyBasePluginIE` is private) and `__all__`. Modules can similarly be excluded by prefixing the module name with an underscore (e.g. `_myplugin.py`).
|
||||||
|
|
||||||
To replace an existing extractor with a subclass of one, set the `plugin_name` class keyword argument (e.g. `MyPluginIE(ABuiltInIE, plugin_name='myplugin')` will replace `ABuiltInIE` with `MyPluginIE`). Since the extractor replaces the parent, you should exclude the subclass extractor from being imported separately by making it private using one of the methods described above.
|
To replace an existing extractor with a subclass of one, set the `plugin_name` class keyword argument (e.g. `class MyPluginIE(ABuiltInIE, plugin_name='myplugin')` will replace `ABuiltInIE` with `MyPluginIE`). Since the extractor replaces the parent, you should exclude the subclass extractor from being imported separately by making it private using one of the methods described above.
|
||||||
|
|
||||||
If you are a plugin author, add [yt-dlp-plugins](https://github.com/topics/yt-dlp-plugins) as a topic to your repository for discoverability.
|
If you are a plugin author, add [yt-dlp-plugins](https://github.com/topics/yt-dlp-plugins) as a topic to your repository for discoverability.
|
||||||
|
|
||||||
|
|||||||
5
pyproject.toml
Normal file
5
pyproject.toml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
[build-system]
|
||||||
|
build-backend = 'setuptools.build_meta'
|
||||||
|
# https://github.com/yt-dlp/yt-dlp/issues/5941
|
||||||
|
# https://github.com/pypa/distutils/issues/17
|
||||||
|
requires = ['setuptools > 50']
|
||||||
@@ -26,7 +26,7 @@ markers =
|
|||||||
|
|
||||||
[tox:tox]
|
[tox:tox]
|
||||||
skipsdist = true
|
skipsdist = true
|
||||||
envlist = py{36,37,38,39,310},pypy{36,37,38,39}
|
envlist = py{36,37,38,39,310,311},pypy{36,37,38,39}
|
||||||
skip_missing_interpreters = true
|
skip_missing_interpreters = true
|
||||||
|
|
||||||
[testenv] # tox
|
[testenv] # tox
|
||||||
|
|||||||
8
setup.py
8
setup.py
@@ -1,8 +1,12 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
import os.path
|
# Allow execution from anywhere
|
||||||
import subprocess
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
import subprocess
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ # Supported sites
|
|||||||
- **afreecatv:user**
|
- **afreecatv:user**
|
||||||
- **AirMozilla**
|
- **AirMozilla**
|
||||||
- **AirTV**
|
- **AirTV**
|
||||||
|
- **AitubeKZVideo**
|
||||||
- **AliExpressLive**
|
- **AliExpressLive**
|
||||||
- **AlJazeera**
|
- **AlJazeera**
|
||||||
- **Allocine**
|
- **Allocine**
|
||||||
@@ -352,6 +353,8 @@ # Supported sites
|
|||||||
- **DrTuber**
|
- **DrTuber**
|
||||||
- **drtv**
|
- **drtv**
|
||||||
- **drtv:live**
|
- **drtv:live**
|
||||||
|
- **drtv:season**
|
||||||
|
- **drtv:series**
|
||||||
- **DTube**
|
- **DTube**
|
||||||
- **duboku**: www.duboku.io
|
- **duboku**: www.duboku.io
|
||||||
- **duboku:list**: www.duboku.io entire series
|
- **duboku:list**: www.duboku.io entire series
|
||||||
@@ -1199,7 +1202,6 @@ # Supported sites
|
|||||||
- **SaltTVLive**: [<abbr title="netrc machine"><em>salttv</em></abbr>]
|
- **SaltTVLive**: [<abbr title="netrc machine"><em>salttv</em></abbr>]
|
||||||
- **SaltTVRecordings**: [<abbr title="netrc machine"><em>salttv</em></abbr>]
|
- **SaltTVRecordings**: [<abbr title="netrc machine"><em>salttv</em></abbr>]
|
||||||
- **SampleFocus**
|
- **SampleFocus**
|
||||||
- **SamplePlugin**: (**Currently broken**)
|
|
||||||
- **Sangiin**: 参議院インターネット審議中継 (archive)
|
- **Sangiin**: 参議院インターネット審議中継 (archive)
|
||||||
- **Sapo**: SAPO Vídeos
|
- **Sapo**: SAPO Vídeos
|
||||||
- **savefrom.net**
|
- **savefrom.net**
|
||||||
@@ -1375,10 +1377,14 @@ # Supported sites
|
|||||||
- **ThisAmericanLife**
|
- **ThisAmericanLife**
|
||||||
- **ThisAV**
|
- **ThisAV**
|
||||||
- **ThisOldHouse**
|
- **ThisOldHouse**
|
||||||
|
- **ThisVid**
|
||||||
|
- **ThisVidMember**
|
||||||
|
- **ThisVidPlaylist**
|
||||||
- **ThreeSpeak**
|
- **ThreeSpeak**
|
||||||
- **ThreeSpeakUser**
|
- **ThreeSpeakUser**
|
||||||
- **TikTok**
|
- **TikTok**
|
||||||
- **tiktok:effect**: (**Currently broken**)
|
- **tiktok:effect**: (**Currently broken**)
|
||||||
|
- **tiktok:live**
|
||||||
- **tiktok:sound**: (**Currently broken**)
|
- **tiktok:sound**: (**Currently broken**)
|
||||||
- **tiktok:tag**: (**Currently broken**)
|
- **tiktok:tag**: (**Currently broken**)
|
||||||
- **tiktok:user**: (**Currently broken**)
|
- **tiktok:user**: (**Currently broken**)
|
||||||
@@ -1580,6 +1586,7 @@ # Supported sites
|
|||||||
- **VoiceRepublic**
|
- **VoiceRepublic**
|
||||||
- **voicy**
|
- **voicy**
|
||||||
- **voicy:channel**
|
- **voicy:channel**
|
||||||
|
- **VolejTV**
|
||||||
- **Voot**
|
- **Voot**
|
||||||
- **VootSeries**
|
- **VootSeries**
|
||||||
- **VoxMedia**
|
- **VoxMedia**
|
||||||
@@ -1651,6 +1658,7 @@ # Supported sites
|
|||||||
- **WWE**
|
- **WWE**
|
||||||
- **wyborcza:video**
|
- **wyborcza:video**
|
||||||
- **WyborczaPodcast**
|
- **WyborczaPodcast**
|
||||||
|
- **Xanimu**
|
||||||
- **XBef**
|
- **XBef**
|
||||||
- **XboxClips**
|
- **XboxClips**
|
||||||
- **XFileShare**: XFileShare based sites: Aparat, ClipWatching, GoUnlimited, GoVid, HolaVid, Streamty, TheVideoBee, Uqload, VidBom, vidlo, VidLocker, VidShare, VUp, WolfStream, XVideoSharing
|
- **XFileShare**: XFileShare based sites: Aparat, ClipWatching, GoUnlimited, GoVid, HolaVid, Streamty, TheVideoBee, Uqload, VidBom, vidlo, VidLocker, VidShare, VUp, WolfStream, XVideoSharing
|
||||||
@@ -1694,7 +1702,7 @@ # Supported sites
|
|||||||
- **YouPorn**
|
- **YouPorn**
|
||||||
- **YourPorn**
|
- **YourPorn**
|
||||||
- **YourUpload**
|
- **YourUpload**
|
||||||
- **youtube+sample+NSIG+AGB**: YouTube
|
- **youtube**: YouTube
|
||||||
- **youtube:clip**
|
- **youtube:clip**
|
||||||
- **youtube:favorites**: YouTube liked videos; ":ytfav" keyword (requires cookies)
|
- **youtube:favorites**: YouTube liked videos; ":ytfav" keyword (requires cookies)
|
||||||
- **youtube:history**: Youtube watch history; ":ythis" keyword (requires cookies)
|
- **youtube:history**: Youtube watch history; ":ythis" keyword (requires cookies)
|
||||||
|
|||||||
227
test/test_config.py
Normal file
227
test/test_config.py
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# Allow direct execution
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
import unittest.mock
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import itertools
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from yt_dlp.compat import compat_expanduser
|
||||||
|
from yt_dlp.options import create_parser, parseOpts
|
||||||
|
from yt_dlp.utils import Config, get_executable_path
|
||||||
|
|
||||||
|
ENVIRON_DEFAULTS = {
|
||||||
|
'HOME': None,
|
||||||
|
'XDG_CONFIG_HOME': '/_xdg_config_home/',
|
||||||
|
'USERPROFILE': 'C:/Users/testing/',
|
||||||
|
'APPDATA': 'C:/Users/testing/AppData/Roaming/',
|
||||||
|
'HOMEDRIVE': 'C:/',
|
||||||
|
'HOMEPATH': 'Users/testing/',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def set_environ(**kwargs):
|
||||||
|
saved_environ = os.environ.copy()
|
||||||
|
|
||||||
|
for name, value in {**ENVIRON_DEFAULTS, **kwargs}.items():
|
||||||
|
if value is None:
|
||||||
|
os.environ.pop(name, None)
|
||||||
|
else:
|
||||||
|
os.environ[name] = value
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
os.environ.clear()
|
||||||
|
os.environ.update(saved_environ)
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_expected_groups():
|
||||||
|
xdg_config_home = os.getenv('XDG_CONFIG_HOME') or compat_expanduser('~/.config')
|
||||||
|
appdata_dir = os.getenv('appdata')
|
||||||
|
home_dir = compat_expanduser('~')
|
||||||
|
return {
|
||||||
|
'Portable': [
|
||||||
|
Path(get_executable_path(), 'yt-dlp.conf'),
|
||||||
|
],
|
||||||
|
'Home': [
|
||||||
|
Path('yt-dlp.conf'),
|
||||||
|
],
|
||||||
|
'User': [
|
||||||
|
Path(xdg_config_home, 'yt-dlp.conf'),
|
||||||
|
Path(xdg_config_home, 'yt-dlp', 'config'),
|
||||||
|
Path(xdg_config_home, 'yt-dlp', 'config.txt'),
|
||||||
|
*((
|
||||||
|
Path(appdata_dir, 'yt-dlp.conf'),
|
||||||
|
Path(appdata_dir, 'yt-dlp', 'config'),
|
||||||
|
Path(appdata_dir, 'yt-dlp', 'config.txt'),
|
||||||
|
) if appdata_dir else ()),
|
||||||
|
Path(home_dir, 'yt-dlp.conf'),
|
||||||
|
Path(home_dir, 'yt-dlp.conf.txt'),
|
||||||
|
Path(home_dir, '.yt-dlp', 'config'),
|
||||||
|
Path(home_dir, '.yt-dlp', 'config.txt'),
|
||||||
|
],
|
||||||
|
'System': [
|
||||||
|
Path('/etc/yt-dlp.conf'),
|
||||||
|
Path('/etc/yt-dlp/config'),
|
||||||
|
Path('/etc/yt-dlp/config.txt'),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TestConfig(unittest.TestCase):
|
||||||
|
maxDiff = None
|
||||||
|
|
||||||
|
@set_environ()
|
||||||
|
def test_config__ENVIRON_DEFAULTS_sanity(self):
|
||||||
|
expected = make_expected()
|
||||||
|
self.assertCountEqual(
|
||||||
|
set(expected), expected,
|
||||||
|
'ENVIRON_DEFAULTS produces non unique names')
|
||||||
|
|
||||||
|
def test_config_all_environ_values(self):
|
||||||
|
for name, value in ENVIRON_DEFAULTS.items():
|
||||||
|
for new_value in (None, '', '.', value or '/some/dir'):
|
||||||
|
with set_environ(**{name: new_value}):
|
||||||
|
self._simple_grouping_test()
|
||||||
|
|
||||||
|
def test_config_default_expected_locations(self):
|
||||||
|
files, _ = self._simple_config_test()
|
||||||
|
self.assertEqual(
|
||||||
|
files, make_expected(),
|
||||||
|
'Not all expected locations have been checked')
|
||||||
|
|
||||||
|
def test_config_default_grouping(self):
|
||||||
|
self._simple_grouping_test()
|
||||||
|
|
||||||
|
def _simple_grouping_test(self):
|
||||||
|
expected_groups = make_expected_groups()
|
||||||
|
for name, group in expected_groups.items():
|
||||||
|
for index, existing_path in enumerate(group):
|
||||||
|
result, opts = self._simple_config_test(existing_path)
|
||||||
|
expected = expected_from_expected_groups(expected_groups, existing_path)
|
||||||
|
self.assertEqual(
|
||||||
|
result, expected,
|
||||||
|
f'The checked locations do not match the expected ({name}, {index})')
|
||||||
|
self.assertEqual(
|
||||||
|
opts.outtmpl['default'], '1',
|
||||||
|
f'The used result value was incorrect ({name}, {index})')
|
||||||
|
|
||||||
|
def _simple_config_test(self, *stop_paths):
|
||||||
|
encountered = 0
|
||||||
|
paths = []
|
||||||
|
|
||||||
|
def read_file(filename, default=[]):
|
||||||
|
nonlocal encountered
|
||||||
|
path = Path(filename)
|
||||||
|
paths.append(path)
|
||||||
|
if path in stop_paths:
|
||||||
|
encountered += 1
|
||||||
|
return ['-o', f'{encountered}']
|
||||||
|
|
||||||
|
with ConfigMock(read_file):
|
||||||
|
_, opts, _ = parseOpts([], False)
|
||||||
|
|
||||||
|
return paths, opts
|
||||||
|
|
||||||
|
@set_environ()
|
||||||
|
def test_config_early_exit_commandline(self):
|
||||||
|
self._early_exit_test(0, '--ignore-config')
|
||||||
|
|
||||||
|
@set_environ()
|
||||||
|
def test_config_early_exit_files(self):
|
||||||
|
for index, _ in enumerate(make_expected(), 1):
|
||||||
|
self._early_exit_test(index)
|
||||||
|
|
||||||
|
def _early_exit_test(self, allowed_reads, *args):
|
||||||
|
reads = 0
|
||||||
|
|
||||||
|
def read_file(filename, default=[]):
|
||||||
|
nonlocal reads
|
||||||
|
reads += 1
|
||||||
|
|
||||||
|
if reads > allowed_reads:
|
||||||
|
self.fail('The remaining config was not ignored')
|
||||||
|
elif reads == allowed_reads:
|
||||||
|
return ['--ignore-config']
|
||||||
|
|
||||||
|
with ConfigMock(read_file):
|
||||||
|
parseOpts(args, False)
|
||||||
|
|
||||||
|
@set_environ()
|
||||||
|
def test_config_override_commandline(self):
|
||||||
|
self._override_test(0, '-o', 'pass')
|
||||||
|
|
||||||
|
@set_environ()
|
||||||
|
def test_config_override_files(self):
|
||||||
|
for index, _ in enumerate(make_expected(), 1):
|
||||||
|
self._override_test(index)
|
||||||
|
|
||||||
|
def _override_test(self, start_index, *args):
|
||||||
|
index = 0
|
||||||
|
|
||||||
|
def read_file(filename, default=[]):
|
||||||
|
nonlocal index
|
||||||
|
index += 1
|
||||||
|
|
||||||
|
if index > start_index:
|
||||||
|
return ['-o', 'fail']
|
||||||
|
elif index == start_index:
|
||||||
|
return ['-o', 'pass']
|
||||||
|
|
||||||
|
with ConfigMock(read_file):
|
||||||
|
_, opts, _ = parseOpts(args, False)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
opts.outtmpl['default'], 'pass',
|
||||||
|
'The earlier group did not override the later ones')
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def ConfigMock(read_file=None):
|
||||||
|
with unittest.mock.patch('yt_dlp.options.Config') as mock:
|
||||||
|
mock.return_value = Config(create_parser())
|
||||||
|
if read_file is not None:
|
||||||
|
mock.read_file = read_file
|
||||||
|
|
||||||
|
yield mock
|
||||||
|
|
||||||
|
|
||||||
|
def make_expected(*filepaths):
|
||||||
|
return expected_from_expected_groups(_generate_expected_groups(), *filepaths)
|
||||||
|
|
||||||
|
|
||||||
|
def make_expected_groups(*filepaths):
|
||||||
|
return _filter_expected_groups(_generate_expected_groups(), filepaths)
|
||||||
|
|
||||||
|
|
||||||
|
def expected_from_expected_groups(expected_groups, *filepaths):
|
||||||
|
return list(itertools.chain.from_iterable(
|
||||||
|
_filter_expected_groups(expected_groups, filepaths).values()))
|
||||||
|
|
||||||
|
|
||||||
|
def _filter_expected_groups(expected, filepaths):
|
||||||
|
if not filepaths:
|
||||||
|
return expected
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
for group, paths in expected.items():
|
||||||
|
new_paths = []
|
||||||
|
for path in paths:
|
||||||
|
new_paths.append(path)
|
||||||
|
if path in filepaths:
|
||||||
|
break
|
||||||
|
|
||||||
|
result[group] = new_paths
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
@@ -586,7 +586,6 @@ def __init__(self, params=None, auto_init=True):
|
|||||||
self._playlist_urls = set()
|
self._playlist_urls = set()
|
||||||
self.cache = Cache(self)
|
self.cache = Cache(self)
|
||||||
|
|
||||||
windows_enable_vt_mode()
|
|
||||||
stdout = sys.stderr if self.params.get('logtostderr') else sys.stdout
|
stdout = sys.stderr if self.params.get('logtostderr') else sys.stdout
|
||||||
self._out_files = Namespace(
|
self._out_files = Namespace(
|
||||||
out=stdout,
|
out=stdout,
|
||||||
@@ -595,6 +594,12 @@ def __init__(self, params=None, auto_init=True):
|
|||||||
console=None if compat_os_name == 'nt' else next(
|
console=None if compat_os_name == 'nt' else next(
|
||||||
filter(supports_terminal_sequences, (sys.stderr, sys.stdout)), None)
|
filter(supports_terminal_sequences, (sys.stderr, sys.stdout)), None)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
windows_enable_vt_mode()
|
||||||
|
except Exception as e:
|
||||||
|
self.write_debug(f'Failed to enable VT mode: {e}')
|
||||||
|
|
||||||
self._allow_colors = Namespace(**{
|
self._allow_colors = Namespace(**{
|
||||||
type_: not self.params.get('no_color') and supports_terminal_sequences(stream)
|
type_: not self.params.get('no_color') and supports_terminal_sequences(stream)
|
||||||
for type_, stream in self._out_files.items_ if type_ != 'console'
|
for type_, stream in self._out_files.items_ if type_ != 'console'
|
||||||
|
|||||||
@@ -262,7 +262,8 @@ def _aria2c_filename(fn):
|
|||||||
return fn if os.path.isabs(fn) else f'.{os.path.sep}{fn}'
|
return fn if os.path.isabs(fn) else f'.{os.path.sep}{fn}'
|
||||||
|
|
||||||
def _call_downloader(self, tmpfilename, info_dict):
|
def _call_downloader(self, tmpfilename, info_dict):
|
||||||
if 'no-external-downloader-progress' not in self.params.get('compat_opts', []):
|
# FIXME: Disabled due to https://github.com/yt-dlp/yt-dlp/issues/5931
|
||||||
|
if False and 'no-external-downloader-progress' not in self.params.get('compat_opts', []):
|
||||||
info_dict['__rpc'] = {
|
info_dict['__rpc'] = {
|
||||||
'port': find_available_port() or 19190,
|
'port': find_available_port() or 19190,
|
||||||
'secret': str(uuid.uuid4()),
|
'secret': str(uuid.uuid4()),
|
||||||
|
|||||||
@@ -79,6 +79,7 @@
|
|||||||
)
|
)
|
||||||
from .airmozilla import AirMozillaIE
|
from .airmozilla import AirMozillaIE
|
||||||
from .airtv import AirTVIE
|
from .airtv import AirTVIE
|
||||||
|
from .aitube import AitubeKZVideoIE
|
||||||
from .aljazeera import AlJazeeraIE
|
from .aljazeera import AlJazeeraIE
|
||||||
from .alphaporno import AlphaPornoIE
|
from .alphaporno import AlphaPornoIE
|
||||||
from .amara import AmaraIE
|
from .amara import AmaraIE
|
||||||
@@ -474,6 +475,8 @@
|
|||||||
from .drtv import (
|
from .drtv import (
|
||||||
DRTVIE,
|
DRTVIE,
|
||||||
DRTVLiveIE,
|
DRTVLiveIE,
|
||||||
|
DRTVSeasonIE,
|
||||||
|
DRTVSeriesIE,
|
||||||
)
|
)
|
||||||
from .dtube import DTubeIE
|
from .dtube import DTubeIE
|
||||||
from .dvtv import DVTVIE
|
from .dvtv import DVTVIE
|
||||||
@@ -1889,6 +1892,7 @@
|
|||||||
TikTokEffectIE,
|
TikTokEffectIE,
|
||||||
TikTokTagIE,
|
TikTokTagIE,
|
||||||
TikTokVMIE,
|
TikTokVMIE,
|
||||||
|
TikTokLiveIE,
|
||||||
DouyinIE,
|
DouyinIE,
|
||||||
)
|
)
|
||||||
from .tinypic import TinyPicIE
|
from .tinypic import TinyPicIE
|
||||||
@@ -2184,6 +2188,7 @@
|
|||||||
VoicyIE,
|
VoicyIE,
|
||||||
VoicyChannelIE,
|
VoicyChannelIE,
|
||||||
)
|
)
|
||||||
|
from .volejtv import VolejTVIE
|
||||||
from .voot import (
|
from .voot import (
|
||||||
VootIE,
|
VootIE,
|
||||||
VootSeriesIE,
|
VootSeriesIE,
|
||||||
@@ -2266,6 +2271,7 @@
|
|||||||
WSJArticleIE,
|
WSJArticleIE,
|
||||||
)
|
)
|
||||||
from .wwe import WWEIE
|
from .wwe import WWEIE
|
||||||
|
from .xanimu import XanimuIE
|
||||||
from .xbef import XBefIE
|
from .xbef import XBefIE
|
||||||
from .xboxclips import XboxClipsIE
|
from .xboxclips import XboxClipsIE
|
||||||
from .xfileshare import XFileShareIE
|
from .xfileshare import XFileShareIE
|
||||||
|
|||||||
60
yt_dlp/extractor/aitube.py
Normal file
60
yt_dlp/extractor/aitube.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import int_or_none, merge_dicts
|
||||||
|
|
||||||
|
|
||||||
|
class AitubeKZVideoIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://aitube\.kz/(?:video|embed/)\?(?:[^\?]+)?id=(?P<id>[\w-]+)'
|
||||||
|
_TESTS = [{
|
||||||
|
# id paramater as first parameter
|
||||||
|
'url': 'https://aitube.kz/video?id=9291d29b-c038-49a1-ad42-3da2051d353c&playlistId=d55b1f5f-ef2a-4f23-b646-2a86275b86b7&season=1',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '9291d29b-c038-49a1-ad42-3da2051d353c',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'duration': 2174.0,
|
||||||
|
'channel_id': '94962f73-013b-432c-8853-1bd78ca860fe',
|
||||||
|
'like_count': int,
|
||||||
|
'channel': 'ASTANA TV',
|
||||||
|
'comment_count': int,
|
||||||
|
'view_count': int,
|
||||||
|
'description': 'Смотреть любимые сериалы и видео, поделиться видео и сериалами с друзьями и близкими',
|
||||||
|
'thumbnail': 'https://cdn.static02.aitube.kz/kz.aitudala.aitube.staticaccess/files/ddf2a2ff-bee3-409b-b5f2-2a8202bba75b',
|
||||||
|
'upload_date': '20221102',
|
||||||
|
'timestamp': 1667370519,
|
||||||
|
'title': 'Ангел хранитель 1 серия',
|
||||||
|
'channel_follower_count': int,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
# embed url
|
||||||
|
'url': 'https://aitube.kz/embed/?id=9291d29b-c038-49a1-ad42-3da2051d353c',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
# id parameter is not as first paramater
|
||||||
|
'url': 'https://aitube.kz/video?season=1&id=9291d29b-c038-49a1-ad42-3da2051d353c&playlistId=d55b1f5f-ef2a-4f23-b646-2a86275b86b7',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
|
nextjs_data = self._search_nextjs_data(webpage, video_id)['props']['pageProps']['videoInfo']
|
||||||
|
json_ld_data = self._search_json_ld(webpage, video_id)
|
||||||
|
|
||||||
|
formats, subtitles = self._extract_m3u8_formats_and_subtitles(
|
||||||
|
f'https://api-http.aitube.kz/kz.aitudala.aitube.staticaccess/video/{video_id}/video', video_id)
|
||||||
|
|
||||||
|
return merge_dicts({
|
||||||
|
'id': video_id,
|
||||||
|
'title': nextjs_data.get('title') or self._html_search_meta(['name', 'og:title'], webpage),
|
||||||
|
'description': nextjs_data.get('description'),
|
||||||
|
'formats': formats,
|
||||||
|
'subtitles': subtitles,
|
||||||
|
'view_count': (nextjs_data.get('viewCount')
|
||||||
|
or int_or_none(self._html_search_meta('ya:ovs:views_total', webpage))),
|
||||||
|
'like_count': nextjs_data.get('likeCount'),
|
||||||
|
'channel': nextjs_data.get('channelTitle'),
|
||||||
|
'channel_id': nextjs_data.get('channelId'),
|
||||||
|
'thumbnail': nextjs_data.get('coverUrl'),
|
||||||
|
'comment_count': nextjs_data.get('commentCount'),
|
||||||
|
'channel_follower_count': int_or_none(nextjs_data.get('channelSubscriberCount')),
|
||||||
|
}, json_ld_data)
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
format_field,
|
format_field,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
make_archive_id,
|
make_archive_id,
|
||||||
|
merge_dicts,
|
||||||
mimetype2ext,
|
mimetype2ext,
|
||||||
parse_count,
|
parse_count,
|
||||||
parse_qs,
|
parse_qs,
|
||||||
@@ -934,6 +935,10 @@ class BiliIntlIE(BiliIntlBaseIE):
|
|||||||
'title': 'E2 - The First Night',
|
'title': 'E2 - The First Night',
|
||||||
'thumbnail': r're:^https://pic\.bstarstatic\.com/ogv/.+\.png$',
|
'thumbnail': r're:^https://pic\.bstarstatic\.com/ogv/.+\.png$',
|
||||||
'episode_number': 2,
|
'episode_number': 2,
|
||||||
|
'upload_date': '20201009',
|
||||||
|
'episode': 'Episode 2',
|
||||||
|
'timestamp': 1602259500,
|
||||||
|
'description': 'md5:297b5a17155eb645e14a14b385ab547e',
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
# Non-Bstation page
|
# Non-Bstation page
|
||||||
@@ -944,6 +949,10 @@ class BiliIntlIE(BiliIntlBaseIE):
|
|||||||
'title': 'E3 - Who?',
|
'title': 'E3 - Who?',
|
||||||
'thumbnail': r're:^https://pic\.bstarstatic\.com/ogv/.+\.png$',
|
'thumbnail': r're:^https://pic\.bstarstatic\.com/ogv/.+\.png$',
|
||||||
'episode_number': 3,
|
'episode_number': 3,
|
||||||
|
'description': 'md5:e1a775e71a35c43f141484715470ad09',
|
||||||
|
'episode': 'Episode 3',
|
||||||
|
'upload_date': '20211219',
|
||||||
|
'timestamp': 1639928700,
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
# Subtitle with empty content
|
# Subtitle with empty content
|
||||||
@@ -956,6 +965,17 @@ class BiliIntlIE(BiliIntlBaseIE):
|
|||||||
'episode_number': 140,
|
'episode_number': 140,
|
||||||
},
|
},
|
||||||
'skip': 'According to the copyright owner\'s request, you may only watch the video after you log in.'
|
'skip': 'According to the copyright owner\'s request, you may only watch the video after you log in.'
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.bilibili.tv/en/video/2041863208',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '2041863208',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'timestamp': 1670874843,
|
||||||
|
'description': 'Scheduled for April 2023.\nStudio: ufotable',
|
||||||
|
'thumbnail': r're:https?://pic[-\.]bstarstatic.+/ugc/.+\.jpg$',
|
||||||
|
'upload_date': '20221212',
|
||||||
|
'title': 'Kimetsu no Yaiba Season 3 Official Trailer - Bstation',
|
||||||
|
}
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.biliintl.com/en/play/34613/341736',
|
'url': 'https://www.biliintl.com/en/play/34613/341736',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
@@ -989,7 +1009,7 @@ def _extract_video_metadata(self, url, video_id, season_id):
|
|||||||
self._search_json(r'window\.__INITIAL_(?:DATA|STATE)__\s*=', webpage, 'preload state', video_id, default={})
|
self._search_json(r'window\.__INITIAL_(?:DATA|STATE)__\s*=', webpage, 'preload state', video_id, default={})
|
||||||
or self._search_nuxt_data(webpage, video_id, '__initialState', fatal=False, traverse=None))
|
or self._search_nuxt_data(webpage, video_id, '__initialState', fatal=False, traverse=None))
|
||||||
video_data = traverse_obj(
|
video_data = traverse_obj(
|
||||||
initial_data, ('OgvVideo', 'epDetail'), ('UgcVideo', 'videoData'), ('ugc', 'archive'), expected_type=dict)
|
initial_data, ('OgvVideo', 'epDetail'), ('UgcVideo', 'videoData'), ('ugc', 'archive'), expected_type=dict) or {}
|
||||||
|
|
||||||
if season_id and not video_data:
|
if season_id and not video_data:
|
||||||
# Non-Bstation layout, read through episode list
|
# Non-Bstation layout, read through episode list
|
||||||
@@ -998,7 +1018,12 @@ def _extract_video_metadata(self, url, video_id, season_id):
|
|||||||
'sections', ..., 'episodes', lambda _, v: str(v['episode_id']) == video_id
|
'sections', ..., 'episodes', lambda _, v: str(v['episode_id']) == video_id
|
||||||
), expected_type=dict, get_all=False)
|
), expected_type=dict, get_all=False)
|
||||||
|
|
||||||
return self._parse_video_metadata(video_data)
|
# XXX: webpage metadata may not accurate, it just used to not crash when video_data not found
|
||||||
|
return merge_dicts(
|
||||||
|
self._parse_video_metadata(video_data), self._search_json_ld(webpage, video_id), {
|
||||||
|
'title': self._html_search_meta('og:title', webpage),
|
||||||
|
'description': self._html_search_meta('og:description', webpage)
|
||||||
|
})
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
season_id, ep_id, aid = self._match_valid_url(url).group('season_id', 'ep_id', 'aid')
|
season_id, ep_id, aid = self._match_valid_url(url).group('season_id', 'ep_id', 'aid')
|
||||||
@@ -1014,21 +1039,32 @@ def _real_extract(self, url):
|
|||||||
|
|
||||||
class BiliIntlSeriesIE(BiliIntlBaseIE):
|
class BiliIntlSeriesIE(BiliIntlBaseIE):
|
||||||
IE_NAME = 'biliIntl:series'
|
IE_NAME = 'biliIntl:series'
|
||||||
_VALID_URL = r'https?://(?:www\.)?bili(?:bili\.tv|intl\.com)/(?:[a-zA-Z]{2}/)?play/(?P<id>\d+)/?(?:[?#]|$)'
|
_VALID_URL = r'https?://(?:www\.)?bili(?:bili\.tv|intl\.com)/(?:[a-zA-Z]{2}/)?(?:play|media)/(?P<id>\d+)/?(?:[?#]|$)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.bilibili.tv/en/play/34613',
|
'url': 'https://www.bilibili.tv/en/play/34613',
|
||||||
'playlist_mincount': 15,
|
'playlist_mincount': 15,
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '34613',
|
'id': '34613',
|
||||||
'title': 'Fly Me to the Moon',
|
'title': 'TONIKAWA: Over the Moon For You',
|
||||||
'description': 'md5:a861ee1c4dc0acfad85f557cc42ac627',
|
'description': 'md5:297b5a17155eb645e14a14b385ab547e',
|
||||||
'categories': ['Romance', 'Comedy', 'Slice of life'],
|
'categories': ['Slice of life', 'Comedy', 'Romance'],
|
||||||
'thumbnail': r're:^https://pic\.bstarstatic\.com/ogv/.+\.png$',
|
'thumbnail': r're:^https://pic\.bstarstatic\.com/ogv/.+\.png$',
|
||||||
'view_count': int,
|
'view_count': int,
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.bilibili.tv/en/media/1048837',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '1048837',
|
||||||
|
'title': 'SPY×FAMILY',
|
||||||
|
'description': 'md5:b4434eb1a9a97ad2bccb779514b89f17',
|
||||||
|
'categories': ['Adventure', 'Action', 'Comedy'],
|
||||||
|
'thumbnail': r're:^https://pic\.bstarstatic\.com/ogv/.+\.jpg$',
|
||||||
|
'view_count': int,
|
||||||
|
},
|
||||||
|
'playlist_mincount': 25,
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.biliintl.com/en/play/34613',
|
'url': 'https://www.biliintl.com/en/play/34613',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
|||||||
@@ -1263,11 +1263,8 @@ def _html_search_regex(self, pattern, string, name, default=NO_DEFAULT, fatal=Tr
|
|||||||
"""
|
"""
|
||||||
res = self._search_regex(pattern, string, name, default, fatal, flags, group)
|
res = self._search_regex(pattern, string, name, default, fatal, flags, group)
|
||||||
if isinstance(res, tuple):
|
if isinstance(res, tuple):
|
||||||
return [clean_html(r).strip() for r in res]
|
return tuple(map(clean_html, res))
|
||||||
elif res:
|
return clean_html(res)
|
||||||
return clean_html(res).strip()
|
|
||||||
else:
|
|
||||||
return res
|
|
||||||
|
|
||||||
def _get_netrc_login_info(self, netrc_machine=None):
|
def _get_netrc_login_info(self, netrc_machine=None):
|
||||||
username = None
|
username = None
|
||||||
|
|||||||
@@ -291,7 +291,8 @@ def entries():
|
|||||||
'season_id': episode.get('season_id'),
|
'season_id': episode.get('season_id'),
|
||||||
'season_number': episode.get('season_number'),
|
'season_number': episode.get('season_number'),
|
||||||
'episode': episode.get('title'),
|
'episode': episode.get('title'),
|
||||||
'episode_number': episode.get('sequence_number')
|
'episode_number': episode.get('sequence_number'),
|
||||||
|
'language': episode.get('audio_locale'),
|
||||||
}
|
}
|
||||||
|
|
||||||
return self.playlist_result(entries(), internal_id, series_response.get('title'))
|
return self.playlist_result(entries(), internal_id, series_response.get('title'))
|
||||||
|
|||||||
@@ -2,22 +2,24 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..aes import aes_cbc_decrypt_bytes, unpad_pkcs7
|
from ..aes import aes_cbc_decrypt_bytes, unpad_pkcs7
|
||||||
from ..compat import compat_urllib_parse_unquote
|
from ..compat import compat_urllib_parse_unquote
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
int_or_none,
|
|
||||||
float_or_none,
|
float_or_none,
|
||||||
|
int_or_none,
|
||||||
mimetype2ext,
|
mimetype2ext,
|
||||||
str_or_none,
|
str_or_none,
|
||||||
|
traverse_obj,
|
||||||
try_get,
|
try_get,
|
||||||
unified_timestamp,
|
unified_timestamp,
|
||||||
update_url_query,
|
update_url_query,
|
||||||
url_or_none,
|
url_or_none,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SERIES_API = 'https://production-cdn.dr-massive.com/api/page?device=web_browser&item_detail_expand=all&lang=da&max_list_prefetch=3&path=%s'
|
||||||
|
|
||||||
|
|
||||||
class DRTVIE(InfoExtractor):
|
class DRTVIE(InfoExtractor):
|
||||||
_VALID_URL = r'''(?x)
|
_VALID_URL = r'''(?x)
|
||||||
@@ -141,13 +143,13 @@ class DRTVIE(InfoExtractor):
|
|||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
raw_video_id = self._match_id(url)
|
||||||
|
|
||||||
webpage = self._download_webpage(url, video_id)
|
webpage = self._download_webpage(url, raw_video_id)
|
||||||
|
|
||||||
if '>Programmet er ikke længere tilgængeligt' in webpage:
|
if '>Programmet er ikke længere tilgængeligt' in webpage:
|
||||||
raise ExtractorError(
|
raise ExtractorError(
|
||||||
'Video %s is not available' % video_id, expected=True)
|
'Video %s is not available' % raw_video_id, expected=True)
|
||||||
|
|
||||||
video_id = self._search_regex(
|
video_id = self._search_regex(
|
||||||
(r'data-(?:material-identifier|episode-slug)="([^"]+)"',
|
(r'data-(?:material-identifier|episode-slug)="([^"]+)"',
|
||||||
@@ -182,6 +184,10 @@ def _real_extract(self, url):
|
|||||||
data = self._download_json(
|
data = self._download_json(
|
||||||
programcard_url, video_id, 'Downloading video JSON', query=query)
|
programcard_url, video_id, 'Downloading video JSON', query=query)
|
||||||
|
|
||||||
|
supplementary_data = self._download_json(
|
||||||
|
SERIES_API % f'/episode/{raw_video_id}', raw_video_id,
|
||||||
|
default={}) if re.search(r'_\d+$', raw_video_id) else {}
|
||||||
|
|
||||||
title = str_or_none(data.get('Title')) or re.sub(
|
title = str_or_none(data.get('Title')) or re.sub(
|
||||||
r'\s*\|\s*(?:TV\s*\|\s*DR|DRTV)$', '',
|
r'\s*\|\s*(?:TV\s*\|\s*DR|DRTV)$', '',
|
||||||
self._og_search_title(webpage))
|
self._og_search_title(webpage))
|
||||||
@@ -313,8 +319,8 @@ def decrypt_uri(e):
|
|||||||
'season': str_or_none(data.get('SeasonTitle')),
|
'season': str_or_none(data.get('SeasonTitle')),
|
||||||
'season_number': int_or_none(data.get('SeasonNumber')),
|
'season_number': int_or_none(data.get('SeasonNumber')),
|
||||||
'season_id': str_or_none(data.get('SeasonUrn')),
|
'season_id': str_or_none(data.get('SeasonUrn')),
|
||||||
'episode': str_or_none(data.get('EpisodeTitle')),
|
'episode': traverse_obj(supplementary_data, ('entries', 0, 'item', 'contextualTitle')) or str_or_none(data.get('EpisodeTitle')),
|
||||||
'episode_number': int_or_none(data.get('EpisodeNumber')),
|
'episode_number': traverse_obj(supplementary_data, ('entries', 0, 'item', 'episodeNumber')) or int_or_none(data.get('EpisodeNumber')),
|
||||||
'release_year': int_or_none(data.get('ProductionYear')),
|
'release_year': int_or_none(data.get('ProductionYear')),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,3 +378,92 @@ def _real_extract(self, url):
|
|||||||
'formats': formats,
|
'formats': formats,
|
||||||
'is_live': True,
|
'is_live': True,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DRTVSeasonIE(InfoExtractor):
|
||||||
|
IE_NAME = 'drtv:season'
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?(?:dr\.dk|dr-massive\.com)/drtv/saeson/(?P<display_id>[\w-]+)_(?P<id>\d+)'
|
||||||
|
_GEO_COUNTRIES = ['DK']
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.dr.dk/drtv/saeson/frank-and-kastaniegaarden_9008',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '9008',
|
||||||
|
'display_id': 'frank-and-kastaniegaarden',
|
||||||
|
'title': 'Frank & Kastaniegaarden',
|
||||||
|
'series': 'Frank & Kastaniegaarden',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 8
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.dr.dk/drtv/saeson/frank-and-kastaniegaarden_8761',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '8761',
|
||||||
|
'display_id': 'frank-and-kastaniegaarden',
|
||||||
|
'title': 'Frank & Kastaniegaarden',
|
||||||
|
'series': 'Frank & Kastaniegaarden',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 19
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
display_id, season_id = self._match_valid_url(url).group('display_id', 'id')
|
||||||
|
data = self._download_json(SERIES_API % f'/saeson/{display_id}_{season_id}', display_id)
|
||||||
|
|
||||||
|
entries = [{
|
||||||
|
'_type': 'url',
|
||||||
|
'url': f'https://www.dr.dk/drtv{episode["path"]}',
|
||||||
|
'ie_key': DRTVIE.ie_key(),
|
||||||
|
'title': episode.get('title'),
|
||||||
|
'episode': episode.get('episodeName'),
|
||||||
|
'description': episode.get('shortDescription'),
|
||||||
|
'series': traverse_obj(data, ('entries', 0, 'item', 'title')),
|
||||||
|
'season_number': traverse_obj(data, ('entries', 0, 'item', 'seasonNumber')),
|
||||||
|
'episode_number': episode.get('episodeNumber'),
|
||||||
|
} for episode in traverse_obj(data, ('entries', 0, 'item', 'episodes', 'items'))]
|
||||||
|
|
||||||
|
return {
|
||||||
|
'_type': 'playlist',
|
||||||
|
'id': season_id,
|
||||||
|
'display_id': display_id,
|
||||||
|
'title': traverse_obj(data, ('entries', 0, 'item', 'title')),
|
||||||
|
'series': traverse_obj(data, ('entries', 0, 'item', 'title')),
|
||||||
|
'entries': entries,
|
||||||
|
'season_number': traverse_obj(data, ('entries', 0, 'item', 'seasonNumber'))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DRTVSeriesIE(InfoExtractor):
|
||||||
|
IE_NAME = 'drtv:series'
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?(?:dr\.dk|dr-massive\.com)/drtv/serie/(?P<display_id>[\w-]+)_(?P<id>\d+)'
|
||||||
|
_GEO_COUNTRIES = ['DK']
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.dr.dk/drtv/serie/frank-and-kastaniegaarden_6954',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '6954',
|
||||||
|
'display_id': 'frank-and-kastaniegaarden',
|
||||||
|
'title': 'Frank & Kastaniegaarden',
|
||||||
|
'series': 'Frank & Kastaniegaarden',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 15
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
display_id, series_id = self._match_valid_url(url).group('display_id', 'id')
|
||||||
|
data = self._download_json(SERIES_API % f'/serie/{display_id}_{series_id}', display_id)
|
||||||
|
|
||||||
|
entries = [{
|
||||||
|
'_type': 'url',
|
||||||
|
'url': f'https://www.dr.dk/drtv{season.get("path")}',
|
||||||
|
'ie_key': DRTVSeasonIE.ie_key(),
|
||||||
|
'title': season.get('title'),
|
||||||
|
'series': traverse_obj(data, ('entries', 0, 'item', 'title')),
|
||||||
|
'season_number': traverse_obj(data, ('entries', 0, 'item', 'seasonNumber'))
|
||||||
|
} for season in traverse_obj(data, ('entries', 0, 'item', 'show', 'seasons', 'items'))]
|
||||||
|
|
||||||
|
return {
|
||||||
|
'_type': 'playlist',
|
||||||
|
'id': series_id,
|
||||||
|
'display_id': display_id,
|
||||||
|
'title': traverse_obj(data, ('entries', 0, 'item', 'title')),
|
||||||
|
'series': traverse_obj(data, ('entries', 0, 'item', 'title')),
|
||||||
|
'entries': entries
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,9 +23,19 @@ class JojIE(InfoExtractor):
|
|||||||
'id': 'a388ec4c-6019-4a4a-9312-b1bee194e932',
|
'id': 'a388ec4c-6019-4a4a-9312-b1bee194e932',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'NOVÉ BÝVANIE',
|
'title': 'NOVÉ BÝVANIE',
|
||||||
'thumbnail': r're:^https?://.*\.jpg$',
|
'thumbnail': r're:^https?://.*?$',
|
||||||
'duration': 3118,
|
'duration': 3118,
|
||||||
}
|
}
|
||||||
|
}, {
|
||||||
|
'url': 'https://media.joj.sk/embed/CSM0Na0l0p1',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'CSM0Na0l0p1',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'height': 576,
|
||||||
|
'title': 'Extrémne rodiny 2 - POKRAČOVANIE (2012/04/09 21:30:00)',
|
||||||
|
'duration': 3937,
|
||||||
|
'thumbnail': r're:^https?://.*?$',
|
||||||
|
}
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://media.joj.sk/embed/9i1cxv',
|
'url': 'https://media.joj.sk/embed/9i1cxv',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
@@ -43,10 +53,10 @@ def _real_extract(self, url):
|
|||||||
webpage = self._download_webpage(
|
webpage = self._download_webpage(
|
||||||
'https://media.joj.sk/embed/%s' % video_id, video_id)
|
'https://media.joj.sk/embed/%s' % video_id, video_id)
|
||||||
|
|
||||||
title = self._search_regex(
|
title = (self._search_json(r'videoTitle\s*:', webpage, 'title', video_id,
|
||||||
(r'videoTitle\s*:\s*(["\'])(?P<title>(?:(?!\1).)+)\1',
|
contains_pattern=r'["\'].+["\']', default=None)
|
||||||
r'<title>(?P<title>[^<]+)'), webpage, 'title',
|
or self._html_extract_title(webpage, default=None)
|
||||||
default=None, group='title') or self._og_search_title(webpage)
|
or self._og_search_title(webpage))
|
||||||
|
|
||||||
bitrates = self._parse_json(
|
bitrates = self._parse_json(
|
||||||
self._search_regex(
|
self._search_regex(
|
||||||
@@ -58,11 +68,13 @@ def _real_extract(self, url):
|
|||||||
for format_url in try_get(bitrates, lambda x: x['mp4'], list) or []:
|
for format_url in try_get(bitrates, lambda x: x['mp4'], list) or []:
|
||||||
if isinstance(format_url, compat_str):
|
if isinstance(format_url, compat_str):
|
||||||
height = self._search_regex(
|
height = self._search_regex(
|
||||||
r'(\d+)[pP]\.', format_url, 'height', default=None)
|
r'(\d+)[pP]|(pal)\.', format_url, 'height', default=None)
|
||||||
|
if height == 'pal':
|
||||||
|
height = 576
|
||||||
formats.append({
|
formats.append({
|
||||||
'url': format_url,
|
'url': format_url,
|
||||||
'format_id': format_field(height, None, '%sp'),
|
'format_id': format_field(height, None, '%sp'),
|
||||||
'height': int(height),
|
'height': int_or_none(height),
|
||||||
})
|
})
|
||||||
if not formats:
|
if not formats:
|
||||||
playlist = self._download_xml(
|
playlist = self._download_xml(
|
||||||
|
|||||||
@@ -136,6 +136,7 @@ def _real_extract(self, url):
|
|||||||
query = {
|
query = {
|
||||||
'mbr': 'true',
|
'mbr': 'true',
|
||||||
'manifest': 'm3u',
|
'manifest': 'm3u',
|
||||||
|
'switch': 'HLSServiceSecure',
|
||||||
}
|
}
|
||||||
video_id = video_data['mpxGuid']
|
video_id = video_data['mpxGuid']
|
||||||
tp_path = 'NnzsPC/media/guid/%s/%s' % (video_data.get('mpxAccountId') or '2410887629', video_id)
|
tp_path = 'NnzsPC/media/guid/%s/%s' % (video_data.get('mpxAccountId') or '2410887629', video_id)
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class RedditIE(InfoExtractor):
|
|||||||
'dislike_count': int,
|
'dislike_count': int,
|
||||||
'comment_count': int,
|
'comment_count': int,
|
||||||
'age_limit': 0,
|
'age_limit': 0,
|
||||||
|
'channel_id': 'videos',
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
@@ -55,6 +56,7 @@ class RedditIE(InfoExtractor):
|
|||||||
'dislike_count': int,
|
'dislike_count': int,
|
||||||
'comment_count': int,
|
'comment_count': int,
|
||||||
'age_limit': 0,
|
'age_limit': 0,
|
||||||
|
'channel_id': 'aww',
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
# videos embedded in reddit text post
|
# videos embedded in reddit text post
|
||||||
@@ -165,6 +167,7 @@ def add_thumbnail(src):
|
|||||||
'thumbnails': thumbnails,
|
'thumbnails': thumbnails,
|
||||||
'timestamp': float_or_none(data.get('created_utc')),
|
'timestamp': float_or_none(data.get('created_utc')),
|
||||||
'uploader': data.get('author'),
|
'uploader': data.get('author'),
|
||||||
|
'channel_id': data.get('subreddit'),
|
||||||
'like_count': int_or_none(data.get('ups')),
|
'like_count': int_or_none(data.get('ups')),
|
||||||
'dislike_count': int_or_none(data.get('downs')),
|
'dislike_count': int_or_none(data.get('downs')),
|
||||||
'comment_count': int_or_none(data.get('num_comments')),
|
'comment_count': int_or_none(data.get('num_comments')),
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
HEADRequest,
|
HEADRequest,
|
||||||
LazyList,
|
LazyList,
|
||||||
UnsupportedError,
|
UnsupportedError,
|
||||||
|
UserNotLive,
|
||||||
get_element_by_id,
|
get_element_by_id,
|
||||||
get_first,
|
get_first,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
@@ -980,3 +981,42 @@ def _real_extract(self, url):
|
|||||||
if self.suitable(new_url): # Prevent infinite loop in case redirect fails
|
if self.suitable(new_url): # Prevent infinite loop in case redirect fails
|
||||||
raise UnsupportedError(new_url)
|
raise UnsupportedError(new_url)
|
||||||
return self.url_result(new_url)
|
return self.url_result(new_url)
|
||||||
|
|
||||||
|
|
||||||
|
class TikTokLiveIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?tiktok\.com/@(?P<id>[\w\.-]+)/live'
|
||||||
|
IE_NAME = 'tiktok:live'
|
||||||
|
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.tiktok.com/@iris04201/live',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
uploader = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, uploader, headers={'User-Agent': 'User-Agent:Mozilla/5.0'})
|
||||||
|
room_id = self._html_search_regex(r'snssdk\d*://live\?room_id=(\d+)', webpage, 'room ID', default=None)
|
||||||
|
if not room_id:
|
||||||
|
raise UserNotLive(video_id=uploader)
|
||||||
|
live_info = traverse_obj(self._download_json(
|
||||||
|
'https://www.tiktok.com/api/live/detail/', room_id, query={
|
||||||
|
'aid': '1988',
|
||||||
|
'roomID': room_id,
|
||||||
|
}), 'LiveRoomInfo', expected_type=dict, default={})
|
||||||
|
|
||||||
|
if 'status' not in live_info:
|
||||||
|
raise ExtractorError('Unexpected response from TikTok API')
|
||||||
|
# status = 2 if live else 4
|
||||||
|
if not int_or_none(live_info['status']) == 2:
|
||||||
|
raise UserNotLive(video_id=uploader)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': room_id,
|
||||||
|
'title': live_info.get('title') or self._html_search_meta(['og:title', 'twitter:title'], webpage, default=''),
|
||||||
|
'uploader': uploader,
|
||||||
|
'uploader_id': traverse_obj(live_info, ('ownerInfo', 'id')),
|
||||||
|
'creator': traverse_obj(live_info, ('ownerInfo', 'nickname')),
|
||||||
|
'concurrent_view_count': traverse_obj(live_info, ('liveRoomStats', 'userCount'), expected_type=int),
|
||||||
|
'formats': self._extract_m3u8_formats(live_info['liveUrl'], room_id, 'mp4', live=True),
|
||||||
|
'is_live': True,
|
||||||
|
}
|
||||||
|
|||||||
40
yt_dlp/extractor/volejtv.py
Normal file
40
yt_dlp/extractor/volejtv.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
from .common import InfoExtractor
|
||||||
|
|
||||||
|
|
||||||
|
class VolejTVIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://volej\.tv/video/(?P<id>\d+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://volej.tv/video/725742/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '725742',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'description': 'Zápas VK Královo Pole vs VK Prostějov 10.12.2022 v 19:00 na Volej.TV',
|
||||||
|
'thumbnail': 'https://volej.tv/images/og/16/17186/og.png',
|
||||||
|
'title': 'VK Královo Pole vs VK Prostějov',
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
'url': 'https://volej.tv/video/725605/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '725605',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'thumbnail': 'https://volej.tv/images/og/15/17185/og.png',
|
||||||
|
'title': 'VK Lvi Praha vs VK Euro Sitex Příbram',
|
||||||
|
'description': 'Zápas VK Lvi Praha vs VK Euro Sitex Příbram 11.12.2022 v 19:00 na Volej.TV',
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
json_data = self._search_json(
|
||||||
|
r'<\s*!\[CDATA[^=]+=', webpage, 'CDATA', video_id)
|
||||||
|
formats, subtitle = self._extract_m3u8_formats_and_subtitles(
|
||||||
|
json_data['urls']['hls'], video_id)
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': self._html_search_meta(['og:title', 'twitter:title'], webpage),
|
||||||
|
'thumbnail': self._html_search_meta(['og:image', 'twitter:image'], webpage),
|
||||||
|
'description': self._html_search_meta(['description', 'og:description', 'twitter:description'], webpage),
|
||||||
|
'formats': formats,
|
||||||
|
'subtitles': subtitle,
|
||||||
|
}
|
||||||
51
yt_dlp/extractor/xanimu.py
Normal file
51
yt_dlp/extractor/xanimu.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import int_or_none
|
||||||
|
|
||||||
|
|
||||||
|
class XanimuIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?xanimu\.com/(?P<id>[^/]+)/?'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://xanimu.com/51944-the-princess-the-frog-hentai/',
|
||||||
|
'md5': '899b88091d753d92dad4cb63bbf357a7',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '51944-the-princess-the-frog-hentai',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'The Princess + The Frog Hentai',
|
||||||
|
'thumbnail': 'https://xanimu.com/storage/2020/09/the-princess-and-the-frog-hentai.jpg',
|
||||||
|
'description': r're:^Enjoy The Princess \+ The Frog Hentai',
|
||||||
|
'duration': 207.0,
|
||||||
|
'age_limit': 18
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
'url': 'https://xanimu.com/huge-expansion/',
|
||||||
|
'only_matching': True
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
|
formats = []
|
||||||
|
for format in ['videoHigh', 'videoLow']:
|
||||||
|
format_url = self._search_json(r'var\s+%s\s*=' % re.escape(format), webpage, format,
|
||||||
|
video_id, default=None, contains_pattern=r'[\'"]([^\'"]+)[\'"]')
|
||||||
|
if format_url:
|
||||||
|
formats.append({
|
||||||
|
'url': format_url,
|
||||||
|
'format_id': format,
|
||||||
|
'quality': -2 if format.endswith('Low') else None,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'formats': formats,
|
||||||
|
'title': self._search_regex(r'[\'"]headline[\'"]:\s*[\'"]([^"]+)[\'"]', webpage,
|
||||||
|
'title', default=None) or self._html_extract_title(webpage),
|
||||||
|
'thumbnail': self._html_search_meta('thumbnailUrl', webpage, default=None),
|
||||||
|
'description': self._html_search_meta('description', webpage, default=None),
|
||||||
|
'duration': int_or_none(self._search_regex(r'duration:\s*[\'"]([^\'"]+?)[\'"]',
|
||||||
|
webpage, 'duration', fatal=False)),
|
||||||
|
'age_limit': 18
|
||||||
|
}
|
||||||
@@ -2650,18 +2650,19 @@ def mpd_feed(format_id, delay):
|
|||||||
"""
|
"""
|
||||||
@returns (manifest_url, manifest_stream_number, is_live) or None
|
@returns (manifest_url, manifest_stream_number, is_live) or None
|
||||||
"""
|
"""
|
||||||
|
for retry in self.RetryManager(fatal=False):
|
||||||
with lock:
|
with lock:
|
||||||
refetch_manifest(format_id, delay)
|
refetch_manifest(format_id, delay)
|
||||||
|
|
||||||
f = next((f for f in formats if f['format_id'] == format_id), None)
|
f = next((f for f in formats if f['format_id'] == format_id), None)
|
||||||
if not f:
|
if not f:
|
||||||
if not is_live:
|
if not is_live:
|
||||||
self.to_screen(f'{video_id}: Video is no longer live')
|
retry.error = f'{video_id}: Video is no longer live'
|
||||||
else:
|
else:
|
||||||
self.report_warning(
|
retry.error = f'Cannot find refreshed manifest for format {format_id}{bug_reports_message()}'
|
||||||
f'Cannot find refreshed manifest for format {format_id}{bug_reports_message()}')
|
continue
|
||||||
return None
|
|
||||||
return f['manifest_url'], f['manifest_stream_number'], is_live
|
return f['manifest_url'], f['manifest_stream_number'], is_live
|
||||||
|
return None
|
||||||
|
|
||||||
for f in formats:
|
for f in formats:
|
||||||
f['is_live'] = is_live
|
f['is_live'] = is_live
|
||||||
|
|||||||
@@ -40,49 +40,28 @@
|
|||||||
|
|
||||||
|
|
||||||
def parseOpts(overrideArguments=None, ignore_config_files='if_override'):
|
def parseOpts(overrideArguments=None, ignore_config_files='if_override'):
|
||||||
|
PACKAGE_NAME = 'yt-dlp'
|
||||||
|
|
||||||
root = Config(create_parser())
|
root = Config(create_parser())
|
||||||
if ignore_config_files == 'if_override':
|
if ignore_config_files == 'if_override':
|
||||||
ignore_config_files = overrideArguments is not None
|
ignore_config_files = overrideArguments is not None
|
||||||
|
|
||||||
|
def read_config(*paths):
|
||||||
|
path = os.path.join(*paths)
|
||||||
|
conf = Config.read_file(path, default=None)
|
||||||
|
if conf is not None:
|
||||||
|
return conf, path
|
||||||
|
|
||||||
def _load_from_config_dirs(config_dirs):
|
def _load_from_config_dirs(config_dirs):
|
||||||
for config_dir in config_dirs:
|
for config_dir in config_dirs:
|
||||||
conf_file_path = os.path.join(config_dir, 'config')
|
head, tail = os.path.split(config_dir)
|
||||||
conf = Config.read_file(conf_file_path, default=None)
|
assert tail == PACKAGE_NAME or config_dir == os.path.join(compat_expanduser('~'), f'.{PACKAGE_NAME}')
|
||||||
if conf is None:
|
|
||||||
conf_file_path += '.txt'
|
|
||||||
conf = Config.read_file(conf_file_path, default=None)
|
|
||||||
if conf is not None:
|
|
||||||
return conf, conf_file_path
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
def _read_user_conf(package_name, default=None):
|
yield read_config(head, f'{PACKAGE_NAME}.conf')
|
||||||
# .config/package_name.conf
|
if tail.startswith('.'): # ~/.PACKAGE_NAME
|
||||||
xdg_config_home = os.getenv('XDG_CONFIG_HOME') or compat_expanduser('~/.config')
|
yield read_config(head, f'{PACKAGE_NAME}.conf.txt')
|
||||||
user_conf_file = os.path.join(xdg_config_home, '%s.conf' % package_name)
|
yield read_config(config_dir, 'config')
|
||||||
user_conf = Config.read_file(user_conf_file, default=None)
|
yield read_config(config_dir, 'config.txt')
|
||||||
if user_conf is not None:
|
|
||||||
return user_conf, user_conf_file
|
|
||||||
|
|
||||||
# home (~/package_name.conf or ~/package_name.conf.txt)
|
|
||||||
user_conf_file = os.path.join(compat_expanduser('~'), '%s.conf' % package_name)
|
|
||||||
user_conf = Config.read_file(user_conf_file, default=None)
|
|
||||||
if user_conf is None:
|
|
||||||
user_conf_file += '.txt'
|
|
||||||
user_conf = Config.read_file(user_conf_file, default=None)
|
|
||||||
if user_conf is not None:
|
|
||||||
return user_conf, user_conf_file
|
|
||||||
|
|
||||||
# Package config directories (e.g. ~/.config/package_name/package_name.txt)
|
|
||||||
user_conf, user_conf_file = _load_from_config_dirs(get_user_config_dirs(package_name))
|
|
||||||
if user_conf is not None:
|
|
||||||
return user_conf, user_conf_file
|
|
||||||
return default if default is not None else [], None
|
|
||||||
|
|
||||||
def _read_system_conf(package_name, default=None):
|
|
||||||
system_conf, system_conf_file = _load_from_config_dirs(get_system_config_dirs(package_name))
|
|
||||||
if system_conf is not None:
|
|
||||||
return system_conf, system_conf_file
|
|
||||||
return default if default is not None else [], None
|
|
||||||
|
|
||||||
def add_config(label, path=None, func=None):
|
def add_config(label, path=None, func=None):
|
||||||
""" Adds config and returns whether to continue """
|
""" Adds config and returns whether to continue """
|
||||||
@@ -90,21 +69,21 @@ def add_config(label, path=None, func=None):
|
|||||||
return False
|
return False
|
||||||
elif func:
|
elif func:
|
||||||
assert path is None
|
assert path is None
|
||||||
args, current_path = func('yt-dlp')
|
args, current_path = next(
|
||||||
|
filter(None, _load_from_config_dirs(func(PACKAGE_NAME))), (None, None))
|
||||||
else:
|
else:
|
||||||
current_path = os.path.join(path, 'yt-dlp.conf')
|
current_path = os.path.join(path, 'yt-dlp.conf')
|
||||||
args = Config.read_file(current_path, default=None)
|
args = Config.read_file(current_path, default=None)
|
||||||
if args is not None:
|
if args is not None:
|
||||||
root.append_config(args, current_path, label=label)
|
root.append_config(args, current_path, label=label)
|
||||||
return True
|
return True
|
||||||
return True
|
|
||||||
|
|
||||||
def load_configs():
|
def load_configs():
|
||||||
yield not ignore_config_files
|
yield not ignore_config_files
|
||||||
yield add_config('Portable', get_executable_path())
|
yield add_config('Portable', get_executable_path())
|
||||||
yield add_config('Home', expand_path(root.parse_known_args()[0].paths.get('home', '')).strip())
|
yield add_config('Home', expand_path(root.parse_known_args()[0].paths.get('home', '')).strip())
|
||||||
yield add_config('User', func=_read_user_conf)
|
yield add_config('User', func=get_user_config_dirs)
|
||||||
yield add_config('System', func=_read_system_conf)
|
yield add_config('System', func=get_system_config_dirs)
|
||||||
|
|
||||||
opts = optparse.Values({'verbose': True, 'print_help': False})
|
opts = optparse.Values({'verbose': True, 'print_help': False})
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
import importlib.util
|
import importlib.util
|
||||||
import inspect
|
import inspect
|
||||||
import itertools
|
import itertools
|
||||||
import os
|
|
||||||
import pkgutil
|
import pkgutil
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
@@ -14,11 +13,11 @@
|
|||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
|
|
||||||
from .compat import functools # isort: split
|
from .compat import functools # isort: split
|
||||||
from .compat import compat_expanduser
|
|
||||||
from .utils import (
|
from .utils import (
|
||||||
get_executable_path,
|
get_executable_path,
|
||||||
get_system_config_dirs,
|
get_system_config_dirs,
|
||||||
get_user_config_dirs,
|
get_user_config_dirs,
|
||||||
|
orderedSet,
|
||||||
write_string,
|
write_string,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -57,7 +56,7 @@ def search_locations(self, fullname):
|
|||||||
candidate_locations = []
|
candidate_locations = []
|
||||||
|
|
||||||
def _get_package_paths(*root_paths, containing_folder='plugins'):
|
def _get_package_paths(*root_paths, containing_folder='plugins'):
|
||||||
for config_dir in map(Path, root_paths):
|
for config_dir in orderedSet(map(Path, root_paths), lazy=True):
|
||||||
plugin_dir = config_dir / containing_folder
|
plugin_dir = config_dir / containing_folder
|
||||||
if not plugin_dir.is_dir():
|
if not plugin_dir.is_dir():
|
||||||
continue
|
continue
|
||||||
@@ -65,15 +64,15 @@ def _get_package_paths(*root_paths, containing_folder='plugins'):
|
|||||||
|
|
||||||
# Load from yt-dlp config folders
|
# Load from yt-dlp config folders
|
||||||
candidate_locations.extend(_get_package_paths(
|
candidate_locations.extend(_get_package_paths(
|
||||||
*get_user_config_dirs('yt-dlp'), *get_system_config_dirs('yt-dlp'),
|
*get_user_config_dirs('yt-dlp'),
|
||||||
|
*get_system_config_dirs('yt-dlp'),
|
||||||
containing_folder='plugins'))
|
containing_folder='plugins'))
|
||||||
|
|
||||||
# Load from yt-dlp-plugins folders
|
# Load from yt-dlp-plugins folders
|
||||||
candidate_locations.extend(_get_package_paths(
|
candidate_locations.extend(_get_package_paths(
|
||||||
get_executable_path(),
|
get_executable_path(),
|
||||||
compat_expanduser('~'),
|
*get_user_config_dirs(''),
|
||||||
'/etc',
|
*get_system_config_dirs(''),
|
||||||
os.getenv('XDG_CONFIG_HOME') or compat_expanduser('~/.config'),
|
|
||||||
containing_folder='yt-dlp-plugins'))
|
containing_folder='yt-dlp-plugins'))
|
||||||
|
|
||||||
candidate_locations.extend(map(Path, sys.path)) # PYTHONPATH
|
candidate_locations.extend(map(Path, sys.path)) # PYTHONPATH
|
||||||
|
|||||||
@@ -44,6 +44,7 @@
|
|||||||
'ts': 'mpegts',
|
'ts': 'mpegts',
|
||||||
'wma': 'asf',
|
'wma': 'asf',
|
||||||
'wmv': 'asf',
|
'wmv': 'asf',
|
||||||
|
'weba': 'webm',
|
||||||
'vtt': 'webvtt',
|
'vtt': 'webvtt',
|
||||||
}
|
}
|
||||||
ACODECS = {
|
ACODECS = {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
Popen,
|
Popen,
|
||||||
cached_method,
|
cached_method,
|
||||||
deprecation_warning,
|
deprecation_warning,
|
||||||
|
remove_end,
|
||||||
shell_quote,
|
shell_quote,
|
||||||
system_identifier,
|
system_identifier,
|
||||||
traverse_obj,
|
traverse_obj,
|
||||||
@@ -42,8 +43,7 @@ def _get_variant_and_executable_path():
|
|||||||
# Ref: https://en.wikipedia.org/wiki/Uname#Examples
|
# Ref: https://en.wikipedia.org/wiki/Uname#Examples
|
||||||
if machine[1:] in ('x86', 'x86_64', 'amd64', 'i386', 'i686'):
|
if machine[1:] in ('x86', 'x86_64', 'amd64', 'i386', 'i686'):
|
||||||
machine = '_x86' if platform.architecture()[0][:2] == '32' else ''
|
machine = '_x86' if platform.architecture()[0][:2] == '32' else ''
|
||||||
# NB: https://github.com/yt-dlp/yt-dlp/issues/5632
|
return f'{remove_end(sys.platform, "32")}{machine}_exe', path
|
||||||
return f'{sys.platform}{machine}_exe', path
|
|
||||||
|
|
||||||
path = os.path.dirname(__file__)
|
path = os.path.dirname(__file__)
|
||||||
if isinstance(__loader__, zipimporter):
|
if isinstance(__loader__, zipimporter):
|
||||||
@@ -74,8 +74,8 @@ def current_git_head():
|
|||||||
_FILE_SUFFIXES = {
|
_FILE_SUFFIXES = {
|
||||||
'zip': '',
|
'zip': '',
|
||||||
'py2exe': '_min.exe',
|
'py2exe': '_min.exe',
|
||||||
'win32_exe': '.exe',
|
'win_exe': '.exe',
|
||||||
'win32_x86_exe': '_x86.exe',
|
'win_x86_exe': '_x86.exe',
|
||||||
'darwin_exe': '_macos',
|
'darwin_exe': '_macos',
|
||||||
'darwin_legacy_exe': '_macos_legacy',
|
'darwin_legacy_exe': '_macos_legacy',
|
||||||
'linux_exe': '_linux',
|
'linux_exe': '_linux',
|
||||||
@@ -264,7 +264,8 @@ def update(self):
|
|||||||
self._report_error('Unable to overwrite current version')
|
self._report_error('Unable to overwrite current version')
|
||||||
return os.rename(old_filename, self.filename)
|
return os.rename(old_filename, self.filename)
|
||||||
|
|
||||||
if detect_variant() in ('win32_exe', 'py2exe'):
|
variant = detect_variant()
|
||||||
|
if variant.startswith('win') or variant == 'py2exe':
|
||||||
atexit.register(Popen, f'ping 127.0.0.1 -n 5 -w 1000 & del /F "{old_filename}"',
|
atexit.register(Popen, f'ping 127.0.0.1 -n 5 -w 1000 & del /F "{old_filename}"',
|
||||||
shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
elif old_filename:
|
elif old_filename:
|
||||||
|
|||||||
@@ -3529,7 +3529,7 @@ def mimetype2ext(mt, default=NO_DEFAULT):
|
|||||||
# Per RFC 3003, audio/mpeg can be .mp1, .mp2 or .mp3.
|
# Per RFC 3003, audio/mpeg can be .mp1, .mp2 or .mp3.
|
||||||
# Using .mp3 as it's the most popular one
|
# Using .mp3 as it's the most popular one
|
||||||
'audio/mpeg': 'mp3',
|
'audio/mpeg': 'mp3',
|
||||||
'audio/webm': 'weba',
|
'audio/webm': 'webm',
|
||||||
'audio/x-matroska': 'mka',
|
'audio/x-matroska': 'mka',
|
||||||
'audio/x-mpegurl': 'm3u',
|
'audio/x-mpegurl': 'm3u',
|
||||||
'midi': 'mid',
|
'midi': 'mid',
|
||||||
@@ -5387,36 +5387,22 @@ def get_executable_path():
|
|||||||
|
|
||||||
|
|
||||||
def get_user_config_dirs(package_name):
|
def get_user_config_dirs(package_name):
|
||||||
locations = set()
|
|
||||||
|
|
||||||
# .config (e.g. ~/.config/package_name)
|
# .config (e.g. ~/.config/package_name)
|
||||||
xdg_config_home = os.getenv('XDG_CONFIG_HOME') or compat_expanduser('~/.config')
|
xdg_config_home = os.getenv('XDG_CONFIG_HOME') or compat_expanduser('~/.config')
|
||||||
config_dir = os.path.join(xdg_config_home, package_name)
|
yield os.path.join(xdg_config_home, package_name)
|
||||||
if os.path.isdir(config_dir):
|
|
||||||
locations.add(config_dir)
|
|
||||||
|
|
||||||
# appdata (%APPDATA%/package_name)
|
# appdata (%APPDATA%/package_name)
|
||||||
appdata_dir = os.getenv('appdata')
|
appdata_dir = os.getenv('appdata')
|
||||||
if appdata_dir:
|
if appdata_dir:
|
||||||
config_dir = os.path.join(appdata_dir, package_name)
|
yield os.path.join(appdata_dir, package_name)
|
||||||
if os.path.isdir(config_dir):
|
|
||||||
locations.add(config_dir)
|
|
||||||
|
|
||||||
# home (~/.package_name)
|
# home (~/.package_name)
|
||||||
user_config_directory = os.path.join(compat_expanduser('~'), '.%s' % package_name)
|
yield os.path.join(compat_expanduser('~'), f'.{package_name}')
|
||||||
if os.path.isdir(user_config_directory):
|
|
||||||
locations.add(user_config_directory)
|
|
||||||
|
|
||||||
return locations
|
|
||||||
|
|
||||||
|
|
||||||
def get_system_config_dirs(package_name):
|
def get_system_config_dirs(package_name):
|
||||||
locations = set()
|
|
||||||
# /etc/package_name
|
# /etc/package_name
|
||||||
system_config_directory = os.path.join('/etc', package_name)
|
yield os.path.join('/etc', package_name)
|
||||||
if os.path.isdir(system_config_directory):
|
|
||||||
locations.add(system_config_directory)
|
|
||||||
return locations
|
|
||||||
|
|
||||||
|
|
||||||
def traverse_obj(
|
def traverse_obj(
|
||||||
@@ -5659,7 +5645,6 @@ def windows_enable_vt_mode():
|
|||||||
|
|
||||||
dll = ctypes.WinDLL('kernel32', use_last_error=False)
|
dll = ctypes.WinDLL('kernel32', use_last_error=False)
|
||||||
handle = os.open('CONOUT$', os.O_RDWR)
|
handle = os.open('CONOUT$', os.O_RDWR)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
h_out = ctypes.wintypes.HANDLE(msvcrt.get_osfhandle(handle))
|
h_out = ctypes.wintypes.HANDLE(msvcrt.get_osfhandle(handle))
|
||||||
dw_original_mode = ctypes.wintypes.DWORD()
|
dw_original_mode = ctypes.wintypes.DWORD()
|
||||||
@@ -5671,14 +5656,12 @@ def windows_enable_vt_mode():
|
|||||||
dw_original_mode.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING))
|
dw_original_mode.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING))
|
||||||
if not success:
|
if not success:
|
||||||
raise Exception('SetConsoleMode failed')
|
raise Exception('SetConsoleMode failed')
|
||||||
except Exception as e:
|
finally:
|
||||||
write_string(f'WARNING: Cannot enable VT mode - {e}')
|
os.close(handle)
|
||||||
else:
|
|
||||||
global WINDOWS_VT_MODE
|
global WINDOWS_VT_MODE
|
||||||
WINDOWS_VT_MODE = True
|
WINDOWS_VT_MODE = True
|
||||||
supports_terminal_sequences.cache_clear()
|
supports_terminal_sequences.cache_clear()
|
||||||
finally:
|
|
||||||
os.close(handle)
|
|
||||||
|
|
||||||
|
|
||||||
_terminal_sequences_re = re.compile('\033\\[[^m]+m')
|
_terminal_sequences_re = re.compile('\033\\[[^m]+m')
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Autogenerated by devscripts/update-version.py
|
# Autogenerated by devscripts/update-version.py
|
||||||
|
|
||||||
__version__ = '2023.01.02'
|
__version__ = '2023.01.06'
|
||||||
|
|
||||||
RELEASE_GIT_HEAD = 'd83b0ad80'
|
RELEASE_GIT_HEAD = '6becd2508'
|
||||||
|
|
||||||
VARIANT = None
|
VARIANT = None
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user