mirror of
https://github.com/yt-dlp/yt-dlp
synced 2025-12-17 14:45:42 +07:00
Compare commits
52 Commits
2023.06.21
...
2023.07.06
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc0619f62d | ||
|
|
b532a34810 | ||
|
|
3121512228 | ||
|
|
f8b4bcc0a7 | ||
|
|
1ceb657bdd | ||
|
|
ad8902f616 | ||
|
|
94ed638a43 | ||
|
|
bc344cd456 | ||
|
|
906c0bdcd8 | ||
|
|
337734d4a8 | ||
|
|
fa44802809 | ||
|
|
47bcd43724 | ||
|
|
662ef1e910 | ||
|
|
6355b5f1e1 | ||
|
|
90db9a3c00 | ||
|
|
49296437a8 | ||
|
|
1cffd621cb | ||
|
|
3b7f5300c5 | ||
|
|
4dc4d8473c | ||
|
|
8776349ef6 | ||
|
|
af1fd12f67 | ||
|
|
fcbc9ed760 | ||
|
|
a2be9781fb | ||
|
|
8f05fbae2a | ||
|
|
5b4b92769a | ||
|
|
91302ed349 | ||
|
|
f393bbe724 | ||
|
|
8a8af356e3 | ||
|
|
d949c10c45 | ||
|
|
ef8509c300 | ||
|
|
5e16cf92eb | ||
|
|
f0a1ff1181 | ||
|
|
58786a10f2 | ||
|
|
e59e20744e | ||
|
|
89bed01374 | ||
|
|
de4cf77ec1 | ||
|
|
812cdfa06c | ||
|
|
cd810afe2a | ||
|
|
b4e0d75848 | ||
|
|
71dc18fa29 | ||
|
|
98cb1eda7a | ||
|
|
774aa09dd6 | ||
|
|
f2ff0f6f19 | ||
|
|
5fd8367496 | ||
|
|
0dff8e4d1e | ||
|
|
1e75d97db2 | ||
|
|
81ca451480 | ||
|
|
a4486bfc1d | ||
|
|
3f756c8c40 | ||
|
|
7f9c6a63b1 | ||
|
|
db22142f6f | ||
|
|
d7cd97e8d8 |
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 that yt-dlp is broken on a **supported** site
|
- label: I'm reporting that yt-dlp is broken on a **supported** site
|
||||||
required: true
|
required: true
|
||||||
- label: I've verified that I'm running yt-dlp version **2023.06.21** ([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.07.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
|
||||||
@@ -64,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.06.21 [9d339c4] (win32_exe)
|
[debug] yt-dlp version 2023.07.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
|
||||||
@@ -72,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.06.21, Current version: 2023.06.21
|
Latest version: 2023.07.06, Current version: 2023.07.06
|
||||||
yt-dlp is up to date (2023.06.21)
|
yt-dlp is up to date (2023.07.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.06.21** ([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.07.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
|
||||||
@@ -76,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.06.21 [9d339c4] (win32_exe)
|
[debug] yt-dlp version 2023.07.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
|
||||||
@@ -84,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.06.21, Current version: 2023.06.21
|
Latest version: 2023.07.06, Current version: 2023.07.06
|
||||||
yt-dlp is up to date (2023.06.21)
|
yt-dlp is up to date (2023.07.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.06.21** ([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.07.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
|
||||||
@@ -72,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.06.21 [9d339c4] (win32_exe)
|
[debug] yt-dlp version 2023.07.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
|
||||||
@@ -80,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.06.21, Current version: 2023.06.21
|
Latest version: 2023.07.06, Current version: 2023.07.06
|
||||||
yt-dlp is up to date (2023.06.21)
|
yt-dlp is up to date (2023.07.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.06.21** ([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.07.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
|
||||||
@@ -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.06.21 [9d339c4] (win32_exe)
|
[debug] yt-dlp version 2023.07.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,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.06.21, Current version: 2023.06.21
|
Latest version: 2023.07.06, Current version: 2023.07.06
|
||||||
yt-dlp is up to date (2023.06.21)
|
yt-dlp is up to date (2023.07.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.06.21** ([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.07.06** ([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
|
||||||
@@ -53,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.06.21 [9d339c4] (win32_exe)
|
[debug] yt-dlp version 2023.07.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
|
||||||
@@ -61,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.06.21, Current version: 2023.06.21
|
Latest version: 2023.07.06, Current version: 2023.07.06
|
||||||
yt-dlp is up to date (2023.06.21)
|
yt-dlp is up to date (2023.07.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.06.21** ([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.07.06** ([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
|
||||||
@@ -59,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.06.21 [9d339c4] (win32_exe)
|
[debug] yt-dlp version 2023.07.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
|
||||||
@@ -67,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.06.21, Current version: 2023.06.21
|
Latest version: 2023.07.06, Current version: 2023.07.06
|
||||||
yt-dlp is up to date (2023.06.21)
|
yt-dlp is up to date (2023.07.06)
|
||||||
<more lines>
|
<more lines>
|
||||||
render: shell
|
render: shell
|
||||||
|
|||||||
65
.github/workflows/codeql.yml
vendored
Normal file
65
.github/workflows/codeql.yml
vendored
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
name: "CodeQL"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ 'master', 'gh-pages', 'release' ]
|
||||||
|
pull_request:
|
||||||
|
# The branches below must be a subset of the branches above
|
||||||
|
branches: [ 'master' ]
|
||||||
|
schedule:
|
||||||
|
- cron: '59 11 * * 5'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
language: [ 'python' ]
|
||||||
|
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||||
|
# Use only 'java' to analyze code written in Java, Kotlin or both
|
||||||
|
# Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
|
||||||
|
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
# Initializes the CodeQL tools for scanning.
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v2
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
|
# By default, queries listed here will override any specified in a config file.
|
||||||
|
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||||
|
|
||||||
|
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||||
|
# queries: security-extended,security-and-quality
|
||||||
|
|
||||||
|
|
||||||
|
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
|
||||||
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v2
|
||||||
|
|
||||||
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
|
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||||
|
|
||||||
|
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
||||||
|
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
||||||
|
|
||||||
|
# - run: |
|
||||||
|
# echo "Run, Build Application using script"
|
||||||
|
# ./location_of_script_within_repo/buildscript.sh
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v2
|
||||||
|
with:
|
||||||
|
category: "/language:${{matrix.language}}"
|
||||||
20
.github/workflows/potential-duplicates.yml
vendored
20
.github/workflows/potential-duplicates.yml
vendored
@@ -1,20 +0,0 @@
|
|||||||
name: Potential Duplicates
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: [opened, edited]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
run:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: wow-actions/potential-duplicates@v1
|
|
||||||
with:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
label: potential-duplicate
|
|
||||||
state: all
|
|
||||||
threshold: 0.3
|
|
||||||
comment: |
|
|
||||||
This issue is potentially a duplicate of one of the following issues:
|
|
||||||
{{#issues}}
|
|
||||||
- #{{ number }} ({{ accuracy }}%)
|
|
||||||
{{/issues}}
|
|
||||||
12
CONTRIBUTORS
12
CONTRIBUTORS
@@ -455,3 +455,15 @@ vampirefrog
|
|||||||
vidiot720
|
vidiot720
|
||||||
viktor-enzell
|
viktor-enzell
|
||||||
zhgwn
|
zhgwn
|
||||||
|
barthelmannk
|
||||||
|
berkanteber
|
||||||
|
OverlordQ
|
||||||
|
rexlambert22
|
||||||
|
Ti4eeT4e
|
||||||
|
AmanSal1
|
||||||
|
bbilly1
|
||||||
|
meliber
|
||||||
|
nnoboa
|
||||||
|
rdamas
|
||||||
|
RfadnjdExt
|
||||||
|
urectanc
|
||||||
|
|||||||
83
Changelog.md
83
Changelog.md
@@ -4,6 +4,89 @@ # Changelog
|
|||||||
# To create a release, dispatch the https://github.com/yt-dlp/yt-dlp/actions/workflows/release.yml workflow on master
|
# To create a release, dispatch the https://github.com/yt-dlp/yt-dlp/actions/workflows/release.yml workflow on master
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
### 2023.07.06
|
||||||
|
|
||||||
|
#### Important changes
|
||||||
|
- Security: [[CVE-2023-35934](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-35934)] Fix [Cookie leak](https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-v8mc-9377-rwjj)
|
||||||
|
- `--add-header Cookie:` is deprecated and auto-scoped to input URL domains
|
||||||
|
- Cookies are scoped when passed to external downloaders
|
||||||
|
- Add `cookie` field to info.json and deprecate `http_headers.Cookie`
|
||||||
|
|
||||||
|
#### Core changes
|
||||||
|
- [Allow extractors to mark formats as potentially DRM](https://github.com/yt-dlp/yt-dlp/commit/bc344cd456380999c1ee74554dfd432a38f32ec7) ([#7396](https://github.com/yt-dlp/yt-dlp/issues/7396)) by [pukkandan](https://github.com/pukkandan)
|
||||||
|
- [Bugfix for b4e0d75848e9447cee2cd3646ce54d4744a7ff56](https://github.com/yt-dlp/yt-dlp/commit/e59e20744eb32ce4b6ea0dece7c673be8376a710) by [pukkandan](https://github.com/pukkandan)
|
||||||
|
- [Change how `Cookie` headers are handled](https://github.com/yt-dlp/yt-dlp/commit/3121512228487c9c690d3d39bfd2579addf96e07) by [Grub4K](https://github.com/Grub4K)
|
||||||
|
- [Prevent `Cookie` leaks on HTTP redirect](https://github.com/yt-dlp/yt-dlp/commit/f8b4bcc0a791274223723488bfbfc23ea3276641) by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
- **formats**: [Fix best fallback for storyboards](https://github.com/yt-dlp/yt-dlp/commit/906c0bdcd8974340d619e99ccd613c163eb0d0c2) by [pukkandan](https://github.com/pukkandan)
|
||||||
|
- **outtmpl**: [Pad `playlist_index` etc even when with internal formatting](https://github.com/yt-dlp/yt-dlp/commit/47bcd437247152e0af5b3ebc5592db7bb66855c2) by [pukkandan](https://github.com/pukkandan)
|
||||||
|
- **utils**: clean_podcast_url: [Handle protocol in redirect URL](https://github.com/yt-dlp/yt-dlp/commit/91302ed349f34dc26cc1d661bb45a4b71f4417f7) by [pukkandan](https://github.com/pukkandan)
|
||||||
|
|
||||||
|
#### Extractor changes
|
||||||
|
- **abc**: [Fix extraction](https://github.com/yt-dlp/yt-dlp/commit/8f05fbae2a79ce0713077ccc68b354e63216bf20) ([#7434](https://github.com/yt-dlp/yt-dlp/issues/7434)) by [meliber](https://github.com/meliber)
|
||||||
|
- **AdultSwim**: [Extract subtitles from m3u8](https://github.com/yt-dlp/yt-dlp/commit/5e16cf92eb496b7c1541a6b1d727cb87542984db) ([#7421](https://github.com/yt-dlp/yt-dlp/issues/7421)) by [nnoboa](https://github.com/nnoboa)
|
||||||
|
- **crunchyroll**: music: [Fix `_VALID_URL`](https://github.com/yt-dlp/yt-dlp/commit/5b4b92769afcc398475e481bfa839f1158902fe9) ([#7439](https://github.com/yt-dlp/yt-dlp/issues/7439)) by [AmanSal1](https://github.com/AmanSal1), [rdamas](https://github.com/rdamas)
|
||||||
|
- **Douyin**: [Fix extraction from webpage](https://github.com/yt-dlp/yt-dlp/commit/a2be9781fbf4d7e4db245c277ca2ecc41cf3a7b2) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **googledrive**: [Fix source format extraction](https://github.com/yt-dlp/yt-dlp/commit/3b7f5300c577fef40464d46d4e4037a69d51fe82) ([#7395](https://github.com/yt-dlp/yt-dlp/issues/7395)) by [RfadnjdExt](https://github.com/RfadnjdExt)
|
||||||
|
- **kick**: [Fix `_VALID_URL`](https://github.com/yt-dlp/yt-dlp/commit/ef8509c300ea50da86aea447eb214d3d6f6db6bb) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **qdance**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/f0a1ff118145b6449982ba401f9a9f656ecd8062) ([#7420](https://github.com/yt-dlp/yt-dlp/issues/7420)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **sbs**: [Python 3.7 compat](https://github.com/yt-dlp/yt-dlp/commit/f393bbe724b1fc6c7f754a5da507e807b2b40ad2) by [pukkandan](https://github.com/pukkandan)
|
||||||
|
- **stacommu**: [Add extractors](https://github.com/yt-dlp/yt-dlp/commit/af1fd12f675220df6793fc019dff320bc76e8080) ([#7432](https://github.com/yt-dlp/yt-dlp/issues/7432)) by [urectanc](https://github.com/urectanc)
|
||||||
|
- **twitter**
|
||||||
|
- [Fix unauthenticated extraction](https://github.com/yt-dlp/yt-dlp/commit/49296437a8e5fa91dacb5446e51ab588474c85d3) ([#7476](https://github.com/yt-dlp/yt-dlp/issues/7476)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- spaces: [Fix extraction](https://github.com/yt-dlp/yt-dlp/commit/1cffd621cb371f1563563cfb2fe37d137e8a7bee) ([#7512](https://github.com/yt-dlp/yt-dlp/issues/7512)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **vidlii**: [Handle relative URLs](https://github.com/yt-dlp/yt-dlp/commit/ad8902f616ad2541f9b9626738f1393fad89a64c) by [pukkandan](https://github.com/pukkandan)
|
||||||
|
- **vk**: VKPlay, VKPlayLive: [Add extractors](https://github.com/yt-dlp/yt-dlp/commit/8776349ef6b1f644584a92dfa00a05208a48edc4) ([#7358](https://github.com/yt-dlp/yt-dlp/issues/7358)) by [c-basalt](https://github.com/c-basalt)
|
||||||
|
- **youtube**
|
||||||
|
- [Add extractor-arg `formats`](https://github.com/yt-dlp/yt-dlp/commit/58786a10f212bd63f9ad1d0b4d9e4d31c3b385e2) by [pukkandan](https://github.com/pukkandan)
|
||||||
|
- [Avoid false DRM detection](https://github.com/yt-dlp/yt-dlp/commit/94ed638a437fc766699d440e978982e24ce6a30a) ([#7396](https://github.com/yt-dlp/yt-dlp/issues/7396)) by [pukkandan](https://github.com/pukkandan)
|
||||||
|
- [Fix comments' `is_favorited`](https://github.com/yt-dlp/yt-dlp/commit/89bed013741a776506f60380b7fd89d27d0710b4) ([#7390](https://github.com/yt-dlp/yt-dlp/issues/7390)) by [bbilly1](https://github.com/bbilly1)
|
||||||
|
- [Ignore incomplete data for comment threads by default](https://github.com/yt-dlp/yt-dlp/commit/4dc4d8473c085900edc841c87c20041233d25b1f) ([#7475](https://github.com/yt-dlp/yt-dlp/issues/7475)) by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
- [Process `post_live` over 2 hours](https://github.com/yt-dlp/yt-dlp/commit/d949c10c45bfc359bdacd52e6a180169b8128958) by [pukkandan](https://github.com/pukkandan)
|
||||||
|
- stories: [Remove](https://github.com/yt-dlp/yt-dlp/commit/90db9a3c00ca80492c6a58c542e4cbf4c2710866) ([#7459](https://github.com/yt-dlp/yt-dlp/issues/7459)) by [pukkandan](https://github.com/pukkandan)
|
||||||
|
- tab: [Support shorts-only playlists](https://github.com/yt-dlp/yt-dlp/commit/fcbc9ed760be6e3455bbadfaf277b4504b06f068) ([#7425](https://github.com/yt-dlp/yt-dlp/issues/7425)) by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
|
||||||
|
#### Downloader changes
|
||||||
|
- **aria2c**: [Add `--no-conf`](https://github.com/yt-dlp/yt-dlp/commit/8a8af356e3bba98a7f7d333aff0777d5d92130c8) by [pukkandan](https://github.com/pukkandan)
|
||||||
|
- **external**: [Scope cookies](https://github.com/yt-dlp/yt-dlp/commit/1ceb657bdd254ad961489e5060f2ccc7d556b729) by [bashonly](https://github.com/bashonly), [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
- **http**: [Avoid infinite loop when no data is received](https://github.com/yt-dlp/yt-dlp/commit/662ef1e910b72e57957f06589925b2332ba52821) by [pukkandan](https://github.com/pukkandan)
|
||||||
|
|
||||||
|
#### Misc. changes
|
||||||
|
- [Add CodeQL workflow](https://github.com/yt-dlp/yt-dlp/commit/6355b5f1e1e8e7f4ef866d71d51e03baf0e82f17) ([#7497](https://github.com/yt-dlp/yt-dlp/issues/7497)) by [pukkandan](https://github.com/pukkandan)
|
||||||
|
- **cleanup**: Miscellaneous: [337734d](https://github.com/yt-dlp/yt-dlp/commit/337734d4a8a6500bc65434843db346b5cbd05e81) by [pukkandan](https://github.com/pukkandan)
|
||||||
|
- **docs**: [Minor fixes](https://github.com/yt-dlp/yt-dlp/commit/b532a3481046e1eabb6232ee8196fb696c356ff6) by [pukkandan](https://github.com/pukkandan)
|
||||||
|
- **make_changelog**: [Skip reverted commits](https://github.com/yt-dlp/yt-dlp/commit/fa44802809d189fca0f4782263d48d6533384503) by [pukkandan](https://github.com/pukkandan)
|
||||||
|
|
||||||
|
### 2023.06.22
|
||||||
|
|
||||||
|
#### Core changes
|
||||||
|
- [Fix bug in db3ad8a67661d7b234a6954d9c6a4a9b1749f5eb](https://github.com/yt-dlp/yt-dlp/commit/d7cd97e8d8d42b500fea9abb2aa4ac9b0f98b2ad) by [pukkandan](https://github.com/pukkandan)
|
||||||
|
- [Improve `--download-sections`](https://github.com/yt-dlp/yt-dlp/commit/b4e0d75848e9447cee2cd3646ce54d4744a7ff56) by [pukkandan](https://github.com/pukkandan)
|
||||||
|
- Support negative time-ranges
|
||||||
|
- Add `*from-url` to obey time-ranges in URL
|
||||||
|
- [Indicate `filesize` approximated from `tbr` better](https://github.com/yt-dlp/yt-dlp/commit/0dff8e4d1e6e9fb938f4256ea9af7d81f42fd54f) by [pukkandan](https://github.com/pukkandan)
|
||||||
|
|
||||||
|
#### Extractor changes
|
||||||
|
- [Support multiple `_VALID_URL`s](https://github.com/yt-dlp/yt-dlp/commit/5fd8367496b42c7b900b896a0d5460561a2859de) ([#5812](https://github.com/yt-dlp/yt-dlp/issues/5812)) by [nixxo](https://github.com/nixxo)
|
||||||
|
- **dplay**: GlobalCyclingNetworkPlus: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/774aa09dd6aa61ced9ec818d1f67e53414d22762) ([#7360](https://github.com/yt-dlp/yt-dlp/issues/7360)) by [bashonly](https://github.com/bashonly)
|
||||||
|
- **dropout**: [Fix season extraction](https://github.com/yt-dlp/yt-dlp/commit/db22142f6f817ff673d417b4b78e8db497bf8ab3) ([#7304](https://github.com/yt-dlp/yt-dlp/issues/7304)) by [OverlordQ](https://github.com/OverlordQ)
|
||||||
|
- **motherless**: [Add gallery support, fix groups](https://github.com/yt-dlp/yt-dlp/commit/f2ff0f6f1914b82d4a51681a72cc0828115dcb4a) ([#7211](https://github.com/yt-dlp/yt-dlp/issues/7211)) by [rexlambert22](https://github.com/rexlambert22), [Ti4eeT4e](https://github.com/Ti4eeT4e)
|
||||||
|
- **nebula**: [Fix extractor](https://github.com/yt-dlp/yt-dlp/commit/3f756c8c4095b942cf49788eb0862ceaf57847f2) ([#7156](https://github.com/yt-dlp/yt-dlp/issues/7156)) by [Lamieur](https://github.com/Lamieur), [rohieb](https://github.com/rohieb)
|
||||||
|
- **rheinmaintv**: [Add extractor](https://github.com/yt-dlp/yt-dlp/commit/98cb1eda7a4cf67c96078980dbd63e6c06ad7f7c) ([#7311](https://github.com/yt-dlp/yt-dlp/issues/7311)) by [barthelmannk](https://github.com/barthelmannk)
|
||||||
|
- **youtube**
|
||||||
|
- [Add `ios` to default clients used](https://github.com/yt-dlp/yt-dlp/commit/1e75d97db21152acc764b30a688e516f04b8a142) by [pukkandan](https://github.com/pukkandan)
|
||||||
|
- IOS is affected neither by 403 nor by nsig so helps mitigate them preemptively
|
||||||
|
- IOS also has higher bit-rate 'premium' formats though they are not labeled as such
|
||||||
|
- [Improve description parsing performance](https://github.com/yt-dlp/yt-dlp/commit/71dc18fa29263a1ff0472c23d81bfc8dd4422d48) ([#7315](https://github.com/yt-dlp/yt-dlp/issues/7315)) by [berkanteber](https://github.com/berkanteber), [pukkandan](https://github.com/pukkandan)
|
||||||
|
- [Improve nsig function name extraction](https://github.com/yt-dlp/yt-dlp/commit/cd810afe2ac5567c822b7424800fc470ef2d0045) by [pukkandan](https://github.com/pukkandan)
|
||||||
|
- [Workaround 403 for android formats](https://github.com/yt-dlp/yt-dlp/commit/81ca451480051d7ce1a31c017e005358345a9149) by [pukkandan](https://github.com/pukkandan)
|
||||||
|
|
||||||
|
#### Misc. changes
|
||||||
|
- [Revert "Add automatic duplicate issue detection"](https://github.com/yt-dlp/yt-dlp/commit/a4486bfc1dc7057efca9dd3fe70d7fa25c56f700) by [pukkandan](https://github.com/pukkandan)
|
||||||
|
- **cleanup**
|
||||||
|
- Miscellaneous
|
||||||
|
- [7f9c6a6](https://github.com/yt-dlp/yt-dlp/commit/7f9c6a63b16e145495479e9f666f5b9e2ee69e2f) by [bashonly](https://github.com/bashonly)
|
||||||
|
- [812cdfa](https://github.com/yt-dlp/yt-dlp/commit/812cdfa06c33a40e73a8e04b3e6f42c084666a43) by [pukkandan](https://github.com/pukkandan)
|
||||||
|
|
||||||
### 2023.06.21
|
### 2023.06.21
|
||||||
|
|
||||||
#### Important changes
|
#### Important changes
|
||||||
|
|||||||
44
README.md
44
README.md
@@ -12,7 +12,7 @@
|
|||||||
[](LICENSE "License")
|
[](LICENSE "License")
|
||||||
[](https://github.com/yt-dlp/yt-dlp/actions "CI Status")
|
[](https://github.com/yt-dlp/yt-dlp/actions "CI Status")
|
||||||
[](https://github.com/yt-dlp/yt-dlp/commits "Commit History")
|
[](https://github.com/yt-dlp/yt-dlp/commits "Commit History")
|
||||||
[](https://github.com/yt-dlp/yt-dlp/commits "Commit History")
|
[](https://github.com/yt-dlp/yt-dlp/pulse/monthly "Last activity")
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<!-- MANPAGE: END EXCLUDED SECTION -->
|
<!-- MANPAGE: END EXCLUDED SECTION -->
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
|
|
||||||
# NEW FEATURES
|
# NEW FEATURES
|
||||||
|
|
||||||
* Merged with **youtube-dl v2021.12.17+ [commit/2dd6c6e](https://github.com/ytdl-org/youtube-dl/commit/2dd6c6e)** ([exceptions](https://github.com/yt-dlp/yt-dlp/issues/21)) and **youtube-dlc v2020.11.11-3+ [commit/f9401f2](https://github.com/blackjack4494/yt-dlc/commit/f9401f2a91987068139c5f757b12fc711d4c0cee)**: You get all the features and patches of [youtube-dlc](https://github.com/blackjack4494/yt-dlc) in addition to the latest [youtube-dl](https://github.com/ytdl-org/youtube-dl)
|
* Forked from [**yt-dlc@f9401f2**](https://github.com/blackjack4494/yt-dlc/commit/f9401f2a91987068139c5f757b12fc711d4c0cee) and merged with [**youtube-dl@42f2d4**](https://github.com/ytdl-org/youtube-dl/commit/07af47960f3bb262ead02490ce65c8c45c01741e) ([exceptions](https://github.com/yt-dlp/yt-dlp/issues/21))
|
||||||
|
|
||||||
* **[SponsorBlock Integration](#sponsorblock-options)**: You can mark/remove sponsor sections in YouTube videos by utilizing the [SponsorBlock](https://sponsor.ajay.app) API
|
* **[SponsorBlock Integration](#sponsorblock-options)**: You can mark/remove sponsor sections in YouTube videos by utilizing the [SponsorBlock](https://sponsor.ajay.app) API
|
||||||
|
|
||||||
@@ -610,12 +610,14 @@ ## Download Options:
|
|||||||
--no-hls-use-mpegts Do not use the mpegts container for HLS
|
--no-hls-use-mpegts Do not use the mpegts container for HLS
|
||||||
videos. This is default when not downloading
|
videos. This is default when not downloading
|
||||||
live streams
|
live streams
|
||||||
--download-sections REGEX Download only chapters whose title matches
|
--download-sections REGEX Download only chapters that match the
|
||||||
the given regular expression. Time ranges
|
regular expression. A "*" prefix denotes
|
||||||
prefixed by a "*" can also be used in place
|
time-range instead of chapter. Negative
|
||||||
of chapters to download the specified range.
|
timestamps are calculated from the end.
|
||||||
Needs ffmpeg. This option can be used
|
"*from-url" can be used to download between
|
||||||
multiple times to download multiple
|
the "start_time" and "end_time" extracted
|
||||||
|
from the URL. Needs ffmpeg. This option can
|
||||||
|
be used multiple times to download multiple
|
||||||
sections, e.g. --download-sections
|
sections, e.g. --download-sections
|
||||||
"*10:15-inf" --download-sections "intro"
|
"*10:15-inf" --download-sections "intro"
|
||||||
--downloader [PROTO:]NAME Name or path of the external downloader to
|
--downloader [PROTO:]NAME Name or path of the external downloader to
|
||||||
@@ -1221,7 +1223,7 @@ ### Authentication with netrc
|
|||||||
|
|
||||||
The default location of the .netrc file is `~` (see below).
|
The default location of the .netrc file is `~` (see below).
|
||||||
|
|
||||||
As an alternative to using the `.netrc` file, which has the disadvantage of keeping your passwords in a plain text file, you can configure a custom shell command to provide the credentials for an extractor. This is done by providing the `--netrc-cmd` parameter, it shall output the credentials in the netrc format and return `0` on success, other values will be treated as an error. `{}` in the command will be replaced by the name of the extractor to make it possible to select the credentials for the right extractor (To use literal braces, double them like `{{}}`).
|
As an alternative to using the `.netrc` file, which has the disadvantage of keeping your passwords in a plain text file, you can configure a custom shell command to provide the credentials for an extractor. This is done by providing the `--netrc-cmd` parameter, it shall output the credentials in the netrc format and return `0` on success, other values will be treated as an error. `{}` in the command will be replaced by the name of the extractor to make it possible to select the credentials for the right extractor.
|
||||||
|
|
||||||
E.g. To use an encrypted `.netrc` file stored as `.authinfo.gpg`
|
E.g. To use an encrypted `.netrc` file stored as `.authinfo.gpg`
|
||||||
```
|
```
|
||||||
@@ -1321,7 +1323,7 @@ # OUTPUT TEMPLATE
|
|||||||
- `extractor` (string): Name of the extractor
|
- `extractor` (string): Name of the extractor
|
||||||
- `extractor_key` (string): Key name of the extractor
|
- `extractor_key` (string): Key name of the extractor
|
||||||
- `epoch` (numeric): Unix epoch of when the information extraction was completed
|
- `epoch` (numeric): Unix epoch of when the information extraction was completed
|
||||||
- `autonumber` (numeric): Number that will be increased with each download, starting at `--autonumber-start`
|
- `autonumber` (numeric): Number that will be increased with each download, starting at `--autonumber-start`, padded with leading zeros to 5 digits
|
||||||
- `video_autonumber` (numeric): Number that will be increased with each video
|
- `video_autonumber` (numeric): Number that will be increased with each video
|
||||||
- `n_entries` (numeric): Total number of extracted items in the playlist
|
- `n_entries` (numeric): Total number of extracted items in the playlist
|
||||||
- `playlist_id` (string): Identifier of the playlist that contains the video
|
- `playlist_id` (string): Identifier of the playlist that contains the video
|
||||||
@@ -1507,7 +1509,7 @@ # FORMAT SELECTION
|
|||||||
|
|
||||||
## Filtering Formats
|
## Filtering Formats
|
||||||
|
|
||||||
You can also filter the video formats by putting a condition in brackets, as in `-f "best[height=720]"` (or `-f "[filesize>10M]"`).
|
You can also filter the video formats by putting a condition in brackets, as in `-f "best[height=720]"` (or `-f "[filesize>10M]"` since filters without a selector are interpreted as `best`).
|
||||||
|
|
||||||
The following numeric meta fields can be used with comparisons `<`, `<=`, `>`, `>=`, `=` (equals), `!=` (not equals):
|
The following numeric meta fields can be used with comparisons `<`, `<=`, `>`, `>=`, `=` (equals), `!=` (not equals):
|
||||||
|
|
||||||
@@ -1543,7 +1545,7 @@ ## Filtering Formats
|
|||||||
|
|
||||||
**Note**: None of the aforementioned meta fields are guaranteed to be present since this solely depends on the metadata obtained by particular extractor, i.e. the metadata offered by the website. Any other field made available by the extractor can also be used for filtering.
|
**Note**: None of the aforementioned meta fields are guaranteed to be present since this solely depends on the metadata obtained by particular extractor, i.e. the metadata offered by the website. Any other field made available by the extractor can also be used for filtering.
|
||||||
|
|
||||||
Formats for which the value is not known are excluded unless you put a question mark (`?`) after the operator. You can combine format filters, so `-f "[height<=?720][tbr>500]"` selects up to 720p videos (or videos where the height is not known) with a bitrate of at least 500 KBit/s. You can also use the filters with `all` to download all formats that satisfy the filter, e.g. `-f "all[vcodec=none]"` selects all audio-only formats.
|
Formats for which the value is not known are excluded unless you put a question mark (`?`) after the operator. You can combine format filters, so `-f "bv[height<=?720][tbr>500]"` selects up to 720p videos (or videos where the height is not known) with a bitrate of at least 500 KBit/s. You can also use the filters with `all` to download all formats that satisfy the filter, e.g. `-f "all[vcodec=none]"` selects all audio-only formats.
|
||||||
|
|
||||||
Format selectors can also be grouped using parentheses; e.g. `-f "(mp4,webm)[height<480]"` will download the best pre-merged mp4 and webm formats with a height lower than 480.
|
Format selectors can also be grouped using parentheses; e.g. `-f "(mp4,webm)[height<480]"` will download the best pre-merged mp4 and webm formats with a height lower than 480.
|
||||||
|
|
||||||
@@ -1780,7 +1782,7 @@ # Do not set any "synopsis" in the video metadata
|
|||||||
$ yt-dlp --parse-metadata ":(?P<meta_synopsis>)"
|
$ yt-dlp --parse-metadata ":(?P<meta_synopsis>)"
|
||||||
|
|
||||||
# Remove "formats" field from the infojson by setting it to an empty string
|
# Remove "formats" field from the infojson by setting it to an empty string
|
||||||
$ yt-dlp --parse-metadata ":(?P<formats>)" -j
|
$ yt-dlp --parse-metadata "video::(?P<formats>)" --write-info-json
|
||||||
|
|
||||||
# Replace all spaces and "_" in title and uploader with a `-`
|
# Replace all spaces and "_" in title and uploader with a `-`
|
||||||
$ yt-dlp --replace-in-metadata "title,uploader" "[ _]" "-"
|
$ yt-dlp --replace-in-metadata "title,uploader" "[ _]" "-"
|
||||||
@@ -1798,13 +1800,12 @@ # EXTRACTOR ARGUMENTS
|
|||||||
#### youtube
|
#### youtube
|
||||||
* `lang`: Prefer translated metadata (`title`, `description` etc) of this language code (case-sensitive). By default, the video primary language metadata is preferred, with a fallback to `en` translated. See [youtube.py](https://github.com/yt-dlp/yt-dlp/blob/c26f9b991a0681fd3ea548d535919cec1fbbd430/yt_dlp/extractor/youtube.py#L381-L390) for list of supported content language codes
|
* `lang`: Prefer translated metadata (`title`, `description` etc) of this language code (case-sensitive). By default, the video primary language metadata is preferred, with a fallback to `en` translated. See [youtube.py](https://github.com/yt-dlp/yt-dlp/blob/c26f9b991a0681fd3ea548d535919cec1fbbd430/yt_dlp/extractor/youtube.py#L381-L390) for list of supported content language codes
|
||||||
* `skip`: One or more of `hls`, `dash` or `translated_subs` to skip extraction of the m3u8 manifests, dash manifests and [auto-translated subtitles](https://github.com/yt-dlp/yt-dlp/issues/4090#issuecomment-1158102032) respectively
|
* `skip`: One or more of `hls`, `dash` or `translated_subs` to skip extraction of the m3u8 manifests, dash manifests and [auto-translated subtitles](https://github.com/yt-dlp/yt-dlp/issues/4090#issuecomment-1158102032) respectively
|
||||||
* `player_client`: Clients to extract video data from. The main clients are `web`, `android` and `ios` with variants `_music`, `_embedded`, `_embedscreen`, `_creator` (e.g. `web_embedded`); and `mweb` and `tv_embedded` (agegate bypass) with no variants. By default, `android,web` is used, but `tv_embedded` and `creator` variants are added as required for age-gated videos. Similarly, the music variants are added for `music.youtube.com` urls. You can use `all` to use all the clients, and `default` for the default clients.
|
* `player_client`: Clients to extract video data from. The main clients are `web`, `android` and `ios` with variants `_music`, `_embedded`, `_embedscreen`, `_creator` (e.g. `web_embedded`); and `mweb` and `tv_embedded` (agegate bypass) with no variants. By default, `ios,android,web` is used, but `tv_embedded` and `creator` variants are added as required for age-gated videos. Similarly, the music variants are added for `music.youtube.com` urls. You can use `all` to use all the clients, and `default` for the default clients.
|
||||||
* `player_skip`: Skip some network requests that are generally needed for robust extraction. One or more of `configs` (skip client configs), `webpage` (skip initial webpage), `js` (skip js player). While these options can help reduce the number of requests needed or avoid some rate-limiting, they could cause some issues. See [#860](https://github.com/yt-dlp/yt-dlp/pull/860) for more details
|
* `player_skip`: Skip some network requests that are generally needed for robust extraction. One or more of `configs` (skip client configs), `webpage` (skip initial webpage), `js` (skip js player). While these options can help reduce the number of requests needed or avoid some rate-limiting, they could cause some issues. See [#860](https://github.com/yt-dlp/yt-dlp/pull/860) for more details
|
||||||
* `comment_sort`: `top` or `new` (default) - choose comment sorting mode (on YouTube's side)
|
* `comment_sort`: `top` or `new` (default) - choose comment sorting mode (on YouTube's side)
|
||||||
* `max_comments`: Limit the amount of comments to gather. Comma-separated list of integers representing `max-comments,max-parents,max-replies,max-replies-per-thread`. Default is `all,all,all,all`
|
* `max_comments`: Limit the amount of comments to gather. Comma-separated list of integers representing `max-comments,max-parents,max-replies,max-replies-per-thread`. Default is `all,all,all,all`
|
||||||
* E.g. `all,all,1000,10` will get a maximum of 1000 replies total, with up to 10 replies per thread. `1000,all,100` will get a maximum of 1000 comments, with a maximum of 100 replies total
|
* E.g. `all,all,1000,10` will get a maximum of 1000 replies total, with up to 10 replies per thread. `1000,all,100` will get a maximum of 1000 comments, with a maximum of 100 replies total
|
||||||
* `include_duplicate_formats`: Extract formats with identical content but different URLs or protocol. This is useful if some of the formats are unavailable or throttled.
|
* `formats`: Change the types of formats to return. `dashy` (convert HTTP to DASH), `duplicate` (identical content but different URLs or protocol; includes `dashy`), `incomplete` (cannot be downloaded completely - live dash and post-live m3u8)
|
||||||
* `include_incomplete_formats`: Extract formats that cannot be downloaded completely (live dash and post-live m3u8)
|
|
||||||
* `innertube_host`: Innertube API host to use for all API requests; e.g. `studio.youtube.com`, `youtubei.googleapis.com`. Note that cookies exported from one subdomain will not work on others
|
* `innertube_host`: Innertube API host to use for all API requests; e.g. `studio.youtube.com`, `youtubei.googleapis.com`. Note that cookies exported from one subdomain will not work on others
|
||||||
* `innertube_key`: Innertube API key to use for all API requests
|
* `innertube_key`: Innertube API key to use for all API requests
|
||||||
|
|
||||||
@@ -1851,14 +1852,11 @@ #### tiktok
|
|||||||
#### rokfinchannel
|
#### rokfinchannel
|
||||||
* `tab`: Which tab to download - one of `new`, `top`, `videos`, `podcasts`, `streams`, `stacks`
|
* `tab`: Which tab to download - one of `new`, `top`, `videos`, `podcasts`, `streams`, `stacks`
|
||||||
|
|
||||||
#### twitter
|
#### stacommu, wrestleuniverse
|
||||||
* `legacy_api`: Force usage of the legacy Twitter API instead of the GraphQL API for tweet extraction. Has no effect if login cookies are passed
|
|
||||||
|
|
||||||
### wrestleuniverse
|
|
||||||
* `device_id`: UUID value assigned by the website and used to enforce device limits for paid livestream content. Can be found in browser local storage
|
* `device_id`: UUID value assigned by the website and used to enforce device limits for paid livestream content. Can be found in browser local storage
|
||||||
|
|
||||||
#### twitchstream (Twitch)
|
#### twitch
|
||||||
* `client_id`: Client ID value to be sent with GraphQL requests, e.g. `twitchstream:client_id=kimne78kx3ncx6brgo4mv6wki5h1ko`
|
* `client_id`: Client ID value to be sent with GraphQL requests, e.g. `twitch:client_id=kimne78kx3ncx6brgo4mv6wki5h1ko`
|
||||||
|
|
||||||
#### nhkradirulive (NHK らじる★らじる LIVE)
|
#### nhkradirulive (NHK らじる★らじる LIVE)
|
||||||
* `area`: Which regional variation to extract. Valid areas are: `sapporo`, `sendai`, `tokyo`, `nagoya`, `osaka`, `hiroshima`, `matsuyama`, `fukuoka`. Defaults to `tokyo`
|
* `area`: Which regional variation to extract. Valid areas are: `sapporo`, `sendai`, `tokyo`, `nagoya`, `osaka`, `hiroshima`, `matsuyama`, `fukuoka`. Defaults to `tokyo`
|
||||||
@@ -1952,7 +1950,7 @@ # EMBEDDING YT-DLP
|
|||||||
ydl.download(URLS)
|
ydl.download(URLS)
|
||||||
```
|
```
|
||||||
|
|
||||||
Most likely, you'll want to use various options. For a list of options available, have a look at [`yt_dlp/YoutubeDL.py`](yt_dlp/YoutubeDL.py#L184).
|
Most likely, you'll want to use various options. For a list of options available, have a look at [`yt_dlp/YoutubeDL.py`](yt_dlp/YoutubeDL.py#L183) or `help(yt_dlp.YoutubeDL)` in a Python shell. If you are already familiar with the CLI, you can use [`devscripts/cli_to_api.py`](https://github.com/yt-dlp/yt-dlp/blob/master/devscripts/cli_to_api.py) to translate any CLI switches to `YoutubeDL` params.
|
||||||
|
|
||||||
**Tip**: If you are porting your code from youtube-dl to yt-dlp, one important point to look out for is that we do not guarantee the return value of `YoutubeDL.extract_info` to be json serializable, or even be a dictionary. It will be dictionary-like, but if you want to ensure it is a serializable dictionary, pass it through `YoutubeDL.sanitize_info` as shown in the [example below](#extracting-information)
|
**Tip**: If you are porting your code from youtube-dl to yt-dlp, one important point to look out for is that we do not guarantee the return value of `YoutubeDL.extract_info` to be json serializable, or even be a dictionary. It will be dictionary-like, but if you want to ensure it is a serializable dictionary, pass it through `YoutubeDL.sanitize_info` as shown in the [example below](#extracting-information)
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"action": "add",
|
"action": "add",
|
||||||
"when": "776d1c3f0c9b00399896dd2e40e78e9a43218109",
|
"when": "29cb20bd563c02671b31dd840139e93dd37150a1",
|
||||||
"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`"
|
"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",
|
"action": "add",
|
||||||
"when": "776d1c3f0c9b00399896dd2e40e78e9a43218109",
|
"when": "5038f6d713303e0967d002216e7a88652401c22a",
|
||||||
"short": "[priority] **YouTube throttling fixes!**"
|
"short": "[priority] **YouTube throttling fixes!**"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -35,5 +35,34 @@
|
|||||||
"when": "8417f26b8a819cd7ffcd4e000ca3e45033e670fb",
|
"when": "8417f26b8a819cd7ffcd4e000ca3e45033e670fb",
|
||||||
"short": "Add option `--color` (#6904)",
|
"short": "Add option `--color` (#6904)",
|
||||||
"authors": ["Grub4K"]
|
"authors": ["Grub4K"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "change",
|
||||||
|
"when": "b4e0d75848e9447cee2cd3646ce54d4744a7ff56",
|
||||||
|
"short": "Improve `--download-sections`\n - Support negative time-ranges\n - Add `*from-url` to obey time-ranges in URL",
|
||||||
|
"authors": ["pukkandan"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "change",
|
||||||
|
"when": "1e75d97db21152acc764b30a688e516f04b8a142",
|
||||||
|
"short": "[extractor/youtube] Add `ios` to default clients used\n - IOS is affected neither by 403 nor by nsig so helps mitigate them preemptively\n - IOS also has higher bit-rate 'premium' formats though they are not labeled as such",
|
||||||
|
"authors": ["pukkandan"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "change",
|
||||||
|
"when": "f2ff0f6f1914b82d4a51681a72cc0828115dcb4a",
|
||||||
|
"short": "[extractor/motherless] Add gallery support, fix groups (#7211)",
|
||||||
|
"authors": ["rexlambert22", "Ti4eeT4e"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "change",
|
||||||
|
"when": "a4486bfc1dc7057efca9dd3fe70d7fa25c56f700",
|
||||||
|
"short": "[misc] Revert \"Add automatic duplicate issue detection\"",
|
||||||
|
"authors": ["pukkandan"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "add",
|
||||||
|
"when": "1ceb657bdd254ad961489e5060f2ccc7d556b729",
|
||||||
|
"short": "[priority] Security: [[CVE-2023-35934](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-35934)] Fix [Cookie leak](https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-v8mc-9377-rwjj)\n - `--add-header Cookie:` is deprecated and auto-scoped to input URL domains\n - Cookies are scoped when passed to external downloaders\n - Add `cookie` field to info.json and deprecate `http_headers.Cookie`"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
age_restricted,
|
age_restricted,
|
||||||
bug_reports_message,
|
bug_reports_message,
|
||||||
classproperty,
|
classproperty,
|
||||||
|
variadic,
|
||||||
write_string,
|
write_string,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ def commit_lookup(cls):
|
|||||||
'dependencies',
|
'dependencies',
|
||||||
'jsinterp',
|
'jsinterp',
|
||||||
'outtmpl',
|
'outtmpl',
|
||||||
|
'formats',
|
||||||
'plugins',
|
'plugins',
|
||||||
'update',
|
'update',
|
||||||
'upstream',
|
'upstream',
|
||||||
@@ -68,9 +69,9 @@ def commit_lookup(cls):
|
|||||||
'misc',
|
'misc',
|
||||||
'test',
|
'test',
|
||||||
},
|
},
|
||||||
cls.EXTRACTOR: {'extractor'},
|
cls.EXTRACTOR: {'extractor', 'ie'},
|
||||||
cls.DOWNLOADER: {'downloader'},
|
cls.DOWNLOADER: {'downloader', 'fd'},
|
||||||
cls.POSTPROCESSOR: {'postprocessor'},
|
cls.POSTPROCESSOR: {'postprocessor', 'pp'},
|
||||||
}.items()
|
}.items()
|
||||||
for name in names
|
for name in names
|
||||||
}
|
}
|
||||||
@@ -196,7 +197,7 @@ def _prepare_cleanup_misc_items(self, items):
|
|||||||
for commit_infos in cleanup_misc_items.values():
|
for commit_infos in cleanup_misc_items.values():
|
||||||
sorted_items.append(CommitInfo(
|
sorted_items.append(CommitInfo(
|
||||||
'cleanup', ('Miscellaneous',), ', '.join(
|
'cleanup', ('Miscellaneous',), ', '.join(
|
||||||
self._format_message_link(None, info.commit.hash)
|
self._format_message_link(None, info.commit.hash).strip()
|
||||||
for info in sorted(commit_infos, key=lambda item: item.commit.hash or '')),
|
for info in sorted(commit_infos, key=lambda item: item.commit.hash or '')),
|
||||||
[], Commit(None, '', commit_infos[0].commit.authors), []))
|
[], Commit(None, '', commit_infos[0].commit.authors), []))
|
||||||
|
|
||||||
@@ -205,10 +206,10 @@ def _prepare_cleanup_misc_items(self, items):
|
|||||||
def format_single_change(self, info):
|
def format_single_change(self, info):
|
||||||
message = self._format_message_link(info.message, info.commit.hash)
|
message = self._format_message_link(info.message, info.commit.hash)
|
||||||
if info.issues:
|
if info.issues:
|
||||||
message = f'{message} ({self._format_issues(info.issues)})'
|
message = message.replace('\n', f' ({self._format_issues(info.issues)})\n', 1)
|
||||||
|
|
||||||
if info.commit.authors:
|
if info.commit.authors:
|
||||||
message = f'{message} by {self._format_authors(info.commit.authors)}'
|
message = message.replace('\n', f' by {self._format_authors(info.commit.authors)}\n', 1)
|
||||||
|
|
||||||
if info.fixes:
|
if info.fixes:
|
||||||
fix_message = ', '.join(f'{self._format_message_link(None, fix.hash)}' for fix in info.fixes)
|
fix_message = ', '.join(f'{self._format_message_link(None, fix.hash)}' for fix in info.fixes)
|
||||||
@@ -217,14 +218,16 @@ def format_single_change(self, info):
|
|||||||
if authors != info.commit.authors:
|
if authors != info.commit.authors:
|
||||||
fix_message = f'{fix_message} by {self._format_authors(authors)}'
|
fix_message = f'{fix_message} by {self._format_authors(authors)}'
|
||||||
|
|
||||||
message = f'{message} (With fixes in {fix_message})'
|
message = message.replace('\n', f' (With fixes in {fix_message})\n', 1)
|
||||||
|
|
||||||
return message
|
return message[:-1]
|
||||||
|
|
||||||
def _format_message_link(self, message, hash):
|
def _format_message_link(self, message, hash):
|
||||||
assert message or hash, 'Improperly defined commit message or override'
|
assert message or hash, 'Improperly defined commit message or override'
|
||||||
message = message if message else hash[:HASH_LENGTH]
|
message = message if message else hash[:HASH_LENGTH]
|
||||||
return f'[{message}]({self.repo_url}/commit/{hash})' if hash else message
|
if not hash:
|
||||||
|
return f'{message}\n'
|
||||||
|
return f'[{message}\n'.replace('\n', f']({self.repo_url}/commit/{hash})\n', 1)
|
||||||
|
|
||||||
def _format_issues(self, issues):
|
def _format_issues(self, issues):
|
||||||
return ', '.join(f'[#{issue}]({self.repo_url}/issues/{issue})' for issue in issues)
|
return ', '.join(f'[#{issue}]({self.repo_url}/issues/{issue})' for issue in issues)
|
||||||
@@ -250,6 +253,7 @@ class CommitRange:
|
|||||||
(?:\ \((?P<issues>\#\d+(?:,\ \#\d+)*)\))?
|
(?:\ \((?P<issues>\#\d+(?:,\ \#\d+)*)\))?
|
||||||
''', re.VERBOSE | re.DOTALL)
|
''', re.VERBOSE | re.DOTALL)
|
||||||
EXTRACTOR_INDICATOR_RE = re.compile(r'(?:Fix|Add)\s+Extractors?', re.IGNORECASE)
|
EXTRACTOR_INDICATOR_RE = re.compile(r'(?:Fix|Add)\s+Extractors?', re.IGNORECASE)
|
||||||
|
REVERT_RE = re.compile(r'(?i:Revert)\s+([\da-f]{40})')
|
||||||
FIXES_RE = re.compile(r'(?i:Fix(?:es)?(?:\s+bugs?)?(?:\s+in|\s+for)?|Revert)\s+([\da-f]{40})')
|
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]+)')
|
UPSTREAM_MERGE_RE = re.compile(r'Update to ytdl-commit-([\da-f]+)')
|
||||||
|
|
||||||
@@ -277,7 +281,7 @@ def _get_commits_and_fixes(self, default_author):
|
|||||||
self.COMMAND, 'log', f'--format=%H%n%s%n%b%n{self.COMMIT_SEPARATOR}',
|
self.COMMAND, 'log', f'--format=%H%n%s%n%b%n{self.COMMIT_SEPARATOR}',
|
||||||
f'{self._start}..{self._end}' if self._start else self._end).stdout
|
f'{self._start}..{self._end}' if self._start else self._end).stdout
|
||||||
|
|
||||||
commits = {}
|
commits, reverts = {}, {}
|
||||||
fixes = defaultdict(list)
|
fixes = defaultdict(list)
|
||||||
lines = iter(result.splitlines(False))
|
lines = iter(result.splitlines(False))
|
||||||
for i, commit_hash in enumerate(lines):
|
for i, commit_hash in enumerate(lines):
|
||||||
@@ -298,6 +302,11 @@ def _get_commits_and_fixes(self, default_author):
|
|||||||
logger.debug(f'Reached Release commit, breaking: {commit}')
|
logger.debug(f'Reached Release commit, breaking: {commit}')
|
||||||
break
|
break
|
||||||
|
|
||||||
|
revert_match = self.REVERT_RE.fullmatch(commit.short)
|
||||||
|
if revert_match:
|
||||||
|
reverts[revert_match.group(1)] = commit
|
||||||
|
continue
|
||||||
|
|
||||||
fix_match = self.FIXES_RE.search(commit.short)
|
fix_match = self.FIXES_RE.search(commit.short)
|
||||||
if fix_match:
|
if fix_match:
|
||||||
commitish = fix_match.group(1)
|
commitish = fix_match.group(1)
|
||||||
@@ -305,6 +314,13 @@ def _get_commits_and_fixes(self, default_author):
|
|||||||
|
|
||||||
commits[commit.hash] = commit
|
commits[commit.hash] = commit
|
||||||
|
|
||||||
|
for commitish, revert_commit in reverts.items():
|
||||||
|
reverted = commits.pop(commitish, None)
|
||||||
|
if reverted:
|
||||||
|
logger.debug(f'{commit} fully reverted {reverted}')
|
||||||
|
else:
|
||||||
|
commits[revert_commit.hash] = revert_commit
|
||||||
|
|
||||||
for commitish, fix_commits in fixes.items():
|
for commitish, fix_commits in fixes.items():
|
||||||
if commitish in commits:
|
if commitish in commits:
|
||||||
hashes = ', '.join(commit.hash[:HASH_LENGTH] for commit in fix_commits)
|
hashes = ', '.join(commit.hash[:HASH_LENGTH] for commit in fix_commits)
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ ignore = E402,E501,E731,E741,W503
|
|||||||
max_line_length = 120
|
max_line_length = 120
|
||||||
per_file_ignores =
|
per_file_ignores =
|
||||||
devscripts/lazy_load_template.py: F401
|
devscripts/lazy_load_template.py: F401
|
||||||
yt_dlp/utils/__init__.py: F401, F403
|
|
||||||
|
|
||||||
|
|
||||||
[autoflake]
|
[autoflake]
|
||||||
|
|||||||
@@ -515,6 +515,7 @@ # Supported sites
|
|||||||
- **GlattvisionTVLive**: [*glattvisiontv*](## "netrc machine")
|
- **GlattvisionTVLive**: [*glattvisiontv*](## "netrc machine")
|
||||||
- **GlattvisionTVRecordings**: [*glattvisiontv*](## "netrc machine")
|
- **GlattvisionTVRecordings**: [*glattvisiontv*](## "netrc machine")
|
||||||
- **Glide**: Glide mobile video messages (glide.me)
|
- **Glide**: Glide mobile video messages (glide.me)
|
||||||
|
- **GlobalCyclingNetworkPlus**
|
||||||
- **GlobalPlayerAudio**
|
- **GlobalPlayerAudio**
|
||||||
- **GlobalPlayerAudioEpisode**
|
- **GlobalPlayerAudioEpisode**
|
||||||
- **GlobalPlayerLive**
|
- **GlobalPlayerLive**
|
||||||
@@ -814,6 +815,7 @@ # Supported sites
|
|||||||
- **MonsterSirenHypergryphMusic**
|
- **MonsterSirenHypergryphMusic**
|
||||||
- **Morningstar**: morningstar.com
|
- **Morningstar**: morningstar.com
|
||||||
- **Motherless**
|
- **Motherless**
|
||||||
|
- **MotherlessGallery**
|
||||||
- **MotherlessGroup**
|
- **MotherlessGroup**
|
||||||
- **Motorsport**: motorsport.com
|
- **Motorsport**: motorsport.com
|
||||||
- **MotorTrend**
|
- **MotorTrend**
|
||||||
@@ -1134,6 +1136,7 @@ # Supported sites
|
|||||||
- **puhutv:serie**
|
- **puhutv:serie**
|
||||||
- **Puls4**
|
- **Puls4**
|
||||||
- **Pyvideo**
|
- **Pyvideo**
|
||||||
|
- **QDance**: [*qdance*](## "netrc machine")
|
||||||
- **QingTing**
|
- **QingTing**
|
||||||
- **qqmusic**: QQ音乐
|
- **qqmusic**: QQ音乐
|
||||||
- **qqmusic:album**: QQ音乐 - 专辑
|
- **qqmusic:album**: QQ音乐 - 专辑
|
||||||
@@ -1198,6 +1201,7 @@ # Supported sites
|
|||||||
- **Restudy**
|
- **Restudy**
|
||||||
- **Reuters**
|
- **Reuters**
|
||||||
- **ReverbNation**
|
- **ReverbNation**
|
||||||
|
- **RheinMainTV**
|
||||||
- **RICE**
|
- **RICE**
|
||||||
- **RMCDecouverte**
|
- **RMCDecouverte**
|
||||||
- **RockstarGames**
|
- **RockstarGames**
|
||||||
@@ -1360,6 +1364,8 @@ # Supported sites
|
|||||||
- **sr:mediathek**: Saarländischer Rundfunk
|
- **sr:mediathek**: Saarländischer Rundfunk
|
||||||
- **SRGSSR**
|
- **SRGSSR**
|
||||||
- **SRGSSRPlay**: srf.ch, rts.ch, rsi.ch, rtr.ch and swissinfo.ch play sites
|
- **SRGSSRPlay**: srf.ch, rts.ch, rsi.ch, rtr.ch and swissinfo.ch play sites
|
||||||
|
- **StacommuLive**: [*stacommu*](## "netrc machine")
|
||||||
|
- **StacommuVOD**: [*stacommu*](## "netrc machine")
|
||||||
- **StagePlusVODConcert**: [*stageplus*](## "netrc machine")
|
- **StagePlusVODConcert**: [*stageplus*](## "netrc machine")
|
||||||
- **stanfordoc**: Stanford Open ClassRoom
|
- **stanfordoc**: Stanford Open ClassRoom
|
||||||
- **StarTrek**
|
- **StarTrek**
|
||||||
@@ -1644,6 +1650,8 @@ # Supported sites
|
|||||||
- **vk**: [*vk*](## "netrc machine") VK
|
- **vk**: [*vk*](## "netrc machine") VK
|
||||||
- **vk:uservideos**: [*vk*](## "netrc machine") VK - User's Videos
|
- **vk:uservideos**: [*vk*](## "netrc machine") VK - User's Videos
|
||||||
- **vk:wallpost**: [*vk*](## "netrc machine")
|
- **vk:wallpost**: [*vk*](## "netrc machine")
|
||||||
|
- **VKPlay**
|
||||||
|
- **VKPlayLive**
|
||||||
- **vm.tiktok**
|
- **vm.tiktok**
|
||||||
- **Vocaroo**
|
- **Vocaroo**
|
||||||
- **Vodlocker**
|
- **Vodlocker**
|
||||||
@@ -1797,7 +1805,6 @@ # Supported sites
|
|||||||
- **youtube:search:date**: YouTube search, newest videos first; "ytsearchdate:" prefix
|
- **youtube:search:date**: YouTube search, newest videos first; "ytsearchdate:" prefix
|
||||||
- **youtube:search_url**: YouTube search URLs with sorting and filter support
|
- **youtube:search_url**: YouTube search URLs with sorting and filter support
|
||||||
- **youtube:shorts:pivot:audio**: YouTube Shorts audio pivot (Shorts using audio of a given video)
|
- **youtube:shorts:pivot:audio**: YouTube Shorts audio pivot (Shorts using audio of a given video)
|
||||||
- **youtube:stories**: YouTube channel stories; "ytstories:" prefix
|
|
||||||
- **youtube:subscriptions**: YouTube subscriptions feed; ":ytsubs" keyword (requires cookies)
|
- **youtube:subscriptions**: YouTube subscriptions feed; ":ytsubs" keyword (requires cookies)
|
||||||
- **youtube:tab**: YouTube Tabs
|
- **youtube:tab**: YouTube Tabs
|
||||||
- **youtube:user**: YouTube user videos; "ytuser:" prefix
|
- **youtube:user**: YouTube user videos; "ytuser:" prefix
|
||||||
|
|||||||
@@ -684,7 +684,7 @@ def test(tmpl, expected, *, info=None, **params):
|
|||||||
test('%(id)s.%(ext)s', '1234.mp4')
|
test('%(id)s.%(ext)s', '1234.mp4')
|
||||||
test('%(duration_string)s', ('27:46:40', '27-46-40'))
|
test('%(duration_string)s', ('27:46:40', '27-46-40'))
|
||||||
test('%(resolution)s', '1080p')
|
test('%(resolution)s', '1080p')
|
||||||
test('%(playlist_index)s', '001')
|
test('%(playlist_index|)s', '001')
|
||||||
test('%(playlist_autonumber)s', '02')
|
test('%(playlist_autonumber)s', '02')
|
||||||
test('%(autonumber)s', '00001')
|
test('%(autonumber)s', '00001')
|
||||||
test('%(autonumber+2)03d', '005', autonumber_start=3)
|
test('%(autonumber+2)03d', '005', autonumber_start=3)
|
||||||
@@ -1213,6 +1213,62 @@ def _real_extract(self, url):
|
|||||||
self.assertEqual(downloaded['extractor'], 'Video')
|
self.assertEqual(downloaded['extractor'], 'Video')
|
||||||
self.assertEqual(downloaded['extractor_key'], 'Video')
|
self.assertEqual(downloaded['extractor_key'], 'Video')
|
||||||
|
|
||||||
|
def test_header_cookies(self):
|
||||||
|
from http.cookiejar import Cookie
|
||||||
|
|
||||||
|
ydl = FakeYDL()
|
||||||
|
ydl.report_warning = lambda *_, **__: None
|
||||||
|
|
||||||
|
def cookie(name, value, version=None, domain='', path='', secure=False, expires=None):
|
||||||
|
return Cookie(
|
||||||
|
version or 0, name, value, None, False,
|
||||||
|
domain, bool(domain), bool(domain), path, bool(path),
|
||||||
|
secure, expires, False, None, None, rest={})
|
||||||
|
|
||||||
|
_test_url = 'https://yt.dlp/test'
|
||||||
|
|
||||||
|
def test(encoded_cookies, cookies, headers=False, round_trip=None, error=None):
|
||||||
|
def _test():
|
||||||
|
ydl.cookiejar.clear()
|
||||||
|
ydl._load_cookies(encoded_cookies, from_headers=headers)
|
||||||
|
if headers:
|
||||||
|
ydl._apply_header_cookies(_test_url)
|
||||||
|
data = {'url': _test_url}
|
||||||
|
ydl._calc_headers(data)
|
||||||
|
self.assertCountEqual(
|
||||||
|
map(vars, ydl.cookiejar), map(vars, cookies),
|
||||||
|
'Extracted cookiejar.Cookie is not the same')
|
||||||
|
if not headers:
|
||||||
|
self.assertEqual(
|
||||||
|
data.get('cookies'), round_trip or encoded_cookies,
|
||||||
|
'Cookie is not the same as round trip')
|
||||||
|
ydl.__dict__['_YoutubeDL__header_cookies'] = []
|
||||||
|
|
||||||
|
with self.subTest(msg=encoded_cookies):
|
||||||
|
if not error:
|
||||||
|
_test()
|
||||||
|
return
|
||||||
|
with self.assertRaisesRegex(Exception, error):
|
||||||
|
_test()
|
||||||
|
|
||||||
|
test('test=value; Domain=.yt.dlp', [cookie('test', 'value', domain='.yt.dlp')])
|
||||||
|
test('test=value', [cookie('test', 'value')], error='Unscoped cookies are not allowed')
|
||||||
|
test('cookie1=value1; Domain=.yt.dlp; Path=/test; cookie2=value2; Domain=.yt.dlp; Path=/', [
|
||||||
|
cookie('cookie1', 'value1', domain='.yt.dlp', path='/test'),
|
||||||
|
cookie('cookie2', 'value2', domain='.yt.dlp', path='/')])
|
||||||
|
test('test=value; Domain=.yt.dlp; Path=/test; Secure; Expires=9999999999', [
|
||||||
|
cookie('test', 'value', domain='.yt.dlp', path='/test', secure=True, expires=9999999999)])
|
||||||
|
test('test="value; "; path=/test; domain=.yt.dlp', [
|
||||||
|
cookie('test', 'value; ', domain='.yt.dlp', path='/test')],
|
||||||
|
round_trip='test="value\\073 "; Domain=.yt.dlp; Path=/test')
|
||||||
|
test('name=; Domain=.yt.dlp', [cookie('name', '', domain='.yt.dlp')],
|
||||||
|
round_trip='name=""; Domain=.yt.dlp')
|
||||||
|
|
||||||
|
test('test=value', [cookie('test', 'value', domain='.yt.dlp')], headers=True)
|
||||||
|
test('cookie1=value; Domain=.yt.dlp; cookie2=value', [], headers=True, error='Invalid syntax')
|
||||||
|
ydl.deprecated_feature = ydl.report_error
|
||||||
|
test('test=value', [], headers=True, error='Passing cookies as a header is a potential security risk')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
133
test/test_downloader_external.py
Normal file
133
test/test_downloader_external.py
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# Allow direct execution
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
import http.cookiejar
|
||||||
|
|
||||||
|
from test.helper import FakeYDL
|
||||||
|
from yt_dlp.downloader.external import (
|
||||||
|
Aria2cFD,
|
||||||
|
AxelFD,
|
||||||
|
CurlFD,
|
||||||
|
FFmpegFD,
|
||||||
|
HttpieFD,
|
||||||
|
WgetFD,
|
||||||
|
)
|
||||||
|
|
||||||
|
TEST_COOKIE = {
|
||||||
|
'version': 0,
|
||||||
|
'name': 'test',
|
||||||
|
'value': 'ytdlp',
|
||||||
|
'port': None,
|
||||||
|
'port_specified': False,
|
||||||
|
'domain': '.example.com',
|
||||||
|
'domain_specified': True,
|
||||||
|
'domain_initial_dot': False,
|
||||||
|
'path': '/',
|
||||||
|
'path_specified': True,
|
||||||
|
'secure': False,
|
||||||
|
'expires': None,
|
||||||
|
'discard': False,
|
||||||
|
'comment': None,
|
||||||
|
'comment_url': None,
|
||||||
|
'rest': {},
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_INFO = {'url': 'http://www.example.com/'}
|
||||||
|
|
||||||
|
|
||||||
|
class TestHttpieFD(unittest.TestCase):
|
||||||
|
def test_make_cmd(self):
|
||||||
|
with FakeYDL() as ydl:
|
||||||
|
downloader = HttpieFD(ydl, {})
|
||||||
|
self.assertEqual(
|
||||||
|
downloader._make_cmd('test', TEST_INFO),
|
||||||
|
['http', '--download', '--output', 'test', 'http://www.example.com/'])
|
||||||
|
|
||||||
|
# Test cookie header is added
|
||||||
|
ydl.cookiejar.set_cookie(http.cookiejar.Cookie(**TEST_COOKIE))
|
||||||
|
self.assertEqual(
|
||||||
|
downloader._make_cmd('test', TEST_INFO),
|
||||||
|
['http', '--download', '--output', 'test', 'http://www.example.com/', 'Cookie:test=ytdlp'])
|
||||||
|
|
||||||
|
|
||||||
|
class TestAxelFD(unittest.TestCase):
|
||||||
|
def test_make_cmd(self):
|
||||||
|
with FakeYDL() as ydl:
|
||||||
|
downloader = AxelFD(ydl, {})
|
||||||
|
self.assertEqual(
|
||||||
|
downloader._make_cmd('test', TEST_INFO),
|
||||||
|
['axel', '-o', 'test', '--', 'http://www.example.com/'])
|
||||||
|
|
||||||
|
# Test cookie header is added
|
||||||
|
ydl.cookiejar.set_cookie(http.cookiejar.Cookie(**TEST_COOKIE))
|
||||||
|
self.assertEqual(
|
||||||
|
downloader._make_cmd('test', TEST_INFO),
|
||||||
|
['axel', '-o', 'test', 'Cookie: test=ytdlp', '--max-redirect=0', '--', 'http://www.example.com/'])
|
||||||
|
|
||||||
|
|
||||||
|
class TestWgetFD(unittest.TestCase):
|
||||||
|
def test_make_cmd(self):
|
||||||
|
with FakeYDL() as ydl:
|
||||||
|
downloader = WgetFD(ydl, {})
|
||||||
|
self.assertNotIn('--load-cookies', downloader._make_cmd('test', TEST_INFO))
|
||||||
|
# Test cookiejar tempfile arg is added
|
||||||
|
ydl.cookiejar.set_cookie(http.cookiejar.Cookie(**TEST_COOKIE))
|
||||||
|
self.assertIn('--load-cookies', downloader._make_cmd('test', TEST_INFO))
|
||||||
|
|
||||||
|
|
||||||
|
class TestCurlFD(unittest.TestCase):
|
||||||
|
def test_make_cmd(self):
|
||||||
|
with FakeYDL() as ydl:
|
||||||
|
downloader = CurlFD(ydl, {})
|
||||||
|
self.assertNotIn('--cookie-jar', downloader._make_cmd('test', TEST_INFO))
|
||||||
|
# Test cookiejar tempfile arg is added
|
||||||
|
ydl.cookiejar.set_cookie(http.cookiejar.Cookie(**TEST_COOKIE))
|
||||||
|
self.assertIn('--cookie-jar', downloader._make_cmd('test', TEST_INFO))
|
||||||
|
|
||||||
|
|
||||||
|
class TestAria2cFD(unittest.TestCase):
|
||||||
|
def test_make_cmd(self):
|
||||||
|
with FakeYDL() as ydl:
|
||||||
|
downloader = Aria2cFD(ydl, {})
|
||||||
|
downloader._make_cmd('test', TEST_INFO)
|
||||||
|
self.assertFalse(hasattr(downloader, '_cookies_tempfile'))
|
||||||
|
|
||||||
|
# Test cookiejar tempfile arg is added
|
||||||
|
ydl.cookiejar.set_cookie(http.cookiejar.Cookie(**TEST_COOKIE))
|
||||||
|
cmd = downloader._make_cmd('test', TEST_INFO)
|
||||||
|
self.assertIn(f'--load-cookies={downloader._cookies_tempfile}', cmd)
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipUnless(FFmpegFD.available(), 'ffmpeg not found')
|
||||||
|
class TestFFmpegFD(unittest.TestCase):
|
||||||
|
_args = []
|
||||||
|
|
||||||
|
def _test_cmd(self, args):
|
||||||
|
self._args = args
|
||||||
|
|
||||||
|
def test_make_cmd(self):
|
||||||
|
with FakeYDL() as ydl:
|
||||||
|
downloader = FFmpegFD(ydl, {})
|
||||||
|
downloader._debug_cmd = self._test_cmd
|
||||||
|
|
||||||
|
downloader._call_downloader('test', {**TEST_INFO, 'ext': 'mp4'})
|
||||||
|
self.assertEqual(self._args, [
|
||||||
|
'ffmpeg', '-y', '-hide_banner', '-i', 'http://www.example.com/',
|
||||||
|
'-c', 'copy', '-f', 'mp4', 'file:test'])
|
||||||
|
|
||||||
|
# Test cookies arg is added
|
||||||
|
ydl.cookiejar.set_cookie(http.cookiejar.Cookie(**TEST_COOKIE))
|
||||||
|
downloader._call_downloader('test', {**TEST_INFO, 'ext': 'mp4'})
|
||||||
|
self.assertEqual(self._args, [
|
||||||
|
'ffmpeg', '-y', '-hide_banner', '-cookies', 'test=ytdlp; path=/; domain=.example.com;\r\n',
|
||||||
|
'-i', 'http://www.example.com/', '-c', 'copy', '-f', 'mp4', 'file:test'])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
@@ -132,6 +132,11 @@ def do_GET(self):
|
|||||||
self._method('GET')
|
self._method('GET')
|
||||||
elif self.path.startswith('/headers'):
|
elif self.path.startswith('/headers'):
|
||||||
self._headers()
|
self._headers()
|
||||||
|
elif self.path.startswith('/308-to-headers'):
|
||||||
|
self.send_response(308)
|
||||||
|
self.send_header('Location', '/headers')
|
||||||
|
self.send_header('Content-Length', '0')
|
||||||
|
self.end_headers()
|
||||||
elif self.path == '/trailing_garbage':
|
elif self.path == '/trailing_garbage':
|
||||||
payload = b'<html><video src="/vid.mp4" /></html>'
|
payload = b'<html><video src="/vid.mp4" /></html>'
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
@@ -270,6 +275,7 @@ def do_req(redirect_status, method):
|
|||||||
self.assertEqual(do_req(303, 'PUT'), ('', 'GET'))
|
self.assertEqual(do_req(303, 'PUT'), ('', 'GET'))
|
||||||
|
|
||||||
# 301 and 302 turn POST only into a GET
|
# 301 and 302 turn POST only into a GET
|
||||||
|
# XXX: we should also test if the Content-Type and Content-Length headers are removed
|
||||||
self.assertEqual(do_req(301, 'POST'), ('', 'GET'))
|
self.assertEqual(do_req(301, 'POST'), ('', 'GET'))
|
||||||
self.assertEqual(do_req(301, 'HEAD'), ('', 'HEAD'))
|
self.assertEqual(do_req(301, 'HEAD'), ('', 'HEAD'))
|
||||||
self.assertEqual(do_req(302, 'POST'), ('', 'GET'))
|
self.assertEqual(do_req(302, 'POST'), ('', 'GET'))
|
||||||
@@ -313,6 +319,31 @@ def test_cookiejar(self):
|
|||||||
data = ydl.urlopen(sanitized_Request(f'http://127.0.0.1:{self.http_port}/headers')).read()
|
data = ydl.urlopen(sanitized_Request(f'http://127.0.0.1:{self.http_port}/headers')).read()
|
||||||
self.assertIn(b'Cookie: test=ytdlp', data)
|
self.assertIn(b'Cookie: test=ytdlp', data)
|
||||||
|
|
||||||
|
def test_passed_cookie_header(self):
|
||||||
|
# We should accept a Cookie header being passed as in normal headers and handle it appropriately.
|
||||||
|
with FakeYDL() as ydl:
|
||||||
|
# Specified Cookie header should be used
|
||||||
|
res = ydl.urlopen(
|
||||||
|
sanitized_Request(f'http://127.0.0.1:{self.http_port}/headers',
|
||||||
|
headers={'Cookie': 'test=test'})).read().decode('utf-8')
|
||||||
|
self.assertIn('Cookie: test=test', res)
|
||||||
|
|
||||||
|
# Specified Cookie header should be removed on any redirect
|
||||||
|
res = ydl.urlopen(
|
||||||
|
sanitized_Request(f'http://127.0.0.1:{self.http_port}/308-to-headers', headers={'Cookie': 'test=test'})).read().decode('utf-8')
|
||||||
|
self.assertNotIn('Cookie: test=test', res)
|
||||||
|
|
||||||
|
# Specified Cookie header should override global cookiejar for that request
|
||||||
|
ydl.cookiejar.set_cookie(http.cookiejar.Cookie(
|
||||||
|
version=0, name='test', value='ytdlp', port=None, port_specified=False,
|
||||||
|
domain='127.0.0.1', domain_specified=True, domain_initial_dot=False, path='/',
|
||||||
|
path_specified=True, secure=False, expires=None, discard=False, comment=None,
|
||||||
|
comment_url=None, rest={}))
|
||||||
|
|
||||||
|
data = ydl.urlopen(sanitized_Request(f'http://127.0.0.1:{self.http_port}/headers', headers={'Cookie': 'test=test'})).read()
|
||||||
|
self.assertNotIn(b'Cookie: test=ytdlp', data)
|
||||||
|
self.assertIn(b'Cookie: test=test', data)
|
||||||
|
|
||||||
def test_no_compression_compat_header(self):
|
def test_no_compression_compat_header(self):
|
||||||
with FakeYDL() as ydl:
|
with FakeYDL() as ydl:
|
||||||
data = ydl.urlopen(
|
data = ydl.urlopen(
|
||||||
|
|||||||
@@ -159,6 +159,10 @@
|
|||||||
'https://www.youtube.com/s/player/8c7583ff/player_ias.vflset/en_US/base.js',
|
'https://www.youtube.com/s/player/8c7583ff/player_ias.vflset/en_US/base.js',
|
||||||
'1wWCVpRR96eAmMI87L', 'KSkWAVv1ZQxC3A',
|
'1wWCVpRR96eAmMI87L', 'KSkWAVv1ZQxC3A',
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
'https://www.youtube.com/s/player/b7910ca8/player_ias.vflset/en_US/base.js',
|
||||||
|
'_hXMCwMt9qE310D', 'LoZMgkkofRMCZQ',
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import collections
|
import collections
|
||||||
import contextlib
|
import contextlib
|
||||||
|
import copy
|
||||||
import datetime
|
import datetime
|
||||||
import errno
|
import errno
|
||||||
import fileinput
|
import fileinput
|
||||||
import functools
|
import functools
|
||||||
|
import http.cookiejar
|
||||||
import io
|
import io
|
||||||
import itertools
|
import itertools
|
||||||
import json
|
import json
|
||||||
@@ -25,7 +27,7 @@
|
|||||||
from .cache import Cache
|
from .cache import Cache
|
||||||
from .compat import urllib # isort: split
|
from .compat import urllib # isort: split
|
||||||
from .compat import compat_os_name, compat_shlex_quote
|
from .compat import compat_os_name, compat_shlex_quote
|
||||||
from .cookies import load_cookies
|
from .cookies import LenientSimpleCookie, load_cookies
|
||||||
from .downloader import FFmpegFD, get_suitable_downloader, shorten_protocol_name
|
from .downloader import FFmpegFD, get_suitable_downloader, shorten_protocol_name
|
||||||
from .downloader.rtmp import rtmpdump_version
|
from .downloader.rtmp import rtmpdump_version
|
||||||
from .extractor import gen_extractor_classes, get_info_extractor
|
from .extractor import gen_extractor_classes, get_info_extractor
|
||||||
@@ -673,6 +675,9 @@ def process_color_policy(stream):
|
|||||||
if auto_init and auto_init != 'no_verbose_header':
|
if auto_init and auto_init != 'no_verbose_header':
|
||||||
self.print_debug_header()
|
self.print_debug_header()
|
||||||
|
|
||||||
|
self.__header_cookies = []
|
||||||
|
self._load_cookies(traverse_obj(self.params.get('http_headers'), 'cookie', casesense=False)) # compat
|
||||||
|
|
||||||
def check_deprecated(param, option, suggestion):
|
def check_deprecated(param, option, suggestion):
|
||||||
if self.params.get(param) is not None:
|
if self.params.get(param) is not None:
|
||||||
self.report_warning(f'{option} is deprecated. Use {suggestion} instead')
|
self.report_warning(f'{option} is deprecated. Use {suggestion} instead')
|
||||||
@@ -983,6 +988,7 @@ def trouble(self, message=None, tb=None, is_error=True):
|
|||||||
ID='green',
|
ID='green',
|
||||||
DELIM='blue',
|
DELIM='blue',
|
||||||
ERROR='red',
|
ERROR='red',
|
||||||
|
BAD_FORMAT='light red',
|
||||||
WARNING='yellow',
|
WARNING='yellow',
|
||||||
SUPPRESS='light black',
|
SUPPRESS='light black',
|
||||||
)
|
)
|
||||||
@@ -1271,21 +1277,20 @@ def create_key(outer_mobj):
|
|||||||
return outer_mobj.group(0)
|
return outer_mobj.group(0)
|
||||||
key = outer_mobj.group('key')
|
key = outer_mobj.group('key')
|
||||||
mobj = re.match(INTERNAL_FORMAT_RE, key)
|
mobj = re.match(INTERNAL_FORMAT_RE, key)
|
||||||
initial_field = mobj.group('fields') if mobj else ''
|
value, replacement, default, last_field = None, None, na, ''
|
||||||
value, replacement, default = None, None, na
|
|
||||||
while mobj:
|
while mobj:
|
||||||
mobj = mobj.groupdict()
|
mobj = mobj.groupdict()
|
||||||
default = mobj['default'] if mobj['default'] is not None else default
|
default = mobj['default'] if mobj['default'] is not None else default
|
||||||
value = get_value(mobj)
|
value = get_value(mobj)
|
||||||
replacement = mobj['replacement']
|
last_field, replacement = mobj['fields'], mobj['replacement']
|
||||||
if value is None and mobj['alternate']:
|
if value is None and mobj['alternate']:
|
||||||
mobj = re.match(INTERNAL_FORMAT_RE, mobj['remaining'][1:])
|
mobj = re.match(INTERNAL_FORMAT_RE, mobj['remaining'][1:])
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
|
|
||||||
fmt = outer_mobj.group('format')
|
fmt = outer_mobj.group('format')
|
||||||
if fmt == 's' and value is not None and key in field_size_compat_map.keys():
|
if fmt == 's' and value is not None and last_field in field_size_compat_map.keys():
|
||||||
fmt = f'0{field_size_compat_map[key]:d}d'
|
fmt = f'0{field_size_compat_map[last_field]:d}d'
|
||||||
|
|
||||||
if None not in (value, replacement):
|
if None not in (value, replacement):
|
||||||
try:
|
try:
|
||||||
@@ -1322,7 +1327,7 @@ def create_key(outer_mobj):
|
|||||||
value = format_decimal_suffix(value, f'%{num_fmt}f%s' if num_fmt else '%d%s',
|
value = format_decimal_suffix(value, f'%{num_fmt}f%s' if num_fmt else '%d%s',
|
||||||
factor=1024 if '#' in flags else 1000)
|
factor=1024 if '#' in flags else 1000)
|
||||||
elif fmt[-1] == 'S': # filename sanitization
|
elif fmt[-1] == 'S': # filename sanitization
|
||||||
value, fmt = filename_sanitizer(initial_field, value, restricted='#' in flags), str_fmt
|
value, fmt = filename_sanitizer(last_field, value, restricted='#' in flags), str_fmt
|
||||||
elif fmt[-1] == 'c':
|
elif fmt[-1] == 'c':
|
||||||
if value:
|
if value:
|
||||||
value = str(value)[0]
|
value = str(value)[0]
|
||||||
@@ -1341,7 +1346,7 @@ def create_key(outer_mobj):
|
|||||||
elif fmt[-1] == 'a':
|
elif fmt[-1] == 'a':
|
||||||
value, fmt = ascii(value), str_fmt
|
value, fmt = ascii(value), str_fmt
|
||||||
if fmt[-1] in 'csra':
|
if fmt[-1] in 'csra':
|
||||||
value = sanitizer(initial_field, value)
|
value = sanitizer(last_field, value)
|
||||||
|
|
||||||
key = '%s\0%s' % (key.replace('%', '%\0'), outer_mobj.group('format'))
|
key = '%s\0%s' % (key.replace('%', '%\0'), outer_mobj.group('format'))
|
||||||
TMPL_DICT[key] = value
|
TMPL_DICT[key] = value
|
||||||
@@ -1625,8 +1630,60 @@ def progress(msg):
|
|||||||
self.to_screen('')
|
self.to_screen('')
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
def _load_cookies(self, data, *, from_headers=True):
|
||||||
|
"""Loads cookies from a `Cookie` header
|
||||||
|
|
||||||
|
This tries to work around the security vulnerability of passing cookies to every domain.
|
||||||
|
See: https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-v8mc-9377-rwjj
|
||||||
|
The unscoped cookies are saved for later to be stored in the jar with a limited scope.
|
||||||
|
|
||||||
|
@param data The Cookie header as string to load the cookies from
|
||||||
|
@param from_headers If `False`, allows Set-Cookie syntax in the cookie string (at least a domain will be required)
|
||||||
|
"""
|
||||||
|
for cookie in LenientSimpleCookie(data).values():
|
||||||
|
if from_headers and any(cookie.values()):
|
||||||
|
raise ValueError('Invalid syntax in Cookie Header')
|
||||||
|
|
||||||
|
domain = cookie.get('domain') or ''
|
||||||
|
expiry = cookie.get('expires')
|
||||||
|
if expiry == '': # 0 is valid
|
||||||
|
expiry = None
|
||||||
|
prepared_cookie = http.cookiejar.Cookie(
|
||||||
|
cookie.get('version') or 0, cookie.key, cookie.value, None, False,
|
||||||
|
domain, True, True, cookie.get('path') or '', bool(cookie.get('path')),
|
||||||
|
cookie.get('secure') or False, expiry, False, None, None, {})
|
||||||
|
|
||||||
|
if domain:
|
||||||
|
self.cookiejar.set_cookie(prepared_cookie)
|
||||||
|
elif from_headers:
|
||||||
|
self.deprecated_feature(
|
||||||
|
'Passing cookies as a header is a potential security risk; '
|
||||||
|
'they will be scoped to the domain of the downloaded urls. '
|
||||||
|
'Please consider loading cookies from a file or browser instead.')
|
||||||
|
self.__header_cookies.append(prepared_cookie)
|
||||||
|
else:
|
||||||
|
self.report_error('Unscoped cookies are not allowed; please specify some sort of scoping',
|
||||||
|
tb=False, is_error=False)
|
||||||
|
|
||||||
|
def _apply_header_cookies(self, url):
|
||||||
|
"""Applies stray header cookies to the provided url
|
||||||
|
|
||||||
|
This loads header cookies and scopes them to the domain provided in `url`.
|
||||||
|
While this is not ideal, it helps reduce the risk of them being sent
|
||||||
|
to an unintended destination while mostly maintaining compatibility.
|
||||||
|
"""
|
||||||
|
parsed = urllib.parse.urlparse(url)
|
||||||
|
if not parsed.hostname:
|
||||||
|
return
|
||||||
|
|
||||||
|
for cookie in map(copy.copy, self.__header_cookies):
|
||||||
|
cookie.domain = f'.{parsed.hostname}'
|
||||||
|
self.cookiejar.set_cookie(cookie)
|
||||||
|
|
||||||
@_handle_extraction_exceptions
|
@_handle_extraction_exceptions
|
||||||
def __extract_info(self, url, ie, download, extra_info, process):
|
def __extract_info(self, url, ie, download, extra_info, process):
|
||||||
|
self._apply_header_cookies(url)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ie_result = ie.extract(url)
|
ie_result = ie.extract(url)
|
||||||
except UserNotLive as e:
|
except UserNotLive as e:
|
||||||
@@ -2086,8 +2143,6 @@ def syntax_error(note, start):
|
|||||||
allow_multiple_streams = {'audio': self.params.get('allow_multiple_audio_streams', False),
|
allow_multiple_streams = {'audio': self.params.get('allow_multiple_audio_streams', False),
|
||||||
'video': self.params.get('allow_multiple_video_streams', False)}
|
'video': self.params.get('allow_multiple_video_streams', False)}
|
||||||
|
|
||||||
check_formats = self.params.get('check_formats') == 'selected'
|
|
||||||
|
|
||||||
def _parse_filter(tokens):
|
def _parse_filter(tokens):
|
||||||
filter_parts = []
|
filter_parts = []
|
||||||
for type, string_, start, _, _ in tokens:
|
for type, string_, start, _, _ in tokens:
|
||||||
@@ -2260,10 +2315,19 @@ def _merge(formats_pair):
|
|||||||
return new_dict
|
return new_dict
|
||||||
|
|
||||||
def _check_formats(formats):
|
def _check_formats(formats):
|
||||||
if not check_formats:
|
if (self.params.get('check_formats') is not None
|
||||||
|
or self.params.get('allow_unplayable_formats')):
|
||||||
yield from formats
|
yield from formats
|
||||||
return
|
return
|
||||||
yield from self._check_formats(formats)
|
elif self.params.get('check_formats') == 'selected':
|
||||||
|
yield from self._check_formats(formats)
|
||||||
|
return
|
||||||
|
|
||||||
|
for f in formats:
|
||||||
|
if f.get('has_drm'):
|
||||||
|
yield from self._check_formats([f])
|
||||||
|
else:
|
||||||
|
yield f
|
||||||
|
|
||||||
def _build_selector_function(selector):
|
def _build_selector_function(selector):
|
||||||
if isinstance(selector, list): # ,
|
if isinstance(selector, list): # ,
|
||||||
@@ -2407,9 +2471,24 @@ def _calc_headers(self, info_dict):
|
|||||||
if 'Youtubedl-No-Compression' in res: # deprecated
|
if 'Youtubedl-No-Compression' in res: # deprecated
|
||||||
res.pop('Youtubedl-No-Compression', None)
|
res.pop('Youtubedl-No-Compression', None)
|
||||||
res['Accept-Encoding'] = 'identity'
|
res['Accept-Encoding'] = 'identity'
|
||||||
cookies = self.cookiejar.get_cookie_header(info_dict['url'])
|
cookies = self.cookiejar.get_cookies_for_url(info_dict['url'])
|
||||||
if cookies:
|
if cookies:
|
||||||
res['Cookie'] = cookies
|
encoder = LenientSimpleCookie()
|
||||||
|
values = []
|
||||||
|
for cookie in cookies:
|
||||||
|
_, value = encoder.value_encode(cookie.value)
|
||||||
|
values.append(f'{cookie.name}={value}')
|
||||||
|
if cookie.domain:
|
||||||
|
values.append(f'Domain={cookie.domain}')
|
||||||
|
if cookie.path:
|
||||||
|
values.append(f'Path={cookie.path}')
|
||||||
|
if cookie.secure:
|
||||||
|
values.append('Secure')
|
||||||
|
if cookie.expires:
|
||||||
|
values.append(f'Expires={cookie.expires}')
|
||||||
|
if cookie.version:
|
||||||
|
values.append(f'Version={cookie.version}')
|
||||||
|
info_dict['cookies'] = '; '.join(values)
|
||||||
|
|
||||||
if 'X-Forwarded-For' not in res:
|
if 'X-Forwarded-For' not in res:
|
||||||
x_forwarded_for_ip = info_dict.get('__x_forwarded_for_ip')
|
x_forwarded_for_ip = info_dict.get('__x_forwarded_for_ip')
|
||||||
@@ -2615,10 +2694,10 @@ def sanitize_numeric_fields(info):
|
|||||||
if field_preference:
|
if field_preference:
|
||||||
info_dict['_format_sort_fields'] = field_preference
|
info_dict['_format_sort_fields'] = field_preference
|
||||||
|
|
||||||
# or None ensures --clean-infojson removes it
|
info_dict['_has_drm'] = any( # or None ensures --clean-infojson removes it
|
||||||
info_dict['_has_drm'] = any(f.get('has_drm') for f in formats) or None
|
f.get('has_drm') and f['has_drm'] != 'maybe' for f in formats) or None
|
||||||
if not self.params.get('allow_unplayable_formats'):
|
if not self.params.get('allow_unplayable_formats'):
|
||||||
formats = [f for f in formats if not f.get('has_drm')]
|
formats = [f for f in formats if not f.get('has_drm') or f['has_drm'] == 'maybe']
|
||||||
|
|
||||||
if formats and all(f.get('acodec') == f.get('vcodec') == 'none' for f in formats):
|
if formats and all(f.get('acodec') == f.get('vcodec') == 'none' for f in formats):
|
||||||
self.report_warning(
|
self.report_warning(
|
||||||
@@ -2666,7 +2745,8 @@ def is_wellformed(f):
|
|||||||
format['dynamic_range'] = 'SDR'
|
format['dynamic_range'] = 'SDR'
|
||||||
if format.get('aspect_ratio') is None:
|
if format.get('aspect_ratio') is None:
|
||||||
format['aspect_ratio'] = try_call(lambda: round(format['width'] / format['height'], 2))
|
format['aspect_ratio'] = try_call(lambda: round(format['width'] / format['height'], 2))
|
||||||
if (info_dict.get('duration') and format.get('tbr')
|
if (not format.get('manifest_url') # For fragmented formats, "tbr" is often max bitrate and not average
|
||||||
|
and info_dict.get('duration') and format.get('tbr')
|
||||||
and not format.get('filesize') and not format.get('filesize_approx')):
|
and not format.get('filesize') and not format.get('filesize_approx')):
|
||||||
format['filesize_approx'] = int(info_dict['duration'] * format['tbr'] * (1024 / 8))
|
format['filesize_approx'] = int(info_dict['duration'] * format['tbr'] * (1024 / 8))
|
||||||
format['http_headers'] = self._calc_headers(collections.ChainMap(format, info_dict))
|
format['http_headers'] = self._calc_headers(collections.ChainMap(format, info_dict))
|
||||||
@@ -2766,11 +2846,8 @@ def is_wellformed(f):
|
|||||||
formats_to_download = list(format_selector({
|
formats_to_download = list(format_selector({
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
'has_merged_format': any('none' not in (f.get('acodec'), f.get('vcodec')) for f in formats),
|
'has_merged_format': any('none' not in (f.get('acodec'), f.get('vcodec')) for f in formats),
|
||||||
'incomplete_formats': (
|
'incomplete_formats': (all(f.get('vcodec') == 'none' for f in formats) # No formats with video
|
||||||
# All formats are video-only or
|
or all(f.get('acodec') == 'none' for f in formats)), # OR, No formats with audio
|
||||||
all(f.get('vcodec') != 'none' and f.get('acodec') == 'none' for f in formats)
|
|
||||||
# all formats are audio-only
|
|
||||||
or all(f.get('vcodec') == 'none' and f.get('acodec') != 'none' for f in formats)),
|
|
||||||
}))
|
}))
|
||||||
if interactive_format_selection and not formats_to_download:
|
if interactive_format_selection and not formats_to_download:
|
||||||
self.report_error('Requested format is not available', tb=False, is_error=False)
|
self.report_error('Requested format is not available', tb=False, is_error=False)
|
||||||
@@ -2805,11 +2882,13 @@ def to_screen(*msg):
|
|||||||
new_info.update(fmt)
|
new_info.update(fmt)
|
||||||
offset, duration = info_dict.get('section_start') or 0, info_dict.get('duration') or float('inf')
|
offset, duration = info_dict.get('section_start') or 0, info_dict.get('duration') or float('inf')
|
||||||
end_time = offset + min(chapter.get('end_time', duration), duration)
|
end_time = offset + min(chapter.get('end_time', duration), duration)
|
||||||
|
# duration may not be accurate. So allow deviations <1sec
|
||||||
|
if end_time == float('inf') or end_time > offset + duration + 1:
|
||||||
|
end_time = None
|
||||||
if chapter or offset:
|
if chapter or offset:
|
||||||
new_info.update({
|
new_info.update({
|
||||||
'section_start': offset + chapter.get('start_time', 0),
|
'section_start': offset + chapter.get('start_time', 0),
|
||||||
# duration may not be accurate. So allow deviations <1sec
|
'section_end': end_time,
|
||||||
'section_end': end_time if end_time <= offset + duration + 1 else None,
|
|
||||||
'section_title': chapter.get('title'),
|
'section_title': chapter.get('title'),
|
||||||
'section_number': chapter.get('index'),
|
'section_number': chapter.get('index'),
|
||||||
})
|
})
|
||||||
@@ -3416,6 +3495,8 @@ def download_with_info_file(self, info_filename):
|
|||||||
infos = [self.sanitize_info(info, self.params.get('clean_infojson', True))
|
infos = [self.sanitize_info(info, self.params.get('clean_infojson', True))
|
||||||
for info in variadic(json.loads('\n'.join(f)))]
|
for info in variadic(json.loads('\n'.join(f)))]
|
||||||
for info in infos:
|
for info in infos:
|
||||||
|
self._load_cookies(info.get('cookies'), from_headers=False)
|
||||||
|
self._load_cookies(traverse_obj(info.get('http_headers'), 'Cookie', casesense=False)) # compat
|
||||||
try:
|
try:
|
||||||
self.__download_wrapper(self.process_ie_result)(info, download=True)
|
self.__download_wrapper(self.process_ie_result)(info, download=True)
|
||||||
except (DownloadError, EntryNotInPlaylist, ReExtractInfo) as e:
|
except (DownloadError, EntryNotInPlaylist, ReExtractInfo) as e:
|
||||||
@@ -3685,7 +3766,7 @@ def render_formats_table(self, info_dict):
|
|||||||
|
|
||||||
def simplified_codec(f, field):
|
def simplified_codec(f, field):
|
||||||
assert field in ('acodec', 'vcodec')
|
assert field in ('acodec', 'vcodec')
|
||||||
codec = f.get(field, 'unknown')
|
codec = f.get(field)
|
||||||
if not codec:
|
if not codec:
|
||||||
return 'unknown'
|
return 'unknown'
|
||||||
elif codec != 'none':
|
elif codec != 'none':
|
||||||
@@ -3707,8 +3788,11 @@ def simplified_codec(f, field):
|
|||||||
format_field(f, 'fps', '\t%d', func=round),
|
format_field(f, 'fps', '\t%d', func=round),
|
||||||
format_field(f, 'dynamic_range', '%s', ignore=(None, 'SDR')).replace('HDR', ''),
|
format_field(f, 'dynamic_range', '%s', ignore=(None, 'SDR')).replace('HDR', ''),
|
||||||
format_field(f, 'audio_channels', '\t%s'),
|
format_field(f, 'audio_channels', '\t%s'),
|
||||||
delim,
|
delim, (
|
||||||
format_field(f, 'filesize', ' \t%s', func=format_bytes) + format_field(f, 'filesize_approx', '~\t%s', func=format_bytes),
|
format_field(f, 'filesize', ' \t%s', func=format_bytes)
|
||||||
|
or format_field(f, 'filesize_approx', '≈\t%s', func=format_bytes)
|
||||||
|
or format_field(try_call(lambda: format_bytes(int(info_dict['duration'] * f['tbr'] * (1024 / 8)))),
|
||||||
|
None, self._format_out('~\t%s', self.Styles.SUPPRESS))),
|
||||||
format_field(f, 'tbr', '\t%dk', func=round),
|
format_field(f, 'tbr', '\t%dk', func=round),
|
||||||
shorten_protocol_name(f.get('protocol', '')),
|
shorten_protocol_name(f.get('protocol', '')),
|
||||||
delim,
|
delim,
|
||||||
@@ -3717,14 +3801,13 @@ def simplified_codec(f, field):
|
|||||||
simplified_codec(f, 'acodec'),
|
simplified_codec(f, 'acodec'),
|
||||||
format_field(f, 'abr', '\t%dk', func=round),
|
format_field(f, 'abr', '\t%dk', func=round),
|
||||||
format_field(f, 'asr', '\t%s', func=format_decimal_suffix),
|
format_field(f, 'asr', '\t%s', func=format_decimal_suffix),
|
||||||
join_nonempty(
|
join_nonempty(format_field(f, 'language', '[%s]'), join_nonempty(
|
||||||
self._format_out('UNSUPPORTED', 'light red') if f.get('ext') in ('f4f', 'f4m') else None,
|
self._format_out('UNSUPPORTED', self.Styles.BAD_FORMAT) if f.get('ext') in ('f4f', 'f4m') else None,
|
||||||
self._format_out('DRM', 'light red') if f.get('has_drm') else None,
|
(self._format_out('Maybe DRM', self.Styles.WARNING) if f.get('has_drm') == 'maybe'
|
||||||
format_field(f, 'language', '[%s]'),
|
else self._format_out('DRM', self.Styles.BAD_FORMAT) if f.get('has_drm') else None),
|
||||||
join_nonempty(format_field(f, 'format_note'),
|
format_field(f, 'format_note'),
|
||||||
format_field(f, 'container', ignore=(None, f.get('ext'))),
|
format_field(f, 'container', ignore=(None, f.get('ext'))),
|
||||||
delim=', '),
|
delim=', '), delim=' '),
|
||||||
delim=' '),
|
|
||||||
] for f in formats if f.get('preference') is None or f['preference'] >= -1000]
|
] for f in formats if f.get('preference') is None or f['preference'] >= -1000]
|
||||||
header_line = self._list_format_headers(
|
header_line = self._list_format_headers(
|
||||||
'ID', 'EXT', 'RESOLUTION', '\tFPS', 'HDR', 'CH', delim, '\tFILESIZE', '\tTBR', 'PROTO',
|
'ID', 'EXT', 'RESOLUTION', '\tFPS', 'HDR', 'CH', delim, '\tFILESIZE', '\tTBR', 'PROTO',
|
||||||
|
|||||||
@@ -320,26 +320,49 @@ def validate_outtmpl(tmpl, msg):
|
|||||||
opts.skip_download = None
|
opts.skip_download = None
|
||||||
del opts.outtmpl['default']
|
del opts.outtmpl['default']
|
||||||
|
|
||||||
def parse_chapters(name, value):
|
def parse_chapters(name, value, advanced=False):
|
||||||
chapters, ranges = [], []
|
|
||||||
parse_timestamp = lambda x: float('inf') if x in ('inf', 'infinite') else parse_duration(x)
|
parse_timestamp = lambda x: float('inf') if x in ('inf', 'infinite') else parse_duration(x)
|
||||||
for regex in value or []:
|
TIMESTAMP_RE = r'''(?x)(?:
|
||||||
if regex.startswith('*'):
|
(?P<start_sign>-?)(?P<start>[^-]+)
|
||||||
for range_ in map(str.strip, regex[1:].split(',')):
|
)?\s*-\s*(?:
|
||||||
mobj = range_ != '-' and re.fullmatch(r'([^-]+)?\s*-\s*([^-]+)?', range_)
|
(?P<end_sign>-?)(?P<end>[^-]+)
|
||||||
dur = mobj and (parse_timestamp(mobj.group(1) or '0'), parse_timestamp(mobj.group(2) or 'inf'))
|
)?'''
|
||||||
if None in (dur or [None]):
|
|
||||||
raise ValueError(f'invalid {name} time range "{regex}". Must be of the form "*start-end"')
|
|
||||||
ranges.append(dur)
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
chapters.append(re.compile(regex))
|
|
||||||
except re.error as err:
|
|
||||||
raise ValueError(f'invalid {name} regex "{regex}" - {err}')
|
|
||||||
return chapters, ranges
|
|
||||||
|
|
||||||
opts.remove_chapters, opts.remove_ranges = parse_chapters('--remove-chapters', opts.remove_chapters)
|
chapters, ranges, from_url = [], [], False
|
||||||
opts.download_ranges = download_range_func(*parse_chapters('--download-sections', opts.download_ranges))
|
for regex in value or []:
|
||||||
|
if advanced and regex == '*from-url':
|
||||||
|
from_url = True
|
||||||
|
continue
|
||||||
|
elif not regex.startswith('*'):
|
||||||
|
try:
|
||||||
|
chapters.append(re.compile(regex))
|
||||||
|
except re.error as err:
|
||||||
|
raise ValueError(f'invalid {name} regex "{regex}" - {err}')
|
||||||
|
continue
|
||||||
|
|
||||||
|
for range_ in map(str.strip, regex[1:].split(',')):
|
||||||
|
mobj = range_ != '-' and re.fullmatch(TIMESTAMP_RE, range_)
|
||||||
|
dur = mobj and [parse_timestamp(mobj.group('start') or '0'), parse_timestamp(mobj.group('end') or 'inf')]
|
||||||
|
signs = mobj and (mobj.group('start_sign'), mobj.group('end_sign'))
|
||||||
|
|
||||||
|
err = None
|
||||||
|
if None in (dur or [None]):
|
||||||
|
err = 'Must be of the form "*start-end"'
|
||||||
|
elif not advanced and any(signs):
|
||||||
|
err = 'Negative timestamps are not allowed'
|
||||||
|
else:
|
||||||
|
dur[0] *= -1 if signs[0] else 1
|
||||||
|
dur[1] *= -1 if signs[1] else 1
|
||||||
|
if dur[1] == float('-inf'):
|
||||||
|
err = '"-inf" is not a valid end'
|
||||||
|
if err:
|
||||||
|
raise ValueError(f'invalid {name} time range "{regex}". {err}')
|
||||||
|
ranges.append(dur)
|
||||||
|
|
||||||
|
return chapters, ranges, from_url
|
||||||
|
|
||||||
|
opts.remove_chapters, opts.remove_ranges, _ = parse_chapters('--remove-chapters', opts.remove_chapters)
|
||||||
|
opts.download_ranges = download_range_func(*parse_chapters('--download-sections', opts.download_ranges, True))
|
||||||
|
|
||||||
# Cookies from browser
|
# Cookies from browser
|
||||||
if opts.cookiesfrombrowser:
|
if opts.cookiesfrombrowser:
|
||||||
|
|||||||
@@ -1327,6 +1327,13 @@ def get_cookie_header(self, url):
|
|||||||
self.add_cookie_header(cookie_req)
|
self.add_cookie_header(cookie_req)
|
||||||
return cookie_req.get_header('Cookie')
|
return cookie_req.get_header('Cookie')
|
||||||
|
|
||||||
|
def get_cookies_for_url(self, url):
|
||||||
|
"""Generate a list of Cookie objects for a given url"""
|
||||||
|
# Policy `_now` attribute must be set before calling `_cookies_for_request`
|
||||||
|
# Ref: https://github.com/python/cpython/blob/3.7/Lib/http/cookiejar.py#L1360
|
||||||
|
self._policy._now = self._now = int(time.time())
|
||||||
|
return self._cookies_for_request(urllib.request.Request(escape_url(sanitize_url(url))))
|
||||||
|
|
||||||
def clear(self, *args, **kwargs):
|
def clear(self, *args, **kwargs):
|
||||||
with contextlib.suppress(KeyError):
|
with contextlib.suppress(KeyError):
|
||||||
return super().clear(*args, **kwargs)
|
return super().clear(*args, **kwargs)
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
timetuple_from_msec,
|
timetuple_from_msec,
|
||||||
try_call,
|
try_call,
|
||||||
)
|
)
|
||||||
|
from ..utils.traversal import traverse_obj
|
||||||
|
|
||||||
|
|
||||||
class FileDownloader:
|
class FileDownloader:
|
||||||
@@ -255,7 +256,8 @@ def sanitize_open(self, filename, open_mode):
|
|||||||
|
|
||||||
@wrap_file_access('remove')
|
@wrap_file_access('remove')
|
||||||
def try_remove(self, filename):
|
def try_remove(self, filename):
|
||||||
os.remove(filename)
|
if os.path.isfile(filename):
|
||||||
|
os.remove(filename)
|
||||||
|
|
||||||
@wrap_file_access('rename')
|
@wrap_file_access('rename')
|
||||||
def try_rename(self, old_filename, new_filename):
|
def try_rename(self, old_filename, new_filename):
|
||||||
@@ -418,7 +420,6 @@ def download(self, filename, info_dict, subtitle=False):
|
|||||||
"""Download to a filename using the info from info_dict
|
"""Download to a filename using the info from info_dict
|
||||||
Return True on success and False otherwise
|
Return True on success and False otherwise
|
||||||
"""
|
"""
|
||||||
|
|
||||||
nooverwrites_and_exists = (
|
nooverwrites_and_exists = (
|
||||||
not self.params.get('overwrites', True)
|
not self.params.get('overwrites', True)
|
||||||
and os.path.exists(encodeFilename(filename))
|
and os.path.exists(encodeFilename(filename))
|
||||||
@@ -452,6 +453,11 @@ def download(self, filename, info_dict, subtitle=False):
|
|||||||
self.to_screen(f'[download] Sleeping {sleep_interval:.2f} seconds ...')
|
self.to_screen(f'[download] Sleeping {sleep_interval:.2f} seconds ...')
|
||||||
time.sleep(sleep_interval)
|
time.sleep(sleep_interval)
|
||||||
|
|
||||||
|
# Filter the `Cookie` header from the info_dict to prevent leaks.
|
||||||
|
# See: https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-v8mc-9377-rwjj
|
||||||
|
info_dict['http_headers'] = dict(traverse_obj(info_dict, (
|
||||||
|
'http_headers', {dict.items}, lambda _, pair: pair[0].lower() != 'cookie'))) or None
|
||||||
|
|
||||||
ret = self.real_download(filename, info_dict)
|
ret = self.real_download(filename, info_dict)
|
||||||
self._finish_multiline_status()
|
self._finish_multiline_status()
|
||||||
return ret, True
|
return ret, True
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import enum
|
import enum
|
||||||
import json
|
import json
|
||||||
import os.path
|
import os
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
@@ -42,6 +43,7 @@ class ExternalFD(FragmentFD):
|
|||||||
def real_download(self, filename, info_dict):
|
def real_download(self, filename, info_dict):
|
||||||
self.report_destination(filename)
|
self.report_destination(filename)
|
||||||
tmpfilename = self.temp_name(filename)
|
tmpfilename = self.temp_name(filename)
|
||||||
|
self._cookies_tempfile = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
started = time.time()
|
started = time.time()
|
||||||
@@ -54,6 +56,9 @@ def real_download(self, filename, info_dict):
|
|||||||
# should take place
|
# should take place
|
||||||
retval = 0
|
retval = 0
|
||||||
self.to_screen('[%s] Interrupted by user' % self.get_basename())
|
self.to_screen('[%s] Interrupted by user' % self.get_basename())
|
||||||
|
finally:
|
||||||
|
if self._cookies_tempfile:
|
||||||
|
self.try_remove(self._cookies_tempfile)
|
||||||
|
|
||||||
if retval == 0:
|
if retval == 0:
|
||||||
status = {
|
status = {
|
||||||
@@ -125,6 +130,16 @@ def _configuration_args(self, keys=None, *args, **kwargs):
|
|||||||
self.get_basename(), self.params.get('external_downloader_args'), self.EXE_NAME,
|
self.get_basename(), self.params.get('external_downloader_args'), self.EXE_NAME,
|
||||||
keys, *args, **kwargs)
|
keys, *args, **kwargs)
|
||||||
|
|
||||||
|
def _write_cookies(self):
|
||||||
|
if not self.ydl.cookiejar.filename:
|
||||||
|
tmp_cookies = tempfile.NamedTemporaryFile(suffix='.cookies', delete=False)
|
||||||
|
tmp_cookies.close()
|
||||||
|
self._cookies_tempfile = tmp_cookies.name
|
||||||
|
self.to_screen(f'[download] Writing temporary cookies file to "{self._cookies_tempfile}"')
|
||||||
|
# real_download resets _cookies_tempfile; if it's None then save() will write to cookiejar.filename
|
||||||
|
self.ydl.cookiejar.save(self._cookies_tempfile)
|
||||||
|
return self.ydl.cookiejar.filename or self._cookies_tempfile
|
||||||
|
|
||||||
def _call_downloader(self, tmpfilename, info_dict):
|
def _call_downloader(self, tmpfilename, info_dict):
|
||||||
""" Either overwrite this or implement _make_cmd """
|
""" Either overwrite this or implement _make_cmd """
|
||||||
cmd = [encodeArgument(a) for a in self._make_cmd(tmpfilename, info_dict)]
|
cmd = [encodeArgument(a) for a in self._make_cmd(tmpfilename, info_dict)]
|
||||||
@@ -184,6 +199,8 @@ class CurlFD(ExternalFD):
|
|||||||
|
|
||||||
def _make_cmd(self, tmpfilename, info_dict):
|
def _make_cmd(self, tmpfilename, info_dict):
|
||||||
cmd = [self.exe, '--location', '-o', tmpfilename, '--compressed']
|
cmd = [self.exe, '--location', '-o', tmpfilename, '--compressed']
|
||||||
|
if self.ydl.cookiejar.get_cookie_header(info_dict['url']):
|
||||||
|
cmd += ['--cookie-jar', self._write_cookies()]
|
||||||
if info_dict.get('http_headers') is not None:
|
if info_dict.get('http_headers') is not None:
|
||||||
for key, val in info_dict['http_headers'].items():
|
for key, val in info_dict['http_headers'].items():
|
||||||
cmd += ['--header', f'{key}: {val}']
|
cmd += ['--header', f'{key}: {val}']
|
||||||
@@ -214,6 +231,9 @@ def _make_cmd(self, tmpfilename, info_dict):
|
|||||||
if info_dict.get('http_headers') is not None:
|
if info_dict.get('http_headers') is not None:
|
||||||
for key, val in info_dict['http_headers'].items():
|
for key, val in info_dict['http_headers'].items():
|
||||||
cmd += ['-H', f'{key}: {val}']
|
cmd += ['-H', f'{key}: {val}']
|
||||||
|
cookie_header = self.ydl.cookiejar.get_cookie_header(info_dict['url'])
|
||||||
|
if cookie_header:
|
||||||
|
cmd += [f'Cookie: {cookie_header}', '--max-redirect=0']
|
||||||
cmd += self._configuration_args()
|
cmd += self._configuration_args()
|
||||||
cmd += ['--', info_dict['url']]
|
cmd += ['--', info_dict['url']]
|
||||||
return cmd
|
return cmd
|
||||||
@@ -223,7 +243,9 @@ class WgetFD(ExternalFD):
|
|||||||
AVAILABLE_OPT = '--version'
|
AVAILABLE_OPT = '--version'
|
||||||
|
|
||||||
def _make_cmd(self, tmpfilename, info_dict):
|
def _make_cmd(self, tmpfilename, info_dict):
|
||||||
cmd = [self.exe, '-O', tmpfilename, '-nv', '--no-cookies', '--compression=auto']
|
cmd = [self.exe, '-O', tmpfilename, '-nv', '--compression=auto']
|
||||||
|
if self.ydl.cookiejar.get_cookie_header(info_dict['url']):
|
||||||
|
cmd += ['--load-cookies', self._write_cookies()]
|
||||||
if info_dict.get('http_headers') is not None:
|
if info_dict.get('http_headers') is not None:
|
||||||
for key, val in info_dict['http_headers'].items():
|
for key, val in info_dict['http_headers'].items():
|
||||||
cmd += ['--header', f'{key}: {val}']
|
cmd += ['--header', f'{key}: {val}']
|
||||||
@@ -271,7 +293,7 @@ def _call_downloader(self, tmpfilename, info_dict):
|
|||||||
return super()._call_downloader(tmpfilename, info_dict)
|
return super()._call_downloader(tmpfilename, info_dict)
|
||||||
|
|
||||||
def _make_cmd(self, tmpfilename, info_dict):
|
def _make_cmd(self, tmpfilename, info_dict):
|
||||||
cmd = [self.exe, '-c',
|
cmd = [self.exe, '-c', '--no-conf',
|
||||||
'--console-log-level=warn', '--summary-interval=0', '--download-result=hide',
|
'--console-log-level=warn', '--summary-interval=0', '--download-result=hide',
|
||||||
'--http-accept-gzip=true', '--file-allocation=none', '-x16', '-j16', '-s16']
|
'--http-accept-gzip=true', '--file-allocation=none', '-x16', '-j16', '-s16']
|
||||||
if 'fragments' in info_dict:
|
if 'fragments' in info_dict:
|
||||||
@@ -279,6 +301,8 @@ def _make_cmd(self, tmpfilename, info_dict):
|
|||||||
else:
|
else:
|
||||||
cmd += ['--min-split-size', '1M']
|
cmd += ['--min-split-size', '1M']
|
||||||
|
|
||||||
|
if self.ydl.cookiejar.get_cookie_header(info_dict['url']):
|
||||||
|
cmd += [f'--load-cookies={self._write_cookies()}']
|
||||||
if info_dict.get('http_headers') is not None:
|
if info_dict.get('http_headers') is not None:
|
||||||
for key, val in info_dict['http_headers'].items():
|
for key, val in info_dict['http_headers'].items():
|
||||||
cmd += ['--header', f'{key}: {val}']
|
cmd += ['--header', f'{key}: {val}']
|
||||||
@@ -417,6 +441,14 @@ def _make_cmd(self, tmpfilename, info_dict):
|
|||||||
if info_dict.get('http_headers') is not None:
|
if info_dict.get('http_headers') is not None:
|
||||||
for key, val in info_dict['http_headers'].items():
|
for key, val in info_dict['http_headers'].items():
|
||||||
cmd += [f'{key}:{val}']
|
cmd += [f'{key}:{val}']
|
||||||
|
|
||||||
|
# httpie 3.1.0+ removes the Cookie header on redirect, so this should be safe for now. [1]
|
||||||
|
# If we ever need cookie handling for redirects, we can export the cookiejar into a session. [2]
|
||||||
|
# 1: https://github.com/httpie/httpie/security/advisories/GHSA-9w4w-cpc8-h2fq
|
||||||
|
# 2: https://httpie.io/docs/cli/sessions
|
||||||
|
cookie_header = self.ydl.cookiejar.get_cookie_header(info_dict['url'])
|
||||||
|
if cookie_header:
|
||||||
|
cmd += [f'Cookie:{cookie_header}']
|
||||||
return cmd
|
return cmd
|
||||||
|
|
||||||
|
|
||||||
@@ -527,6 +559,11 @@ def _call_downloader(self, tmpfilename, info_dict):
|
|||||||
|
|
||||||
selected_formats = info_dict.get('requested_formats') or [info_dict]
|
selected_formats = info_dict.get('requested_formats') or [info_dict]
|
||||||
for i, fmt in enumerate(selected_formats):
|
for i, fmt in enumerate(selected_formats):
|
||||||
|
cookies = self.ydl.cookiejar.get_cookies_for_url(fmt['url'])
|
||||||
|
if cookies:
|
||||||
|
args.extend(['-cookies', ''.join(
|
||||||
|
f'{cookie.name}={cookie.value}; path={cookie.path}; domain={cookie.domain};\r\n'
|
||||||
|
for cookie in cookies)])
|
||||||
if fmt.get('http_headers') and re.match(r'^https?://', fmt['url']):
|
if fmt.get('http_headers') and re.match(r'^https?://', fmt['url']):
|
||||||
# Trailing \r\n after each HTTP header is important to prevent warning from ffmpeg/avconv:
|
# Trailing \r\n after each HTTP header is important to prevent warning from ffmpeg/avconv:
|
||||||
# [http @ 00000000003d2fa0] No trailing CRLF found in HTTP header.
|
# [http @ 00000000003d2fa0] No trailing CRLF found in HTTP header.
|
||||||
|
|||||||
@@ -300,9 +300,7 @@ def frag_progress_hook(s):
|
|||||||
def _finish_frag_download(self, ctx, info_dict):
|
def _finish_frag_download(self, ctx, info_dict):
|
||||||
ctx['dest_stream'].close()
|
ctx['dest_stream'].close()
|
||||||
if self.__do_ytdl_file(ctx):
|
if self.__do_ytdl_file(ctx):
|
||||||
ytdl_filename = encodeFilename(self.ytdl_filename(ctx['filename']))
|
self.try_remove(self.ytdl_filename(ctx['filename']))
|
||||||
if os.path.isfile(ytdl_filename):
|
|
||||||
self.try_remove(ytdl_filename)
|
|
||||||
elapsed = time.time() - ctx['started']
|
elapsed = time.time() - ctx['started']
|
||||||
|
|
||||||
to_file = ctx['tmpfilename'] != '-'
|
to_file = ctx['tmpfilename'] != '-'
|
||||||
|
|||||||
@@ -28,7 +28,16 @@ class HlsFD(FragmentFD):
|
|||||||
FD_NAME = 'hlsnative'
|
FD_NAME = 'hlsnative'
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def can_download(manifest, info_dict, allow_unplayable_formats=False):
|
def _has_drm(manifest): # TODO: https://github.com/yt-dlp/yt-dlp/pull/5039
|
||||||
|
return bool(re.search('|'.join((
|
||||||
|
r'#EXT-X-(?:SESSION-)?KEY:.*?URI="skd://', # Apple FairPlay
|
||||||
|
r'#EXT-X-(?:SESSION-)?KEY:.*?KEYFORMAT="com\.apple\.streamingkeydelivery"', # Apple FairPlay
|
||||||
|
r'#EXT-X-(?:SESSION-)?KEY:.*?KEYFORMAT="com\.microsoft\.playready"', # Microsoft PlayReady
|
||||||
|
r'#EXT-X-FAXS-CM:', # Adobe Flash Access
|
||||||
|
)), manifest))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def can_download(cls, manifest, info_dict, allow_unplayable_formats=False):
|
||||||
UNSUPPORTED_FEATURES = [
|
UNSUPPORTED_FEATURES = [
|
||||||
# r'#EXT-X-BYTERANGE', # playlists composed of byte ranges of media files [2]
|
# r'#EXT-X-BYTERANGE', # playlists composed of byte ranges of media files [2]
|
||||||
|
|
||||||
@@ -50,13 +59,15 @@ def can_download(manifest, info_dict, allow_unplayable_formats=False):
|
|||||||
]
|
]
|
||||||
if not allow_unplayable_formats:
|
if not allow_unplayable_formats:
|
||||||
UNSUPPORTED_FEATURES += [
|
UNSUPPORTED_FEATURES += [
|
||||||
r'#EXT-X-KEY:METHOD=(?!NONE|AES-128)', # encrypted streams [1]
|
r'#EXT-X-KEY:METHOD=(?!NONE|AES-128)', # encrypted streams [1], but not necessarily DRM
|
||||||
]
|
]
|
||||||
|
|
||||||
def check_results():
|
def check_results():
|
||||||
yield not info_dict.get('is_live')
|
yield not info_dict.get('is_live')
|
||||||
for feature in UNSUPPORTED_FEATURES:
|
for feature in UNSUPPORTED_FEATURES:
|
||||||
yield not re.search(feature, manifest)
|
yield not re.search(feature, manifest)
|
||||||
|
if not allow_unplayable_formats:
|
||||||
|
yield not cls._has_drm(manifest)
|
||||||
return all(check_results())
|
return all(check_results())
|
||||||
|
|
||||||
def real_download(self, filename, info_dict):
|
def real_download(self, filename, info_dict):
|
||||||
@@ -81,14 +92,13 @@ def real_download(self, filename, info_dict):
|
|||||||
message = ('Live HLS streams are not supported by the native downloader. If this is a livestream, '
|
message = ('Live HLS streams are not supported by the native downloader. If this is a livestream, '
|
||||||
f'please {install_ffmpeg}add "--downloader ffmpeg --hls-use-mpegts" to your command')
|
f'please {install_ffmpeg}add "--downloader ffmpeg --hls-use-mpegts" to your command')
|
||||||
if not can_download:
|
if not can_download:
|
||||||
has_drm = re.search('|'.join([
|
if self._has_drm(s) and not self.params.get('allow_unplayable_formats'):
|
||||||
r'#EXT-X-FAXS-CM:', # Adobe Flash Access
|
if info_dict.get('has_drm') and self.params.get('test'):
|
||||||
r'#EXT-X-(?:SESSION-)?KEY:.*?URI="skd://', # Apple FairPlay
|
self.to_screen(f'[{self.FD_NAME}] This format is DRM protected', skip_eol=True)
|
||||||
]), s)
|
else:
|
||||||
if has_drm and not self.params.get('allow_unplayable_formats'):
|
self.report_error(
|
||||||
self.report_error(
|
'This format is DRM protected; Try selecting another format with --format or '
|
||||||
'This video is DRM protected; Try selecting another format with --format or '
|
'add --check-formats to automatically fallback to the next best format', tb=False)
|
||||||
'add --check-formats to automatically fallback to the next best format')
|
|
||||||
return False
|
return False
|
||||||
message = message or 'Unsupported features have been detected'
|
message = message or 'Unsupported features have been detected'
|
||||||
fd = FFmpegFD(self.ydl, self.params)
|
fd = FFmpegFD(self.ydl, self.params)
|
||||||
|
|||||||
@@ -339,15 +339,15 @@ def retry(e):
|
|||||||
elif speed:
|
elif speed:
|
||||||
ctx.throttle_start = None
|
ctx.throttle_start = None
|
||||||
|
|
||||||
if not is_test and ctx.chunk_size and ctx.content_len is not None and byte_counter < ctx.content_len:
|
|
||||||
ctx.resume_len = byte_counter
|
|
||||||
# ctx.block_size = block_size
|
|
||||||
raise NextFragment()
|
|
||||||
|
|
||||||
if ctx.stream is None:
|
if ctx.stream is None:
|
||||||
self.to_stderr('\n')
|
self.to_stderr('\n')
|
||||||
self.report_error('Did not get any data blocks')
|
self.report_error('Did not get any data blocks')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if not is_test and ctx.chunk_size and ctx.content_len is not None and byte_counter < ctx.content_len:
|
||||||
|
ctx.resume_len = byte_counter
|
||||||
|
raise NextFragment()
|
||||||
|
|
||||||
if ctx.tmpfilename != '-':
|
if ctx.tmpfilename != '-':
|
||||||
ctx.stream.close()
|
ctx.stream.close()
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
YoutubeSearchURLIE,
|
YoutubeSearchURLIE,
|
||||||
YoutubeMusicSearchURLIE,
|
YoutubeMusicSearchURLIE,
|
||||||
YoutubeSubscriptionsIE,
|
YoutubeSubscriptionsIE,
|
||||||
YoutubeStoriesIE,
|
|
||||||
YoutubeTruncatedIDIE,
|
YoutubeTruncatedIDIE,
|
||||||
YoutubeTruncatedURLIE,
|
YoutubeTruncatedURLIE,
|
||||||
YoutubeYtBeIE,
|
YoutubeYtBeIE,
|
||||||
@@ -497,6 +496,7 @@
|
|||||||
DiscoveryPlusItalyIE,
|
DiscoveryPlusItalyIE,
|
||||||
DiscoveryPlusItalyShowIE,
|
DiscoveryPlusItalyShowIE,
|
||||||
DiscoveryPlusIndiaShowIE,
|
DiscoveryPlusIndiaShowIE,
|
||||||
|
GlobalCyclingNetworkPlusIE,
|
||||||
)
|
)
|
||||||
from .dreisat import DreiSatIE
|
from .dreisat import DreiSatIE
|
||||||
from .drbonanza import DRBonanzaIE
|
from .drbonanza import DRBonanzaIE
|
||||||
@@ -1119,7 +1119,8 @@
|
|||||||
from .morningstar import MorningstarIE
|
from .morningstar import MorningstarIE
|
||||||
from .motherless import (
|
from .motherless import (
|
||||||
MotherlessIE,
|
MotherlessIE,
|
||||||
MotherlessGroupIE
|
MotherlessGroupIE,
|
||||||
|
MotherlessGalleryIE,
|
||||||
)
|
)
|
||||||
from .motorsport import MotorsportIE
|
from .motorsport import MotorsportIE
|
||||||
from .movieclips import MovieClipsIE
|
from .movieclips import MovieClipsIE
|
||||||
@@ -1529,6 +1530,7 @@
|
|||||||
)
|
)
|
||||||
from .puls4 import Puls4IE
|
from .puls4 import Puls4IE
|
||||||
from .pyvideo import PyvideoIE
|
from .pyvideo import PyvideoIE
|
||||||
|
from .qdance import QDanceIE
|
||||||
from .qingting import QingTingIE
|
from .qingting import QingTingIE
|
||||||
from .qqmusic import (
|
from .qqmusic import (
|
||||||
QQMusicIE,
|
QQMusicIE,
|
||||||
@@ -1615,6 +1617,7 @@
|
|||||||
from .restudy import RestudyIE
|
from .restudy import RestudyIE
|
||||||
from .reuters import ReutersIE
|
from .reuters import ReutersIE
|
||||||
from .reverbnation import ReverbNationIE
|
from .reverbnation import ReverbNationIE
|
||||||
|
from .rheinmaintv import RheinMainTVIE
|
||||||
from .rice import RICEIE
|
from .rice import RICEIE
|
||||||
from .rmcdecouverte import RMCDecouverteIE
|
from .rmcdecouverte import RMCDecouverteIE
|
||||||
from .rockstargames import RockstarGamesIE
|
from .rockstargames import RockstarGamesIE
|
||||||
@@ -1851,6 +1854,10 @@
|
|||||||
SRGSSRPlayIE,
|
SRGSSRPlayIE,
|
||||||
)
|
)
|
||||||
from .srmediathek import SRMediathekIE
|
from .srmediathek import SRMediathekIE
|
||||||
|
from .stacommu import (
|
||||||
|
StacommuLiveIE,
|
||||||
|
StacommuVODIE,
|
||||||
|
)
|
||||||
from .stanfordoc import StanfordOpenClassroomIE
|
from .stanfordoc import StanfordOpenClassroomIE
|
||||||
from .startv import StarTVIE
|
from .startv import StarTVIE
|
||||||
from .steam import (
|
from .steam import (
|
||||||
@@ -2264,6 +2271,8 @@
|
|||||||
VKIE,
|
VKIE,
|
||||||
VKUserVideosIE,
|
VKUserVideosIE,
|
||||||
VKWallPostIE,
|
VKWallPostIE,
|
||||||
|
VKPlayIE,
|
||||||
|
VKPlayLiveIE,
|
||||||
)
|
)
|
||||||
from .vocaroo import VocarooIE
|
from .vocaroo import VocarooIE
|
||||||
from .vodlocker import VodlockerIE
|
from .vodlocker import VodlockerIE
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
int_or_none,
|
int_or_none,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
str_or_none,
|
str_or_none,
|
||||||
|
traverse_obj,
|
||||||
try_get,
|
try_get,
|
||||||
unescapeHTML,
|
unescapeHTML,
|
||||||
update_url_query,
|
update_url_query,
|
||||||
@@ -85,6 +86,15 @@ class ABCIE(InfoExtractor):
|
|||||||
'uploader': 'Behind the News',
|
'uploader': 'Behind the News',
|
||||||
'uploader_id': 'behindthenews',
|
'uploader_id': 'behindthenews',
|
||||||
}
|
}
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.abc.net.au/news/2023-06-25/wagner-boss-orders-troops-back-to-bases-to-avoid-bloodshed/102520540',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '102520540',
|
||||||
|
'title': 'Wagner Group retreating from Russia, leader Prigozhin to move to Belarus',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'description': 'Wagner troops leave Rostov-on-Don and\xa0Yevgeny Prigozhin will move to Belarus under a deal brokered by Belarusian President Alexander Lukashenko to end the mutiny.',
|
||||||
|
'thumbnail': 'https://live-production.wcms.abc-cdn.net.au/0c170f5b57f0105c432f366c0e8e267b?impolicy=wcms_crop_resize&cropH=2813&cropW=5000&xPos=0&yPos=249&width=862&height=485',
|
||||||
|
}
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
@@ -107,7 +117,7 @@ def _real_extract(self, url):
|
|||||||
video = True
|
video = True
|
||||||
|
|
||||||
if mobj is None:
|
if mobj is None:
|
||||||
mobj = re.search(r'(?P<type>)"sources": (?P<json_data>\[[^\]]+\]),', webpage)
|
mobj = re.search(r'(?P<type>)"(?:sources|files|renditions)":\s*(?P<json_data>\[[^\]]+\])', webpage)
|
||||||
if mobj is None:
|
if mobj is None:
|
||||||
mobj = re.search(
|
mobj = re.search(
|
||||||
r'inline(?P<type>Video|Audio|YouTube)Data\.push\((?P<json_data>[^)]+)\);',
|
r'inline(?P<type>Video|Audio|YouTube)Data\.push\((?P<json_data>[^)]+)\);',
|
||||||
@@ -121,7 +131,8 @@ def _real_extract(self, url):
|
|||||||
urls_info = self._parse_json(
|
urls_info = self._parse_json(
|
||||||
mobj.group('json_data'), video_id, transform_source=js_to_json)
|
mobj.group('json_data'), video_id, transform_source=js_to_json)
|
||||||
youtube = mobj.group('type') == 'YouTube'
|
youtube = mobj.group('type') == 'YouTube'
|
||||||
video = mobj.group('type') == 'Video' or urls_info[0]['contentType'] == 'video/mp4'
|
video = mobj.group('type') == 'Video' or traverse_obj(
|
||||||
|
urls_info, (0, ('contentType', 'MIMEType')), get_all=False) == 'video/mp4'
|
||||||
|
|
||||||
if not isinstance(urls_info, list):
|
if not isinstance(urls_info, list):
|
||||||
urls_info = [urls_info]
|
urls_info = [urls_info]
|
||||||
|
|||||||
@@ -1473,7 +1473,7 @@ def extract_redirect_url(html, url=None, fatal=False):
|
|||||||
elif 'automatically signed in with' in provider_redirect_page:
|
elif 'automatically signed in with' in provider_redirect_page:
|
||||||
# Seems like comcast is rolling up new way of automatically signing customers
|
# Seems like comcast is rolling up new way of automatically signing customers
|
||||||
oauth_redirect_url = self._html_search_regex(
|
oauth_redirect_url = self._html_search_regex(
|
||||||
r'continue:\s*"(https://oauth.xfinity.com/oauth/authorize\?.+)"', provider_redirect_page,
|
r'continue:\s*"(https://oauth\.xfinity\.com/oauth/authorize\?.+)"', provider_redirect_page,
|
||||||
'oauth redirect (signed)')
|
'oauth redirect (signed)')
|
||||||
# Just need to process the request. No useful data comes back
|
# Just need to process the request. No useful data comes back
|
||||||
self._download_webpage(oauth_redirect_url, video_id, 'Confirming auto login')
|
self._download_webpage(oauth_redirect_url, video_id, 'Confirming auto login')
|
||||||
|
|||||||
@@ -170,8 +170,10 @@ def _real_extract(self, url):
|
|||||||
continue
|
continue
|
||||||
ext = determine_ext(asset_url, mimetype2ext(asset.get('mime_type')))
|
ext = determine_ext(asset_url, mimetype2ext(asset.get('mime_type')))
|
||||||
if ext == 'm3u8':
|
if ext == 'm3u8':
|
||||||
info['formats'].extend(self._extract_m3u8_formats(
|
fmts, subs = self._extract_m3u8_formats_and_subtitles(
|
||||||
asset_url, video_id, 'mp4', m3u8_id='hls', fatal=False))
|
asset_url, video_id, 'mp4', m3u8_id='hls', fatal=False)
|
||||||
|
info['formats'].extend(fmts)
|
||||||
|
self._merge_subtitles(subs, target=info['subtitles'])
|
||||||
elif ext == 'f4m':
|
elif ext == 'f4m':
|
||||||
continue
|
continue
|
||||||
# info['formats'].extend(self._extract_f4m_formats(
|
# info['formats'].extend(self._extract_f4m_formats(
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
from ..compat import compat_etree_fromstring, compat_expanduser, compat_os_name
|
from ..compat import compat_etree_fromstring, compat_expanduser, compat_os_name
|
||||||
from ..cookies import LenientSimpleCookie
|
from ..cookies import LenientSimpleCookie
|
||||||
from ..downloader.f4m import get_base_url, remove_encrypted_media
|
from ..downloader.f4m import get_base_url, remove_encrypted_media
|
||||||
|
from ..downloader.hls import HlsFD
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
IDENTITY,
|
IDENTITY,
|
||||||
JSON_LD_RE,
|
JSON_LD_RE,
|
||||||
@@ -224,7 +225,8 @@ class InfoExtractor:
|
|||||||
width : height ratio as float.
|
width : height ratio as float.
|
||||||
* no_resume The server does not support resuming the
|
* no_resume The server does not support resuming the
|
||||||
(HTTP or RTMP) download. Boolean.
|
(HTTP or RTMP) download. Boolean.
|
||||||
* has_drm The format has DRM and cannot be downloaded. Boolean
|
* has_drm True if the format has DRM and cannot be downloaded.
|
||||||
|
'maybe' if the format may have DRM and has to be tested before download.
|
||||||
* extra_param_to_segment_url A query string to append to each
|
* extra_param_to_segment_url A query string to append to each
|
||||||
fragment's URL, or to update each existing query string
|
fragment's URL, or to update each existing query string
|
||||||
with. Only applied by the native HLS/DASH downloaders.
|
with. Only applied by the native HLS/DASH downloaders.
|
||||||
@@ -475,8 +477,8 @@ class InfoExtractor:
|
|||||||
|
|
||||||
|
|
||||||
Subclasses of this should also be added to the list of extractors and
|
Subclasses of this should also be added to the list of extractors and
|
||||||
should define a _VALID_URL regexp and, re-define the _real_extract() and
|
should define _VALID_URL as a regexp or a Sequence of regexps, and
|
||||||
(optionally) _real_initialize() methods.
|
re-define the _real_extract() and (optionally) _real_initialize() methods.
|
||||||
|
|
||||||
Subclasses may also override suitable() if necessary, but ensure the function
|
Subclasses may also override suitable() if necessary, but ensure the function
|
||||||
signature is preserved and that this function imports everything it needs
|
signature is preserved and that this function imports everything it needs
|
||||||
@@ -566,8 +568,8 @@ def _match_valid_url(cls, url):
|
|||||||
# we have cached the regexp for *this* class, whereas getattr would also
|
# we have cached the regexp for *this* class, whereas getattr would also
|
||||||
# match the superclass
|
# match the superclass
|
||||||
if '_VALID_URL_RE' not in cls.__dict__:
|
if '_VALID_URL_RE' not in cls.__dict__:
|
||||||
cls._VALID_URL_RE = re.compile(cls._VALID_URL)
|
cls._VALID_URL_RE = tuple(map(re.compile, variadic(cls._VALID_URL)))
|
||||||
return cls._VALID_URL_RE.match(url)
|
return next(filter(None, (regex.match(url) for regex in cls._VALID_URL_RE)), None)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def suitable(cls, url):
|
def suitable(cls, url):
|
||||||
@@ -1297,8 +1299,9 @@ def _html_search_regex(self, pattern, string, name, default=NO_DEFAULT, fatal=Tr
|
|||||||
def _get_netrc_login_info(self, netrc_machine=None):
|
def _get_netrc_login_info(self, netrc_machine=None):
|
||||||
netrc_machine = netrc_machine or self._NETRC_MACHINE
|
netrc_machine = netrc_machine or self._NETRC_MACHINE
|
||||||
|
|
||||||
cmd = self.get_param('netrc_cmd', '').format(netrc_machine)
|
cmd = self.get_param('netrc_cmd')
|
||||||
if cmd:
|
if cmd:
|
||||||
|
cmd = cmd.replace('{}', netrc_machine)
|
||||||
self.to_screen(f'Executing command: {cmd}')
|
self.to_screen(f'Executing command: {cmd}')
|
||||||
stdout, _, ret = Popen.run(cmd, text=True, shell=True, stdout=subprocess.PIPE)
|
stdout, _, ret = Popen.run(cmd, text=True, shell=True, stdout=subprocess.PIPE)
|
||||||
if ret != 0:
|
if ret != 0:
|
||||||
@@ -1978,11 +1981,7 @@ def _parse_m3u8_formats_and_subtitles(
|
|||||||
errnote=None, fatal=True, data=None, headers={}, query={},
|
errnote=None, fatal=True, data=None, headers={}, query={},
|
||||||
video_id=None):
|
video_id=None):
|
||||||
formats, subtitles = [], {}
|
formats, subtitles = [], {}
|
||||||
|
has_drm = HlsFD._has_drm(m3u8_doc)
|
||||||
has_drm = re.search('|'.join([
|
|
||||||
r'#EXT-X-FAXS-CM:', # Adobe Flash Access
|
|
||||||
r'#EXT-X-(?:SESSION-)?KEY:.*?URI="skd://', # Apple FairPlay
|
|
||||||
]), m3u8_doc)
|
|
||||||
|
|
||||||
def format_url(url):
|
def format_url(url):
|
||||||
return url if re.match(r'^https?://', url) else urllib.parse.urljoin(m3u8_url, url)
|
return url if re.match(r'^https?://', url) else urllib.parse.urljoin(m3u8_url, url)
|
||||||
|
|||||||
@@ -490,8 +490,21 @@ class CrunchyrollMusicIE(CrunchyrollBaseIE):
|
|||||||
_VALID_URL = r'''(?x)
|
_VALID_URL = r'''(?x)
|
||||||
https?://(?:www\.)?crunchyroll\.com/
|
https?://(?:www\.)?crunchyroll\.com/
|
||||||
(?P<lang>(?:\w{2}(?:-\w{2})?/)?)
|
(?P<lang>(?:\w{2}(?:-\w{2})?/)?)
|
||||||
watch/(?P<type>concert|musicvideo)/(?P<id>\w{10})'''
|
watch/(?P<type>concert|musicvideo)/(?P<id>\w+)'''
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
|
'url': 'https://www.crunchyroll.com/de/watch/musicvideo/MV5B02C79',
|
||||||
|
'info_dict': {
|
||||||
|
'ext': 'mp4',
|
||||||
|
'id': 'MV5B02C79',
|
||||||
|
'display_id': 'egaono-hana',
|
||||||
|
'title': 'Egaono Hana',
|
||||||
|
'track': 'Egaono Hana',
|
||||||
|
'artist': 'Goose house',
|
||||||
|
'thumbnail': r're:(?i)^https://www.crunchyroll.com/imgsrv/.*\.jpeg?$',
|
||||||
|
'genre': ['J-Pop'],
|
||||||
|
},
|
||||||
|
'params': {'skip_download': 'm3u8'},
|
||||||
|
}, {
|
||||||
'url': 'https://www.crunchyroll.com/watch/musicvideo/MV88BB7F2C',
|
'url': 'https://www.crunchyroll.com/watch/musicvideo/MV88BB7F2C',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
@@ -519,11 +532,14 @@ class CrunchyrollMusicIE(CrunchyrollBaseIE):
|
|||||||
},
|
},
|
||||||
'params': {'skip_download': 'm3u8'},
|
'params': {'skip_download': 'm3u8'},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.crunchyroll.com/watch/musicvideo/MV88BB7F2C/crossing-field',
|
'url': 'https://www.crunchyroll.com/de/watch/musicvideo/MV5B02C79/egaono-hana',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.crunchyroll.com/watch/concert/MC2E2AC135/live-is-smile-always-364joker-at-yokohama-arena',
|
'url': 'https://www.crunchyroll.com/watch/concert/MC2E2AC135/live-is-smile-always-364joker-at-yokohama-arena',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.crunchyroll.com/watch/musicvideo/MV88BB7F2C/crossing-field',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
_API_ENDPOINT = 'music'
|
_API_ENDPOINT = 'music'
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ def _download_video_playback_info(self, disco_base, video_id, headers):
|
|||||||
return streaming_list
|
return streaming_list
|
||||||
|
|
||||||
def _get_disco_api_info(self, url, display_id, disco_host, realm, country, domain=''):
|
def _get_disco_api_info(self, url, display_id, disco_host, realm, country, domain=''):
|
||||||
|
country = self.get_param('geo_bypass_country') or country
|
||||||
geo_countries = [country.upper()]
|
geo_countries = [country.upper()]
|
||||||
self._initialize_geo_bypass({
|
self._initialize_geo_bypass({
|
||||||
'countries': geo_countries,
|
'countries': geo_countries,
|
||||||
@@ -1001,3 +1002,39 @@ class DiscoveryPlusIndiaShowIE(DiscoveryPlusShowBaseIE):
|
|||||||
_SHOW_STR = 'show'
|
_SHOW_STR = 'show'
|
||||||
_INDEX = 4
|
_INDEX = 4
|
||||||
_VIDEO_IE = DiscoveryPlusIndiaIE
|
_VIDEO_IE = DiscoveryPlusIndiaIE
|
||||||
|
|
||||||
|
|
||||||
|
class GlobalCyclingNetworkPlusIE(DiscoveryPlusBaseIE):
|
||||||
|
_VALID_URL = r'https?://plus\.globalcyclingnetwork\.com/watch/(?P<id>\d+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://plus.globalcyclingnetwork.com/watch/1397691',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '1397691',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'The Athertons: Mountain Biking\'s Fastest Family',
|
||||||
|
'description': 'md5:75a81937fcd8b989eec6083a709cd837',
|
||||||
|
'thumbnail': 'https://us1-prod-images.disco-api.com/2021/03/04/eb9e3026-4849-3001-8281-9356466f0557.png',
|
||||||
|
'series': 'gcn',
|
||||||
|
'creator': 'Gcn',
|
||||||
|
'upload_date': '20210309',
|
||||||
|
'timestamp': 1615248000,
|
||||||
|
'duration': 2531.0,
|
||||||
|
'tags': [],
|
||||||
|
},
|
||||||
|
'skip': 'Subscription required',
|
||||||
|
'params': {'skip_download': 'm3u8'},
|
||||||
|
}]
|
||||||
|
|
||||||
|
_PRODUCT = 'web'
|
||||||
|
_DISCO_API_PARAMS = {
|
||||||
|
'disco_host': 'disco-api-prod.globalcyclingnetwork.com',
|
||||||
|
'realm': 'gcn',
|
||||||
|
'country': 'us',
|
||||||
|
}
|
||||||
|
|
||||||
|
def _update_disco_api_headers(self, headers, disco_base, display_id, realm):
|
||||||
|
headers.update({
|
||||||
|
'x-disco-params': f'realm={realm}',
|
||||||
|
'x-disco-client': f'WEB:UNKNOWN:{self._PRODUCT}:27.3.2',
|
||||||
|
'Authorization': self._get_auth(disco_base, display_id, realm),
|
||||||
|
})
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
|
import functools
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from .vimeo import VHXEmbedIE
|
from .vimeo import VHXEmbedIE
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
|
OnDemandPagedList,
|
||||||
clean_html,
|
clean_html,
|
||||||
|
extract_attributes,
|
||||||
get_element_by_class,
|
get_element_by_class,
|
||||||
get_element_by_id,
|
get_element_by_id,
|
||||||
get_elements_by_class,
|
get_elements_html_by_class,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
join_nonempty,
|
traverse_obj,
|
||||||
unified_strdate,
|
unified_strdate,
|
||||||
urlencode_postdata,
|
urlencode_postdata,
|
||||||
)
|
)
|
||||||
@@ -162,12 +166,13 @@ def _real_extract(self, url):
|
|||||||
|
|
||||||
|
|
||||||
class DropoutSeasonIE(InfoExtractor):
|
class DropoutSeasonIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?dropout\.tv/(?P<id>[^\/$&?#]+)(?:/?$|/season:[0-9]+/?$)'
|
_PAGE_SIZE = 24
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?dropout\.tv/(?P<id>[^\/$&?#]+)(?:/?$|/season:(?P<season>[0-9]+)/?$)'
|
||||||
_TESTS = [
|
_TESTS = [
|
||||||
{
|
{
|
||||||
'url': 'https://www.dropout.tv/dimension-20-fantasy-high/season:1',
|
'url': 'https://www.dropout.tv/dimension-20-fantasy-high/season:1',
|
||||||
'note': 'Multi-season series with the season in the url',
|
'note': 'Multi-season series with the season in the url',
|
||||||
'playlist_count': 17,
|
'playlist_count': 24,
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'dimension-20-fantasy-high-season-1',
|
'id': 'dimension-20-fantasy-high-season-1',
|
||||||
'title': 'Dimension 20 Fantasy High - Season 1'
|
'title': 'Dimension 20 Fantasy High - Season 1'
|
||||||
@@ -176,7 +181,7 @@ class DropoutSeasonIE(InfoExtractor):
|
|||||||
{
|
{
|
||||||
'url': 'https://www.dropout.tv/dimension-20-fantasy-high',
|
'url': 'https://www.dropout.tv/dimension-20-fantasy-high',
|
||||||
'note': 'Multi-season series with the season not in the url',
|
'note': 'Multi-season series with the season not in the url',
|
||||||
'playlist_count': 17,
|
'playlist_count': 24,
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'dimension-20-fantasy-high-season-1',
|
'id': 'dimension-20-fantasy-high-season-1',
|
||||||
'title': 'Dimension 20 Fantasy High - Season 1'
|
'title': 'Dimension 20 Fantasy High - Season 1'
|
||||||
@@ -190,29 +195,30 @@ class DropoutSeasonIE(InfoExtractor):
|
|||||||
'id': 'dimension-20-shriek-week-season-1',
|
'id': 'dimension-20-shriek-week-season-1',
|
||||||
'title': 'Dimension 20 Shriek Week - Season 1'
|
'title': 'Dimension 20 Shriek Week - Season 1'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url': 'https://www.dropout.tv/breaking-news-no-laugh-newsroom/season:3',
|
||||||
|
'note': 'Multi-season series with season in the url that requires pagination',
|
||||||
|
'playlist_count': 25,
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'breaking-news-no-laugh-newsroom-season-3',
|
||||||
|
'title': 'Breaking News No Laugh Newsroom - Season 3'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def _fetch_page(self, url, season_id, page):
|
||||||
|
page += 1
|
||||||
|
webpage = self._download_webpage(
|
||||||
|
f'{url}?page={page}', season_id, note=f'Downloading page {page}', expected_status={400})
|
||||||
|
yield from [self.url_result(item_url, DropoutIE) for item_url in traverse_obj(
|
||||||
|
get_elements_html_by_class('browse-item-link', webpage), (..., {extract_attributes}, 'href'))]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
season_id = self._match_id(url)
|
season_id = self._match_id(url)
|
||||||
|
season_num = self._match_valid_url(url).group('season') or 1
|
||||||
season_title = season_id.replace('-', ' ').title()
|
season_title = season_id.replace('-', ' ').title()
|
||||||
webpage = self._download_webpage(url, season_id)
|
|
||||||
|
|
||||||
entries = [
|
return self.playlist_result(
|
||||||
self.url_result(
|
OnDemandPagedList(functools.partial(self._fetch_page, url, season_id), self._PAGE_SIZE),
|
||||||
url=self._search_regex(r'<a href=["\'](.+?)["\'] class=["\']browse-item-link["\']',
|
f'{season_id}-season-{season_num}', f'{season_title} - Season {season_num}')
|
||||||
item, 'item_url'),
|
|
||||||
ie=DropoutIE.ie_key()
|
|
||||||
) for item in get_elements_by_class('js-collection-item', webpage)
|
|
||||||
]
|
|
||||||
|
|
||||||
seasons = (get_element_by_class('select-dropdown-wrapper', webpage) or '').strip().replace('\n', '')
|
|
||||||
current_season = self._search_regex(r'<option[^>]+selected>([^<]+)</option>',
|
|
||||||
seasons, 'current_season', default='').strip()
|
|
||||||
|
|
||||||
return {
|
|
||||||
'_type': 'playlist',
|
|
||||||
'id': join_nonempty(season_id, current_season.lower().replace(' ', '-')),
|
|
||||||
'title': join_nonempty(season_title, current_season, delim=' - '),
|
|
||||||
'entries': entries
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,7 +5,9 @@
|
|||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
determine_ext,
|
determine_ext,
|
||||||
|
extract_attributes,
|
||||||
get_element_by_class,
|
get_element_by_class,
|
||||||
|
get_element_html_by_id,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
lowercase_escape,
|
lowercase_escape,
|
||||||
try_get,
|
try_get,
|
||||||
@@ -34,6 +36,7 @@ class GoogleDriveIE(InfoExtractor):
|
|||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Big Buck Bunny.mp4',
|
'title': 'Big Buck Bunny.mp4',
|
||||||
'duration': 45,
|
'duration': 45,
|
||||||
|
'thumbnail': 'https://drive.google.com/thumbnail?id=0ByeS4oOUV-49Zzh4R1J6R09zazQ',
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
# video can't be watched anonymously due to view count limit reached,
|
# video can't be watched anonymously due to view count limit reached,
|
||||||
@@ -207,10 +210,10 @@ def get_value(key):
|
|||||||
'export': 'download',
|
'export': 'download',
|
||||||
})
|
})
|
||||||
|
|
||||||
def request_source_file(source_url, kind):
|
def request_source_file(source_url, kind, data=None):
|
||||||
return self._request_webpage(
|
return self._request_webpage(
|
||||||
source_url, video_id, note='Requesting %s file' % kind,
|
source_url, video_id, note='Requesting %s file' % kind,
|
||||||
errnote='Unable to request %s file' % kind, fatal=False)
|
errnote='Unable to request %s file' % kind, fatal=False, data=data)
|
||||||
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):
|
||||||
@@ -237,14 +240,10 @@ def add_source_format(urlh):
|
|||||||
urlh, url, video_id, note='Downloading confirmation page',
|
urlh, url, video_id, note='Downloading confirmation page',
|
||||||
errnote='Unable to confirm download', fatal=False)
|
errnote='Unable to confirm download', fatal=False)
|
||||||
if confirmation_webpage:
|
if confirmation_webpage:
|
||||||
confirm = self._search_regex(
|
confirmed_source_url = extract_attributes(
|
||||||
r'confirm=([^&"\']+)', confirmation_webpage,
|
get_element_html_by_id('download-form', confirmation_webpage) or '').get('action')
|
||||||
'confirmation code', default=None)
|
if confirmed_source_url:
|
||||||
if confirm:
|
urlh = request_source_file(confirmed_source_url, 'confirmed source', data=b'')
|
||||||
confirmed_source_url = update_url_query(source_url, {
|
|
||||||
'confirm': confirm,
|
|
||||||
})
|
|
||||||
urlh = request_source_file(confirmed_source_url, 'confirmed source')
|
|
||||||
if urlh and urlh.headers.get('Content-Disposition'):
|
if urlh and urlh.headers.get('Content-Disposition'):
|
||||||
add_source_format(urlh)
|
add_source_format(urlh)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -527,7 +527,7 @@ def _extract_vms_player_js(self, webpage, video_id):
|
|||||||
if player_js_cache:
|
if player_js_cache:
|
||||||
return player_js_cache
|
return player_js_cache
|
||||||
webpack_js_url = self._proto_relative_url(self._search_regex(
|
webpack_js_url = self._proto_relative_url(self._search_regex(
|
||||||
r'<script src="((?:https?)?//stc.iqiyipic.com/_next/static/chunks/webpack-\w+\.js)"', webpage, 'webpack URL'))
|
r'<script src="((?:https?:)?//stc\.iqiyipic\.com/_next/static/chunks/webpack-\w+\.js)"', webpage, 'webpack URL'))
|
||||||
webpack_js = self._download_webpage(webpack_js_url, video_id, note='Downloading webpack JS', errnote='Unable to download webpack JS')
|
webpack_js = self._download_webpage(webpack_js_url, video_id, note='Downloading webpack JS', errnote='Unable to download webpack JS')
|
||||||
webpack_map = self._search_json(
|
webpack_map = self._search_json(
|
||||||
r'["\']\s*\+\s*', webpack_js, 'JS locations', video_id,
|
r'["\']\s*\+\s*', webpack_js, 'JS locations', video_id,
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ def _call_api(self, path, display_id, note='Downloading API JSON', headers={}, *
|
|||||||
|
|
||||||
|
|
||||||
class KickIE(KickBaseIE):
|
class KickIE(KickBaseIE):
|
||||||
_VALID_URL = r'https?://(?:www\.)?kick\.com/(?!(?:video|categories|search|auth)(?:[/?#]|$))(?P<id>[\w_]+)'
|
_VALID_URL = r'https?://(?:www\.)?kick\.com/(?!(?:video|categories|search|auth)(?:[/?#]|$))(?P<id>[\w-]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://kick.com/yuppy',
|
'url': 'https://kick.com/yuppy',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
|
|||||||
@@ -1,32 +1,39 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import re
|
import re
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..compat import compat_urlparse
|
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
InAdvancePagedList,
|
OnDemandPagedList,
|
||||||
orderedSet,
|
remove_end,
|
||||||
str_to_int,
|
str_to_int,
|
||||||
unified_strdate,
|
unified_strdate,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class MotherlessIE(InfoExtractor):
|
class MotherlessIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?motherless\.com/(?:g/[a-z0-9_]+/)?(?P<id>[A-Z0-9]+)'
|
_VALID_URL = r'https?://(?:www\.)?motherless\.com/(?:g/[a-z0-9_]+/|G[VIG]?[A-F0-9]+/)?(?P<id>[A-F0-9]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://motherless.com/AC3FFE1',
|
'url': 'http://motherless.com/EE97006',
|
||||||
'md5': '310f62e325a9fafe64f68c0bccb6e75f',
|
'md5': 'cb5e7438f7a3c4e886b7bccc1292a3bc',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'AC3FFE1',
|
'id': 'EE97006',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Fucked in the ass while playing PS3',
|
'title': 'Dogging blond Brit getting glazed (comp)',
|
||||||
'categories': ['Gaming', 'anal', 'reluctant', 'rough', 'Wife'],
|
'categories': ['UK', 'slag', 'whore', 'dogging', 'cunt', 'cumhound', 'big tits', 'Pearl Necklace'],
|
||||||
'upload_date': '20100913',
|
'upload_date': '20230519',
|
||||||
'uploader_id': 'famouslyfuckedup',
|
'uploader_id': 'deathbird',
|
||||||
'thumbnail': r're:https?://.*\.jpg',
|
'thumbnail': r're:https?://.*\.jpg',
|
||||||
'age_limit': 18,
|
'age_limit': 18,
|
||||||
}
|
'comment_count': int,
|
||||||
|
'view_count': int,
|
||||||
|
'like_count': int,
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
# Incomplete cert chains
|
||||||
|
'nocheckcertificate': True,
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://motherless.com/532291B',
|
'url': 'http://motherless.com/532291B',
|
||||||
'md5': 'bc59a6b47d1f958e61fbd38a4d31b131',
|
'md5': 'bc59a6b47d1f958e61fbd38a4d31b131',
|
||||||
@@ -49,16 +56,36 @@ class MotherlessIE(InfoExtractor):
|
|||||||
'id': '633979F',
|
'id': '633979F',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Turtlette',
|
'title': 'Turtlette',
|
||||||
'categories': ['superheroine heroine superher'],
|
'categories': ['superheroine heroine superher'],
|
||||||
'upload_date': '20140827',
|
'upload_date': '20140827',
|
||||||
'uploader_id': 'shade0230',
|
'uploader_id': 'shade0230',
|
||||||
'thumbnail': r're:https?://.*\.jpg',
|
'thumbnail': r're:https?://.*\.jpg',
|
||||||
'age_limit': 18,
|
'age_limit': 18,
|
||||||
}
|
'like_count': int,
|
||||||
|
'comment_count': int,
|
||||||
|
'view_count': int,
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'nocheckcertificate': True,
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
# no keywords
|
|
||||||
'url': 'http://motherless.com/8B4BBC1',
|
'url': 'http://motherless.com/8B4BBC1',
|
||||||
'only_matching': True,
|
'info_dict': {
|
||||||
|
'id': '8B4BBC1',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'VIDEO00441.mp4',
|
||||||
|
'categories': [],
|
||||||
|
'upload_date': '20160214',
|
||||||
|
'uploader_id': 'NMWildGirl',
|
||||||
|
'thumbnail': r're:https?://.*\.jpg',
|
||||||
|
'age_limit': 18,
|
||||||
|
'like_count': int,
|
||||||
|
'comment_count': int,
|
||||||
|
'view_count': int,
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'nocheckcertificate': True,
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
# see https://motherless.com/videos/recent for recent videos with
|
# see https://motherless.com/videos/recent for recent videos with
|
||||||
# uploaded date in "ago" format
|
# uploaded date in "ago" format
|
||||||
@@ -72,9 +99,12 @@ class MotherlessIE(InfoExtractor):
|
|||||||
'uploader_id': 'anonymous',
|
'uploader_id': 'anonymous',
|
||||||
'thumbnail': r're:https?://.*\.jpg',
|
'thumbnail': r're:https?://.*\.jpg',
|
||||||
'age_limit': 18,
|
'age_limit': 18,
|
||||||
|
'like_count': int,
|
||||||
|
'comment_count': int,
|
||||||
|
'view_count': int,
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'skip_download': True,
|
'nocheckcertificate': True,
|
||||||
},
|
},
|
||||||
}]
|
}]
|
||||||
|
|
||||||
@@ -128,10 +158,8 @@ def _real_extract(self, url):
|
|||||||
(r'''<span\b[^>]+\bclass\s*=\s*["']username\b[^>]*>([^<]+)</span>''',
|
(r'''<span\b[^>]+\bclass\s*=\s*["']username\b[^>]*>([^<]+)</span>''',
|
||||||
r'''(?s)['"](?:media-meta-member|thumb-member-username)\b[^>]+>\s*<a\b[^>]+\bhref\s*=\s*['"]/m/([^"']+)'''),
|
r'''(?s)['"](?:media-meta-member|thumb-member-username)\b[^>]+>\s*<a\b[^>]+\bhref\s*=\s*['"]/m/([^"']+)'''),
|
||||||
webpage, 'uploader_id', fatal=False)
|
webpage, 'uploader_id', fatal=False)
|
||||||
|
categories = self._html_search_meta('keywords', webpage, default='')
|
||||||
categories = self._html_search_meta('keywords', webpage, default=None)
|
categories = [cat.strip() for cat in categories.split(',') if cat.strip()]
|
||||||
if categories:
|
|
||||||
categories = [cat.strip() for cat in categories.split(',')]
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
@@ -148,102 +176,97 @@ def _real_extract(self, url):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class MotherlessGroupIE(InfoExtractor):
|
class MotherlessPaginatedIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?motherless\.com/gv?/(?P<id>[a-z0-9_]+)'
|
_PAGE_SIZE = 60
|
||||||
|
|
||||||
|
def _correct_path(self, url, item_id):
|
||||||
|
raise NotImplementedError('This method must be implemented by subclasses')
|
||||||
|
|
||||||
|
def _extract_entries(self, webpage, base):
|
||||||
|
for mobj in re.finditer(r'href="[^"]*(?P<href>/[A-F0-9]+)"\s+title="(?P<title>[^"]+)',
|
||||||
|
webpage):
|
||||||
|
video_url = urllib.parse.urljoin(base, mobj.group('href'))
|
||||||
|
video_id = MotherlessIE.get_temp_id(video_url)
|
||||||
|
|
||||||
|
if video_id:
|
||||||
|
yield self.url_result(video_url, MotherlessIE, video_id, mobj.group('title'))
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
item_id = self._match_id(url)
|
||||||
|
real_url = self._correct_path(url, item_id)
|
||||||
|
webpage = self._download_webpage(real_url, item_id, 'Downloading page 1')
|
||||||
|
|
||||||
|
def get_page(idx):
|
||||||
|
page = idx + 1
|
||||||
|
current_page = webpage if not idx else self._download_webpage(
|
||||||
|
real_url, item_id, note=f'Downloading page {page}', query={'page': page})
|
||||||
|
yield from self._extract_entries(current_page, real_url)
|
||||||
|
|
||||||
|
return self.playlist_result(
|
||||||
|
OnDemandPagedList(get_page, self._PAGE_SIZE), item_id,
|
||||||
|
remove_end(self._html_extract_title(webpage), ' | MOTHERLESS.COM ™'))
|
||||||
|
|
||||||
|
|
||||||
|
class MotherlessGroupIE(MotherlessPaginatedIE):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?motherless\.com/g[vifm]?/(?P<id>[a-z0-9_]+)/?(?:$|[#?])'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://motherless.com/g/movie_scenes',
|
'url': 'http://motherless.com/gv/movie_scenes',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'movie_scenes',
|
'id': 'movie_scenes',
|
||||||
'title': 'Movie Scenes',
|
'title': 'Movie Scenes',
|
||||||
'description': 'Hot and sexy scenes from "regular" movies... '
|
|
||||||
'Beautiful actresses fully nude... A looot of '
|
|
||||||
'skin! :)Enjoy!',
|
|
||||||
},
|
},
|
||||||
'playlist_mincount': 662,
|
'playlist_mincount': 540,
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://motherless.com/gv/sex_must_be_funny',
|
'url': 'http://motherless.com/g/sex_must_be_funny',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'sex_must_be_funny',
|
'id': 'sex_must_be_funny',
|
||||||
'title': 'Sex must be funny',
|
'title': 'Sex must be funny',
|
||||||
'description': 'Sex can be funny. Wide smiles,laugh, games, fun of '
|
|
||||||
'any kind!'
|
|
||||||
},
|
},
|
||||||
'playlist_mincount': 0,
|
'playlist_count': 0,
|
||||||
'expected_warnings': [
|
|
||||||
'This group has no videos.',
|
|
||||||
]
|
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://motherless.com/g/beautiful_cock',
|
'url': 'https://motherless.com/gv/beautiful_cock',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'beautiful_cock',
|
'id': 'beautiful_cock',
|
||||||
'title': 'Beautiful Cock',
|
'title': 'Beautiful Cock',
|
||||||
'description': 'Group for lovely cocks yours, mine, a friends anything human',
|
|
||||||
},
|
},
|
||||||
'playlist_mincount': 2500,
|
'playlist_mincount': 2040,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
@classmethod
|
def _correct_path(self, url, item_id):
|
||||||
def suitable(cls, url):
|
return urllib.parse.urljoin(url, f'/gv/{item_id}')
|
||||||
return (False if MotherlessIE.suitable(url)
|
|
||||||
else super(MotherlessGroupIE, cls).suitable(url))
|
|
||||||
|
|
||||||
def _extract_entries(self, webpage, base):
|
|
||||||
entries = []
|
|
||||||
for mobj in re.finditer(
|
|
||||||
r'href="(?P<href>/[^"]+)"[^>]*>(?:\s*<img[^>]+alt="[^-]+-\s(?P<title>[^"]+)")?',
|
|
||||||
webpage):
|
|
||||||
video_url = compat_urlparse.urljoin(base, mobj.group('href'))
|
|
||||||
if not MotherlessIE.suitable(video_url):
|
|
||||||
continue
|
|
||||||
video_id = MotherlessIE._match_id(video_url)
|
|
||||||
title = mobj.group('title')
|
|
||||||
entries.append(self.url_result(
|
|
||||||
video_url, ie=MotherlessIE.ie_key(), video_id=video_id,
|
|
||||||
video_title=title))
|
|
||||||
# Alternative fallback
|
|
||||||
if not entries:
|
|
||||||
entries = [
|
|
||||||
self.url_result(
|
|
||||||
compat_urlparse.urljoin(base, '/' + entry_id),
|
|
||||||
ie=MotherlessIE.ie_key(), video_id=entry_id)
|
|
||||||
for entry_id in orderedSet(re.findall(
|
|
||||||
r'data-codename=["\']([A-Z0-9]+)', webpage))]
|
|
||||||
return entries
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
class MotherlessGalleryIE(MotherlessPaginatedIE):
|
||||||
group_id = self._match_id(url)
|
_VALID_URL = r'https?://(?:www\.)?motherless\.com/G[VIG]?(?P<id>[A-F0-9]+)/?(?:$|[#?])'
|
||||||
page_url = compat_urlparse.urljoin(url, '/gv/%s' % group_id)
|
_TESTS = [{
|
||||||
webpage = self._download_webpage(page_url, group_id)
|
'url': 'https://motherless.com/GV338999F',
|
||||||
title = self._search_regex(
|
'info_dict': {
|
||||||
r'<title>([\w\s]+\w)\s+-', webpage, 'title', fatal=False)
|
'id': '338999F',
|
||||||
description = self._html_search_meta(
|
'title': 'Random',
|
||||||
'description', webpage, fatal=False)
|
},
|
||||||
page_count = str_to_int(self._search_regex(
|
'playlist_mincount': 190,
|
||||||
r'(\d+)\s*</(?:a|span)>\s*<(?:a|span)[^>]+(?:>\s*NEXT|\brel\s*=\s*["\']?next)\b',
|
}, {
|
||||||
webpage, 'page_count', default=0))
|
'url': 'https://motherless.com/GVABD6213',
|
||||||
if not page_count:
|
'info_dict': {
|
||||||
message = self._search_regex(
|
'id': 'ABD6213',
|
||||||
r'''class\s*=\s*['"]error-page\b[^>]*>\s*<p[^>]*>\s*(?P<error_msg>[^<]+)(?<=\S)\s*''',
|
'title': 'Cuties',
|
||||||
webpage, 'error_msg', default=None) or 'This group has no videos.'
|
},
|
||||||
self.report_warning(message, group_id)
|
'playlist_mincount': 2,
|
||||||
page_count = 1
|
}, {
|
||||||
PAGE_SIZE = 80
|
'url': 'https://motherless.com/GVBCF7622',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'BCF7622',
|
||||||
|
'title': 'Vintage',
|
||||||
|
},
|
||||||
|
'playlist_count': 0,
|
||||||
|
}, {
|
||||||
|
'url': 'https://motherless.com/G035DE2F',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '035DE2F',
|
||||||
|
'title': 'General',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 420,
|
||||||
|
}]
|
||||||
|
|
||||||
def _get_page(idx):
|
def _correct_path(self, url, item_id):
|
||||||
if idx > 0:
|
return urllib.parse.urljoin(url, f'/GV{item_id}')
|
||||||
webpage = self._download_webpage(
|
|
||||||
page_url, group_id, query={'page': idx + 1},
|
|
||||||
note='Downloading page %d/%d' % (idx + 1, page_count)
|
|
||||||
)
|
|
||||||
for entry in self._extract_entries(webpage, url):
|
|
||||||
yield entry
|
|
||||||
|
|
||||||
playlist = InAdvancePagedList(_get_page, page_count, PAGE_SIZE)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'_type': 'playlist',
|
|
||||||
'id': group_id,
|
|
||||||
'title': title,
|
|
||||||
'description': description,
|
|
||||||
'entries': playlist
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import urllib.error
|
import urllib.error
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import ExtractorError, parse_iso8601
|
from ..utils import ExtractorError, make_archive_id, parse_iso8601, remove_start
|
||||||
|
|
||||||
_BASE_URL_RE = r'https?://(?:www\.|beta\.)?(?:watchnebula\.com|nebula\.app|nebula\.tv)'
|
_BASE_URL_RE = r'https?://(?:www\.|beta\.)?(?:watchnebula\.com|nebula\.app|nebula\.tv)'
|
||||||
|
|
||||||
@@ -65,19 +65,20 @@ def _fetch_nebula_bearer_token(self):
|
|||||||
return response['token']
|
return response['token']
|
||||||
|
|
||||||
def _fetch_video_formats(self, slug):
|
def _fetch_video_formats(self, slug):
|
||||||
stream_info = self._call_nebula_api(f'https://content.watchnebula.com/video/{slug}/stream/',
|
stream_info = self._call_nebula_api(f'https://content.api.nebula.app/video/{slug}/stream/',
|
||||||
video_id=slug,
|
video_id=slug,
|
||||||
auth_type='bearer',
|
auth_type='bearer',
|
||||||
note='Fetching video stream info')
|
note='Fetching video stream info')
|
||||||
manifest_url = stream_info['manifest']
|
manifest_url = stream_info['manifest']
|
||||||
return self._extract_m3u8_formats_and_subtitles(manifest_url, slug)
|
return self._extract_m3u8_formats_and_subtitles(manifest_url, slug, 'mp4')
|
||||||
|
|
||||||
def _build_video_info(self, episode):
|
def _build_video_info(self, episode):
|
||||||
fmts, subs = self._fetch_video_formats(episode['slug'])
|
fmts, subs = self._fetch_video_formats(episode['slug'])
|
||||||
channel_slug = episode['channel_slug']
|
channel_slug = episode['channel_slug']
|
||||||
channel_title = episode['channel_title']
|
channel_title = episode['channel_title']
|
||||||
|
zype_id = episode.get('zype_id')
|
||||||
return {
|
return {
|
||||||
'id': episode['zype_id'],
|
'id': remove_start(episode['id'], 'video_episode:'),
|
||||||
'display_id': episode['slug'],
|
'display_id': episode['slug'],
|
||||||
'formats': fmts,
|
'formats': fmts,
|
||||||
'subtitles': subs,
|
'subtitles': subs,
|
||||||
@@ -99,6 +100,9 @@ def _build_video_info(self, episode):
|
|||||||
'uploader_url': f'https://nebula.tv/{channel_slug}',
|
'uploader_url': f'https://nebula.tv/{channel_slug}',
|
||||||
'series': channel_title,
|
'series': channel_title,
|
||||||
'creator': channel_title,
|
'creator': channel_title,
|
||||||
|
'extractor_key': NebulaIE.ie_key(),
|
||||||
|
'extractor': NebulaIE.IE_NAME,
|
||||||
|
'_old_archive_ids': [make_archive_id(NebulaIE, zype_id)] if zype_id else None,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _perform_login(self, username=None, password=None):
|
def _perform_login(self, username=None, password=None):
|
||||||
@@ -113,7 +117,7 @@ class NebulaIE(NebulaBaseIE):
|
|||||||
'url': 'https://nebula.tv/videos/that-time-disney-remade-beauty-and-the-beast',
|
'url': 'https://nebula.tv/videos/that-time-disney-remade-beauty-and-the-beast',
|
||||||
'md5': '14944cfee8c7beeea106320c47560efc',
|
'md5': '14944cfee8c7beeea106320c47560efc',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '5c271b40b13fd613090034fd',
|
'id': '84ed544d-4afd-4723-8cd5-2b95261f0abf',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'That Time Disney Remade Beauty and the Beast',
|
'title': 'That Time Disney Remade Beauty and the Beast',
|
||||||
'description': 'Note: this video was originally posted on YouTube with the sponsor read included. We weren’t able to remove it without reducing video quality, so it’s presented here in its original context.',
|
'description': 'Note: this video was originally posted on YouTube with the sponsor read included. We weren’t able to remove it without reducing video quality, so it’s presented here in its original context.',
|
||||||
@@ -137,22 +141,22 @@ class NebulaIE(NebulaBaseIE):
|
|||||||
'url': 'https://nebula.tv/videos/the-logistics-of-d-day-landing-craft-how-the-allies-got-ashore',
|
'url': 'https://nebula.tv/videos/the-logistics-of-d-day-landing-craft-how-the-allies-got-ashore',
|
||||||
'md5': 'd05739cf6c38c09322422f696b569c23',
|
'md5': 'd05739cf6c38c09322422f696b569c23',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '5e7e78171aaf320001fbd6be',
|
'id': '7e623145-1b44-4ca3-aa0b-ed25a247ea34',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Landing Craft - How The Allies Got Ashore',
|
'title': 'Landing Craft - How The Allies Got Ashore',
|
||||||
'description': r're:^In this episode we explore the unsung heroes of D-Day, the landing craft.',
|
'description': r're:^In this episode we explore the unsung heroes of D-Day, the landing craft.',
|
||||||
'upload_date': '20200327',
|
'upload_date': '20200327',
|
||||||
'timestamp': 1585348140,
|
'timestamp': 1585348140,
|
||||||
'channel': 'Real Engineering',
|
'channel': 'Real Engineering — The Logistics of D-Day',
|
||||||
'channel_id': 'realengineering',
|
'channel_id': 'd-day',
|
||||||
'uploader': 'Real Engineering',
|
'uploader': 'Real Engineering — The Logistics of D-Day',
|
||||||
'uploader_id': 'realengineering',
|
'uploader_id': 'd-day',
|
||||||
'series': 'Real Engineering',
|
'series': 'Real Engineering — The Logistics of D-Day',
|
||||||
'display_id': 'the-logistics-of-d-day-landing-craft-how-the-allies-got-ashore',
|
'display_id': 'the-logistics-of-d-day-landing-craft-how-the-allies-got-ashore',
|
||||||
'creator': 'Real Engineering',
|
'creator': 'Real Engineering — The Logistics of D-Day',
|
||||||
'duration': 841,
|
'duration': 841,
|
||||||
'channel_url': 'https://nebula.tv/realengineering',
|
'channel_url': 'https://nebula.tv/d-day',
|
||||||
'uploader_url': 'https://nebula.tv/realengineering',
|
'uploader_url': 'https://nebula.tv/d-day',
|
||||||
'thumbnail': r're:https://\w+\.cloudfront\.net/[\w-]+\.jpeg?.*',
|
'thumbnail': r're:https://\w+\.cloudfront\.net/[\w-]+\.jpeg?.*',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -160,7 +164,7 @@ class NebulaIE(NebulaBaseIE):
|
|||||||
'url': 'https://nebula.tv/videos/money-episode-1-the-draw',
|
'url': 'https://nebula.tv/videos/money-episode-1-the-draw',
|
||||||
'md5': 'ebe28a7ad822b9ee172387d860487868',
|
'md5': 'ebe28a7ad822b9ee172387d860487868',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '5e779ebdd157bc0001d1c75a',
|
'id': 'b96c5714-9e2b-4ec3-b3f1-20f6e89cc553',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Episode 1: The Draw',
|
'title': 'Episode 1: The Draw',
|
||||||
'description': r'contains:There’s free money on offer… if the players can all work together.',
|
'description': r'contains:There’s free money on offer… if the players can all work together.',
|
||||||
@@ -190,7 +194,7 @@ class NebulaIE(NebulaBaseIE):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def _fetch_video_metadata(self, slug):
|
def _fetch_video_metadata(self, slug):
|
||||||
return self._call_nebula_api(f'https://content.watchnebula.com/video/{slug}/',
|
return self._call_nebula_api(f'https://content.api.nebula.app/video/{slug}/',
|
||||||
video_id=slug,
|
video_id=slug,
|
||||||
auth_type='bearer',
|
auth_type='bearer',
|
||||||
note='Fetching video meta data')
|
note='Fetching video meta data')
|
||||||
|
|||||||
150
yt_dlp/extractor/qdance.py
Normal file
150
yt_dlp/extractor/qdance.py
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import json
|
||||||
|
import time
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
ExtractorError,
|
||||||
|
int_or_none,
|
||||||
|
jwt_decode_hs256,
|
||||||
|
str_or_none,
|
||||||
|
traverse_obj,
|
||||||
|
try_call,
|
||||||
|
url_or_none,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class QDanceIE(InfoExtractor):
|
||||||
|
_NETRC_MACHINE = 'qdance'
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?q-dance\.com/network/(?:library|live)/(?P<id>\d+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'note': 'vod',
|
||||||
|
'url': 'https://www.q-dance.com/network/library/146542138',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '146542138',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Sound Rush [LIVE] | Defqon.1 Weekend Festival 2022 | Friday | RED',
|
||||||
|
'display_id': 'sound-rush-live-v3-defqon-1-weekend-festival-2022-friday-red',
|
||||||
|
'description': 'Relive Defqon.1 - Primal Energy 2022 with the sounds of Sound Rush LIVE at the RED on Friday! 🔥',
|
||||||
|
'season': 'Defqon.1 Weekend Festival 2022',
|
||||||
|
'season_id': '31840632',
|
||||||
|
'series': 'Defqon.1',
|
||||||
|
'series_id': '31840378',
|
||||||
|
'thumbnail': 'https://images.q-dance.network/1674829540-20220624171509-220624171509_delio_dn201093-2.jpg',
|
||||||
|
'availability': 'premium_only',
|
||||||
|
'duration': 1829,
|
||||||
|
},
|
||||||
|
'params': {'skip_download': 'm3u8'},
|
||||||
|
}, {
|
||||||
|
'note': 'livestream',
|
||||||
|
'url': 'https://www.q-dance.com/network/live/149170353',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '149170353',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': r're:^Defqon\.1 2023 - Friday - RED',
|
||||||
|
'display_id': 'defqon-1-2023-friday-red',
|
||||||
|
'description': 'md5:3c73fbbd4044e578e696adfc64019163',
|
||||||
|
'season': 'Defqon.1 Weekend Festival 2023',
|
||||||
|
'season_id': '141735599',
|
||||||
|
'series': 'Defqon.1',
|
||||||
|
'series_id': '31840378',
|
||||||
|
'thumbnail': 'https://images.q-dance.network/1686849069-area-thumbs_red.png',
|
||||||
|
'availability': 'subscriber_only',
|
||||||
|
'live_status': 'is_live',
|
||||||
|
'channel_id': 'qdancenetwork.video_149170353',
|
||||||
|
},
|
||||||
|
'skip': 'Completed livestream',
|
||||||
|
}]
|
||||||
|
|
||||||
|
_access_token = None
|
||||||
|
_refresh_token = None
|
||||||
|
|
||||||
|
def _call_login_api(self, data, note='Logging in'):
|
||||||
|
login = self._download_json(
|
||||||
|
'https://members.id-t.com/api/auth/login', None, note, headers={
|
||||||
|
'content-type': 'application/json',
|
||||||
|
'brand': 'qdance',
|
||||||
|
'origin': 'https://www.q-dance.com',
|
||||||
|
'referer': 'https://www.q-dance.com/',
|
||||||
|
}, data=json.dumps(data, separators=(',', ':')).encode(),
|
||||||
|
expected_status=lambda x: True)
|
||||||
|
|
||||||
|
tokens = traverse_obj(login, ('data', {
|
||||||
|
'_id-t-accounts-token': ('accessToken', {str}),
|
||||||
|
'_id-t-accounts-refresh': ('refreshToken', {str}),
|
||||||
|
'_id-t-accounts-id-token': ('idToken', {str}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
if not tokens.get('_id-t-accounts-token'):
|
||||||
|
error = ': '.join(traverse_obj(login, ('error', ('code', 'message'), {str})))
|
||||||
|
if 'validation_error' not in error:
|
||||||
|
raise ExtractorError(f'Q-Dance API said "{error}"')
|
||||||
|
msg = 'Invalid username or password' if 'email' in data else 'Refresh token has expired'
|
||||||
|
raise ExtractorError(msg, expected=True)
|
||||||
|
|
||||||
|
for name, value in tokens.items():
|
||||||
|
self._set_cookie('.q-dance.com', name, value)
|
||||||
|
|
||||||
|
def _perform_login(self, username, password):
|
||||||
|
self._call_login_api({'email': username, 'password': password})
|
||||||
|
|
||||||
|
def _real_initialize(self):
|
||||||
|
cookies = self._get_cookies('https://www.q-dance.com/')
|
||||||
|
self._refresh_token = try_call(lambda: cookies['_id-t-accounts-refresh'].value)
|
||||||
|
self._access_token = try_call(lambda: cookies['_id-t-accounts-token'].value)
|
||||||
|
if not self._access_token:
|
||||||
|
self.raise_login_required()
|
||||||
|
|
||||||
|
def _get_auth(self):
|
||||||
|
if (try_call(lambda: jwt_decode_hs256(self._access_token)['exp']) or 0) <= int(time.time() - 120):
|
||||||
|
if not self._refresh_token:
|
||||||
|
raise ExtractorError(
|
||||||
|
'Cannot refresh access token, login with yt-dlp or refresh cookies in browser')
|
||||||
|
self._call_login_api({'refreshToken': self._refresh_token}, note='Refreshing access token')
|
||||||
|
self._real_initialize()
|
||||||
|
|
||||||
|
return {'Authorization': self._access_token}
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
data = self._search_nuxt_data(webpage, video_id, traverse=('data', 0, 'data'))
|
||||||
|
|
||||||
|
def extract_availability(level):
|
||||||
|
level = int_or_none(level) or 0
|
||||||
|
return self._availability(
|
||||||
|
needs_premium=(level >= 20), needs_subscription=(level >= 15), needs_auth=True)
|
||||||
|
|
||||||
|
info = traverse_obj(data, {
|
||||||
|
'title': ('title', {str.strip}),
|
||||||
|
'description': ('description', {str.strip}),
|
||||||
|
'display_id': ('slug', {str}),
|
||||||
|
'thumbnail': ('thumbnail', {url_or_none}),
|
||||||
|
'duration': ('durationInSeconds', {int_or_none}, {lambda x: x or None}),
|
||||||
|
'availability': ('subscription', 'level', {extract_availability}),
|
||||||
|
'is_live': ('type', {lambda x: x.lower() == 'live'}),
|
||||||
|
'artist': ('acts', ..., {str}),
|
||||||
|
'series': ('event', 'title', {str.strip}),
|
||||||
|
'series_id': ('event', 'id', {str_or_none}),
|
||||||
|
'season': ('eventEdition', 'title', {str.strip}),
|
||||||
|
'season_id': ('eventEdition', 'id', {str_or_none}),
|
||||||
|
'channel_id': ('pubnub', 'channelName', {str}),
|
||||||
|
})
|
||||||
|
|
||||||
|
stream = self._download_json(
|
||||||
|
f'https://dc9h6qmsoymbq.cloudfront.net/api/content/videos/{video_id}/url',
|
||||||
|
video_id, headers=self._get_auth(), expected_status=401)
|
||||||
|
|
||||||
|
m3u8_url = traverse_obj(stream, ('data', 'url', {url_or_none}))
|
||||||
|
if not m3u8_url and traverse_obj(stream, ('error', 'code')) == 'unauthorized':
|
||||||
|
raise ExtractorError('Your account does not have access to this content', expected=True)
|
||||||
|
|
||||||
|
formats = self._extract_m3u8_formats(
|
||||||
|
m3u8_url, video_id, fatal=False, live=True) if m3u8_url else []
|
||||||
|
if not formats:
|
||||||
|
self.raise_no_formats('No active streams found', expected=bool(info.get('is_live')))
|
||||||
|
|
||||||
|
return {
|
||||||
|
**info,
|
||||||
|
'id': video_id,
|
||||||
|
'formats': formats,
|
||||||
|
}
|
||||||
94
yt_dlp/extractor/rheinmaintv.py
Normal file
94
yt_dlp/extractor/rheinmaintv.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import extract_attributes, merge_dicts, remove_end
|
||||||
|
|
||||||
|
|
||||||
|
class RheinMainTVIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?rheinmaintv\.de/sendungen/(?:[\w-]+/)*(?P<video_id>(?P<display_id>[\w-]+)/vom-\d{2}\.\d{2}\.\d{4}(?:/\d+)?)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.rheinmaintv.de/sendungen/beitrag-video/auf-dem-weg-zur-deutschen-meisterschaft/vom-07.11.2022/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'auf-dem-weg-zur-deutschen-meisterschaft-vom-07.11.2022',
|
||||||
|
'ext': 'ismv', # ismv+isma will be merged into mp4
|
||||||
|
'alt_title': 'Auf dem Weg zur Deutschen Meisterschaft',
|
||||||
|
'title': 'Auf dem Weg zur Deutschen Meisterschaft',
|
||||||
|
'upload_date': '20221108',
|
||||||
|
'view_count': int,
|
||||||
|
'display_id': 'auf-dem-weg-zur-deutschen-meisterschaft',
|
||||||
|
'thumbnail': r're:^https://.+\.jpg',
|
||||||
|
'description': 'md5:48c59b74192bc819a9b34af1d5ed1eb9',
|
||||||
|
'timestamp': 1667933057,
|
||||||
|
'duration': 243.0,
|
||||||
|
},
|
||||||
|
'params': {'skip_download': 'ism'},
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.rheinmaintv.de/sendungen/beitrag-video/formationsgemeinschaft-rhein-main-bei-den-deutschen-meisterschaften/vom-14.11.2022/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'formationsgemeinschaft-rhein-main-bei-den-deutschen-meisterschaften-vom-14.11.2022',
|
||||||
|
'ext': 'ismv',
|
||||||
|
'title': 'Formationsgemeinschaft Rhein-Main bei den Deutschen Meisterschaften',
|
||||||
|
'timestamp': 1668526214,
|
||||||
|
'display_id': 'formationsgemeinschaft-rhein-main-bei-den-deutschen-meisterschaften',
|
||||||
|
'alt_title': 'Formationsgemeinschaft Rhein-Main bei den Deutschen Meisterschaften',
|
||||||
|
'view_count': int,
|
||||||
|
'thumbnail': r're:^https://.+\.jpg',
|
||||||
|
'duration': 345.0,
|
||||||
|
'description': 'md5:9370ba29526984006c2cba1372e5c5a0',
|
||||||
|
'upload_date': '20221115',
|
||||||
|
},
|
||||||
|
'params': {'skip_download': 'ism'},
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.rheinmaintv.de/sendungen/beitrag-video/casino-mainz-bei-den-deutschen-meisterschaften/vom-14.11.2022/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'casino-mainz-bei-den-deutschen-meisterschaften-vom-14.11.2022',
|
||||||
|
'ext': 'ismv',
|
||||||
|
'title': 'Casino Mainz bei den Deutschen Meisterschaften',
|
||||||
|
'view_count': int,
|
||||||
|
'timestamp': 1668527402,
|
||||||
|
'alt_title': 'Casino Mainz bei den Deutschen Meisterschaften',
|
||||||
|
'upload_date': '20221115',
|
||||||
|
'display_id': 'casino-mainz-bei-den-deutschen-meisterschaften',
|
||||||
|
'duration': 348.0,
|
||||||
|
'thumbnail': r're:^https://.+\.jpg',
|
||||||
|
'description': 'md5:70fc1660eeba96da17199e5bdff4c0aa',
|
||||||
|
},
|
||||||
|
'params': {'skip_download': 'ism'},
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.rheinmaintv.de/sendungen/beitrag-video/bricks4kids/vom-22.06.2022/',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
mobj = self._match_valid_url(url)
|
||||||
|
display_id = mobj.group('display_id')
|
||||||
|
video_id = mobj.group('video_id').replace('/', '-')
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
|
source, img = self._search_regex(r'(?s)(?P<source><source[^>]*>)(?P<img><img[^>]*>)',
|
||||||
|
webpage, 'video', group=('source', 'img'))
|
||||||
|
source = extract_attributes(source)
|
||||||
|
img = extract_attributes(img)
|
||||||
|
|
||||||
|
raw_json_ld = list(self._yield_json_ld(webpage, video_id))
|
||||||
|
json_ld = self._json_ld(raw_json_ld, video_id)
|
||||||
|
json_ld.pop('url', None)
|
||||||
|
|
||||||
|
ism_manifest_url = (
|
||||||
|
source.get('src')
|
||||||
|
or next(json_ld.get('embedUrl') for json_ld in raw_json_ld if json_ld.get('@type') == 'VideoObject')
|
||||||
|
)
|
||||||
|
formats, subtitles = self._extract_ism_formats_and_subtitles(ism_manifest_url, video_id)
|
||||||
|
|
||||||
|
return merge_dicts({
|
||||||
|
'id': video_id,
|
||||||
|
'display_id': display_id,
|
||||||
|
'title':
|
||||||
|
self._html_search_regex(r'<h1><span class="title">([^<]*)</span>',
|
||||||
|
webpage, 'headline', default=None)
|
||||||
|
or img.get('title') or json_ld.get('title') or self._og_search_title(webpage)
|
||||||
|
or remove_end(self._html_extract_title(webpage), ' -'),
|
||||||
|
'alt_title': img.get('alt'),
|
||||||
|
'description': json_ld.get('description') or self._og_search_description(webpage),
|
||||||
|
'formats': formats,
|
||||||
|
'subtitles': subtitles,
|
||||||
|
'thumbnails': [{'url': img['src']}] if 'src' in img else json_ld.get('thumbnails'),
|
||||||
|
}, json_ld)
|
||||||
@@ -139,8 +139,8 @@ def _real_extract(self, url):
|
|||||||
'release_year': ('releaseYear', {int_or_none}),
|
'release_year': ('releaseYear', {int_or_none}),
|
||||||
'duration': ('duration', ({float_or_none}, {parse_duration})),
|
'duration': ('duration', ({float_or_none}, {parse_duration})),
|
||||||
'is_live': ('liveStream', {bool}),
|
'is_live': ('liveStream', {bool}),
|
||||||
'age_limit': (
|
'age_limit': (('classificationID', 'contentRating'), {str.upper}, {
|
||||||
('classificationID', 'contentRating'), {str.upper}, {self._AUS_TV_PARENTAL_GUIDELINES.get}),
|
lambda x: self._AUS_TV_PARENTAL_GUIDELINES.get(x)}), # dict.get is unhashable in py3.7
|
||||||
}, get_all=False),
|
}, get_all=False),
|
||||||
**traverse_obj(media, {
|
**traverse_obj(media, {
|
||||||
'categories': (('genres', ...), ('taxonomy', ('genre', 'subgenre'), 'name'), {str}),
|
'categories': (('genres', ...), ('taxonomy', ('genre', 'subgenre'), 'name'), {str}),
|
||||||
|
|||||||
148
yt_dlp/extractor/stacommu.py
Normal file
148
yt_dlp/extractor/stacommu.py
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import time
|
||||||
|
|
||||||
|
from .wrestleuniverse import WrestleUniverseBaseIE
|
||||||
|
from ..utils import (
|
||||||
|
int_or_none,
|
||||||
|
traverse_obj,
|
||||||
|
url_or_none,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class StacommuBaseIE(WrestleUniverseBaseIE):
|
||||||
|
_NETRC_MACHINE = 'stacommu'
|
||||||
|
_API_HOST = 'api.stacommu.jp'
|
||||||
|
_LOGIN_QUERY = {'key': 'AIzaSyCR9czxhH2eWuijEhTNWBZ5MCcOYEUTAhg'}
|
||||||
|
_LOGIN_HEADERS = {
|
||||||
|
'Accept': '*/*',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Client-Version': 'Chrome/JsCore/9.9.4/FirebaseCore-web',
|
||||||
|
'Referer': 'https://www.stacommu.jp/',
|
||||||
|
'Origin': 'https://www.stacommu.jp',
|
||||||
|
}
|
||||||
|
|
||||||
|
@WrestleUniverseBaseIE._TOKEN.getter
|
||||||
|
def _TOKEN(self):
|
||||||
|
if self._REAL_TOKEN and self._TOKEN_EXPIRY <= int(time.time()):
|
||||||
|
self._refresh_token()
|
||||||
|
|
||||||
|
return self._REAL_TOKEN
|
||||||
|
|
||||||
|
def _get_formats(self, data, path, video_id=None):
|
||||||
|
if not traverse_obj(data, path) and not data.get('canWatch') and not self._TOKEN:
|
||||||
|
self.raise_login_required(method='password')
|
||||||
|
return super()._get_formats(data, path, video_id)
|
||||||
|
|
||||||
|
def _extract_hls_key(self, data, path, decrypt):
|
||||||
|
encryption_data = traverse_obj(data, path)
|
||||||
|
if traverse_obj(encryption_data, ('encryptType', {int})) == 0:
|
||||||
|
return None
|
||||||
|
return traverse_obj(encryption_data, {'key': ('key', {decrypt}), 'iv': ('iv', {decrypt})})
|
||||||
|
|
||||||
|
|
||||||
|
class StacommuVODIE(StacommuBaseIE):
|
||||||
|
_VALID_URL = r'https?://www\.stacommu\.jp/videos/episodes/(?P<id>[\da-zA-Z]+)'
|
||||||
|
_TESTS = [{
|
||||||
|
# not encrypted
|
||||||
|
'url': 'https://www.stacommu.jp/videos/episodes/aXcVKjHyAENEjard61soZZ',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'aXcVKjHyAENEjard61soZZ',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'スタコミュAWARDの裏側、ほぼ全部見せます!〜晴れ舞台の直前ドキドキ編〜',
|
||||||
|
'description': 'md5:6400275c57ae75c06da36b06f96beb1c',
|
||||||
|
'timestamp': 1679652000,
|
||||||
|
'upload_date': '20230324',
|
||||||
|
'thumbnail': 'https://image.stacommu.jp/6eLobQan8PFtBoU4RL4uGg/6eLobQan8PFtBoU4RL4uGg',
|
||||||
|
'cast': 'count:11',
|
||||||
|
'duration': 250,
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': 'm3u8',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
# encrypted; requires a premium account
|
||||||
|
'url': 'https://www.stacommu.jp/videos/episodes/3hybMByUvzMEqndSeu5LpD',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '3hybMByUvzMEqndSeu5LpD',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'スタプラフェス2023〜裏側ほぼ全部見せます〜#10',
|
||||||
|
'description': 'md5:85494488ccf1dfa1934accdeadd7b340',
|
||||||
|
'timestamp': 1682506800,
|
||||||
|
'upload_date': '20230426',
|
||||||
|
'thumbnail': 'https://image.stacommu.jp/eMdXtEefR4kEyJJMpAFi7x/eMdXtEefR4kEyJJMpAFi7x',
|
||||||
|
'cast': 'count:55',
|
||||||
|
'duration': 312,
|
||||||
|
'hls_aes': {
|
||||||
|
'key': '6bbaf241b8e1fd9f59ecf546a70e4ae7',
|
||||||
|
'iv': '1fc9002a23166c3bb1d240b953d09de9',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': 'm3u8',
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
_API_PATH = 'videoEpisodes'
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
video_info = self._download_metadata(
|
||||||
|
url, video_id, 'ja', ('dehydratedState', 'queries', 0, 'state', 'data'))
|
||||||
|
hls_info, decrypt = self._call_encrypted_api(
|
||||||
|
video_id, ':watch', 'stream information', data={'method': 1})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'formats': self._get_formats(hls_info, ('protocolHls', 'url', {url_or_none}), video_id),
|
||||||
|
'hls_aes': self._extract_hls_key(hls_info, 'protocolHls', decrypt),
|
||||||
|
**traverse_obj(video_info, {
|
||||||
|
'title': ('displayName', {str}),
|
||||||
|
'description': ('description', {str}),
|
||||||
|
'timestamp': ('watchStartTime', {int_or_none}),
|
||||||
|
'thumbnail': ('keyVisualUrl', {url_or_none}),
|
||||||
|
'cast': ('casts', ..., 'displayName', {str}),
|
||||||
|
'duration': ('duration', {int}),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class StacommuLiveIE(StacommuBaseIE):
|
||||||
|
_VALID_URL = r'https?://www\.stacommu\.jp/live/(?P<id>[\da-zA-Z]+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.stacommu.jp/live/d2FJ3zLnndegZJCAEzGM3m',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'd2FJ3zLnndegZJCAEzGM3m',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': '仲村悠菜 2023/05/04',
|
||||||
|
'timestamp': 1683195647,
|
||||||
|
'upload_date': '20230504',
|
||||||
|
'thumbnail': 'https://image.stacommu.jp/pHGF57SPEHE2ke83FS92FN/pHGF57SPEHE2ke83FS92FN',
|
||||||
|
'duration': 5322,
|
||||||
|
'hls_aes': {
|
||||||
|
'key': 'efbb3ec0b8246f61adf1764c5a51213a',
|
||||||
|
'iv': '80621d19a1f19167b64cedb415b05d1c',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': 'm3u8',
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
_API_PATH = 'events'
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
video_info = self._call_api(video_id, msg='video information', query={'al': 'ja'}, auth=False)
|
||||||
|
hls_info, decrypt = self._call_encrypted_api(
|
||||||
|
video_id, ':watchArchive', 'stream information', data={'method': 1})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'formats': self._get_formats(hls_info, ('hls', 'urls', ..., {url_or_none}), video_id),
|
||||||
|
'hls_aes': self._extract_hls_key(hls_info, 'hls', decrypt),
|
||||||
|
**traverse_obj(video_info, {
|
||||||
|
'title': ('displayName', {str}),
|
||||||
|
'timestamp': ('startTime', {int_or_none}),
|
||||||
|
'thumbnail': ('keyVisualUrl', {url_or_none}),
|
||||||
|
'duration': ('duration', {int_or_none}),
|
||||||
|
}),
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ class TestURLIE(InfoExtractor):
|
|||||||
""" Allows addressing of the test cases as test:yout.*be_1 """
|
""" Allows addressing of the test cases as test:yout.*be_1 """
|
||||||
|
|
||||||
IE_DESC = False # Do not list
|
IE_DESC = False # Do not list
|
||||||
_VALID_URL = r'test(?:url)?:(?P<extractor>.*?)(?:_(?P<num>[0-9]+))?$'
|
_VALID_URL = r'test(?:url)?:(?P<extractor>.*?)(?:_(?P<num>\d+|all))?$'
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
from . import gen_extractor_classes
|
from . import gen_extractor_classes
|
||||||
@@ -36,6 +36,10 @@ def _real_extract(self, url):
|
|||||||
extractor = matching_extractors[0]
|
extractor = matching_extractors[0]
|
||||||
|
|
||||||
testcases = tuple(extractor.get_testcases(True))
|
testcases = tuple(extractor.get_testcases(True))
|
||||||
|
if num == 'all':
|
||||||
|
return self.playlist_result(
|
||||||
|
[self.url_result(tc['url'], extractor) for tc in testcases],
|
||||||
|
url, f'{extractor.IE_NAME} tests')
|
||||||
try:
|
try:
|
||||||
tc = testcases[int(num or 0)]
|
tc = testcases[int(num or 0)]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
@@ -43,4 +47,4 @@ def _real_extract(self, url):
|
|||||||
f'Test case {num or 0} not found, got only {len(testcases)} tests', expected=True)
|
f'Test case {num or 0} not found, got only {len(testcases)} tests', expected=True)
|
||||||
|
|
||||||
self.to_screen(f'Test URL: {tc["url"]}')
|
self.to_screen(f'Test URL: {tc["url"]}')
|
||||||
return self.url_result(tc['url'])
|
return self.url_result(tc['url'], extractor)
|
||||||
|
|||||||
@@ -1015,18 +1015,16 @@ def _real_extract(self, url):
|
|||||||
self.to_screen(f'{e}; trying with webpage')
|
self.to_screen(f'{e}; trying with webpage')
|
||||||
|
|
||||||
webpage = self._download_webpage(url, video_id)
|
webpage = self._download_webpage(url, video_id)
|
||||||
render_data_json = self._search_regex(
|
render_data = self._search_json(
|
||||||
r'<script [^>]*\bid=[\'"]RENDER_DATA[\'"][^>]*>(%7B.+%7D)</script>',
|
r'<script [^>]*\bid=[\'"]RENDER_DATA[\'"][^>]*>', webpage, 'render data', video_id,
|
||||||
webpage, 'render data', default=None)
|
contains_pattern=r'%7B(?s:.+)%7D', fatal=False, transform_source=compat_urllib_parse_unquote)
|
||||||
if not render_data_json:
|
if not render_data:
|
||||||
# TODO: Run verification challenge code to generate signature cookies
|
# TODO: Run verification challenge code to generate signature cookies
|
||||||
cookies = self._get_cookies(self._WEBPAGE_HOST)
|
cookies = self._get_cookies(self._WEBPAGE_HOST)
|
||||||
expected = not cookies.get('s_v_web_id') or not cookies.get('ttwid')
|
expected = not cookies.get('s_v_web_id') or not cookies.get('ttwid')
|
||||||
raise ExtractorError(
|
raise ExtractorError(
|
||||||
'Fresh cookies (not necessarily logged in) are needed', expected=expected)
|
'Fresh cookies (not necessarily logged in) are needed', expected=expected)
|
||||||
|
|
||||||
render_data = self._parse_json(
|
|
||||||
render_data_json, video_id, transform_source=compat_urllib_parse_unquote)
|
|
||||||
return self._parse_aweme_video_web(get_first(render_data, ('aweme', 'detail')), url, video_id)
|
return self._parse_aweme_video_web(get_first(render_data, ('aweme', 'detail')), url, video_id)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class TwitchBaseIE(InfoExtractor):
|
|||||||
@property
|
@property
|
||||||
def _CLIENT_ID(self):
|
def _CLIENT_ID(self):
|
||||||
return self._configuration_arg(
|
return self._configuration_arg(
|
||||||
'client_id', ['ue6666qo983tsx6so1t0vnawi233wa'], ie_key=TwitchStreamIE, casesense=True)[0]
|
'client_id', ['ue6666qo983tsx6so1t0vnawi233wa'], ie_key='Twitch', casesense=True)[0]
|
||||||
|
|
||||||
def _perform_login(self, username, password):
|
def _perform_login(self, username, password):
|
||||||
def fail(message):
|
def fail(message):
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
|
import urllib.error
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from .periscope import PeriscopeBaseIE, PeriscopeIE
|
from .periscope import PeriscopeBaseIE, PeriscopeIE
|
||||||
@@ -34,7 +35,6 @@ class TwitterBaseIE(InfoExtractor):
|
|||||||
_GRAPHQL_API_BASE = 'https://twitter.com/i/api/graphql/'
|
_GRAPHQL_API_BASE = 'https://twitter.com/i/api/graphql/'
|
||||||
_BASE_REGEX = r'https?://(?:(?:www|m(?:obile)?)\.)?(?:twitter\.com|twitter3e4tixl4xyajtrzo62zg5vztmjuricljdp2c5kshju4avyoid\.onion)/'
|
_BASE_REGEX = r'https?://(?:(?:www|m(?:obile)?)\.)?(?:twitter\.com|twitter3e4tixl4xyajtrzo62zg5vztmjuricljdp2c5kshju4avyoid\.onion)/'
|
||||||
_AUTH = {'Authorization': 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA'}
|
_AUTH = {'Authorization': 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA'}
|
||||||
_guest_token = None
|
|
||||||
_flow_token = None
|
_flow_token = None
|
||||||
|
|
||||||
_LOGIN_INIT_DATA = json.dumps({
|
_LOGIN_INIT_DATA = json.dumps({
|
||||||
@@ -145,14 +145,6 @@ def _search_dimensions_in_video_url(a_format, video_url):
|
|||||||
def is_logged_in(self):
|
def is_logged_in(self):
|
||||||
return bool(self._get_cookies(self._API_BASE).get('auth_token'))
|
return bool(self._get_cookies(self._API_BASE).get('auth_token'))
|
||||||
|
|
||||||
def _fetch_guest_token(self, headers, display_id):
|
|
||||||
headers.pop('x-guest-token', None)
|
|
||||||
self._guest_token = traverse_obj(self._download_json(
|
|
||||||
f'{self._API_BASE}guest/activate.json', display_id,
|
|
||||||
'Downloading guest token', data=b'', headers=headers), 'guest_token')
|
|
||||||
if not self._guest_token:
|
|
||||||
raise ExtractorError('Could not retrieve guest token')
|
|
||||||
|
|
||||||
def _set_base_headers(self):
|
def _set_base_headers(self):
|
||||||
headers = self._AUTH.copy()
|
headers = self._AUTH.copy()
|
||||||
csrf_token = try_call(lambda: self._get_cookies(self._API_BASE)['ct0'].value)
|
csrf_token = try_call(lambda: self._get_cookies(self._API_BASE)['ct0'].value)
|
||||||
@@ -183,12 +175,15 @@ def _perform_login(self, username, password):
|
|||||||
if self.is_logged_in:
|
if self.is_logged_in:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._request_webpage('https://twitter.com/', None, 'Requesting cookies')
|
webpage = self._download_webpage('https://twitter.com/', None, 'Downloading login page')
|
||||||
headers = self._set_base_headers()
|
headers = self._set_base_headers()
|
||||||
self._fetch_guest_token(headers, None)
|
guest_token = self._search_regex(
|
||||||
|
r'\.cookie\s*=\s*["\']gt=(\d+);', webpage, 'gt', default=None) or self._download_json(
|
||||||
|
f'{self._API_BASE}guest/activate.json', None, 'Downloading guest token',
|
||||||
|
data=b'', headers=headers)['guest_token']
|
||||||
headers.update({
|
headers.update({
|
||||||
'content-type': 'application/json',
|
'content-type': 'application/json',
|
||||||
'x-guest-token': self._guest_token,
|
'x-guest-token': guest_token,
|
||||||
'x-twitter-client-language': 'en',
|
'x-twitter-client-language': 'en',
|
||||||
'x-twitter-active-user': 'yes',
|
'x-twitter-active-user': 'yes',
|
||||||
'Referer': 'https://twitter.com/',
|
'Referer': 'https://twitter.com/',
|
||||||
@@ -285,37 +280,24 @@ def input_dict(subtask_id, text):
|
|||||||
self.report_login()
|
self.report_login()
|
||||||
|
|
||||||
def _call_api(self, path, video_id, query={}, graphql=False):
|
def _call_api(self, path, video_id, query={}, graphql=False):
|
||||||
headers = self._set_base_headers()
|
if not self.is_logged_in:
|
||||||
if self.is_logged_in:
|
self.raise_login_required()
|
||||||
headers.update({
|
|
||||||
|
result = self._download_json(
|
||||||
|
(self._GRAPHQL_API_BASE if graphql else self._API_BASE) + path, video_id,
|
||||||
|
f'Downloading {"GraphQL" if graphql else "legacy API"} JSON', headers={
|
||||||
|
**self._set_base_headers(),
|
||||||
'x-twitter-auth-type': 'OAuth2Session',
|
'x-twitter-auth-type': 'OAuth2Session',
|
||||||
'x-twitter-client-language': 'en',
|
'x-twitter-client-language': 'en',
|
||||||
'x-twitter-active-user': 'yes',
|
'x-twitter-active-user': 'yes',
|
||||||
})
|
}, query=query, expected_status={400, 401, 403, 404} if graphql else {403})
|
||||||
|
|
||||||
for first_attempt in (True, False):
|
if result.get('errors'):
|
||||||
if not self.is_logged_in:
|
errors = ', '.join(set(traverse_obj(result, ('errors', ..., 'message', {str}))))
|
||||||
if not self._guest_token:
|
raise ExtractorError(
|
||||||
self._fetch_guest_token(headers, video_id)
|
f'Error(s) while querying API: {errors or "Unknown error"}', expected=True)
|
||||||
headers['x-guest-token'] = self._guest_token
|
|
||||||
|
|
||||||
allowed_status = {400, 401, 403, 404} if graphql else {403}
|
return result
|
||||||
result = self._download_json(
|
|
||||||
(self._GRAPHQL_API_BASE if graphql else self._API_BASE) + path,
|
|
||||||
video_id, headers=headers, query=query, expected_status=allowed_status,
|
|
||||||
note=f'Downloading {"GraphQL" if graphql else "legacy API"} JSON')
|
|
||||||
|
|
||||||
if result.get('errors'):
|
|
||||||
errors = ', '.join(set(traverse_obj(result, ('errors', ..., 'message', {str}))))
|
|
||||||
if not self.is_logged_in and first_attempt and 'bad guest token' in errors.lower():
|
|
||||||
self.to_screen('Guest token has expired. Refreshing guest token')
|
|
||||||
self._guest_token = None
|
|
||||||
continue
|
|
||||||
|
|
||||||
raise ExtractorError(
|
|
||||||
f'Error(s) while querying API: {errors or "Unknown error"}', expected=True)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _build_graphql_query(self, media_id):
|
def _build_graphql_query(self, media_id):
|
||||||
raise NotImplementedError('Method must be implemented to support GraphQL')
|
raise NotImplementedError('Method must be implemented to support GraphQL')
|
||||||
@@ -457,6 +439,7 @@ class TwitterIE(TwitterBaseIE):
|
|||||||
_VALID_URL = TwitterBaseIE._BASE_REGEX + r'(?:(?:i/web|[^/]+)/status|statuses)/(?P<id>\d+)(?:/(?:video|photo)/(?P<index>\d+))?'
|
_VALID_URL = TwitterBaseIE._BASE_REGEX + r'(?:(?:i/web|[^/]+)/status|statuses)/(?P<id>\d+)(?:/(?:video|photo)/(?P<index>\d+))?'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
|
# comment_count, repost_count, view_count are only available with auth (applies to all tests)
|
||||||
'url': 'https://twitter.com/freethenipple/status/643211948184596480',
|
'url': 'https://twitter.com/freethenipple/status/643211948184596480',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '643211870443208704',
|
'id': '643211870443208704',
|
||||||
@@ -471,10 +454,7 @@ class TwitterIE(TwitterBaseIE):
|
|||||||
'timestamp': 1442188653,
|
'timestamp': 1442188653,
|
||||||
'upload_date': '20150913',
|
'upload_date': '20150913',
|
||||||
'uploader_url': 'https://twitter.com/freethenipple',
|
'uploader_url': 'https://twitter.com/freethenipple',
|
||||||
'comment_count': int,
|
|
||||||
'repost_count': int,
|
|
||||||
'like_count': int,
|
'like_count': int,
|
||||||
'view_count': int,
|
|
||||||
'tags': [],
|
'tags': [],
|
||||||
'age_limit': 18,
|
'age_limit': 18,
|
||||||
},
|
},
|
||||||
@@ -505,8 +485,6 @@ class TwitterIE(TwitterBaseIE):
|
|||||||
'timestamp': 1447395772,
|
'timestamp': 1447395772,
|
||||||
'upload_date': '20151113',
|
'upload_date': '20151113',
|
||||||
'uploader_url': 'https://twitter.com/starwars',
|
'uploader_url': 'https://twitter.com/starwars',
|
||||||
'comment_count': int,
|
|
||||||
'repost_count': int,
|
|
||||||
'like_count': int,
|
'like_count': int,
|
||||||
'tags': ['TV', 'StarWars', 'TheForceAwakens'],
|
'tags': ['TV', 'StarWars', 'TheForceAwakens'],
|
||||||
'age_limit': 0,
|
'age_limit': 0,
|
||||||
@@ -550,10 +528,7 @@ class TwitterIE(TwitterBaseIE):
|
|||||||
'timestamp': 1455777459,
|
'timestamp': 1455777459,
|
||||||
'upload_date': '20160218',
|
'upload_date': '20160218',
|
||||||
'uploader_url': 'https://twitter.com/jaydingeer',
|
'uploader_url': 'https://twitter.com/jaydingeer',
|
||||||
'comment_count': int,
|
|
||||||
'repost_count': int,
|
|
||||||
'like_count': int,
|
'like_count': int,
|
||||||
'view_count': int,
|
|
||||||
'tags': ['Damndaniel'],
|
'tags': ['Damndaniel'],
|
||||||
'age_limit': 0,
|
'age_limit': 0,
|
||||||
},
|
},
|
||||||
@@ -591,10 +566,7 @@ class TwitterIE(TwitterBaseIE):
|
|||||||
'upload_date': '20160412',
|
'upload_date': '20160412',
|
||||||
'uploader_url': 'https://twitter.com/CaptainAmerica',
|
'uploader_url': 'https://twitter.com/CaptainAmerica',
|
||||||
'thumbnail': r're:^https?://.*\.jpg',
|
'thumbnail': r're:^https?://.*\.jpg',
|
||||||
'comment_count': int,
|
|
||||||
'repost_count': int,
|
|
||||||
'like_count': int,
|
'like_count': int,
|
||||||
'view_count': int,
|
|
||||||
'tags': [],
|
'tags': [],
|
||||||
'age_limit': 0,
|
'age_limit': 0,
|
||||||
},
|
},
|
||||||
@@ -641,10 +613,7 @@ class TwitterIE(TwitterBaseIE):
|
|||||||
'timestamp': 1505803395,
|
'timestamp': 1505803395,
|
||||||
'upload_date': '20170919',
|
'upload_date': '20170919',
|
||||||
'uploader_url': 'https://twitter.com/Prefet971',
|
'uploader_url': 'https://twitter.com/Prefet971',
|
||||||
'comment_count': int,
|
|
||||||
'repost_count': int,
|
|
||||||
'like_count': int,
|
'like_count': int,
|
||||||
'view_count': int,
|
|
||||||
'tags': ['Maria'],
|
'tags': ['Maria'],
|
||||||
'age_limit': 0,
|
'age_limit': 0,
|
||||||
},
|
},
|
||||||
@@ -667,10 +636,7 @@ class TwitterIE(TwitterBaseIE):
|
|||||||
'timestamp': 1527623489,
|
'timestamp': 1527623489,
|
||||||
'upload_date': '20180529',
|
'upload_date': '20180529',
|
||||||
'uploader_url': 'https://twitter.com/LisPower1',
|
'uploader_url': 'https://twitter.com/LisPower1',
|
||||||
'comment_count': int,
|
|
||||||
'repost_count': int,
|
|
||||||
'like_count': int,
|
'like_count': int,
|
||||||
'view_count': int,
|
|
||||||
'tags': [],
|
'tags': [],
|
||||||
'age_limit': 0,
|
'age_limit': 0,
|
||||||
},
|
},
|
||||||
@@ -692,10 +658,7 @@ class TwitterIE(TwitterBaseIE):
|
|||||||
'timestamp': 1548184644,
|
'timestamp': 1548184644,
|
||||||
'upload_date': '20190122',
|
'upload_date': '20190122',
|
||||||
'uploader_url': 'https://twitter.com/Twitter',
|
'uploader_url': 'https://twitter.com/Twitter',
|
||||||
'comment_count': int,
|
|
||||||
'repost_count': int,
|
|
||||||
'like_count': int,
|
'like_count': int,
|
||||||
'view_count': int,
|
|
||||||
'tags': [],
|
'tags': [],
|
||||||
'age_limit': 0,
|
'age_limit': 0,
|
||||||
},
|
},
|
||||||
@@ -713,6 +676,7 @@ class TwitterIE(TwitterBaseIE):
|
|||||||
'view_count': int,
|
'view_count': int,
|
||||||
},
|
},
|
||||||
'add_ie': ['TwitterBroadcast'],
|
'add_ie': ['TwitterBroadcast'],
|
||||||
|
'skip': 'Requires authentication',
|
||||||
}, {
|
}, {
|
||||||
# unified card
|
# unified card
|
||||||
'url': 'https://twitter.com/BrooklynNets/status/1349794411333394432?s=20',
|
'url': 'https://twitter.com/BrooklynNets/status/1349794411333394432?s=20',
|
||||||
@@ -729,8 +693,6 @@ class TwitterIE(TwitterBaseIE):
|
|||||||
'timestamp': 1610651040,
|
'timestamp': 1610651040,
|
||||||
'upload_date': '20210114',
|
'upload_date': '20210114',
|
||||||
'uploader_url': 'https://twitter.com/BrooklynNets',
|
'uploader_url': 'https://twitter.com/BrooklynNets',
|
||||||
'comment_count': int,
|
|
||||||
'repost_count': int,
|
|
||||||
'like_count': int,
|
'like_count': int,
|
||||||
'tags': [],
|
'tags': [],
|
||||||
'age_limit': 0,
|
'age_limit': 0,
|
||||||
@@ -753,10 +715,7 @@ class TwitterIE(TwitterBaseIE):
|
|||||||
'thumbnail': r're:^https?://.*\.jpg',
|
'thumbnail': r're:^https?://.*\.jpg',
|
||||||
'duration': 30.03,
|
'duration': 30.03,
|
||||||
'timestamp': 1665025050,
|
'timestamp': 1665025050,
|
||||||
'comment_count': int,
|
|
||||||
'repost_count': int,
|
|
||||||
'like_count': int,
|
'like_count': int,
|
||||||
'view_count': int,
|
|
||||||
'tags': [],
|
'tags': [],
|
||||||
'age_limit': 0,
|
'age_limit': 0,
|
||||||
},
|
},
|
||||||
@@ -765,15 +724,13 @@ class TwitterIE(TwitterBaseIE):
|
|||||||
'url': 'https://twitter.com/UltimaShadowX/status/1577719286659006464',
|
'url': 'https://twitter.com/UltimaShadowX/status/1577719286659006464',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '1577719286659006464',
|
'id': '1577719286659006464',
|
||||||
'title': 'Ultima | #\u0432\u029f\u043c - Test',
|
'title': 'Ultima📛 | #вʟм - Test',
|
||||||
'description': 'Test https://t.co/Y3KEZD7Dad',
|
'description': 'Test https://t.co/Y3KEZD7Dad',
|
||||||
'uploader': 'Ultima | #\u0432\u029f\u043c',
|
'uploader': 'Ultima📛 | #вʟм',
|
||||||
'uploader_id': 'UltimaShadowX',
|
'uploader_id': 'UltimaShadowX',
|
||||||
'uploader_url': 'https://twitter.com/UltimaShadowX',
|
'uploader_url': 'https://twitter.com/UltimaShadowX',
|
||||||
'upload_date': '20221005',
|
'upload_date': '20221005',
|
||||||
'timestamp': 1664992565,
|
'timestamp': 1664992565,
|
||||||
'comment_count': int,
|
|
||||||
'repost_count': int,
|
|
||||||
'like_count': int,
|
'like_count': int,
|
||||||
'tags': [],
|
'tags': [],
|
||||||
'age_limit': 0,
|
'age_limit': 0,
|
||||||
@@ -795,10 +752,7 @@ class TwitterIE(TwitterBaseIE):
|
|||||||
'duration': 21.321,
|
'duration': 21.321,
|
||||||
'timestamp': 1664477766,
|
'timestamp': 1664477766,
|
||||||
'upload_date': '20220929',
|
'upload_date': '20220929',
|
||||||
'comment_count': int,
|
|
||||||
'repost_count': int,
|
|
||||||
'like_count': int,
|
'like_count': int,
|
||||||
'view_count': int,
|
|
||||||
'tags': ['HurricaneIan'],
|
'tags': ['HurricaneIan'],
|
||||||
'age_limit': 0,
|
'age_limit': 0,
|
||||||
},
|
},
|
||||||
@@ -825,6 +779,20 @@ class TwitterIE(TwitterBaseIE):
|
|||||||
},
|
},
|
||||||
'skip': 'Requires authentication',
|
'skip': 'Requires authentication',
|
||||||
}, {
|
}, {
|
||||||
|
# Single Vimeo video result without auth
|
||||||
|
'url': 'https://twitter.com/Srirachachau/status/1395079556562706435',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '551578322',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Dusty & The Mayor',
|
||||||
|
'uploader': 'Michael Chau',
|
||||||
|
'uploader_id': 'user29061007',
|
||||||
|
'uploader_url': 'https://vimeo.com/user29061007',
|
||||||
|
'duration': 478,
|
||||||
|
'thumbnail': 'https://i.vimeocdn.com/video/1139658575-0dfdce6e9a2401fe09feb24bf0d14e6f24a53c12f447ff688ace61009ad4c1ba-d_1280',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
# Playlist result only with auth
|
||||||
'url': 'https://twitter.com/Srirachachau/status/1395079556562706435',
|
'url': 'https://twitter.com/Srirachachau/status/1395079556562706435',
|
||||||
'playlist_mincount': 2,
|
'playlist_mincount': 2,
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@@ -842,6 +810,7 @@ class TwitterIE(TwitterBaseIE):
|
|||||||
'uploader_url': 'https://twitter.com/Srirachachau',
|
'uploader_url': 'https://twitter.com/Srirachachau',
|
||||||
'timestamp': 1621447860,
|
'timestamp': 1621447860,
|
||||||
},
|
},
|
||||||
|
'skip': 'Requires authentication',
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://twitter.com/DavidToons_/status/1578353380363501568',
|
'url': 'https://twitter.com/DavidToons_/status/1578353380363501568',
|
||||||
'playlist_mincount': 2,
|
'playlist_mincount': 2,
|
||||||
@@ -860,6 +829,7 @@ class TwitterIE(TwitterBaseIE):
|
|||||||
'upload_date': '20221007',
|
'upload_date': '20221007',
|
||||||
'age_limit': 0,
|
'age_limit': 0,
|
||||||
},
|
},
|
||||||
|
'skip': 'Requires authentication',
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://twitter.com/primevideouk/status/1578401165338976258',
|
'url': 'https://twitter.com/primevideouk/status/1578401165338976258',
|
||||||
'playlist_count': 2,
|
'playlist_count': 2,
|
||||||
@@ -873,8 +843,6 @@ class TwitterIE(TwitterBaseIE):
|
|||||||
'upload_date': '20221007',
|
'upload_date': '20221007',
|
||||||
'age_limit': 0,
|
'age_limit': 0,
|
||||||
'uploader_url': 'https://twitter.com/primevideouk',
|
'uploader_url': 'https://twitter.com/primevideouk',
|
||||||
'comment_count': int,
|
|
||||||
'repost_count': int,
|
|
||||||
'like_count': int,
|
'like_count': int,
|
||||||
'tags': ['TheRingsOfPower'],
|
'tags': ['TheRingsOfPower'],
|
||||||
},
|
},
|
||||||
@@ -889,11 +857,14 @@ class TwitterIE(TwitterBaseIE):
|
|||||||
'uploader_id': 'MoniqueCamarra',
|
'uploader_id': 'MoniqueCamarra',
|
||||||
'live_status': 'was_live',
|
'live_status': 'was_live',
|
||||||
'release_timestamp': 1658417414,
|
'release_timestamp': 1658417414,
|
||||||
'description': 'md5:acce559345fd49f129c20dbcda3f1201',
|
'description': 'md5:4dc8e972f1d8b3c6580376fabb02a3ad',
|
||||||
'timestamp': 1658407771464,
|
'timestamp': 1658407771,
|
||||||
|
'release_date': '20220721',
|
||||||
|
'upload_date': '20220721',
|
||||||
},
|
},
|
||||||
'add_ie': ['TwitterSpaces'],
|
'add_ie': ['TwitterSpaces'],
|
||||||
'params': {'skip_download': 'm3u8'},
|
'params': {'skip_download': 'm3u8'},
|
||||||
|
'skip': 'Requires authentication',
|
||||||
}, {
|
}, {
|
||||||
# URL specifies video number but --yes-playlist
|
# URL specifies video number but --yes-playlist
|
||||||
'url': 'https://twitter.com/CTVJLaidlaw/status/1600649710662213632/video/1',
|
'url': 'https://twitter.com/CTVJLaidlaw/status/1600649710662213632/video/1',
|
||||||
@@ -903,9 +874,7 @@ class TwitterIE(TwitterBaseIE):
|
|||||||
'title': 'md5:be05989b0722e114103ed3851a0ffae2',
|
'title': 'md5:be05989b0722e114103ed3851a0ffae2',
|
||||||
'timestamp': 1670459604.0,
|
'timestamp': 1670459604.0,
|
||||||
'description': 'md5:591c19ce66fadc2359725d5cd0d1052c',
|
'description': 'md5:591c19ce66fadc2359725d5cd0d1052c',
|
||||||
'comment_count': int,
|
|
||||||
'uploader_id': 'CTVJLaidlaw',
|
'uploader_id': 'CTVJLaidlaw',
|
||||||
'repost_count': int,
|
|
||||||
'tags': ['colorectalcancer', 'cancerjourney', 'imnotaquitter'],
|
'tags': ['colorectalcancer', 'cancerjourney', 'imnotaquitter'],
|
||||||
'upload_date': '20221208',
|
'upload_date': '20221208',
|
||||||
'age_limit': 0,
|
'age_limit': 0,
|
||||||
@@ -924,14 +893,11 @@ class TwitterIE(TwitterBaseIE):
|
|||||||
'timestamp': 1670459604.0,
|
'timestamp': 1670459604.0,
|
||||||
'uploader_id': 'CTVJLaidlaw',
|
'uploader_id': 'CTVJLaidlaw',
|
||||||
'uploader': 'Jocelyn Laidlaw',
|
'uploader': 'Jocelyn Laidlaw',
|
||||||
'repost_count': int,
|
|
||||||
'comment_count': int,
|
|
||||||
'tags': ['colorectalcancer', 'cancerjourney', 'imnotaquitter'],
|
'tags': ['colorectalcancer', 'cancerjourney', 'imnotaquitter'],
|
||||||
'duration': 102.226,
|
'duration': 102.226,
|
||||||
'uploader_url': 'https://twitter.com/CTVJLaidlaw',
|
'uploader_url': 'https://twitter.com/CTVJLaidlaw',
|
||||||
'display_id': '1600649710662213632',
|
'display_id': '1600649710662213632',
|
||||||
'like_count': int,
|
'like_count': int,
|
||||||
'view_count': int,
|
|
||||||
'description': 'md5:591c19ce66fadc2359725d5cd0d1052c',
|
'description': 'md5:591c19ce66fadc2359725d5cd0d1052c',
|
||||||
'upload_date': '20221208',
|
'upload_date': '20221208',
|
||||||
'age_limit': 0,
|
'age_limit': 0,
|
||||||
@@ -957,9 +923,6 @@ class TwitterIE(TwitterBaseIE):
|
|||||||
'age_limit': 18,
|
'age_limit': 18,
|
||||||
'tags': [],
|
'tags': [],
|
||||||
'like_count': int,
|
'like_count': int,
|
||||||
'repost_count': int,
|
|
||||||
'comment_count': int,
|
|
||||||
'view_count': int,
|
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://twitter.com/hlo_again/status/1599108751385972737/video/2',
|
'url': 'https://twitter.com/hlo_again/status/1599108751385972737/video/2',
|
||||||
@@ -972,10 +935,7 @@ class TwitterIE(TwitterBaseIE):
|
|||||||
'like_count': int,
|
'like_count': int,
|
||||||
'uploader_id': 'hlo_again',
|
'uploader_id': 'hlo_again',
|
||||||
'thumbnail': 'https://pbs.twimg.com/ext_tw_video_thumb/1599108643743473680/pu/img/UG3xjov4rgg5sbYM.jpg?name=orig',
|
'thumbnail': 'https://pbs.twimg.com/ext_tw_video_thumb/1599108643743473680/pu/img/UG3xjov4rgg5sbYM.jpg?name=orig',
|
||||||
'repost_count': int,
|
|
||||||
'duration': 9.531,
|
'duration': 9.531,
|
||||||
'comment_count': int,
|
|
||||||
'view_count': int,
|
|
||||||
'upload_date': '20221203',
|
'upload_date': '20221203',
|
||||||
'age_limit': 0,
|
'age_limit': 0,
|
||||||
'timestamp': 1670092210.0,
|
'timestamp': 1670092210.0,
|
||||||
@@ -992,14 +952,11 @@ class TwitterIE(TwitterBaseIE):
|
|||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'uploader_url': 'https://twitter.com/MunTheShinobi',
|
'uploader_url': 'https://twitter.com/MunTheShinobi',
|
||||||
'description': 'This is a genius ad by Apple. \U0001f525\U0001f525\U0001f525\U0001f525\U0001f525 https://t.co/cNsA0MoOml',
|
'description': 'This is a genius ad by Apple. \U0001f525\U0001f525\U0001f525\U0001f525\U0001f525 https://t.co/cNsA0MoOml',
|
||||||
'view_count': int,
|
|
||||||
'thumbnail': 'https://pbs.twimg.com/ext_tw_video_thumb/1600009362759733248/pu/img/XVhFQivj75H_YxxV.jpg?name=orig',
|
'thumbnail': 'https://pbs.twimg.com/ext_tw_video_thumb/1600009362759733248/pu/img/XVhFQivj75H_YxxV.jpg?name=orig',
|
||||||
'age_limit': 0,
|
'age_limit': 0,
|
||||||
'uploader': 'Mün The Shinobi',
|
'uploader': 'Mün The Shinobi',
|
||||||
'repost_count': int,
|
|
||||||
'upload_date': '20221206',
|
'upload_date': '20221206',
|
||||||
'title': 'Mün The Shinobi - This is a genius ad by Apple. \U0001f525\U0001f525\U0001f525\U0001f525\U0001f525',
|
'title': 'Mün The Shinobi - This is a genius ad by Apple. \U0001f525\U0001f525\U0001f525\U0001f525\U0001f525',
|
||||||
'comment_count': int,
|
|
||||||
'like_count': int,
|
'like_count': int,
|
||||||
'tags': [],
|
'tags': [],
|
||||||
'uploader_id': 'MunTheShinobi',
|
'uploader_id': 'MunTheShinobi',
|
||||||
@@ -1007,14 +964,14 @@ class TwitterIE(TwitterBaseIE):
|
|||||||
'timestamp': 1670306984.0,
|
'timestamp': 1670306984.0,
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
# url to retweet id, legacy API
|
# url to retweet id
|
||||||
'url': 'https://twitter.com/liberdalau/status/1623739803874349067',
|
'url': 'https://twitter.com/liberdalau/status/1623739803874349067',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '1623274794488659969',
|
'id': '1623274794488659969',
|
||||||
'display_id': '1623739803874349067',
|
'display_id': '1623739803874349067',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Johnny Bullets - Me after going viral to over 30million people: Whoopsie-daisy',
|
'title': 'Johnny Bullets - Me after going viral to over 30million people: Whoopsie-daisy',
|
||||||
'description': 'md5:e873616a4a8fe0f93e71872678a672f3',
|
'description': 'md5:224d62f54b0cdef8e33d4c56c41ac503',
|
||||||
'uploader': 'Johnny Bullets',
|
'uploader': 'Johnny Bullets',
|
||||||
'uploader_id': 'Johnnybull3ts',
|
'uploader_id': 'Johnnybull3ts',
|
||||||
'uploader_url': 'https://twitter.com/Johnnybull3ts',
|
'uploader_url': 'https://twitter.com/Johnnybull3ts',
|
||||||
@@ -1025,10 +982,7 @@ class TwitterIE(TwitterBaseIE):
|
|||||||
'upload_date': '20230208',
|
'upload_date': '20230208',
|
||||||
'thumbnail': r're:https://pbs\.twimg\.com/ext_tw_video_thumb/.+',
|
'thumbnail': r're:https://pbs\.twimg\.com/ext_tw_video_thumb/.+',
|
||||||
'like_count': int,
|
'like_count': int,
|
||||||
'repost_count': int,
|
|
||||||
'comment_count': int,
|
|
||||||
},
|
},
|
||||||
'params': {'extractor_args': {'twitter': {'legacy_api': ['']}}},
|
|
||||||
}, {
|
}, {
|
||||||
# onion route
|
# onion route
|
||||||
'url': 'https://twitter3e4tixl4xyajtrzo62zg5vztmjuricljdp2c5kshju4avyoid.onion/TwitterBlue/status/1484226494708662273',
|
'url': 'https://twitter3e4tixl4xyajtrzo62zg5vztmjuricljdp2c5kshju4avyoid.onion/TwitterBlue/status/1484226494708662273',
|
||||||
@@ -1079,8 +1033,6 @@ def _graphql_to_legacy(self, data, twid):
|
|||||||
|
|
||||||
if 'tombstone' in result:
|
if 'tombstone' in result:
|
||||||
cause = remove_end(traverse_obj(result, ('tombstone', 'text', 'text', {str})), '. Learn more')
|
cause = remove_end(traverse_obj(result, ('tombstone', 'text', 'text', {str})), '. Learn more')
|
||||||
if cause and 'adult content' in cause:
|
|
||||||
self.raise_login_required(cause)
|
|
||||||
raise ExtractorError(f'Twitter API says: {cause or "Unknown error"}', expected=True)
|
raise ExtractorError(f'Twitter API says: {cause or "Unknown error"}', expected=True)
|
||||||
|
|
||||||
status = result.get('legacy', {})
|
status = result.get('legacy', {})
|
||||||
@@ -1136,19 +1088,22 @@ def _build_graphql_query(self, media_id):
|
|||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
twid, selected_index = self._match_valid_url(url).group('id', 'index')
|
twid, selected_index = self._match_valid_url(url).group('id', 'index')
|
||||||
if self._configuration_arg('legacy_api') and not self.is_logged_in:
|
if not self.is_logged_in:
|
||||||
status = traverse_obj(self._call_api(f'statuses/show/{twid}.json', twid, {
|
try:
|
||||||
'cards_platform': 'Web-12',
|
status = self._download_json(
|
||||||
'include_cards': 1,
|
'https://cdn.syndication.twimg.com/tweet-result', twid, 'Downloading syndication JSON',
|
||||||
'include_reply_count': 1,
|
headers={'User-Agent': 'Googlebot'}, query={'id': twid})
|
||||||
'include_user_entities': 0,
|
self.to_screen(f'Some metadata is missing without authentication. {self._login_hint()}')
|
||||||
'tweet_mode': 'extended',
|
except ExtractorError as e:
|
||||||
}), 'retweeted_status', None)
|
if isinstance(e.cause, urllib.error.HTTPError) and e.cause.code == 404:
|
||||||
|
self.raise_login_required('Requested tweet may only be available when logged in')
|
||||||
|
raise
|
||||||
else:
|
else:
|
||||||
result = self._call_graphql_api('zZXycP0V6H7m-2r0mOnFcA/TweetDetail', twid)
|
status = self._graphql_to_legacy(
|
||||||
status = self._graphql_to_legacy(result, twid)
|
self._call_graphql_api('zZXycP0V6H7m-2r0mOnFcA/TweetDetail', twid), twid)
|
||||||
|
|
||||||
title = description = status['full_text'].replace('\n', ' ')
|
title = description = traverse_obj(
|
||||||
|
status, (('full_text', 'text'), {lambda x: x.replace('\n', ' ')}), get_all=False) or ''
|
||||||
# strip 'https -_t.co_BJYgOjSeGA' junk from filenames
|
# strip 'https -_t.co_BJYgOjSeGA' junk from filenames
|
||||||
title = re.sub(r'\s+(https?://[^ ]+)', '', title)
|
title = re.sub(r'\s+(https?://[^ ]+)', '', title)
|
||||||
user = status.get('user') or {}
|
user = status.get('user') or {}
|
||||||
@@ -1174,12 +1129,16 @@ def _real_extract(self, url):
|
|||||||
|
|
||||||
def extract_from_video_info(media):
|
def extract_from_video_info(media):
|
||||||
media_id = traverse_obj(media, 'id_str', 'id', expected_type=str_or_none)
|
media_id = traverse_obj(media, 'id_str', 'id', expected_type=str_or_none)
|
||||||
|
if not media_id:
|
||||||
|
# workaround for non-authenticated responses
|
||||||
|
media_id = traverse_obj(media, (
|
||||||
|
'video_info', 'variants', ..., 'url',
|
||||||
|
{lambda x: re.search(r'_video/(\d+)/', x)[1]}), get_all=False)
|
||||||
self.write_debug(f'Extracting from video info: {media_id}')
|
self.write_debug(f'Extracting from video info: {media_id}')
|
||||||
video_info = media.get('video_info') or {}
|
|
||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
subtitles = {}
|
subtitles = {}
|
||||||
for variant in video_info.get('variants', []):
|
for variant in traverse_obj(media, ('video_info', 'variants', ...)):
|
||||||
fmts, subs = self._extract_variant_formats(variant, twid)
|
fmts, subs = self._extract_variant_formats(variant, twid)
|
||||||
subtitles = self._merge_subtitles(subtitles, subs)
|
subtitles = self._merge_subtitles(subtitles, subs)
|
||||||
formats.extend(fmts)
|
formats.extend(fmts)
|
||||||
@@ -1199,12 +1158,12 @@ def add_thumbnail(name, size):
|
|||||||
add_thumbnail('orig', media.get('original_info') or {})
|
add_thumbnail('orig', media.get('original_info') or {})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': media_id,
|
'id': media_id or twid,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
'subtitles': subtitles,
|
'subtitles': subtitles,
|
||||||
'thumbnails': thumbnails,
|
'thumbnails': thumbnails,
|
||||||
'view_count': traverse_obj(media, ('mediaStats', 'viewCount', {int_or_none})),
|
'view_count': traverse_obj(media, ('mediaStats', 'viewCount', {int_or_none})),
|
||||||
'duration': float_or_none(video_info.get('duration_millis'), 1000),
|
'duration': float_or_none(traverse_obj(media, ('video_info', 'duration_millis')), 1000),
|
||||||
# The codec of http formats are unknown
|
# The codec of http formats are unknown
|
||||||
'_format_sort_fields': ('res', 'br', 'size', 'proto'),
|
'_format_sort_fields': ('res', 'br', 'size', 'proto'),
|
||||||
}
|
}
|
||||||
@@ -1284,12 +1243,15 @@ def get_binding_value(k):
|
|||||||
}
|
}
|
||||||
|
|
||||||
videos = traverse_obj(status, (
|
videos = traverse_obj(status, (
|
||||||
(None, 'quoted_status'), 'extended_entities', 'media', lambda _, m: m['type'] != 'photo', {dict}))
|
('mediaDetails', ((None, 'quoted_status'), 'extended_entities', 'media')),
|
||||||
|
lambda _, m: m['type'] != 'photo', {dict}))
|
||||||
|
|
||||||
if self._yes_playlist(twid, selected_index, video_label='URL-specified video number'):
|
if self._yes_playlist(twid, selected_index, video_label='URL-specified video number'):
|
||||||
selected_entries = (*map(extract_from_video_info, videos), *extract_from_card_info(status.get('card')))
|
selected_entries = (*map(extract_from_video_info, videos), *extract_from_card_info(status.get('card')))
|
||||||
else:
|
else:
|
||||||
desired_obj = traverse_obj(status, ('extended_entities', 'media', int(selected_index) - 1, {dict}))
|
desired_obj = traverse_obj(status, (
|
||||||
|
('mediaDetails', ((None, 'quoted_status'), 'extended_entities', 'media')),
|
||||||
|
int(selected_index) - 1, {dict}), get_all=False)
|
||||||
if not desired_obj:
|
if not desired_obj:
|
||||||
raise ExtractorError(f'Video #{selected_index} is unavailable', expected=True)
|
raise ExtractorError(f'Video #{selected_index} is unavailable', expected=True)
|
||||||
elif desired_obj.get('type') != 'video':
|
elif desired_obj.get('type') != 'video':
|
||||||
@@ -1436,7 +1398,10 @@ class TwitterSpacesIE(TwitterBaseIE):
|
|||||||
'uploader': r're:Lucio Di Gaetano.*?',
|
'uploader': r're:Lucio Di Gaetano.*?',
|
||||||
'uploader_id': 'luciodigaetano',
|
'uploader_id': 'luciodigaetano',
|
||||||
'live_status': 'was_live',
|
'live_status': 'was_live',
|
||||||
'timestamp': 1659877956397,
|
'timestamp': 1659877956,
|
||||||
|
'upload_date': '20220807',
|
||||||
|
'release_timestamp': 1659904215,
|
||||||
|
'release_date': '20220807',
|
||||||
},
|
},
|
||||||
'params': {'skip_download': 'm3u8'},
|
'params': {'skip_download': 'm3u8'},
|
||||||
}]
|
}]
|
||||||
@@ -1482,26 +1447,31 @@ def _real_extract(self, url):
|
|||||||
|
|
||||||
metadata = space_data['metadata']
|
metadata = space_data['metadata']
|
||||||
live_status = try_call(lambda: self.SPACE_STATUS[metadata['state'].lower()])
|
live_status = try_call(lambda: self.SPACE_STATUS[metadata['state'].lower()])
|
||||||
|
is_live = live_status == 'is_live'
|
||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
if live_status == 'is_upcoming':
|
if live_status == 'is_upcoming':
|
||||||
self.raise_no_formats('Twitter Space not started yet', expected=True)
|
self.raise_no_formats('Twitter Space not started yet', expected=True)
|
||||||
elif live_status == 'post_live':
|
elif not is_live and not metadata.get('is_space_available_for_replay'):
|
||||||
self.raise_no_formats('Twitter Space ended but not downloadable yet', expected=True)
|
self.raise_no_formats('Twitter Space ended and replay is disabled', expected=True)
|
||||||
else:
|
elif metadata.get('media_key'):
|
||||||
source = self._call_api(
|
source = traverse_obj(
|
||||||
f'live_video_stream/status/{metadata["media_key"]}', metadata['media_key'])['source']
|
self._call_api(f'live_video_stream/status/{metadata["media_key"]}', metadata['media_key']),
|
||||||
|
('source', ('noRedirectPlaybackUrl', 'location'), {url_or_none}), get_all=False)
|
||||||
# XXX: Native downloader does not work
|
|
||||||
formats = self._extract_m3u8_formats(
|
formats = self._extract_m3u8_formats(
|
||||||
traverse_obj(source, 'noRedirectPlaybackUrl', 'location'),
|
source, metadata['media_key'], 'm4a', live=is_live, fatal=False,
|
||||||
metadata['media_key'], 'm4a', 'm3u8', live=live_status == 'is_live',
|
headers={'Referer': 'https://twitter.com/'}) if source else []
|
||||||
headers={'Referer': 'https://twitter.com/'})
|
|
||||||
for fmt in formats:
|
for fmt in formats:
|
||||||
fmt.update({'vcodec': 'none', 'acodec': 'aac'})
|
fmt.update({'vcodec': 'none', 'acodec': 'aac'})
|
||||||
|
if not is_live:
|
||||||
|
fmt['container'] = 'm4a_dash'
|
||||||
|
|
||||||
participants = ', '.join(traverse_obj(
|
participants = ', '.join(traverse_obj(
|
||||||
space_data, ('participants', 'speakers', ..., 'display_name'))) or 'nobody yet'
|
space_data, ('participants', 'speakers', ..., 'display_name'))) or 'nobody yet'
|
||||||
|
|
||||||
|
if not formats and live_status == 'post_live':
|
||||||
|
self.raise_no_formats('Twitter Space ended but not downloadable yet', expected=True)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': space_id,
|
'id': space_id,
|
||||||
'title': metadata.get('title'),
|
'title': metadata.get('title'),
|
||||||
@@ -1513,7 +1483,7 @@ def _real_extract(self, url):
|
|||||||
'live_status': live_status,
|
'live_status': live_status,
|
||||||
'release_timestamp': try_call(
|
'release_timestamp': try_call(
|
||||||
lambda: int_or_none(metadata['scheduled_start'], scale=1000)),
|
lambda: int_or_none(metadata['scheduled_start'], scale=1000)),
|
||||||
'timestamp': metadata.get('created_at'),
|
'timestamp': int_or_none(metadata.get('created_at'), scale=1000),
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ def _real_extract(self, url):
|
|||||||
r'src\s*:\s*(["\'])(?P<url>(?:https?://)?(?:(?!\1).)+)\1',
|
r'src\s*:\s*(["\'])(?P<url>(?:https?://)?(?:(?!\1).)+)\1',
|
||||||
webpage) or []]
|
webpage) or []]
|
||||||
for source in sources:
|
for source in sources:
|
||||||
|
source = urljoin(url, source)
|
||||||
height = int(self._search_regex(r'(\d+).mp4', source, 'height', default=360))
|
height = int(self._search_regex(r'(\d+).mp4', source, 'height', default=360))
|
||||||
if self._request_webpage(HEADRequest(source), video_id, f'Checking {height}p url', errnote=False):
|
if self._request_webpage(HEADRequest(source), video_id, f'Checking {height}p url', errnote=False):
|
||||||
formats.append({
|
formats.append({
|
||||||
|
|||||||
@@ -11,11 +11,13 @@
|
|||||||
from .youtube import YoutubeIE
|
from .youtube import YoutubeIE
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
|
UserNotLive,
|
||||||
clean_html,
|
clean_html,
|
||||||
get_element_by_class,
|
get_element_by_class,
|
||||||
get_element_html_by_id,
|
get_element_html_by_id,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
join_nonempty,
|
join_nonempty,
|
||||||
|
parse_resolution,
|
||||||
str_or_none,
|
str_or_none,
|
||||||
str_to_int,
|
str_to_int,
|
||||||
try_call,
|
try_call,
|
||||||
@@ -25,6 +27,7 @@
|
|||||||
url_or_none,
|
url_or_none,
|
||||||
urlencode_postdata,
|
urlencode_postdata,
|
||||||
urljoin,
|
urljoin,
|
||||||
|
traverse_obj,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -701,3 +704,139 @@ def _real_extract(self, url):
|
|||||||
return self.playlist_result(
|
return self.playlist_result(
|
||||||
entries, post_id, join_nonempty(uploader, f'Wall post {post_id}', delim=' - '),
|
entries, post_id, join_nonempty(uploader, f'Wall post {post_id}', delim=' - '),
|
||||||
clean_html(get_element_by_class('wall_post_text', webpage)))
|
clean_html(get_element_by_class('wall_post_text', webpage)))
|
||||||
|
|
||||||
|
|
||||||
|
class VKPlayBaseIE(InfoExtractor):
|
||||||
|
_RESOLUTIONS = {
|
||||||
|
'tiny': '256x144',
|
||||||
|
'lowest': '426x240',
|
||||||
|
'low': '640x360',
|
||||||
|
'medium': '852x480',
|
||||||
|
'high': '1280x720',
|
||||||
|
'full_hd': '1920x1080',
|
||||||
|
'quad_hd': '2560x1440',
|
||||||
|
}
|
||||||
|
|
||||||
|
def _extract_from_initial_state(self, url, video_id, path):
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
video_info = traverse_obj(self._search_json(
|
||||||
|
r'<script[^>]+\bid="initial-state"[^>]*>', webpage, 'initial state', video_id),
|
||||||
|
path, expected_type=dict)
|
||||||
|
if not video_info:
|
||||||
|
raise ExtractorError('Unable to extract video info from html inline initial state')
|
||||||
|
return video_info
|
||||||
|
|
||||||
|
def _extract_formats(self, stream_info, video_id):
|
||||||
|
formats = []
|
||||||
|
for stream in traverse_obj(stream_info, (
|
||||||
|
'data', 0, 'playerUrls', lambda _, v: url_or_none(v['url']) and v['type'])):
|
||||||
|
url = stream['url']
|
||||||
|
format_id = str_or_none(stream['type'])
|
||||||
|
if format_id in ('hls', 'live_hls', 'live_playback_hls') or '.m3u8' in url:
|
||||||
|
formats.extend(self._extract_m3u8_formats(url, video_id, m3u8_id=format_id, fatal=False))
|
||||||
|
elif format_id == 'dash':
|
||||||
|
formats.extend(self._extract_mpd_formats(url, video_id, mpd_id=format_id, fatal=False))
|
||||||
|
elif format_id in ('live_dash', 'live_playback_dash'):
|
||||||
|
self.write_debug(f'Not extracting unsupported format "{format_id}"')
|
||||||
|
else:
|
||||||
|
formats.append({
|
||||||
|
'url': url,
|
||||||
|
'ext': 'mp4',
|
||||||
|
'format_id': format_id,
|
||||||
|
**parse_resolution(self._RESOLUTIONS.get(format_id)),
|
||||||
|
})
|
||||||
|
return formats
|
||||||
|
|
||||||
|
def _extract_common_meta(self, stream_info):
|
||||||
|
return traverse_obj(stream_info, {
|
||||||
|
'id': ('id', {str_or_none}),
|
||||||
|
'title': ('title', {str}),
|
||||||
|
'release_timestamp': ('startTime', {int_or_none}),
|
||||||
|
'thumbnail': ('previewUrl', {url_or_none}),
|
||||||
|
'view_count': ('count', 'views', {int_or_none}),
|
||||||
|
'like_count': ('count', 'likes', {int_or_none}),
|
||||||
|
'categories': ('category', 'title', {str}, {lambda x: [x] if x else None}),
|
||||||
|
'uploader': (('user', ('blog', 'owner')), 'nick', {str}),
|
||||||
|
'uploader_id': (('user', ('blog', 'owner')), 'id', {str_or_none}),
|
||||||
|
'duration': ('duration', {int_or_none}),
|
||||||
|
'is_live': ('isOnline', {bool}),
|
||||||
|
'concurrent_view_count': ('count', 'viewers', {int_or_none}),
|
||||||
|
}, get_all=False)
|
||||||
|
|
||||||
|
|
||||||
|
class VKPlayIE(VKPlayBaseIE):
|
||||||
|
_VALID_URL = r'https?://vkplay\.live/(?P<username>[^/]+)/record/(?P<id>[a-f0-9\-]+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://vkplay.live/zitsmann/record/f5e6e3b5-dc52-4d14-965d-0680dd2882da',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'f5e6e3b5-dc52-4d14-965d-0680dd2882da',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Atomic Heart (пробуем!) спасибо подписчику EKZO!',
|
||||||
|
'uploader': 'ZitsmanN',
|
||||||
|
'uploader_id': '13159830',
|
||||||
|
'release_timestamp': 1683461378,
|
||||||
|
'release_date': '20230507',
|
||||||
|
'thumbnail': r're:https://images.vkplay.live/public_video_stream/record/f5e6e3b5-dc52-4d14-965d-0680dd2882da/preview\?change_time=\d+',
|
||||||
|
'duration': 10608,
|
||||||
|
'view_count': int,
|
||||||
|
'like_count': int,
|
||||||
|
'categories': ['Atomic Heart'],
|
||||||
|
},
|
||||||
|
'params': {'skip_download': 'm3u8'},
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
username, video_id = self._match_valid_url(url).groups()
|
||||||
|
|
||||||
|
record_info = traverse_obj(self._download_json(
|
||||||
|
f'https://api.vkplay.live/v1/blog/{username}/public_video_stream/record/{video_id}', video_id, fatal=False),
|
||||||
|
('data', 'record', {dict}))
|
||||||
|
if not record_info:
|
||||||
|
record_info = self._extract_from_initial_state(url, video_id, ('record', 'currentRecord', 'data'))
|
||||||
|
|
||||||
|
return {
|
||||||
|
**self._extract_common_meta(record_info),
|
||||||
|
'id': video_id,
|
||||||
|
'formats': self._extract_formats(record_info, video_id),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class VKPlayLiveIE(VKPlayBaseIE):
|
||||||
|
_VALID_URL = r'https?://vkplay\.live/(?P<id>[^/]+)/?(?:[#?]|$)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://vkplay.live/bayda',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'f02c321e-427b-408d-b12f-ae34e53e0ea2',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': r're:эскапизм крута .*',
|
||||||
|
'uploader': 'Bayda',
|
||||||
|
'uploader_id': 12279401,
|
||||||
|
'release_timestamp': 1687209962,
|
||||||
|
'release_date': '20230619',
|
||||||
|
'thumbnail': r're:https://images.vkplay.live/public_video_stream/12279401/preview\?change_time=\d+',
|
||||||
|
'view_count': int,
|
||||||
|
'concurrent_view_count': int,
|
||||||
|
'like_count': int,
|
||||||
|
'categories': ['EVE Online'],
|
||||||
|
'live_status': 'is_live',
|
||||||
|
},
|
||||||
|
'skip': 'livestream',
|
||||||
|
'params': {'skip_download': True},
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
username = self._match_id(url)
|
||||||
|
|
||||||
|
stream_info = self._download_json(
|
||||||
|
f'https://api.vkplay.live/v1/blog/{username}/public_video_stream', username, fatal=False)
|
||||||
|
if not stream_info:
|
||||||
|
stream_info = self._extract_from_initial_state(url, username, ('stream', 'stream', 'data', 'stream'))
|
||||||
|
|
||||||
|
formats = self._extract_formats(stream_info, username)
|
||||||
|
if not formats and not traverse_obj(stream_info, ('isOnline', {bool})):
|
||||||
|
raise UserNotLive(video_id=username)
|
||||||
|
|
||||||
|
return {
|
||||||
|
**self._extract_common_meta(stream_info),
|
||||||
|
'formats': formats,
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ def _extract_packed(self, webpage):
|
|||||||
packed = self._search_regex(
|
packed = self._search_regex(
|
||||||
r'(eval\(function.+)', webpage, 'packed code')
|
r'(eval\(function.+)', webpage, 'packed code')
|
||||||
unpacked = decode_packed_codes(packed)
|
unpacked = decode_packed_codes(packed)
|
||||||
digits = self._search_regex(r'\[((?:\d+,?)+)\]', unpacked, 'digits')
|
digits = self._search_regex(r'\[([\d,]+)\]', unpacked, 'digits')
|
||||||
digits = [int(digit) for digit in digits.split(',')]
|
digits = [int(digit) for digit in digits.split(',')]
|
||||||
key_digit = self._search_regex(
|
key_digit = self._search_regex(
|
||||||
r'fromCharCode\(.+?(\d+)\)}', unpacked, 'key digit')
|
r'fromCharCode\(.+?(\d+)\)}', unpacked, 'key digit')
|
||||||
|
|||||||
@@ -14,12 +14,14 @@
|
|||||||
try_call,
|
try_call,
|
||||||
url_or_none,
|
url_or_none,
|
||||||
urlencode_postdata,
|
urlencode_postdata,
|
||||||
|
variadic,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class WrestleUniverseBaseIE(InfoExtractor):
|
class WrestleUniverseBaseIE(InfoExtractor):
|
||||||
_NETRC_MACHINE = 'wrestleuniverse'
|
_NETRC_MACHINE = 'wrestleuniverse'
|
||||||
_VALID_URL_TMPL = r'https?://(?:www\.)?wrestle-universe\.com/(?:(?P<lang>\w{2})/)?%s/(?P<id>\w+)'
|
_VALID_URL_TMPL = r'https?://(?:www\.)?wrestle-universe\.com/(?:(?P<lang>\w{2})/)?%s/(?P<id>\w+)'
|
||||||
|
_API_HOST = 'api.wrestle-universe.com'
|
||||||
_API_PATH = None
|
_API_PATH = None
|
||||||
_REAL_TOKEN = None
|
_REAL_TOKEN = None
|
||||||
_TOKEN_EXPIRY = None
|
_TOKEN_EXPIRY = None
|
||||||
@@ -67,24 +69,28 @@ def _perform_login(self, username, password):
|
|||||||
'returnSecureToken': True,
|
'returnSecureToken': True,
|
||||||
'email': username,
|
'email': username,
|
||||||
'password': password,
|
'password': password,
|
||||||
}, separators=(',', ':')).encode())
|
}, separators=(',', ':')).encode(), expected_status=400)
|
||||||
|
token = traverse_obj(login, ('idToken', {str}))
|
||||||
|
if not token:
|
||||||
|
raise ExtractorError(
|
||||||
|
f'Unable to log in: {traverse_obj(login, ("error", "message"))}', expected=True)
|
||||||
self._REFRESH_TOKEN = traverse_obj(login, ('refreshToken', {str}))
|
self._REFRESH_TOKEN = traverse_obj(login, ('refreshToken', {str}))
|
||||||
if not self._REFRESH_TOKEN:
|
if not self._REFRESH_TOKEN:
|
||||||
self.report_warning('No refresh token was granted')
|
self.report_warning('No refresh token was granted')
|
||||||
self._TOKEN = traverse_obj(login, ('idToken', {str}))
|
self._TOKEN = token
|
||||||
|
|
||||||
def _real_initialize(self):
|
def _real_initialize(self):
|
||||||
if WrestleUniverseBaseIE._DEVICE_ID:
|
if self._DEVICE_ID:
|
||||||
return
|
return
|
||||||
|
|
||||||
WrestleUniverseBaseIE._DEVICE_ID = self._configuration_arg('device_id', [None], ie_key='WrestleUniverse')[0]
|
self._DEVICE_ID = self._configuration_arg('device_id', [None], ie_key=self._NETRC_MACHINE)[0]
|
||||||
if not WrestleUniverseBaseIE._DEVICE_ID:
|
if not self._DEVICE_ID:
|
||||||
WrestleUniverseBaseIE._DEVICE_ID = self.cache.load(self._NETRC_MACHINE, 'device_id')
|
self._DEVICE_ID = self.cache.load(self._NETRC_MACHINE, 'device_id')
|
||||||
if WrestleUniverseBaseIE._DEVICE_ID:
|
if self._DEVICE_ID:
|
||||||
return
|
return
|
||||||
WrestleUniverseBaseIE._DEVICE_ID = str(uuid.uuid4())
|
self._DEVICE_ID = str(uuid.uuid4())
|
||||||
|
|
||||||
self.cache.store(self._NETRC_MACHINE, 'device_id', WrestleUniverseBaseIE._DEVICE_ID)
|
self.cache.store(self._NETRC_MACHINE, 'device_id', self._DEVICE_ID)
|
||||||
|
|
||||||
def _refresh_token(self):
|
def _refresh_token(self):
|
||||||
refresh = self._download_json(
|
refresh = self._download_json(
|
||||||
@@ -108,10 +114,10 @@ def _call_api(self, video_id, param='', msg='API', auth=True, data=None, query={
|
|||||||
if data:
|
if data:
|
||||||
headers['Content-Type'] = 'application/json;charset=utf-8'
|
headers['Content-Type'] = 'application/json;charset=utf-8'
|
||||||
data = json.dumps(data, separators=(',', ':')).encode()
|
data = json.dumps(data, separators=(',', ':')).encode()
|
||||||
if auth:
|
if auth and self._TOKEN:
|
||||||
headers['Authorization'] = f'Bearer {self._TOKEN}'
|
headers['Authorization'] = f'Bearer {self._TOKEN}'
|
||||||
return self._download_json(
|
return self._download_json(
|
||||||
f'https://api.wrestle-universe.com/v1/{self._API_PATH}/{video_id}{param}', video_id,
|
f'https://{self._API_HOST}/v1/{self._API_PATH}/{video_id}{param}', video_id,
|
||||||
note=f'Downloading {msg} JSON', errnote=f'Failed to download {msg} JSON',
|
note=f'Downloading {msg} JSON', errnote=f'Failed to download {msg} JSON',
|
||||||
data=data, headers=headers, query=query, fatal=fatal)
|
data=data, headers=headers, query=query, fatal=fatal)
|
||||||
|
|
||||||
@@ -137,12 +143,13 @@ def decrypt(data):
|
|||||||
}, query=query, fatal=fatal)
|
}, query=query, fatal=fatal)
|
||||||
return api_json, decrypt
|
return api_json, decrypt
|
||||||
|
|
||||||
def _download_metadata(self, url, video_id, lang, props_key):
|
def _download_metadata(self, url, video_id, lang, props_keys):
|
||||||
metadata = self._call_api(video_id, msg='metadata', query={'al': lang or 'ja'}, auth=False, fatal=False)
|
metadata = self._call_api(video_id, msg='metadata', query={'al': lang or 'ja'}, auth=False, fatal=False)
|
||||||
if not metadata:
|
if not metadata:
|
||||||
webpage = self._download_webpage(url, video_id)
|
webpage = self._download_webpage(url, video_id)
|
||||||
nextjs_data = self._search_nextjs_data(webpage, video_id)
|
nextjs_data = self._search_nextjs_data(webpage, video_id)
|
||||||
metadata = traverse_obj(nextjs_data, ('props', 'pageProps', props_key, {dict})) or {}
|
metadata = traverse_obj(nextjs_data, (
|
||||||
|
'props', 'pageProps', *variadic(props_keys, (str, bytes, dict, set)), {dict})) or {}
|
||||||
return metadata
|
return metadata
|
||||||
|
|
||||||
def _get_formats(self, data, path, video_id=None):
|
def _get_formats(self, data, path, video_id=None):
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ def build_innertube_clients():
|
|||||||
THIRD_PARTY = {
|
THIRD_PARTY = {
|
||||||
'embedUrl': 'https://www.youtube.com/', # Can be any valid URL
|
'embedUrl': 'https://www.youtube.com/', # Can be any valid URL
|
||||||
}
|
}
|
||||||
BASE_CLIENTS = ('android', 'web', 'tv', 'ios', 'mweb')
|
BASE_CLIENTS = ('ios', 'android', 'web', 'tv', 'mweb')
|
||||||
priority = qualities(BASE_CLIENTS[::-1])
|
priority = qualities(BASE_CLIENTS[::-1])
|
||||||
|
|
||||||
for client, ytcfg in tuple(INNERTUBE_CLIENTS.items()):
|
for client, ytcfg in tuple(INNERTUBE_CLIENTS.items()):
|
||||||
@@ -2499,29 +2499,6 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|||||||
'uploader_id': '@abaointokyo',
|
'uploader_id': '@abaointokyo',
|
||||||
},
|
},
|
||||||
'params': {'skip_download': True}
|
'params': {'skip_download': True}
|
||||||
}, {
|
|
||||||
# Story. Requires specific player params to work.
|
|
||||||
'url': 'https://www.youtube.com/watch?v=vv8qTUWmulI',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'vv8qTUWmulI',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'availability': 'unlisted',
|
|
||||||
'view_count': int,
|
|
||||||
'channel_id': 'UCzIZ8HrzDgc-pNQDUG6avBA',
|
|
||||||
'upload_date': '20220526',
|
|
||||||
'categories': ['Education'],
|
|
||||||
'title': 'Story',
|
|
||||||
'channel': 'IT\'S HISTORY',
|
|
||||||
'description': '',
|
|
||||||
'duration': 12,
|
|
||||||
'playable_in_embed': True,
|
|
||||||
'age_limit': 0,
|
|
||||||
'live_status': 'not_live',
|
|
||||||
'tags': [],
|
|
||||||
'thumbnail': 'https://i.ytimg.com/vi_webp/vv8qTUWmulI/maxresdefault.webp',
|
|
||||||
'channel_url': 'https://www.youtube.com/channel/UCzIZ8HrzDgc-pNQDUG6avBA',
|
|
||||||
},
|
|
||||||
'skip': 'stories get removed after some period of time',
|
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.youtube.com/watch?v=tjjjtzRLHvA',
|
'url': 'https://www.youtube.com/watch?v=tjjjtzRLHvA',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@@ -3140,7 +3117,7 @@ def _extract_n_function_name(self, jscode):
|
|||||||
return funcname
|
return funcname
|
||||||
|
|
||||||
return json.loads(js_to_json(self._search_regex(
|
return json.loads(js_to_json(self._search_regex(
|
||||||
rf'var {re.escape(funcname)}\s*=\s*(\[.+?\]);', jscode,
|
rf'var {re.escape(funcname)}\s*=\s*(\[.+?\])\s*[,;]', jscode,
|
||||||
f'Initial JS player n function list ({funcname}.{idx})')))[int(idx)]
|
f'Initial JS player n function list ({funcname}.{idx})')))[int(idx)]
|
||||||
|
|
||||||
def _extract_n_function_code(self, video_id, player_url):
|
def _extract_n_function_code(self, video_id, player_url):
|
||||||
@@ -3356,7 +3333,7 @@ def _extract_comment(self, comment_renderer, parent=None):
|
|||||||
info['author_is_uploader'] = author_is_uploader
|
info['author_is_uploader'] = author_is_uploader
|
||||||
|
|
||||||
comment_abr = traverse_obj(
|
comment_abr = traverse_obj(
|
||||||
comment_renderer, ('actionsButtons', 'commentActionButtonsRenderer'), expected_type=dict)
|
comment_renderer, ('actionButtons', 'commentActionButtonsRenderer'), expected_type=dict)
|
||||||
if comment_abr is not None:
|
if comment_abr is not None:
|
||||||
info['is_favorited'] = 'creatorHeart' in comment_abr
|
info['is_favorited'] = 'creatorHeart' in comment_abr
|
||||||
|
|
||||||
@@ -3426,7 +3403,9 @@ def extract_thread(contents):
|
|||||||
# Pinned comments may appear a second time in newest first sort
|
# Pinned comments may appear a second time in newest first sort
|
||||||
# See: https://github.com/yt-dlp/yt-dlp/issues/6712
|
# See: https://github.com/yt-dlp/yt-dlp/issues/6712
|
||||||
continue
|
continue
|
||||||
self.report_warning('Detected YouTube comments looping. Stopping comment extraction as we probably cannot get any more.')
|
self.report_warning(
|
||||||
|
'Detected YouTube comments looping. Stopping comment extraction '
|
||||||
|
f'{"for this thread" if parent else ""} as we probably cannot get any more.')
|
||||||
yield
|
yield
|
||||||
else:
|
else:
|
||||||
tracker['seen_comment_ids'].add(comment['id'])
|
tracker['seen_comment_ids'].add(comment['id'])
|
||||||
@@ -3517,12 +3496,18 @@ def extract_thread(contents):
|
|||||||
# Ignore incomplete data error for replies if retries didn't work.
|
# Ignore incomplete data error for replies if retries didn't work.
|
||||||
# This is to allow any other parent comments and comment threads to be downloaded.
|
# This is to allow any other parent comments and comment threads to be downloaded.
|
||||||
# See: https://github.com/yt-dlp/yt-dlp/issues/4669
|
# See: https://github.com/yt-dlp/yt-dlp/issues/4669
|
||||||
if 'incomplete data' in str(e).lower() and parent and self.get_param('ignoreerrors') is True:
|
if 'incomplete data' in str(e).lower() and parent:
|
||||||
self.report_warning(
|
if self.get_param('ignoreerrors') in (True, 'only_download'):
|
||||||
'Received incomplete data for a comment reply thread and retrying did not help. '
|
self.report_warning(
|
||||||
'Ignoring to let other comments be downloaded.')
|
'Received incomplete data for a comment reply thread and retrying did not help. '
|
||||||
else:
|
'Ignoring to let other comments be downloaded. Pass --no-ignore-errors to not ignore.')
|
||||||
raise
|
return
|
||||||
|
else:
|
||||||
|
raise ExtractorError(
|
||||||
|
'Incomplete data received for comment reply thread. '
|
||||||
|
'Pass --ignore-errors to ignore and allow rest of comments to download.',
|
||||||
|
expected=True)
|
||||||
|
raise
|
||||||
is_forced_continuation = False
|
is_forced_continuation = False
|
||||||
continuation = None
|
continuation = None
|
||||||
for continuation_items in traverse_obj(response, continuation_items_path, expected_type=list, default=[]):
|
for continuation_items in traverse_obj(response, continuation_items_path, expected_type=list, default=[]):
|
||||||
@@ -3599,7 +3584,7 @@ def _is_agegated(player_response):
|
|||||||
def _is_unplayable(player_response):
|
def _is_unplayable(player_response):
|
||||||
return traverse_obj(player_response, ('playabilityStatus', 'status')) == 'UNPLAYABLE'
|
return traverse_obj(player_response, ('playabilityStatus', 'status')) == 'UNPLAYABLE'
|
||||||
|
|
||||||
_STORY_PLAYER_PARAMS = '8AEB'
|
_PLAYER_PARAMS = 'CgIQBg=='
|
||||||
|
|
||||||
def _extract_player_response(self, client, video_id, master_ytcfg, player_ytcfg, player_url, initial_pr, smuggled_data):
|
def _extract_player_response(self, client, video_id, master_ytcfg, player_ytcfg, player_url, initial_pr, smuggled_data):
|
||||||
|
|
||||||
@@ -3612,8 +3597,8 @@ def _extract_player_response(self, client, video_id, master_ytcfg, player_ytcfg,
|
|||||||
yt_query = {
|
yt_query = {
|
||||||
'videoId': video_id,
|
'videoId': video_id,
|
||||||
}
|
}
|
||||||
if smuggled_data.get('is_story') or _split_innertube_client(client)[0] == 'android':
|
if _split_innertube_client(client)[0] == 'android':
|
||||||
yt_query['params'] = self._STORY_PLAYER_PARAMS
|
yt_query['params'] = self._PLAYER_PARAMS
|
||||||
|
|
||||||
yt_query.update(self._generate_player_context(sts))
|
yt_query.update(self._generate_player_context(sts))
|
||||||
return self._extract_response(
|
return self._extract_response(
|
||||||
@@ -3625,7 +3610,7 @@ def _extract_player_response(self, client, video_id, master_ytcfg, player_ytcfg,
|
|||||||
|
|
||||||
def _get_requested_clients(self, url, smuggled_data):
|
def _get_requested_clients(self, url, smuggled_data):
|
||||||
requested_clients = []
|
requested_clients = []
|
||||||
default = ['android', 'web']
|
default = ['ios', 'android', 'web']
|
||||||
allowed_clients = sorted(
|
allowed_clients = sorted(
|
||||||
(client for client in INNERTUBE_CLIENTS.keys() if client[:1] != '_'),
|
(client for client in INNERTUBE_CLIENTS.keys() if client[:1] != '_'),
|
||||||
key=lambda client: INNERTUBE_CLIENTS[client]['priority'], reverse=True)
|
key=lambda client: INNERTUBE_CLIENTS[client]['priority'], reverse=True)
|
||||||
@@ -3737,7 +3722,7 @@ def append_client(*client_names):
|
|||||||
|
|
||||||
def _needs_live_processing(self, live_status, duration):
|
def _needs_live_processing(self, live_status, duration):
|
||||||
if (live_status == 'is_live' and self.get_param('live_from_start')
|
if (live_status == 'is_live' and self.get_param('live_from_start')
|
||||||
or live_status == 'post_live' and (duration or 0) > 4 * 3600):
|
or live_status == 'post_live' and (duration or 0) > 2 * 3600):
|
||||||
return live_status
|
return live_status
|
||||||
|
|
||||||
def _extract_formats_and_subtitles(self, streaming_data, video_id, player_url, live_status, duration):
|
def _extract_formats_and_subtitles(self, streaming_data, video_id, player_url, live_status, duration):
|
||||||
@@ -3752,7 +3737,12 @@ def _extract_formats_and_subtitles(self, streaming_data, video_id, player_url, l
|
|||||||
'small', 'medium', 'large', 'hd720', 'hd1080', 'hd1440', 'hd2160', 'hd2880', 'highres'
|
'small', 'medium', 'large', 'hd720', 'hd1080', 'hd1440', 'hd2160', 'hd2880', 'highres'
|
||||||
])
|
])
|
||||||
streaming_formats = traverse_obj(streaming_data, (..., ('formats', 'adaptiveFormats'), ...))
|
streaming_formats = traverse_obj(streaming_data, (..., ('formats', 'adaptiveFormats'), ...))
|
||||||
all_formats = self._configuration_arg('include_duplicate_formats')
|
format_types = self._configuration_arg('formats')
|
||||||
|
all_formats = 'duplicate' in format_types
|
||||||
|
if self._configuration_arg('include_duplicate_formats'):
|
||||||
|
all_formats = True
|
||||||
|
self._downloader.deprecated_feature('[youtube] include_duplicate_formats extractor argument is deprecated. '
|
||||||
|
'Use formats=duplicate extractor argument instead')
|
||||||
|
|
||||||
def build_fragments(f):
|
def build_fragments(f):
|
||||||
return LazyList({
|
return LazyList({
|
||||||
@@ -3892,18 +3882,23 @@ def build_fragments(f):
|
|||||||
if single_stream and dct.get('ext'):
|
if single_stream and dct.get('ext'):
|
||||||
dct['container'] = dct['ext'] + '_dash'
|
dct['container'] = dct['ext'] + '_dash'
|
||||||
|
|
||||||
if all_formats and dct['filesize']:
|
if (all_formats or 'dashy' in format_types) and dct['filesize']:
|
||||||
yield {
|
yield {
|
||||||
**dct,
|
**dct,
|
||||||
'format_id': f'{dct["format_id"]}-dashy' if all_formats else dct['format_id'],
|
'format_id': f'{dct["format_id"]}-dashy' if all_formats else dct['format_id'],
|
||||||
'protocol': 'http_dash_segments',
|
'protocol': 'http_dash_segments',
|
||||||
'fragments': build_fragments(dct),
|
'fragments': build_fragments(dct),
|
||||||
}
|
}
|
||||||
dct['downloader_options'] = {'http_chunk_size': CHUNK_SIZE}
|
if all_formats or 'dashy' not in format_types:
|
||||||
yield dct
|
dct['downloader_options'] = {'http_chunk_size': CHUNK_SIZE}
|
||||||
|
yield dct
|
||||||
|
|
||||||
needs_live_processing = self._needs_live_processing(live_status, duration)
|
needs_live_processing = self._needs_live_processing(live_status, duration)
|
||||||
skip_bad_formats = not self._configuration_arg('include_incomplete_formats')
|
skip_bad_formats = 'incomplete' not in format_types
|
||||||
|
if self._configuration_arg('include_incomplete_formats'):
|
||||||
|
skip_bad_formats = False
|
||||||
|
self._downloader.deprecated_feature('[youtube] include_incomplete_formats extractor argument is deprecated. '
|
||||||
|
'Use formats=incomplete extractor argument instead')
|
||||||
|
|
||||||
skip_manifests = set(self._configuration_arg('skip'))
|
skip_manifests = set(self._configuration_arg('skip'))
|
||||||
if (not self.get_param('youtube_include_hls_manifest', True)
|
if (not self.get_param('youtube_include_hls_manifest', True)
|
||||||
@@ -3915,7 +3910,7 @@ def build_fragments(f):
|
|||||||
skip_manifests.add('dash')
|
skip_manifests.add('dash')
|
||||||
if self._configuration_arg('include_live_dash'):
|
if self._configuration_arg('include_live_dash'):
|
||||||
self._downloader.deprecated_feature('[youtube] include_live_dash extractor argument is deprecated. '
|
self._downloader.deprecated_feature('[youtube] include_live_dash extractor argument is deprecated. '
|
||||||
'Use include_incomplete_formats extractor argument instead')
|
'Use formats=incomplete extractor argument instead')
|
||||||
elif skip_bad_formats and live_status == 'is_live' and needs_live_processing != 'is_live':
|
elif skip_bad_formats and live_status == 'is_live' and needs_live_processing != 'is_live':
|
||||||
skip_manifests.add('dash')
|
skip_manifests.add('dash')
|
||||||
|
|
||||||
@@ -3932,6 +3927,13 @@ def process_manifest_format(f, proto, client_name, itag):
|
|||||||
elif itag:
|
elif itag:
|
||||||
f['format_id'] = itag
|
f['format_id'] = itag
|
||||||
|
|
||||||
|
if f.get('source_preference') is None:
|
||||||
|
f['source_preference'] = -1
|
||||||
|
|
||||||
|
if itag in ('616', '235'):
|
||||||
|
f['format_note'] = join_nonempty(f.get('format_note'), 'Premium', delim=' ')
|
||||||
|
f['source_preference'] += 100
|
||||||
|
|
||||||
f['quality'] = q(itag_qualities.get(try_get(f, lambda f: f['format_id'].split('-')[0]), -1))
|
f['quality'] = q(itag_qualities.get(try_get(f, lambda f: f['format_id'].split('-')[0]), -1))
|
||||||
if f['quality'] == -1 and f.get('height'):
|
if f['quality'] == -1 and f.get('height'):
|
||||||
f['quality'] = q(res_qualities[min(res_qualities, key=lambda x: abs(x - f['height']))])
|
f['quality'] = q(res_qualities[min(res_qualities, key=lambda x: abs(x - f['height']))])
|
||||||
@@ -3939,6 +3941,10 @@ def process_manifest_format(f, proto, client_name, itag):
|
|||||||
f['format_note'] = join_nonempty(f.get('format_note'), client_name, delim=', ')
|
f['format_note'] = join_nonempty(f.get('format_note'), client_name, delim=', ')
|
||||||
if f.get('fps') and f['fps'] <= 1:
|
if f.get('fps') and f['fps'] <= 1:
|
||||||
del f['fps']
|
del f['fps']
|
||||||
|
|
||||||
|
if proto == 'hls' and f.get('has_drm'):
|
||||||
|
f['has_drm'] = 'maybe'
|
||||||
|
f['source_preference'] -= 5
|
||||||
return True
|
return True
|
||||||
|
|
||||||
subtitles = {}
|
subtitles = {}
|
||||||
@@ -4011,8 +4017,6 @@ def _download_player_responses(self, url, smuggled_data, video_id, webpage_url):
|
|||||||
webpage = None
|
webpage = None
|
||||||
if 'webpage' not in self._configuration_arg('player_skip'):
|
if 'webpage' not in self._configuration_arg('player_skip'):
|
||||||
query = {'bpctr': '9999999999', 'has_verified': '1'}
|
query = {'bpctr': '9999999999', 'has_verified': '1'}
|
||||||
if smuggled_data.get('is_story'):
|
|
||||||
query['pp'] = self._STORY_PLAYER_PARAMS
|
|
||||||
webpage = self._download_webpage(
|
webpage = self._download_webpage(
|
||||||
webpage_url, video_id, fatal=False, query=query)
|
webpage_url, video_id, fatal=False, query=query)
|
||||||
|
|
||||||
@@ -4040,6 +4044,10 @@ def _list_formats(self, video_id, microformats, video_details, player_responses,
|
|||||||
else None)
|
else None)
|
||||||
streaming_data = traverse_obj(player_responses, (..., 'streamingData'))
|
streaming_data = traverse_obj(player_responses, (..., 'streamingData'))
|
||||||
*formats, subtitles = self._extract_formats_and_subtitles(streaming_data, video_id, player_url, live_status, duration)
|
*formats, subtitles = self._extract_formats_and_subtitles(streaming_data, video_id, player_url, live_status, duration)
|
||||||
|
if all(f.get('has_drm') for f in formats):
|
||||||
|
# If there are no formats that definitely don't have DRM, all have DRM
|
||||||
|
for f in formats:
|
||||||
|
f['has_drm'] = True
|
||||||
|
|
||||||
return live_broadcast_details, live_status, streaming_data, formats, subtitles
|
return live_broadcast_details, live_status, streaming_data, formats, subtitles
|
||||||
|
|
||||||
@@ -4224,7 +4232,7 @@ def is_bad_format(fmt):
|
|||||||
|
|
||||||
for fmt in filter(is_bad_format, formats):
|
for fmt in filter(is_bad_format, formats):
|
||||||
fmt['preference'] = (fmt.get('preference') or -1) - 10
|
fmt['preference'] = (fmt.get('preference') or -1) - 10
|
||||||
fmt['format_note'] = join_nonempty(fmt.get('format_note'), '(Last 4 hours)', delim=' ')
|
fmt['format_note'] = join_nonempty(fmt.get('format_note'), '(Last 2 hours)', delim=' ')
|
||||||
|
|
||||||
if needs_live_processing:
|
if needs_live_processing:
|
||||||
self._prepare_live_from_start_formats(
|
self._prepare_live_from_start_formats(
|
||||||
@@ -4342,15 +4350,21 @@ def process_language(container, base_url, lang_code, sub_name, query):
|
|||||||
info[d_k] = parse_duration(query[k][0])
|
info[d_k] = parse_duration(query[k][0])
|
||||||
|
|
||||||
# Youtube Music Auto-generated description
|
# Youtube Music Auto-generated description
|
||||||
if video_description:
|
if (video_description or '').strip().endswith('\nAuto-generated by YouTube.'):
|
||||||
|
# XXX: Causes catastrophic backtracking if description has "·"
|
||||||
|
# E.g. https://www.youtube.com/watch?v=DoPaAxMQoiI
|
||||||
|
# Simulating atomic groups: (?P<a>[^xy]+)x => (?=(?P<a>[^xy]+))(?P=a)x
|
||||||
|
# reduces it, but does not fully fix it. https://regex101.com/r/8Ssf2h/2
|
||||||
mobj = re.search(
|
mobj = re.search(
|
||||||
r'''(?xs)
|
r'''(?xs)
|
||||||
(?P<track>[^·\n]+)·(?P<artist>[^\n]+)\n+
|
(?=(?P<track>[^\n·]+))(?P=track)·
|
||||||
(?P<album>[^\n]+)
|
(?=(?P<artist>[^\n]+))(?P=artist)\n+
|
||||||
|
(?=(?P<album>[^\n]+))(?P=album)\n
|
||||||
(?:.+?℗\s*(?P<release_year>\d{4})(?!\d))?
|
(?:.+?℗\s*(?P<release_year>\d{4})(?!\d))?
|
||||||
(?:.+?Released on\s*:\s*(?P<release_date>\d{4}-\d{2}-\d{2}))?
|
(?:.+?Released on\s*:\s*(?P<release_date>\d{4}-\d{2}-\d{2}))?
|
||||||
(.+?\nArtist\s*:\s*(?P<clean_artist>[^\n]+))?
|
(.+?\nArtist\s*:\s*
|
||||||
.+\nAuto-generated\ by\ YouTube\.\s*$
|
(?=(?P<clean_artist>[^\n]+))(?P=clean_artist)\n
|
||||||
|
)?.+\nAuto-generated\ by\ YouTube\.\s*$
|
||||||
''', video_description)
|
''', video_description)
|
||||||
if mobj:
|
if mobj:
|
||||||
release_year = mobj.group('release_year')
|
release_year = mobj.group('release_year')
|
||||||
@@ -4878,7 +4892,8 @@ def _extract_entries(self, parent_renderer, continuation_list):
|
|||||||
'videoRenderer': lambda x: [self._video_entry(x)],
|
'videoRenderer': lambda x: [self._video_entry(x)],
|
||||||
'playlistRenderer': lambda x: self._grid_entries({'items': [{'playlistRenderer': x}]}),
|
'playlistRenderer': lambda x: self._grid_entries({'items': [{'playlistRenderer': x}]}),
|
||||||
'channelRenderer': lambda x: self._grid_entries({'items': [{'channelRenderer': x}]}),
|
'channelRenderer': lambda x: self._grid_entries({'items': [{'channelRenderer': x}]}),
|
||||||
'hashtagTileRenderer': lambda x: [self._hashtag_tile_entry(x)]
|
'hashtagTileRenderer': lambda x: [self._hashtag_tile_entry(x)],
|
||||||
|
'richGridRenderer': lambda x: self._extract_entries(x, continuation_list),
|
||||||
}
|
}
|
||||||
for key, renderer in isr_content.items():
|
for key, renderer in isr_content.items():
|
||||||
if key not in known_renderers:
|
if key not in known_renderers:
|
||||||
@@ -6370,6 +6385,28 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
|
|||||||
'channel_is_verified': True,
|
'channel_is_verified': True,
|
||||||
},
|
},
|
||||||
'playlist_mincount': 10,
|
'playlist_mincount': 10,
|
||||||
|
}, {
|
||||||
|
# Playlist with only shorts, shown as reel renderers
|
||||||
|
# FIXME: future: YouTube currently doesn't give continuation for this,
|
||||||
|
# may do in future.
|
||||||
|
'url': 'https://www.youtube.com/playlist?list=UUxqPAgubo4coVn9Lx1FuKcg',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'UUxqPAgubo4coVn9Lx1FuKcg',
|
||||||
|
'channel_url': 'https://www.youtube.com/channel/UCxqPAgubo4coVn9Lx1FuKcg',
|
||||||
|
'view_count': int,
|
||||||
|
'uploader_id': '@BangyShorts',
|
||||||
|
'description': '',
|
||||||
|
'uploader_url': 'https://www.youtube.com/@BangyShorts',
|
||||||
|
'channel_id': 'UCxqPAgubo4coVn9Lx1FuKcg',
|
||||||
|
'channel': 'Bangy Shorts',
|
||||||
|
'uploader': 'Bangy Shorts',
|
||||||
|
'tags': [],
|
||||||
|
'availability': 'public',
|
||||||
|
'modified_date': '20230626',
|
||||||
|
'title': 'Uploads from Bangy Shorts',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 100,
|
||||||
|
'expected_warnings': [r'[Uu]navailable videos (are|will be) hidden'],
|
||||||
}]
|
}]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -7094,22 +7131,6 @@ class YoutubeHistoryIE(YoutubeFeedsInfoExtractor):
|
|||||||
}]
|
}]
|
||||||
|
|
||||||
|
|
||||||
class YoutubeStoriesIE(InfoExtractor):
|
|
||||||
IE_DESC = 'YouTube channel stories; "ytstories:" prefix'
|
|
||||||
IE_NAME = 'youtube:stories'
|
|
||||||
_VALID_URL = r'ytstories:UC(?P<id>[A-Za-z0-9_-]{21}[AQgw])$'
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'ytstories:UCwFCb4jeqaKWnciAYM-ZVHg',
|
|
||||||
'only_matching': True,
|
|
||||||
}]
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
playlist_id = f'RLTD{self._match_id(url)}'
|
|
||||||
return self.url_result(
|
|
||||||
smuggle_url(f'https://www.youtube.com/playlist?list={playlist_id}&playnext=1', {'is_story': True}),
|
|
||||||
ie=YoutubeTabIE, video_id=playlist_id)
|
|
||||||
|
|
||||||
|
|
||||||
class YoutubeShortsAudioPivotIE(InfoExtractor):
|
class YoutubeShortsAudioPivotIE(InfoExtractor):
|
||||||
IE_DESC = 'YouTube Shorts audio pivot (Shorts using audio of a given video)'
|
IE_DESC = 'YouTube Shorts audio pivot (Shorts using audio of a given video)'
|
||||||
IE_NAME = 'youtube:shorts:pivot:audio'
|
IE_NAME = 'youtube:shorts:pivot:audio'
|
||||||
|
|||||||
@@ -1012,8 +1012,9 @@ def _alias_callback(option, opt_str, value, parser, opts, nargs):
|
|||||||
'--download-sections',
|
'--download-sections',
|
||||||
metavar='REGEX', dest='download_ranges', action='append',
|
metavar='REGEX', dest='download_ranges', action='append',
|
||||||
help=(
|
help=(
|
||||||
'Download only chapters whose title matches the given regular expression. '
|
'Download only chapters that match the regular expression. '
|
||||||
'Time ranges prefixed by a "*" can also be used in place of chapters to download the specified range. '
|
'A "*" prefix denotes time-range instead of chapter. Negative timestamps are calculated from the end. '
|
||||||
|
'"*from-url" can be used to download between the "start_time" and "end_time" extracted from the URL. '
|
||||||
'Needs ffmpeg. This option can be used multiple times to download multiple sections, '
|
'Needs ffmpeg. This option can be used multiple times to download multiple sections, '
|
||||||
'e.g. --download-sections "*10:15-inf" --download-sections "intro"'))
|
'e.g. --download-sections "*10:15-inf" --download-sections "intro"'))
|
||||||
downloader.add_option(
|
downloader.add_option(
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# flake8: noqa: F401, F403
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from ..compat.compat_utils import passthrough_module
|
from ..compat.compat_utils import passthrough_module
|
||||||
|
|||||||
@@ -1556,7 +1556,12 @@ def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|||||||
|
|
||||||
new_method = req.get_method()
|
new_method = req.get_method()
|
||||||
new_data = req.data
|
new_data = req.data
|
||||||
remove_headers = []
|
|
||||||
|
# Technically the Cookie header should be in unredirected_hdrs,
|
||||||
|
# however in practice some may set it in normal headers anyway.
|
||||||
|
# We will remove it here to prevent any leaks.
|
||||||
|
remove_headers = ['Cookie']
|
||||||
|
|
||||||
# A 303 must either use GET or HEAD for subsequent request
|
# A 303 must either use GET or HEAD for subsequent request
|
||||||
# https://datatracker.ietf.org/doc/html/rfc7231#section-6.4.4
|
# https://datatracker.ietf.org/doc/html/rfc7231#section-6.4.4
|
||||||
if code == 303 and req.get_method() != 'HEAD':
|
if code == 303 and req.get_method() != 'HEAD':
|
||||||
@@ -1573,7 +1578,7 @@ def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|||||||
new_data = None
|
new_data = None
|
||||||
remove_headers.extend(['Content-Length', 'Content-Type'])
|
remove_headers.extend(['Content-Length', 'Content-Type'])
|
||||||
|
|
||||||
new_headers = {k: v for k, v in req.headers.items() if k.lower() not in remove_headers}
|
new_headers = {k: v for k, v in req.headers.items() if k.title() not in remove_headers}
|
||||||
|
|
||||||
return urllib.request.Request(
|
return urllib.request.Request(
|
||||||
newurl, headers=new_headers, origin_req_host=req.origin_req_host,
|
newurl, headers=new_headers, origin_req_host=req.origin_req_host,
|
||||||
@@ -3507,7 +3512,8 @@ def get_compatible_ext(*, vcodecs, acodecs, vexts, aexts, preferences=None):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
sanitize_codec = functools.partial(try_get, getter=lambda x: x[0].split('.')[0].replace('0', ''))
|
sanitize_codec = functools.partial(
|
||||||
|
try_get, getter=lambda x: x[0].split('.')[0].replace('0', '').lower())
|
||||||
vcodec, acodec = sanitize_codec(vcodecs), sanitize_codec(acodecs)
|
vcodec, acodec = sanitize_codec(vcodecs), sanitize_codec(acodecs)
|
||||||
|
|
||||||
for ext in preferences or COMPATIBLE_CODECS.keys():
|
for ext in preferences or COMPATIBLE_CODECS.keys():
|
||||||
@@ -3753,12 +3759,10 @@ def _match_func(info_dict, incomplete=False):
|
|||||||
|
|
||||||
|
|
||||||
class download_range_func:
|
class download_range_func:
|
||||||
def __init__(self, chapters, ranges):
|
def __init__(self, chapters, ranges, from_info=False):
|
||||||
self.chapters, self.ranges = chapters, ranges
|
self.chapters, self.ranges, self.from_info = chapters, ranges, from_info
|
||||||
|
|
||||||
def __call__(self, info_dict, ydl):
|
def __call__(self, info_dict, ydl):
|
||||||
if not self.ranges and not self.chapters:
|
|
||||||
yield {}
|
|
||||||
|
|
||||||
warning = ('There are no chapters matching the regex' if info_dict.get('chapters')
|
warning = ('There are no chapters matching the regex' if info_dict.get('chapters')
|
||||||
else 'Cannot match chapters since chapter information is unavailable')
|
else 'Cannot match chapters since chapter information is unavailable')
|
||||||
@@ -3770,7 +3774,23 @@ def __call__(self, info_dict, ydl):
|
|||||||
if self.chapters and warning:
|
if self.chapters and warning:
|
||||||
ydl.to_screen(f'[info] {info_dict["id"]}: {warning}')
|
ydl.to_screen(f'[info] {info_dict["id"]}: {warning}')
|
||||||
|
|
||||||
yield from ({'start_time': start, 'end_time': end} for start, end in self.ranges or [])
|
for start, end in self.ranges or []:
|
||||||
|
yield {
|
||||||
|
'start_time': self._handle_negative_timestamp(start, info_dict),
|
||||||
|
'end_time': self._handle_negative_timestamp(end, info_dict),
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.from_info and (info_dict.get('start_time') or info_dict.get('end_time')):
|
||||||
|
yield {
|
||||||
|
'start_time': info_dict.get('start_time') or 0,
|
||||||
|
'end_time': info_dict.get('end_time') or float('inf'),
|
||||||
|
}
|
||||||
|
elif not self.ranges and not self.chapters:
|
||||||
|
yield {}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _handle_negative_timestamp(time, info):
|
||||||
|
return max(info['duration'] + time, 0) if info.get('duration') and time < 0 else time
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return (isinstance(other, download_range_func)
|
return (isinstance(other, download_range_func)
|
||||||
@@ -5098,7 +5118,7 @@ def format_field(obj, field=None, template='%s', ignore=NO_DEFAULT, default='',
|
|||||||
|
|
||||||
|
|
||||||
def clean_podcast_url(url):
|
def clean_podcast_url(url):
|
||||||
return re.sub(r'''(?x)
|
url = re.sub(r'''(?x)
|
||||||
(?:
|
(?:
|
||||||
(?:
|
(?:
|
||||||
chtbl\.com/track|
|
chtbl\.com/track|
|
||||||
@@ -5112,6 +5132,7 @@ def clean_podcast_url(url):
|
|||||||
st\.fm # https://podsights.com/docs/
|
st\.fm # https://podsights.com/docs/
|
||||||
)/e
|
)/e
|
||||||
)/''', '', url)
|
)/''', '', url)
|
||||||
|
return re.sub(r'^\w+://(\w+://)', r'\1', url)
|
||||||
|
|
||||||
|
|
||||||
_HEX_TABLE = '0123456789abcdef'
|
_HEX_TABLE = '0123456789abcdef'
|
||||||
@@ -5723,9 +5744,9 @@ class FormatSorter:
|
|||||||
'source': {'convert': 'float', 'field': 'source_preference', 'default': -1},
|
'source': {'convert': 'float', 'field': 'source_preference', 'default': -1},
|
||||||
|
|
||||||
'codec': {'type': 'combined', 'field': ('vcodec', 'acodec')},
|
'codec': {'type': 'combined', 'field': ('vcodec', 'acodec')},
|
||||||
'br': {'type': 'multiple', 'field': ('tbr', 'vbr', 'abr'),
|
'br': {'type': 'multiple', 'field': ('tbr', 'vbr', 'abr'), 'convert': 'float_none',
|
||||||
'function': lambda it: next(filter(None, it), None)},
|
'function': lambda it: next(filter(None, it), None)},
|
||||||
'size': {'type': 'multiple', 'field': ('filesize', 'fs_approx'),
|
'size': {'type': 'multiple', 'field': ('filesize', 'fs_approx'), 'convert': 'bytes',
|
||||||
'function': lambda it: next(filter(None, it), None)},
|
'function': lambda it: next(filter(None, it), None)},
|
||||||
'ext': {'type': 'combined', 'field': ('vext', 'aext')},
|
'ext': {'type': 'combined', 'field': ('vext', 'aext')},
|
||||||
'res': {'type': 'multiple', 'field': ('height', 'width'),
|
'res': {'type': 'multiple', 'field': ('height', 'width'),
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Autogenerated by devscripts/update-version.py
|
# Autogenerated by devscripts/update-version.py
|
||||||
|
|
||||||
__version__ = '2023.06.21'
|
__version__ = '2023.07.06'
|
||||||
|
|
||||||
RELEASE_GIT_HEAD = '42f2d40b475db66486a4b4fe5b56751a640db5db'
|
RELEASE_GIT_HEAD = 'b532a3481046e1eabb6232ee8196fb696c356ff6'
|
||||||
|
|
||||||
VARIANT = None
|
VARIANT = None
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user