mirror of
https://github.com/yt-dlp/yt-dlp.git
synced 2026-01-17 04:11:41 +00:00
Compare commits
348 Commits
2022.01.21
...
2022.04.08
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dee1d65dc3 | ||
|
|
7884ade65e | ||
|
|
89fabf1125 | ||
|
|
11e1c2e3f8 | ||
|
|
ebc7d3ff1f | ||
|
|
d8a58ddce7 | ||
|
|
4d57133095 | ||
|
|
9b8b7a7b5e | ||
|
|
ab0970b233 | ||
|
|
b52e788eb2 | ||
|
|
316f2650f8 | ||
|
|
bd4073c535 | ||
|
|
22fba53fbd | ||
|
|
61d3665d9d | ||
|
|
870efdee28 | ||
|
|
b506289fe2 | ||
|
|
b63837bce0 | ||
|
|
fcfa8853e4 | ||
|
|
06b1628d3e | ||
|
|
da1ffde15d | ||
|
|
42a4f21a03 | ||
|
|
8973767198 | ||
|
|
0edb3e336c | ||
|
|
ce0593ef61 | ||
|
|
a44ca5a470 | ||
|
|
0a8a7e68fa | ||
|
|
f4d706a931 | ||
|
|
5fa3c9a88f | ||
|
|
04f3fd2c89 | ||
|
|
85e801a9db | ||
|
|
5127e92a94 | ||
|
|
18eac302a2 | ||
|
|
12e022d074 | ||
|
|
265e586d96 | ||
|
|
fbfde1c3e6 | ||
|
|
dc57e74a7f | ||
|
|
a17526e427 | ||
|
|
ad210f4fd4 | ||
|
|
c8e856a551 | ||
|
|
c085e4ec47 | ||
|
|
4c268f9cb7 | ||
|
|
5d45484cc7 | ||
|
|
e6f868a63c | ||
|
|
c4f60dd7cd | ||
|
|
f189faf1ce | ||
|
|
504f789ad5 | ||
|
|
bb5a7cb8ad | ||
|
|
c418e6b5a6 | ||
|
|
11078c6d57 | ||
|
|
5d0aeac0e9 | ||
|
|
180c81509f | ||
|
|
ab2579bb45 | ||
|
|
48e15bb6b1 | ||
|
|
af4944d84b | ||
|
|
e7870111e8 | ||
|
|
8a7f68d0b1 | ||
|
|
9139d2fae0 | ||
|
|
bdd60588b0 | ||
|
|
f5f15c9993 | ||
|
|
cb96c5be70 | ||
|
|
90137ca4be | ||
|
|
1c1b2f96ae | ||
|
|
47b8bf207b | ||
|
|
4628a3aa75 | ||
|
|
5b4bb715e6 | ||
|
|
1235d333ab | ||
|
|
18e4940825 | ||
|
|
c0b6e5c74d | ||
|
|
727029c508 | ||
|
|
5c3895fff1 | ||
|
|
fd2ad7cb24 | ||
|
|
4a3175fc4c | ||
|
|
5cf34021f5 | ||
|
|
34baa9fdf0 | ||
|
|
6db9c4d57d | ||
|
|
3cea3edd1a | ||
|
|
b1a7cd056a | ||
|
|
28787f16c6 | ||
|
|
1fb707badb | ||
|
|
a3f2445e29 | ||
|
|
ae72962643 | ||
|
|
ae6a1b9585 | ||
|
|
231025c463 | ||
|
|
700ccbe3f1 | ||
|
|
12a64f2777 | ||
|
|
b8f2f8f6b3 | ||
|
|
af14914baa | ||
|
|
ea5ca8e7fc | ||
|
|
c2d2ee40eb | ||
|
|
c70c418d33 | ||
|
|
b9c7b1e9b4 | ||
|
|
d5820461e8 | ||
|
|
8a23db9519 | ||
|
|
1f1df1251e | ||
|
|
84842aee2b | ||
|
|
be4685ab7b | ||
|
|
e6552207da | ||
|
|
a2e77303e3 | ||
|
|
510809f1aa | ||
|
|
f4ad919298 | ||
|
|
eeb2a770f3 | ||
|
|
0c14d66ad9 | ||
|
|
52efa4b312 | ||
|
|
028f6437f1 | ||
|
|
43c38abd1f | ||
|
|
e4b98809cf | ||
|
|
16c620bc55 | ||
|
|
5a373d9768 | ||
|
|
7e6a187096 | ||
|
|
3f168f0e45 | ||
|
|
7bdcb4a40e | ||
|
|
497a6c5f57 | ||
|
|
4b3c5d1b81 | ||
|
|
ec47c12f69 | ||
|
|
25791435b7 | ||
|
|
4e34889f1c | ||
|
|
a1b2d84360 | ||
|
|
5dbc77df26 | ||
|
|
d71fd41249 | ||
|
|
d69e55c1d8 | ||
|
|
9f2a6352ea | ||
|
|
aeb21b98f1 | ||
|
|
b3edc8068e | ||
|
|
17322130a9 | ||
|
|
5ca764c506 | ||
|
|
e880c92c65 | ||
|
|
a825ffbffa | ||
|
|
592b748582 | ||
|
|
cf4f42cb97 | ||
|
|
da1d734fbe | ||
|
|
2b38f7b2bc | ||
|
|
76aa991374 | ||
|
|
24e3d87431 | ||
|
|
63b2f88bc7 | ||
|
|
07ff290dce | ||
|
|
51c22ef4e2 | ||
|
|
33b8c411bc | ||
|
|
10331a2672 | ||
|
|
6e6beffd04 | ||
|
|
e491d06d34 | ||
|
|
7a0ba75857 | ||
|
|
e248be3319 | ||
|
|
ff91cf7483 | ||
|
|
a3b7dff015 | ||
|
|
c0c2c57d35 | ||
|
|
aee6ce5867 | ||
|
|
d1b5f70bc9 | ||
|
|
1eae7f94c1 | ||
|
|
535eb16a44 | ||
|
|
9461cb586a | ||
|
|
a405b38f20 | ||
|
|
08d30158ec | ||
|
|
c89bec262c | ||
|
|
151f8f1c02 | ||
|
|
a35155be17 | ||
|
|
e66662b1e0 | ||
|
|
4390d5ec12 | ||
|
|
9e0e6adb2d | ||
|
|
b637c4e22e | ||
|
|
fb6e3f4389 | ||
|
|
409cdd1ec9 | ||
|
|
992f9a730b | ||
|
|
497d2fab6c | ||
|
|
2807d1709b | ||
|
|
b46ccbc6d4 | ||
|
|
1ed7953a74 | ||
|
|
d49669acad | ||
|
|
bed30106f5 | ||
|
|
27231526ae | ||
|
|
50e93e03a7 | ||
|
|
72e995f122 | ||
|
|
8b7539d27c | ||
|
|
e48b3875ec | ||
|
|
2a938746f3 | ||
|
|
933dbf5a55 | ||
|
|
a10aa588b0 | ||
|
|
be8cd3cb1d | ||
|
|
319b6059d2 | ||
|
|
4c3f8c3fb6 | ||
|
|
7265a2190c | ||
|
|
3a4bb9f751 | ||
|
|
b90dbe6c19 | ||
|
|
97bef011ee | ||
|
|
ecca4519b7 | ||
|
|
761fba6d22 | ||
|
|
5bcccbfec3 | ||
|
|
ded9f32667 | ||
|
|
45806d44a7 | ||
|
|
747c0bd127 | ||
|
|
acea8d7cfb | ||
|
|
f1d130902b | ||
|
|
c2ae48dbd5 | ||
|
|
a5c0c20252 | ||
|
|
f494ddada8 | ||
|
|
02fc6feb6e | ||
|
|
7eaf7f9aba | ||
|
|
334b1c4800 | ||
|
|
7c219ea601 | ||
|
|
93c8410d33 | ||
|
|
195c22840c | ||
|
|
f0734e1190 | ||
|
|
15dfb3929c | ||
|
|
3e9b66d761 | ||
|
|
a539f06570 | ||
|
|
b440e1bb22 | ||
|
|
03f830040a | ||
|
|
09b49e1f68 | ||
|
|
1108613f02 | ||
|
|
a30a6ed3e4 | ||
|
|
65d151d58f | ||
|
|
72073451be | ||
|
|
77cc7c6e60 | ||
|
|
971c4847d7 | ||
|
|
7a34b5d628 | ||
|
|
4d4f9a029f | ||
|
|
f099df1463 | ||
|
|
3f4faff748 | ||
|
|
be8d623455 | ||
|
|
a7d4acc018 | ||
|
|
febff4c119 | ||
|
|
ed66a17ef0 | ||
|
|
5625e6073f | ||
|
|
0ad92dfb18 | ||
|
|
60f3e99592 | ||
|
|
8d93e69d67 | ||
|
|
3aa915400d | ||
|
|
dcd55f766d | ||
|
|
2e4cacd038 | ||
|
|
c15c316b21 | ||
|
|
549cb2a836 | ||
|
|
c571b3a6ab | ||
|
|
5b804e3906 | ||
|
|
6bb608d055 | ||
|
|
ae419aa94f | ||
|
|
ac184ab742 | ||
|
|
5c10453827 | ||
|
|
ffa89477ea | ||
|
|
db74de8c54 | ||
|
|
edecb5f81f | ||
|
|
85a0ad0117 | ||
|
|
07ea0014ae | ||
|
|
e1f7f235bd | ||
|
|
fc259cc249 | ||
|
|
9a5b012575 | ||
|
|
df635a09a4 | ||
|
|
812283199a | ||
|
|
5c6dfc1f79 | ||
|
|
c2a8547fdc | ||
|
|
0a19532ead | ||
|
|
2d41e2eceb | ||
|
|
81c5f44c0f | ||
|
|
1f7db8533a | ||
|
|
e8969bda94 | ||
|
|
c82f051dbb | ||
|
|
49895f062e | ||
|
|
60f393e48b | ||
|
|
88afe05695 | ||
|
|
57ebfca39b | ||
|
|
b1cb0525ac | ||
|
|
da42679b87 | ||
|
|
2944835080 | ||
|
|
a3eb987e0e | ||
|
|
7bc33ad0e9 | ||
|
|
2068a60318 | ||
|
|
1ce9a3cb49 | ||
|
|
d49f8db39f | ||
|
|
ab6df717d1 | ||
|
|
0c8d9e5fec | ||
|
|
3f047fc406 | ||
|
|
82b5176783 | ||
|
|
17b183886f | ||
|
|
cd170e8184 | ||
|
|
297e9952b6 | ||
|
|
dca4f46274 | ||
|
|
5dee3ad037 | ||
|
|
079a7cfc71 | ||
|
|
3856407a86 | ||
|
|
db2e129ca0 | ||
|
|
1209b6ca5b | ||
|
|
a3125791c7 | ||
|
|
f1657a98cb | ||
|
|
b761428226 | ||
|
|
c1653e9efb | ||
|
|
84bbc54599 | ||
|
|
1e5d87beee | ||
|
|
22219f2d1f | ||
|
|
5a13fdd225 | ||
|
|
af5c1c553e | ||
|
|
3cea9ec2eb | ||
|
|
28469edd7d | ||
|
|
d5a398988b | ||
|
|
455a15e2dc | ||
|
|
460a1c08b9 | ||
|
|
4918522735 | ||
|
|
65662dffb1 | ||
|
|
5e51f4a8ad | ||
|
|
54bb39065c | ||
|
|
c5332d7fbb | ||
|
|
35cd4c4d88 | ||
|
|
67fb99f193 | ||
|
|
85553414ae | ||
|
|
d16df59db5 | ||
|
|
63c3ee4f63 | ||
|
|
182bda88e8 | ||
|
|
16aa9ea41d | ||
|
|
d6bc443bde | ||
|
|
046cab3915 | ||
|
|
7df07a3b55 | ||
|
|
2d49720f89 | ||
|
|
48416bc4a8 | ||
|
|
6a0546e313 | ||
|
|
dbcea0585f | ||
|
|
f7d4854131 | ||
|
|
403be2eefb | ||
|
|
63bac931c2 | ||
|
|
7c74a01584 | ||
|
|
1d3586d0d5 | ||
|
|
c533c89ce1 | ||
|
|
b8b3f4562a | ||
|
|
1c6f480160 | ||
|
|
f8580bf02f | ||
|
|
19afd9ea51 | ||
|
|
b72270d27e | ||
|
|
706dfe441b | ||
|
|
c4da5ff971 | ||
|
|
e26f9cc1e5 | ||
|
|
fa8fd95118 | ||
|
|
05b23b4156 | ||
|
|
8f028b5f40 | ||
|
|
013322a95e | ||
|
|
fb62afd6f0 | ||
|
|
50600e833d | ||
|
|
fc08bdd6ab | ||
|
|
2568d41f70 | ||
|
|
88f23a18e0 | ||
|
|
bb66c24797 | ||
|
|
2edb38e8ca | ||
|
|
af6793f804 | ||
|
|
b695e3f9bd | ||
|
|
6a5a30f9e2 | ||
|
|
d37707bda4 | ||
|
|
f40ee5e9a0 | ||
|
|
1f13021eca | ||
|
|
e612f66c7c | ||
|
|
87e8e8a7d0 | ||
|
|
e600a5c908 | ||
|
|
50ce204cc2 | ||
|
|
144a3588b4 |
8
.editorconfig
Normal file
8
.editorconfig
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[**.py]
|
||||||
|
charset = utf-8
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -2,3 +2,5 @@
|
|||||||
|
|
||||||
Makefile* text whitespace=-tab-in-indent
|
Makefile* text whitespace=-tab-in-indent
|
||||||
*.sh text eol=lf
|
*.sh text eol=lf
|
||||||
|
*.md diff=markdown
|
||||||
|
*.py diff=python
|
||||||
|
|||||||
8
.github/ISSUE_TEMPLATE/1_broken_site.yml
vendored
8
.github/ISSUE_TEMPLATE/1_broken_site.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Broken site support
|
name: Broken site
|
||||||
description: Report broken or misfunctioning site
|
description: Report broken or misfunctioning site
|
||||||
labels: [triage, site-bug]
|
labels: [triage, site-bug]
|
||||||
body:
|
body:
|
||||||
@@ -11,7 +11,7 @@ body:
|
|||||||
options:
|
options:
|
||||||
- label: I'm reporting a broken site
|
- label: I'm reporting a broken site
|
||||||
required: true
|
required: true
|
||||||
- label: I've verified that I'm running yt-dlp version **2021.12.27**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
|
- label: I've verified that I'm running yt-dlp version **2022.04.08** ([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 alive and playable in a browser
|
- label: I've checked that all provided URLs are alive and playable in a browser
|
||||||
required: true
|
required: true
|
||||||
@@ -51,12 +51,12 @@ body:
|
|||||||
[debug] Portable config file: yt-dlp.conf
|
[debug] Portable config file: yt-dlp.conf
|
||||||
[debug] Portable config: ['-i']
|
[debug] Portable config: ['-i']
|
||||||
[debug] Encodings: locale cp1252, fs utf-8, stdout utf-8, stderr utf-8, pref cp1252
|
[debug] Encodings: locale cp1252, fs utf-8, stdout utf-8, stderr utf-8, pref cp1252
|
||||||
[debug] yt-dlp version 2021.12.27 (exe)
|
[debug] yt-dlp version 2022.04.08 (exe)
|
||||||
[debug] Python version 3.8.8 (CPython 64bit) - Windows-10-10.0.19041-SP0
|
[debug] Python version 3.8.8 (CPython 64bit) - Windows-10-10.0.19041-SP0
|
||||||
[debug] exe versions: ffmpeg 3.0.1, ffprobe 3.0.1
|
[debug] exe versions: ffmpeg 3.0.1, ffprobe 3.0.1
|
||||||
[debug] Optional libraries: Cryptodome, keyring, mutagen, sqlite, websockets
|
[debug] Optional libraries: Cryptodome, keyring, mutagen, sqlite, websockets
|
||||||
[debug] Proxy map: {}
|
[debug] Proxy map: {}
|
||||||
yt-dlp is up to date (2021.12.27)
|
yt-dlp is up to date (2022.04.08)
|
||||||
<more lines>
|
<more lines>
|
||||||
render: shell
|
render: shell
|
||||||
validations:
|
validations:
|
||||||
|
|||||||
@@ -11,7 +11,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 **2021.12.27**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
|
- label: I've verified that I'm running yt-dlp version **2022.04.08** ([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 alive and playable in a browser
|
- label: I've checked that all provided URLs are alive and playable in a browser
|
||||||
required: true
|
required: true
|
||||||
@@ -62,12 +62,12 @@ body:
|
|||||||
[debug] Portable config file: yt-dlp.conf
|
[debug] Portable config file: yt-dlp.conf
|
||||||
[debug] Portable config: ['-i']
|
[debug] Portable config: ['-i']
|
||||||
[debug] Encodings: locale cp1252, fs utf-8, stdout utf-8, stderr utf-8, pref cp1252
|
[debug] Encodings: locale cp1252, fs utf-8, stdout utf-8, stderr utf-8, pref cp1252
|
||||||
[debug] yt-dlp version 2021.12.27 (exe)
|
[debug] yt-dlp version 2022.04.08 (exe)
|
||||||
[debug] Python version 3.8.8 (CPython 64bit) - Windows-10-10.0.19041-SP0
|
[debug] Python version 3.8.8 (CPython 64bit) - Windows-10-10.0.19041-SP0
|
||||||
[debug] exe versions: ffmpeg 3.0.1, ffprobe 3.0.1
|
[debug] exe versions: ffmpeg 3.0.1, ffprobe 3.0.1
|
||||||
[debug] Optional libraries: Cryptodome, keyring, mutagen, sqlite, websockets
|
[debug] Optional libraries: Cryptodome, keyring, mutagen, sqlite, websockets
|
||||||
[debug] Proxy map: {}
|
[debug] Proxy map: {}
|
||||||
yt-dlp is up to date (2021.12.27)
|
yt-dlp is up to date (2022.04.08)
|
||||||
<more lines>
|
<more lines>
|
||||||
render: shell
|
render: shell
|
||||||
validations:
|
validations:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ body:
|
|||||||
options:
|
options:
|
||||||
- label: I'm reporting a site feature request
|
- label: I'm reporting a site feature request
|
||||||
required: true
|
required: true
|
||||||
- label: I've verified that I'm running yt-dlp version **2021.12.27**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
|
- label: I've verified that I'm running yt-dlp version **2022.04.08** ([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 alive and playable in a browser
|
- label: I've checked that all provided URLs are alive and playable in a browser
|
||||||
required: true
|
required: true
|
||||||
@@ -32,7 +32,7 @@ body:
|
|||||||
label: Example URLs
|
label: Example URLs
|
||||||
description: |
|
description: |
|
||||||
Example URLs that can be used to demonstrate the requested feature
|
Example URLs that can be used to demonstrate the requested feature
|
||||||
value: |
|
placeholder: |
|
||||||
https://www.youtube.com/watch?v=BaW_jenozKc
|
https://www.youtube.com/watch?v=BaW_jenozKc
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
@@ -60,12 +60,12 @@ body:
|
|||||||
[debug] Portable config file: yt-dlp.conf
|
[debug] Portable config file: yt-dlp.conf
|
||||||
[debug] Portable config: ['-i']
|
[debug] Portable config: ['-i']
|
||||||
[debug] Encodings: locale cp1252, fs utf-8, stdout utf-8, stderr utf-8, pref cp1252
|
[debug] Encodings: locale cp1252, fs utf-8, stdout utf-8, stderr utf-8, pref cp1252
|
||||||
[debug] yt-dlp version 2021.12.27 (exe)
|
[debug] yt-dlp version 2022.04.08 (exe)
|
||||||
[debug] Python version 3.8.8 (CPython 64bit) - Windows-10-10.0.19041-SP0
|
[debug] Python version 3.8.8 (CPython 64bit) - Windows-10-10.0.19041-SP0
|
||||||
[debug] exe versions: ffmpeg 3.0.1, ffprobe 3.0.1
|
[debug] exe versions: ffmpeg 3.0.1, ffprobe 3.0.1
|
||||||
[debug] Optional libraries: Cryptodome, keyring, mutagen, sqlite, websockets
|
[debug] Optional libraries: Cryptodome, keyring, mutagen, sqlite, websockets
|
||||||
[debug] Proxy map: {}
|
[debug] Proxy map: {}
|
||||||
yt-dlp is up to date (2021.12.27)
|
yt-dlp is up to date (2022.04.08)
|
||||||
<more lines>
|
<more lines>
|
||||||
render: shell
|
render: shell
|
||||||
validations:
|
validations:
|
||||||
|
|||||||
6
.github/ISSUE_TEMPLATE/4_bug_report.yml
vendored
6
.github/ISSUE_TEMPLATE/4_bug_report.yml
vendored
@@ -11,7 +11,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 **2021.12.27**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
|
- label: I've verified that I'm running yt-dlp version **2022.04.08** ([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 alive and playable in a browser
|
- label: I've checked that all provided URLs are alive and playable in a browser
|
||||||
required: true
|
required: true
|
||||||
@@ -45,12 +45,12 @@ body:
|
|||||||
[debug] Portable config file: yt-dlp.conf
|
[debug] Portable config file: yt-dlp.conf
|
||||||
[debug] Portable config: ['-i']
|
[debug] Portable config: ['-i']
|
||||||
[debug] Encodings: locale cp1252, fs utf-8, stdout utf-8, stderr utf-8, pref cp1252
|
[debug] Encodings: locale cp1252, fs utf-8, stdout utf-8, stderr utf-8, pref cp1252
|
||||||
[debug] yt-dlp version 2021.12.27 (exe)
|
[debug] yt-dlp version 2022.04.08 (exe)
|
||||||
[debug] Python version 3.8.8 (CPython 64bit) - Windows-10-10.0.19041-SP0
|
[debug] Python version 3.8.8 (CPython 64bit) - Windows-10-10.0.19041-SP0
|
||||||
[debug] exe versions: ffmpeg 3.0.1, ffprobe 3.0.1
|
[debug] exe versions: ffmpeg 3.0.1, ffprobe 3.0.1
|
||||||
[debug] Optional libraries: Cryptodome, keyring, mutagen, sqlite, websockets
|
[debug] Optional libraries: Cryptodome, keyring, mutagen, sqlite, websockets
|
||||||
[debug] Proxy map: {}
|
[debug] Proxy map: {}
|
||||||
yt-dlp is up to date (2021.12.27)
|
yt-dlp is up to date (2022.04.08)
|
||||||
<more lines>
|
<more lines>
|
||||||
render: shell
|
render: shell
|
||||||
validations:
|
validations:
|
||||||
|
|||||||
25
.github/ISSUE_TEMPLATE/5_feature_request.yml
vendored
25
.github/ISSUE_TEMPLATE/5_feature_request.yml
vendored
@@ -11,7 +11,9 @@ body:
|
|||||||
options:
|
options:
|
||||||
- label: I'm reporting a feature request
|
- label: I'm reporting a feature request
|
||||||
required: true
|
required: true
|
||||||
- label: I've verified that I'm running yt-dlp version **2021.12.27**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
|
- label: I've looked through the [README](https://github.com/yt-dlp/yt-dlp#readme)
|
||||||
|
required: true
|
||||||
|
- label: I've verified that I'm running yt-dlp version **2022.04.08** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
|
||||||
required: true
|
required: true
|
||||||
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues including closed ones. DO NOT post duplicates
|
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues including closed ones. DO NOT post duplicates
|
||||||
required: true
|
required: true
|
||||||
@@ -28,3 +30,24 @@ body:
|
|||||||
placeholder: WRITE DESCRIPTION HERE
|
placeholder: WRITE DESCRIPTION HERE
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: log
|
||||||
|
attributes:
|
||||||
|
label: Verbose log
|
||||||
|
description: |
|
||||||
|
If your feature request involves an existing yt-dlp command, provide the complete verbose output of that command.
|
||||||
|
Add the `-vU` flag to **your** command line you run yt-dlp with (`yt-dlp -vU <your command line>`), copy the WHOLE output and insert it below.
|
||||||
|
It should look similar to this:
|
||||||
|
placeholder: |
|
||||||
|
[debug] Command-line config: ['-vU', 'http://www.youtube.com/watch?v=BaW_jenozKc']
|
||||||
|
[debug] Portable config file: yt-dlp.conf
|
||||||
|
[debug] Portable config: ['-i']
|
||||||
|
[debug] Encodings: locale cp1252, fs utf-8, stdout utf-8, stderr utf-8, pref cp1252
|
||||||
|
[debug] yt-dlp version 2021.12.01 (exe)
|
||||||
|
[debug] Python version 3.8.8 (CPython 64bit) - Windows-10-10.0.19041-SP0
|
||||||
|
[debug] exe versions: ffmpeg 3.0.1, ffprobe 3.0.1
|
||||||
|
[debug] Optional libraries: Cryptodome, keyring, mutagen, sqlite, websockets
|
||||||
|
[debug] Proxy map: {}
|
||||||
|
yt-dlp is up to date (2021.12.01)
|
||||||
|
<more lines>
|
||||||
|
render: shell
|
||||||
|
|||||||
5
.github/ISSUE_TEMPLATE/6_question.yml
vendored
5
.github/ISSUE_TEMPLATE/6_question.yml
vendored
@@ -25,7 +25,8 @@ body:
|
|||||||
Ask your question in an arbitrary form.
|
Ask your question in an arbitrary form.
|
||||||
Please make sure it's worded well enough to be understood, see [is-the-description-of-the-issue-itself-sufficient](https://github.com/ytdl-org/youtube-dl#is-the-description-of-the-issue-itself-sufficient).
|
Please make sure it's worded well enough to be understood, see [is-the-description-of-the-issue-itself-sufficient](https://github.com/ytdl-org/youtube-dl#is-the-description-of-the-issue-itself-sufficient).
|
||||||
Provide any additional information and as much context and examples as possible.
|
Provide any additional information and as much context and examples as possible.
|
||||||
If your question contains "isn't working" or "can you add", this is most likely the wrong template
|
If your question contains "isn't working" or "can you add", this is most likely the wrong template.
|
||||||
|
If you are in doubt if this is the right template, use another template!
|
||||||
placeholder: WRITE QUESTION HERE
|
placeholder: WRITE QUESTION HERE
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
@@ -34,7 +35,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Verbose log
|
label: Verbose log
|
||||||
description: |
|
description: |
|
||||||
If your question involes a yt-dlp command, provide the complete verbose output of that command.
|
If your question involves a yt-dlp command, provide the complete verbose output of that command.
|
||||||
Add the `-vU` flag to **your** command line you run yt-dlp with (`yt-dlp -vU <your command line>`), copy the WHOLE output and insert it below.
|
Add the `-vU` flag to **your** command line you run yt-dlp with (`yt-dlp -vU <your command line>`), copy the WHOLE output and insert it below.
|
||||||
It should look similar to this:
|
It should look similar to this:
|
||||||
placeholder: |
|
placeholder: |
|
||||||
|
|||||||
3
.github/ISSUE_TEMPLATE/config.yml
vendored
3
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -3,3 +3,6 @@ contact_links:
|
|||||||
- name: Get help from the community on Discord
|
- name: Get help from the community on Discord
|
||||||
url: https://discord.gg/H5MNcFW63r
|
url: https://discord.gg/H5MNcFW63r
|
||||||
about: Join the yt-dlp Discord for community-powered support!
|
about: Join the yt-dlp Discord for community-powered support!
|
||||||
|
- name: Matrix Bridge to the Discord server
|
||||||
|
url: https://matrix.to/#/#yt-dlp:matrix.org
|
||||||
|
about: For those who do not want to use Discord
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Broken site support
|
name: Broken site
|
||||||
description: Report broken or misfunctioning site
|
description: Report broken or misfunctioning site
|
||||||
labels: [triage, site-bug]
|
labels: [triage, site-bug]
|
||||||
body:
|
body:
|
||||||
@@ -11,7 +11,7 @@ body:
|
|||||||
options:
|
options:
|
||||||
- label: I'm reporting a broken site
|
- label: I'm reporting a broken site
|
||||||
required: true
|
required: true
|
||||||
- label: I've verified that I'm running yt-dlp version **%(version)s**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
|
- label: I've verified that I'm running yt-dlp version **%(version)s** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
|
||||||
required: true
|
required: true
|
||||||
- label: I've checked that all provided URLs are alive and playable in a browser
|
- label: I've checked that all provided URLs are alive and playable in a browser
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -11,7 +11,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 **%(version)s**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
|
- label: I've verified that I'm running yt-dlp version **%(version)s** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
|
||||||
required: true
|
required: true
|
||||||
- label: I've checked that all provided URLs are alive and playable in a browser
|
- label: I've checked that all provided URLs are alive and playable in a browser
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ body:
|
|||||||
options:
|
options:
|
||||||
- label: I'm reporting a site feature request
|
- label: I'm reporting a site feature request
|
||||||
required: true
|
required: true
|
||||||
- label: I've verified that I'm running yt-dlp version **%(version)s**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
|
- label: I've verified that I'm running yt-dlp version **%(version)s** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
|
||||||
required: true
|
required: true
|
||||||
- label: I've checked that all provided URLs are alive and playable in a browser
|
- label: I've checked that all provided URLs are alive and playable in a browser
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE_tmpl/4_bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE_tmpl/4_bug_report.yml
vendored
@@ -11,7 +11,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 **%(version)s**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
|
- label: I've verified that I'm running yt-dlp version **%(version)s** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
|
||||||
required: true
|
required: true
|
||||||
- label: I've checked that all provided URLs are alive and playable in a browser
|
- label: I've checked that all provided URLs are alive and playable in a browser
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ body:
|
|||||||
options:
|
options:
|
||||||
- label: I'm reporting a feature request
|
- label: I'm reporting a feature request
|
||||||
required: true
|
required: true
|
||||||
- label: I've verified that I'm running yt-dlp version **%(version)s**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
|
- label: I've looked through the [README](https://github.com/yt-dlp/yt-dlp#readme)
|
||||||
|
required: true
|
||||||
|
- label: I've verified that I'm running yt-dlp version **%(version)s** ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) or later (specify commit)
|
||||||
required: true
|
required: true
|
||||||
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues including closed ones. DO NOT post duplicates
|
- label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues including closed ones. DO NOT post duplicates
|
||||||
required: true
|
required: true
|
||||||
@@ -28,3 +30,24 @@ body:
|
|||||||
placeholder: WRITE DESCRIPTION HERE
|
placeholder: WRITE DESCRIPTION HERE
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: log
|
||||||
|
attributes:
|
||||||
|
label: Verbose log
|
||||||
|
description: |
|
||||||
|
If your feature request involves an existing yt-dlp command, provide the complete verbose output of that command.
|
||||||
|
Add the `-vU` flag to **your** command line you run yt-dlp with (`yt-dlp -vU <your command line>`), copy the WHOLE output and insert it below.
|
||||||
|
It should look similar to this:
|
||||||
|
placeholder: |
|
||||||
|
[debug] Command-line config: ['-vU', 'http://www.youtube.com/watch?v=BaW_jenozKc']
|
||||||
|
[debug] Portable config file: yt-dlp.conf
|
||||||
|
[debug] Portable config: ['-i']
|
||||||
|
[debug] Encodings: locale cp1252, fs utf-8, stdout utf-8, stderr utf-8, pref cp1252
|
||||||
|
[debug] yt-dlp version 2021.12.01 (exe)
|
||||||
|
[debug] Python version 3.8.8 (CPython 64bit) - Windows-10-10.0.19041-SP0
|
||||||
|
[debug] exe versions: ffmpeg 3.0.1, ffprobe 3.0.1
|
||||||
|
[debug] Optional libraries: Cryptodome, keyring, mutagen, sqlite, websockets
|
||||||
|
[debug] Proxy map: {}
|
||||||
|
yt-dlp is up to date (2021.12.01)
|
||||||
|
<more lines>
|
||||||
|
render: shell
|
||||||
|
|||||||
5
.github/ISSUE_TEMPLATE_tmpl/6_question.yml
vendored
5
.github/ISSUE_TEMPLATE_tmpl/6_question.yml
vendored
@@ -25,7 +25,8 @@ body:
|
|||||||
Ask your question in an arbitrary form.
|
Ask your question in an arbitrary form.
|
||||||
Please make sure it's worded well enough to be understood, see [is-the-description-of-the-issue-itself-sufficient](https://github.com/ytdl-org/youtube-dl#is-the-description-of-the-issue-itself-sufficient).
|
Please make sure it's worded well enough to be understood, see [is-the-description-of-the-issue-itself-sufficient](https://github.com/ytdl-org/youtube-dl#is-the-description-of-the-issue-itself-sufficient).
|
||||||
Provide any additional information and as much context and examples as possible.
|
Provide any additional information and as much context and examples as possible.
|
||||||
If your question contains "isn't working" or "can you add", this is most likely the wrong template
|
If your question contains "isn't working" or "can you add", this is most likely the wrong template.
|
||||||
|
If you are in doubt if this is the right template, use another template!
|
||||||
placeholder: WRITE QUESTION HERE
|
placeholder: WRITE QUESTION HERE
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
@@ -34,7 +35,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Verbose log
|
label: Verbose log
|
||||||
description: |
|
description: |
|
||||||
If your question involes a yt-dlp command, provide the complete verbose output of that command.
|
If your question involves a yt-dlp command, provide the complete verbose output of that command.
|
||||||
Add the `-vU` flag to **your** command line you run yt-dlp with (`yt-dlp -vU <your command line>`), copy the WHOLE output and insert it below.
|
Add the `-vU` flag to **your** command line you run yt-dlp with (`yt-dlp -vU <your command line>`), copy the WHOLE output and insert it below.
|
||||||
It should look similar to this:
|
It should look similar to this:
|
||||||
placeholder: |
|
placeholder: |
|
||||||
|
|||||||
7
.github/workflows/build.yml
vendored
7
.github/workflows/build.yml
vendored
@@ -161,11 +161,10 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
# In order to create a universal2 application, the version of python3 in /usr/bin has to be used
|
# In order to create a universal2 application, the version of python3 in /usr/bin has to be used
|
||||||
# Pyinstaller is pinned to 4.5.1 because the builds are failing in 4.6, 4.7
|
|
||||||
- name: Install Requirements
|
- name: Install Requirements
|
||||||
run: |
|
run: |
|
||||||
brew install coreutils
|
brew install coreutils
|
||||||
/usr/bin/python3 -m pip install -U --user pip Pyinstaller==4.5.1 -r requirements.txt
|
/usr/bin/python3 -m pip install -U --user pip Pyinstaller==4.10 -r requirements.txt
|
||||||
- name: Bump version
|
- name: Bump version
|
||||||
id: bump_version
|
id: bump_version
|
||||||
run: /usr/bin/python3 devscripts/update-version.py
|
run: /usr/bin/python3 devscripts/update-version.py
|
||||||
@@ -234,7 +233,7 @@ jobs:
|
|||||||
# Custom pyinstaller built with https://github.com/yt-dlp/pyinstaller-builds
|
# Custom pyinstaller built with https://github.com/yt-dlp/pyinstaller-builds
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip setuptools wheel py2exe
|
python -m pip install --upgrade pip setuptools wheel py2exe
|
||||||
pip install "https://yt-dlp.github.io/Pyinstaller-Builds/x86_64/pyinstaller-4.5.1-py3-none-any.whl" -r requirements.txt
|
pip install "https://yt-dlp.github.io/Pyinstaller-Builds/x86_64/pyinstaller-4.10-py3-none-any.whl" -r requirements.txt
|
||||||
- name: Bump version
|
- name: Bump version
|
||||||
id: bump_version
|
id: bump_version
|
||||||
env:
|
env:
|
||||||
@@ -321,7 +320,7 @@ jobs:
|
|||||||
- name: Install Requirements
|
- name: Install Requirements
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip setuptools wheel
|
python -m pip install --upgrade pip setuptools wheel
|
||||||
pip install "https://yt-dlp.github.io/Pyinstaller-Builds/i686/pyinstaller-4.5.1-py3-none-any.whl" -r requirements.txt
|
pip install "https://yt-dlp.github.io/Pyinstaller-Builds/i686/pyinstaller-4.10-py3-none-any.whl" -r requirements.txt
|
||||||
- name: Bump version
|
- name: Bump version
|
||||||
id: bump_version
|
id: bump_version
|
||||||
env:
|
env:
|
||||||
|
|||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -24,6 +24,7 @@ cookies
|
|||||||
|
|
||||||
*.3gp
|
*.3gp
|
||||||
*.ape
|
*.ape
|
||||||
|
*.ass
|
||||||
*.avi
|
*.avi
|
||||||
*.desktop
|
*.desktop
|
||||||
*.flac
|
*.flac
|
||||||
@@ -92,7 +93,7 @@ README.txt
|
|||||||
*.tar.gz
|
*.tar.gz
|
||||||
*.zsh
|
*.zsh
|
||||||
*.spec
|
*.spec
|
||||||
test/testdata/player-*.js
|
test/testdata/sigs/player-*.js
|
||||||
|
|
||||||
# Binary
|
# Binary
|
||||||
/youtube-dl
|
/youtube-dl
|
||||||
@@ -106,6 +107,7 @@ yt-dlp.zip
|
|||||||
*.iml
|
*.iml
|
||||||
.vscode
|
.vscode
|
||||||
*.sublime-*
|
*.sublime-*
|
||||||
|
*.code-workspace
|
||||||
|
|
||||||
# Lazy extractors
|
# Lazy extractors
|
||||||
*/extractor/lazy_extractors.py
|
*/extractor/lazy_extractors.py
|
||||||
@@ -114,3 +116,6 @@ yt-dlp.zip
|
|||||||
ytdlp_plugins/extractor/*
|
ytdlp_plugins/extractor/*
|
||||||
!ytdlp_plugins/extractor/__init__.py
|
!ytdlp_plugins/extractor/__init__.py
|
||||||
!ytdlp_plugins/extractor/sample.py
|
!ytdlp_plugins/extractor/sample.py
|
||||||
|
ytdlp_plugins/postprocessor/*
|
||||||
|
!ytdlp_plugins/postprocessor/__init__.py
|
||||||
|
!ytdlp_plugins/postprocessor/sample.py
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
# .readthedocs.yaml
|
|
||||||
# Read the Docs configuration file
|
|
||||||
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
|
||||||
|
|
||||||
# Required
|
|
||||||
version: 2
|
|
||||||
|
|
||||||
# Build documentation in the docs/ directory with Sphinx
|
|
||||||
sphinx:
|
|
||||||
configuration: docs/conf.py
|
|
||||||
|
|
||||||
# Optionally build your docs in additional formats such as PDF
|
|
||||||
formats:
|
|
||||||
- epub
|
|
||||||
- pdf
|
|
||||||
- htmlzip
|
|
||||||
|
|
||||||
# Optionally set the version of Python and requirements required to build your docs
|
|
||||||
python:
|
|
||||||
version: 3
|
|
||||||
install:
|
|
||||||
- requirements: docs/requirements.txt
|
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
- [Is anyone going to need the feature?](#is-anyone-going-to-need-the-feature)
|
- [Is anyone going to need the feature?](#is-anyone-going-to-need-the-feature)
|
||||||
- [Is your question about yt-dlp?](#is-your-question-about-yt-dlp)
|
- [Is your question about yt-dlp?](#is-your-question-about-yt-dlp)
|
||||||
- [Are you willing to share account details if needed?](#are-you-willing-to-share-account-details-if-needed)
|
- [Are you willing to share account details if needed?](#are-you-willing-to-share-account-details-if-needed)
|
||||||
|
- [Is the website primarily used for piracy](#is-the-website-primarily-used-for-piracy)
|
||||||
- [DEVELOPER INSTRUCTIONS](#developer-instructions)
|
- [DEVELOPER INSTRUCTIONS](#developer-instructions)
|
||||||
- [Adding new feature or making overarching changes](#adding-new-feature-or-making-overarching-changes)
|
- [Adding new feature or making overarching changes](#adding-new-feature-or-making-overarching-changes)
|
||||||
- [Adding support for a new site](#adding-support-for-a-new-site)
|
- [Adding support for a new site](#adding-support-for-a-new-site)
|
||||||
@@ -24,6 +25,7 @@
|
|||||||
- [Collapse fallbacks](#collapse-fallbacks)
|
- [Collapse fallbacks](#collapse-fallbacks)
|
||||||
- [Trailing parentheses](#trailing-parentheses)
|
- [Trailing parentheses](#trailing-parentheses)
|
||||||
- [Use convenience conversion and parsing functions](#use-convenience-conversion-and-parsing-functions)
|
- [Use convenience conversion and parsing functions](#use-convenience-conversion-and-parsing-functions)
|
||||||
|
- [My pull request is labeled pending-fixes](#my-pull-request-is-labeled-pending-fixes)
|
||||||
- [EMBEDDING YT-DLP](README.md#embedding-yt-dlp)
|
- [EMBEDDING YT-DLP](README.md#embedding-yt-dlp)
|
||||||
|
|
||||||
|
|
||||||
@@ -113,7 +115,7 @@ If the issue is with `youtube-dl` (the upstream fork of yt-dlp) and not with yt-
|
|||||||
|
|
||||||
### Are you willing to share account details if needed?
|
### Are you willing to share account details if needed?
|
||||||
|
|
||||||
The maintainers and potential contributors of the project often do not have an account for the website you are asking support for. So any developer interested in solving your issue may ask you for account details. It is your personal discression whether you are willing to share the account in order for the developer to try and solve your issue. However, if you are unwilling or unable to provide details, they obviously cannot work on the issue and it cannot be solved unless some developer who both has an account and is willing/able to contribute decides to solve it.
|
The maintainers and potential contributors of the project often do not have an account for the website you are asking support for. So any developer interested in solving your issue may ask you for account details. It is your personal discretion whether you are willing to share the account in order for the developer to try and solve your issue. However, if you are unwilling or unable to provide details, they obviously cannot work on the issue and it cannot be solved unless some developer who both has an account and is willing/able to contribute decides to solve it.
|
||||||
|
|
||||||
By sharing an account with anyone, you agree to bear all risks associated with it. The maintainers and yt-dlp can't be held responsible for any misuse of the credentials.
|
By sharing an account with anyone, you agree to bear all risks associated with it. The maintainers and yt-dlp can't be held responsible for any misuse of the credentials.
|
||||||
|
|
||||||
@@ -123,6 +125,10 @@ While these steps won't necessarily ensure that no misuse of the account takes p
|
|||||||
- Change the password before sharing the account to something random (use [this](https://passwordsgenerator.net/) if you don't have a random password generator).
|
- Change the password before sharing the account to something random (use [this](https://passwordsgenerator.net/) if you don't have a random password generator).
|
||||||
- Change the password after receiving the account back.
|
- Change the password after receiving the account back.
|
||||||
|
|
||||||
|
### Is the website primarily used for piracy?
|
||||||
|
|
||||||
|
We follow [youtube-dl's policy](https://github.com/ytdl-org/youtube-dl#can-you-add-support-for-this-anime-video-site-or-site-which-shows-current-movies-for-free) to not support services that is primarily used for infringing copyright. Additionally, it has been decided to not to support porn sites that specialize in deep fake. We also cannot support any service that serves only [DRM protected content](https://en.wikipedia.org/wiki/Digital_rights_management).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -210,7 +216,7 @@ After you have ensured this site is distributing its content legally, you can fo
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
1. Add an import in [`yt_dlp/extractor/extractors.py`](yt_dlp/extractor/extractors.py).
|
1. Add an import in [`yt_dlp/extractor/extractors.py`](yt_dlp/extractor/extractors.py).
|
||||||
1. Run `python test/test_download.py TestDownload.test_YourExtractor`. This *should fail* at first, but you can continually re-run it until you're done. If you decide to add more than one test, the tests will then be named `TestDownload.test_YourExtractor`, `TestDownload.test_YourExtractor_1`, `TestDownload.test_YourExtractor_2`, etc. Note that tests with `only_matching` key in test's dict are not counted in. You can also run all the tests in one go with `TestDownload.test_YourExtractor_all`
|
1. Run `python test/test_download.py TestDownload.test_YourExtractor` (note that `YourExtractor` doesn't end with `IE`). This *should fail* at first, but you can continually re-run it until you're done. If you decide to add more than one test, the tests will then be named `TestDownload.test_YourExtractor`, `TestDownload.test_YourExtractor_1`, `TestDownload.test_YourExtractor_2`, etc. Note that tests with `only_matching` key in test's dict are not counted in. You can also run all the tests in one go with `TestDownload.test_YourExtractor_all`
|
||||||
1. Make sure you have atleast one test for your extractor. Even if all videos covered by the extractor are expected to be inaccessible for automated testing, tests should still be added with a `skip` parameter indicating why the particular test is disabled from running.
|
1. Make sure you have atleast one test for your extractor. Even if all videos covered by the extractor are expected to be inaccessible for automated testing, tests should still be added with a `skip` parameter indicating why the particular test is disabled from running.
|
||||||
1. Have a look at [`yt_dlp/extractor/common.py`](yt_dlp/extractor/common.py) for possible helper methods and a [detailed description of what your extractor should and may return](yt_dlp/extractor/common.py#L91-L426). Add tests and code for as many as you want.
|
1. Have a look at [`yt_dlp/extractor/common.py`](yt_dlp/extractor/common.py) for possible helper methods and a [detailed description of what your extractor should and may return](yt_dlp/extractor/common.py#L91-L426). Add tests and code for as many as you want.
|
||||||
1. Make sure your code follows [yt-dlp coding conventions](#yt-dlp-coding-conventions) and check the code with [flake8](https://flake8.pycqa.org/en/latest/index.html#quickstart):
|
1. Make sure your code follows [yt-dlp coding conventions](#yt-dlp-coding-conventions) and check the code with [flake8](https://flake8.pycqa.org/en/latest/index.html#quickstart):
|
||||||
@@ -252,7 +258,11 @@ For extraction to work yt-dlp relies on metadata your extractor extracts and pro
|
|||||||
- `title` (media title)
|
- `title` (media title)
|
||||||
- `url` (media download URL) or `formats`
|
- `url` (media download URL) or `formats`
|
||||||
|
|
||||||
The aforementioned metafields are the critical data that the extraction does not make any sense without and if any of them fail to be extracted then the extractor is considered completely broken. While, in fact, only `id` is technically mandatory, due to compatibility reasons, yt-dlp also treats `title` as mandatory. The extractor is allowed to return the info dict without url or formats in some special cases if it allows the user to extract usefull information with `--ignore-no-formats-error` - Eg: when the video is a live stream that has not started yet.
|
The aforementioned metafields are the critical data that the extraction does not make any sense without and if any of them fail to be extracted then the extractor is considered completely broken. While all extractors must return a `title`, they must also allow it's extraction to be non-fatal.
|
||||||
|
|
||||||
|
For pornographic sites, appropriate `age_limit` must also be returned.
|
||||||
|
|
||||||
|
The extractor is allowed to return the info dict without url or formats in some special cases if it allows the user to extract usefull information with `--ignore-no-formats-error` - Eg: when the video is a live stream that has not started yet.
|
||||||
|
|
||||||
[Any field](yt_dlp/extractor/common.py#219-L426) apart from the aforementioned ones are considered **optional**. That means that extraction should be **tolerant** to situations when sources for these fields can potentially be unavailable (even if they are always available at the moment) and **future-proof** in order not to break the extraction of general purpose mandatory fields.
|
[Any field](yt_dlp/extractor/common.py#219-L426) apart from the aforementioned ones are considered **optional**. That means that extraction should be **tolerant** to situations when sources for these fields can potentially be unavailable (even if they are always available at the moment) and **future-proof** in order not to break the extraction of general purpose mandatory fields.
|
||||||
|
|
||||||
@@ -524,13 +534,13 @@ Extracting variables is acceptable for reducing code duplication and improving r
|
|||||||
Correct:
|
Correct:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
title = self._html_search_regex(r'<title>([^<]+)</title>', webpage, 'title')
|
title = self._html_search_regex(r'<h1>([^<]+)</h1>', webpage, 'title')
|
||||||
```
|
```
|
||||||
|
|
||||||
Incorrect:
|
Incorrect:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
TITLE_RE = r'<title>([^<]+)</title>'
|
TITLE_RE = r'<h1>([^<]+)</h1>'
|
||||||
# ...some lines of code...
|
# ...some lines of code...
|
||||||
title = self._html_search_regex(TITLE_RE, webpage, 'title')
|
title = self._html_search_regex(TITLE_RE, webpage, 'title')
|
||||||
```
|
```
|
||||||
@@ -633,7 +643,7 @@ Wrap all extracted numeric data into safe functions from [`yt_dlp/utils.py`](yt_
|
|||||||
|
|
||||||
Use `url_or_none` for safe URL processing.
|
Use `url_or_none` for safe URL processing.
|
||||||
|
|
||||||
Use `try_get`, `dict_get` and `traverse_obj` for safe metadata extraction from parsed JSON.
|
Use `traverse_obj` and `try_call` (superseeds `dict_get` and `try_get`) for safe metadata extraction from parsed JSON.
|
||||||
|
|
||||||
Use `unified_strdate` for uniform `upload_date` or any `YYYYMMDD` meta field extraction, `unified_timestamp` for uniform `timestamp` extraction, `parse_filesize` for `filesize` extraction, `parse_count` for count meta fields extraction, `parse_resolution`, `parse_duration` for `duration` extraction, `parse_age_limit` for `age_limit` extraction.
|
Use `unified_strdate` for uniform `upload_date` or any `YYYYMMDD` meta field extraction, `unified_timestamp` for uniform `timestamp` extraction, `parse_filesize` for `filesize` extraction, `parse_count` for count meta fields extraction, `parse_resolution`, `parse_duration` for `duration` extraction, `parse_age_limit` for `age_limit` extraction.
|
||||||
|
|
||||||
@@ -654,6 +664,10 @@ duration = float_or_none(video.get('durationMs'), scale=1000)
|
|||||||
view_count = int_or_none(video.get('views'))
|
view_count = int_or_none(video.get('views'))
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# My pull request is labeled pending-fixes
|
||||||
|
|
||||||
|
The `pending-fixes` label is added when there are changes requested to a PR. When the necessary changes are made, the label should be removed. However, despite our best efforts, it may sometimes happen that the maintainer did not see the changes or forgot to remove the label. If your PR is still marked as `pending-fixes` a few days after all requested changes have been made, feel free to ping the maintainer who labeled your issue and ask them to re-review and remove the label.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
57
CONTRIBUTORS
57
CONTRIBUTORS
@@ -146,7 +146,7 @@ chio0hai
|
|||||||
cntrl-s
|
cntrl-s
|
||||||
Deer-Spangle
|
Deer-Spangle
|
||||||
DEvmIb
|
DEvmIb
|
||||||
Grabien
|
Grabien/MaximVol
|
||||||
j54vc1bk
|
j54vc1bk
|
||||||
mpeter50
|
mpeter50
|
||||||
mrpapersonic
|
mrpapersonic
|
||||||
@@ -160,7 +160,7 @@ PilzAdam
|
|||||||
zmousm
|
zmousm
|
||||||
iw0nderhow
|
iw0nderhow
|
||||||
unit193
|
unit193
|
||||||
TwoThousandHedgehogs
|
TwoThousandHedgehogs/KathrynElrod
|
||||||
Jertzukka
|
Jertzukka
|
||||||
cypheron
|
cypheron
|
||||||
Hyeeji
|
Hyeeji
|
||||||
@@ -178,3 +178,56 @@ jaller94
|
|||||||
r5d
|
r5d
|
||||||
julien-hadleyjack
|
julien-hadleyjack
|
||||||
git-anony-mouse
|
git-anony-mouse
|
||||||
|
mdawar
|
||||||
|
trassshhub
|
||||||
|
foghawk
|
||||||
|
k3ns1n
|
||||||
|
teridon
|
||||||
|
mozlima
|
||||||
|
timendum
|
||||||
|
ischmidt20
|
||||||
|
CreaValix
|
||||||
|
sian1468
|
||||||
|
arkamar
|
||||||
|
hyano
|
||||||
|
KiberInfinity
|
||||||
|
tejing1
|
||||||
|
Bricio
|
||||||
|
lazypete365
|
||||||
|
Aniruddh-J
|
||||||
|
blackgear
|
||||||
|
CplPwnies
|
||||||
|
cyberfox1691
|
||||||
|
FestplattenSchnitzel
|
||||||
|
hatienl0i261299
|
||||||
|
iphoting
|
||||||
|
jakeogh
|
||||||
|
lukasfink1
|
||||||
|
lyz-code
|
||||||
|
marieell
|
||||||
|
mdpauley
|
||||||
|
Mipsters
|
||||||
|
mxmehl
|
||||||
|
ofkz
|
||||||
|
P-reducible
|
||||||
|
pycabbage
|
||||||
|
regarten
|
||||||
|
Ronnnny
|
||||||
|
schn0sch
|
||||||
|
s0u1h
|
||||||
|
MrRawes
|
||||||
|
cffswb
|
||||||
|
danielyli
|
||||||
|
1-Byte
|
||||||
|
mehq
|
||||||
|
dzek69
|
||||||
|
aaearon
|
||||||
|
panatexxa
|
||||||
|
kmark
|
||||||
|
un-def
|
||||||
|
goggle
|
||||||
|
Soebb
|
||||||
|
Fam0r
|
||||||
|
bohwaz
|
||||||
|
dodrian
|
||||||
|
vvto33
|
||||||
|
|||||||
458
Changelog.md
458
Changelog.md
@@ -11,6 +11,464 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
|
|
||||||
|
### 2022.04.08
|
||||||
|
|
||||||
|
* Use certificates from `certifi` if installed by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
* Treat multiple `--match-filters` as OR
|
||||||
|
* File locking improvevemnts:
|
||||||
|
* Do not lock downloading file on Windows
|
||||||
|
* Do not prevent download if locking is unsupported
|
||||||
|
* Do not truncate files before locking by [jakeogh](https://github.com/jakeogh), [pukkandan](https://github.com/pukkandan)
|
||||||
|
* Fix non-blocking non-exclusive lock
|
||||||
|
* De-prioritize automatic-subtitles when no `--sub-lang` is given
|
||||||
|
* Exit after `--dump-user-agent`
|
||||||
|
* Fallback to video-only format when selecting by extension
|
||||||
|
* Fix `--abort-on-error` for subtitles
|
||||||
|
* Fix `--no-overwrite` for playlist infojson
|
||||||
|
* Fix `--print` with `--ignore-no-formats` when url is `None` by [flashdagger](https://github.com/flashdagger)
|
||||||
|
* Fix `--sleep-interval`
|
||||||
|
* Fix `--throttled-rate`
|
||||||
|
* Fix `autonumber`
|
||||||
|
* Fix case of `http_headers`
|
||||||
|
* Fix filepath sanitization in `--print-to-file`
|
||||||
|
* Handle float in `--wait-for-video`
|
||||||
|
* Ignore `mhtml` formats from `-f mergeall`
|
||||||
|
* Ignore format-specific fields in initial pass of `--match-filter`
|
||||||
|
* Protect stdout from unexpected progress and console-title
|
||||||
|
* Remove `Accept-Encoding` header from `std_headers` by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
* Remove incorrect warning for `--dateafter`
|
||||||
|
* Show warning when all media formats have DRM
|
||||||
|
* [downloader] Fix invocation of `HttpieFD`
|
||||||
|
* [http] Fix #3215
|
||||||
|
* [http] Reject broken range before request by [Lesmiscore](https://github.com/Lesmiscore), [Jules-A](https://github.com/Jules-A), [pukkandan](https://github.com/pukkandan)
|
||||||
|
* [fragment] Read downloaded fragments only when needed by [Lesmiscore](https://github.com/Lesmiscore)
|
||||||
|
* [http] Retry on more errors by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
* [mhtml] Fix fragments with absolute urls by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
* [extractor] Add `_perform_login` function
|
||||||
|
* [extractor] Allow control characters inside json
|
||||||
|
* [extractor] Support merging subtitles with data by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
* [generic] Extract subtitles from video.js by [Lesmiscore](https://github.com/Lesmiscore)
|
||||||
|
* [ffmpeg] Cache version data
|
||||||
|
* [FFmpegConcat] Ensure final directory exists
|
||||||
|
* [FfmpegMetadata] Write id3v1 tags
|
||||||
|
* [FFmpegVideoConvertor] Add more formats to `--remux-video`
|
||||||
|
* [FFmpegVideoConvertor] Ensure all streams are copied
|
||||||
|
* [MetadataParser] Validate outtmpl early
|
||||||
|
* [outtmpl] Fix replacement/default when used with alternate
|
||||||
|
* [outtmpl] Limit changes during sanitization
|
||||||
|
* [phantomjs] Fix bug
|
||||||
|
* [test] Add `test_locked_file`
|
||||||
|
* [utils] `format_decimal_suffix`: Fix for very large numbers by [s0u1h](https://github.com/s0u1h)
|
||||||
|
* [utils] `traverse_obj`: Allow filtering by value
|
||||||
|
* [utils] Add `filter_dict`, `get_first`, `try_call`
|
||||||
|
* [utils] ExtractorError: Fix for older python versions
|
||||||
|
* [utils] WebSocketsWrapper: Allow omitting `__enter__` invocation by [Lesmiscore](https://github.com/Lesmiscore)
|
||||||
|
* [docs] Add an `.editorconfig` file by [fstirlitz](https://github.com/fstirlitz)
|
||||||
|
* [docs] Clarify the exact `BSD` license of dependencies by [MrRawes](https://github.com/MrRawes)
|
||||||
|
* [docs] Minor improvements by [pukkandan](https://github.com/pukkandan), [cffswb](https://github.com/cffswb), [danielyli](https://github.com/danielyli)
|
||||||
|
* [docs] Remove readthedocs
|
||||||
|
* [build] Add `requirements.txt` to pip distributions
|
||||||
|
* [cleanup, postprocessor] Create `_download_json`
|
||||||
|
* [cleanup, vimeo] Fix tests
|
||||||
|
* [cleanup] Misc fixes and minor cleanup
|
||||||
|
* [cleanup] Use `_html_extract_title`
|
||||||
|
* [AfreecaTV] Add `AfreecaTVUserIE` by [hatienl0i261299](https://github.com/hatienl0i261299)
|
||||||
|
* [arte] Add `format_note` to m3u8 formats
|
||||||
|
* [azmedien] Add TVO Online to supported hosts by [1-Byte](https://github.com/1-Byte)
|
||||||
|
* [BanBye] Add extractor by [mehq](https://github.com/mehq)
|
||||||
|
* [bilibili] Fix extraction of title with quotes by [dzek69](https://github.com/dzek69)
|
||||||
|
* [Craftsy] Add extractor by [Bricio](https://github.com/Bricio)
|
||||||
|
* [Cybrary] Add extractor by [aaearon](https://github.com/aaearon)
|
||||||
|
* [Huya] Add extractor by [hatienl0i261299](https://github.com/hatienl0i261299)
|
||||||
|
* [ITProTV] Add extractor by [aaearon](https://github.com/aaearon)
|
||||||
|
* [Jable] Add extractors by [mehq](https://github.com/mehq)
|
||||||
|
* [LastFM] Add extractors by [mehq](https://github.com/mehq)
|
||||||
|
* [Moviepilot] Add extractor by [panatexxa](https://github.com/panatexxa)
|
||||||
|
* [panopto] Add extractors by [coletdjnz](https://github.com/coletdjnz), [kmark](https://github.com/kmark)
|
||||||
|
* [PokemonSoundLibrary] Add extractor by [Lesmiscore](https://github.com/Lesmiscore)
|
||||||
|
* [WasdTV] Add extractor by [un-def](https://github.com/un-def), [hatienl0i261299](https://github.com/hatienl0i261299)
|
||||||
|
* [adobepass] Fix Suddenlink MSO by [CplPwnies](https://github.com/CplPwnies)
|
||||||
|
* [afreecatv] Match new vod url by [wlritchi](https://github.com/wlritchi)
|
||||||
|
* [AZMedien] Support `tv.telezueri.ch` by [goggle](https://github.com/goggle)
|
||||||
|
* [BiliIntl] Support user-generated videos by [wlritchi](https://github.com/wlritchi)
|
||||||
|
* [BRMediathek] Fix VALID_URL
|
||||||
|
* [crunchyroll:playlist] Implement beta API by [tejing1](https://github.com/tejing1)
|
||||||
|
* [crunchyroll] Fix inheritance
|
||||||
|
* [daftsex] Fix extractor by [Soebb](https://github.com/Soebb)
|
||||||
|
* [dailymotion] Support `geo.dailymotion.com` by [hatienl0i261299](https://github.com/hatienl0i261299)
|
||||||
|
* [ellentube] Extract subtitles from manifest
|
||||||
|
* [elonet] Rewrite extractor by [Fam0r](https://github.com/Fam0r), [pukkandan](https://github.com/pukkandan)
|
||||||
|
* [fptplay] Fix metadata extraction by [hatienl0i261299](https://github.com/hatienl0i261299)
|
||||||
|
* [FranceCulture] Support playlists by [bohwaz](https://github.com/bohwaz)
|
||||||
|
* [go, viu] Extract subtitles from the m3u8 manifest by [fstirlitz](https://github.com/fstirlitz)
|
||||||
|
* [Imdb] Improve extractor by [hatienl0i261299](https://github.com/hatienl0i261299)
|
||||||
|
* [MangoTV] Improve extractor by [hatienl0i261299](https://github.com/hatienl0i261299)
|
||||||
|
* [Nebula] Fix bug in 52efa4b31200119adaa8acf33e50b84fcb6948f0
|
||||||
|
* [niconico] Fix extraction of thumbnails and uploader (#3266)
|
||||||
|
* [niconico] Rewrite NiconicoIE by [Lesmiscore](https://github.com/Lesmiscore)
|
||||||
|
* [nitter] Minor fixes and update instance list by [foghawk](https://github.com/foghawk)
|
||||||
|
* [NRK] Extract timestamp by [hatienl0i261299](https://github.com/hatienl0i261299)
|
||||||
|
* [openrec] Download archived livestreams by [Lesmiscore](https://github.com/Lesmiscore)
|
||||||
|
* [openrec] Refactor extractors by [Lesmiscore](https://github.com/Lesmiscore)
|
||||||
|
* [panopto] Improve subtitle extraction and support slides by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
* [ParamountPlus, CBS] Change VALID_URL by [Sipherdrakon](https://github.com/Sipherdrakon)
|
||||||
|
* [ParamountPlusSeries] Support multiple pages by [dodrian](https://github.com/dodrian)
|
||||||
|
* [Piapro] Extract description with break lines by [Lesmiscore](https://github.com/Lesmiscore)
|
||||||
|
* [rai] Fix extraction of http formas by [nixxo](https://github.com/nixxo)
|
||||||
|
* [rumble] unescape title
|
||||||
|
* [RUTV] Fix format sorting by [Lesmiscore](https://github.com/Lesmiscore)
|
||||||
|
* [ruutu] Detect embeds by [tpikonen](https://github.com/tpikonen)
|
||||||
|
* [tenplay] Improve extractor by [aarubui](https://github.com/aarubui)
|
||||||
|
* [TikTok] Fix URLs with user id by [hatienl0i261299](https://github.com/hatienl0i261299)
|
||||||
|
* [TikTokVM] Fix redirect to user URL
|
||||||
|
* [TVer] Fix extractor by [Lesmiscore](https://github.com/Lesmiscore)
|
||||||
|
* [TVer] Support landing page by [vvto33](https://github.com/vvto33)
|
||||||
|
* [twitcasting] Don't return multi_video for archive with single hls manifest by [Lesmiscore](https://github.com/Lesmiscore)
|
||||||
|
* [veo] Fix `_VALID_URL`
|
||||||
|
* [Veo] Fix extractor by [i6t](https://github.com/i6t)
|
||||||
|
* [viki] Don't attempt to modify URLs with signature by [nyuszika7h](https://github.com/nyuszika7h)
|
||||||
|
* [viu] Fix bypass for preview by [zackmark29](https://github.com/zackmark29)
|
||||||
|
* [viu] Fixed extractor by [zackmark29](https://github.com/zackmark29), [pukkandan](https://github.com/pukkandan)
|
||||||
|
* [web.archive:youtube] Make CDX API requests non-fatal by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
* [wget] Fix proxy by [kikuyan](https://github.com/kikuyan), [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
* [xnxx] Add `xnxx3.com` by [rozari0](https://github.com/rozari0)
|
||||||
|
* [youtube] **Add new age-gate bypass** by [zerodytrash](https://github.com/zerodytrash), [pukkandan](https://github.com/pukkandan)
|
||||||
|
* [youtube] Add extractor-arg to skip auto-translated subs
|
||||||
|
* [youtube] Avoid false positives when detecting damaged formats
|
||||||
|
* [youtube] Detect DRM better by [shirt](https://github.com/shirt-dev)
|
||||||
|
* [youtube] Fix auto-translated automatic captions
|
||||||
|
* [youtube] Fix pagination of `membership` tab
|
||||||
|
* [youtube] Fix uploader for collaborative playlists by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
* [youtube] Improve video upload date handling by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
* [youtube:api] Prefer minified JSON response by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
* [youtube:search] Support hashtag entries by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
* [youtube:tab] Fix duration extraction for shorts by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
* [youtube:tab] Minor improvements
|
||||||
|
* [youtube:tab] Return shorts url if video is a short by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
* [Zattoo] Fix extractors by [goggle](https://github.com/goggle)
|
||||||
|
* [Zingmp3] Fix signature by [hatienl0i261299](https://github.com/hatienl0i261299)
|
||||||
|
|
||||||
|
|
||||||
|
### 2022.03.08.1
|
||||||
|
|
||||||
|
* [cleanup] Refactor `__init__.py`
|
||||||
|
* [build] Fix bug
|
||||||
|
|
||||||
|
### 2022.03.08
|
||||||
|
|
||||||
|
* Merge youtube-dl: Upto [commit/6508688](https://github.com/ytdl-org/youtube-dl/commit/6508688e88c83bb811653083db9351702cd39a6a) (except NDR)
|
||||||
|
* Add regex operator and quoting to format filters by [lukasfink1](https://github.com/lukasfink1)
|
||||||
|
* Add brotli content-encoding support by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
* Add pre-processor stage `after_filter`
|
||||||
|
* Better error message when no `--live-from-start` format
|
||||||
|
* Create necessary directories for `--print-to-file`
|
||||||
|
* Fill more fields for playlists by [Lesmiscore](https://github.com/Lesmiscore)
|
||||||
|
* Fix `-all` for `--sub-langs`
|
||||||
|
* Fix doubling of `video_id` in `ExtractorError`
|
||||||
|
* Fix for when stdout/stderr encoding is `None`
|
||||||
|
* Handle negative duration from extractor
|
||||||
|
* Implement `--add-header` without modifying `std_headers`
|
||||||
|
* Obey `--abort-on-error` for "ffmpeg not installed"
|
||||||
|
* Set `webpage_url_...` from `webpage_url` and not input URL
|
||||||
|
* Tolerate failure to `--write-link` due to unknown URL
|
||||||
|
* [aria2c] Add `--http-accept-gzip=true`
|
||||||
|
* [build] Update pyinstaller to 4.10 by [shirt](https://github.com/shirt-dev)
|
||||||
|
* [cookies] Update MacOS12 `Cookies.binarycookies` location by [mdpauley](https://github.com/mdpauley)
|
||||||
|
* [devscripts] Improve `prepare_manpage`
|
||||||
|
* [downloader] Do not use aria2c for non-native `m3u8`
|
||||||
|
* [downloader] Obey `--file-access-retries` when deleting/renaming by [ehoogeveen-medweb](https://github.com/ehoogeveen-medweb)
|
||||||
|
* [extractor] Allow `http_headers` to be specified for `thumbnails`
|
||||||
|
* [extractor] Extract subtitles from manifests for vimeo, globo, kaltura, svt by [fstirlitz](https://github.com/fstirlitz)
|
||||||
|
* [extractor] Fix for manifests without period duration by [dirkf](https://github.com/dirkf), [pukkandan](https://github.com/pukkandan)
|
||||||
|
* [extractor] Support `--mark-watched` without `_NETRC_MACHINE` by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
* [FFmpegConcat] Abort on `--simulate`
|
||||||
|
* [FormatSort] Consider `acodec`=`ogg` as `vorbis`
|
||||||
|
* [fragment] Fix bugs around resuming with Range by [Lesmiscore](https://github.com/Lesmiscore)
|
||||||
|
* [fragment] Improve `--live-from-start` for YouTube livestreams by [Lesmiscore](https://github.com/Lesmiscore)
|
||||||
|
* [generic] Pass referer to extracted formats
|
||||||
|
* [generic] Set rss `guid` as video id by [Bricio](https://github.com/Bricio)
|
||||||
|
* [options] Better ambiguous option resolution
|
||||||
|
* [options] Rename `--clean-infojson` to `--clean-info-json`
|
||||||
|
* [SponsorBlock] Fixes for highlight and "full video labels" by [nihil-admirari](https://github.com/nihil-admirari)
|
||||||
|
* [Sponsorblock] minor fixes by [nihil-admirari](https://github.com/nihil-admirari)
|
||||||
|
* [utils] Better traceback for `ExtractorError`
|
||||||
|
* [utils] Fix file locking for AOSP by [jakeogh](https://github.com/jakeogh)
|
||||||
|
* [utils] Improve file locking
|
||||||
|
* [utils] OnDemandPagedList: Do not download pages after error
|
||||||
|
* [utils] render_table: Fix character calculation for removing extra gap by [Lesmiscore](https://github.com/Lesmiscore)
|
||||||
|
* [utils] Use `locked_file` for `sanitize_open` by [jakeogh](https://github.com/jakeogh)
|
||||||
|
* [utils] Validate `DateRange` input
|
||||||
|
* [utils] WebSockets wrapper for non-async functions by [Lesmiscore](https://github.com/Lesmiscore)
|
||||||
|
* [cleanup] Don't pass protocol to `_extract_m3u8_formats` for live videos
|
||||||
|
* [cleanup] Remove extractors for some dead websites by [marieell](https://github.com/marieell)
|
||||||
|
* [cleanup, docs] Misc cleanup
|
||||||
|
* [AbemaTV] Add extractors by [Lesmiscore](https://github.com/Lesmiscore)
|
||||||
|
* [adobepass] Add Suddenlink MSO by [CplPwnies](https://github.com/CplPwnies)
|
||||||
|
* [ant1newsgr] Add extractor by [zmousm](https://github.com/zmousm)
|
||||||
|
* [bigo] Add extractor by [Lesmiscore](https://github.com/Lesmiscore)
|
||||||
|
* [Caltrans] Add extractor by [Bricio](https://github.com/Bricio)
|
||||||
|
* [daystar] Add extractor by [hatienl0i261299](https://github.com/hatienl0i261299)
|
||||||
|
* [fc2:live] Add extractor by [Lesmiscore](https://github.com/Lesmiscore)
|
||||||
|
* [fptplay] Add extractor by [hatienl0i261299](https://github.com/hatienl0i261299)
|
||||||
|
* [murrtube] Add extractor by [cyberfox1691](https://github.com/cyberfox1691)
|
||||||
|
* [nfb] Add extractor by [ofkz](https://github.com/ofkz)
|
||||||
|
* [niconico] Add playlist extractors and refactor by [Lesmiscore](https://github.com/Lesmiscore)
|
||||||
|
* [peekvids] Add extractor by [schn0sch](https://github.com/schn0sch)
|
||||||
|
* [piapro] Add extractor by [pycabbage](https://github.com/pycabbage), [Lesmiscore](https://github.com/Lesmiscore)
|
||||||
|
* [rokfin] Add extractor by [P-reducible](https://github.com/P-reducible), [pukkandan](https://github.com/pukkandan)
|
||||||
|
* [rokfin] Add stack and channel extractors by [P-reducible](https://github.com/P-reducible), [pukkandan](https://github.com/pukkandan)
|
||||||
|
* [ruv.is] Add extractor by [iw0nderhow](https://github.com/iw0nderhow)
|
||||||
|
* [telegram] Add extractor by [hatienl0i261299](https://github.com/hatienl0i261299)
|
||||||
|
* [VideocampusSachsen] Add extractors by [FestplattenSchnitzel](https://github.com/FestplattenSchnitzel)
|
||||||
|
* [xinpianchang] Add extractor by [hatienl0i261299](https://github.com/hatienl0i261299)
|
||||||
|
* [abc] Support 1080p by [Ronnnny](https://github.com/Ronnnny)
|
||||||
|
* [afreecatv] Support password-protected livestreams by [wlritchi](https://github.com/wlritchi)
|
||||||
|
* [ard] Fix valid URL
|
||||||
|
* [ATVAt] Detect geo-restriction by [marieell](https://github.com/marieell)
|
||||||
|
* [bandcamp] Detect acodec
|
||||||
|
* [bandcamp] Fix user URLs by [lyz-code](https://github.com/lyz-code)
|
||||||
|
* [bbc] Fix extraction of news articles by [ajj8](https://github.com/ajj8)
|
||||||
|
* [beeg] Fix extractor by [Bricio](https://github.com/Bricio)
|
||||||
|
* [bigo] Fix extractor to not to use `form_params`
|
||||||
|
* [Bilibili] Pass referer for all formats by [blackgear](https://github.com/blackgear)
|
||||||
|
* [Biqle] Fix extractor by [Bricio](https://github.com/Bricio)
|
||||||
|
* [ccma] Fix timestamp parsing by [nyuszika7h](https://github.com/nyuszika7h)
|
||||||
|
* [crunchyroll] Better error reporting on login failure by [tejing1](https://github.com/tejing1)
|
||||||
|
* [cspan] Support of C-Span congress videos by [Grabien](https://github.com/Grabien)
|
||||||
|
* [dropbox] fix regex by [zenerdi0de](https://github.com/zenerdi0de)
|
||||||
|
* [fc2] Fix extraction by [Lesmiscore](https://github.com/Lesmiscore)
|
||||||
|
* [fujitv] Extract resolution for free sources by [YuenSzeHong](https://github.com/YuenSzeHong)
|
||||||
|
* [Gettr] Add `GettrStreamingIE` by [i6t](https://github.com/i6t)
|
||||||
|
* [Gettr] Fix formats order by [i6t](https://github.com/i6t)
|
||||||
|
* [Gettr] Improve extractor by [i6t](https://github.com/i6t)
|
||||||
|
* [globo] Expand valid URL by [Bricio](https://github.com/Bricio)
|
||||||
|
* [lbry] Fix `--ignore-no-formats-error`
|
||||||
|
* [manyvids] Extract `uploader` by [regarten](https://github.com/regarten)
|
||||||
|
* [mildom] Fix linter
|
||||||
|
* [mildom] Rework extractors by [Lesmiscore](https://github.com/Lesmiscore)
|
||||||
|
* [mirrativ] Cleanup extractor code by [Lesmiscore](https://github.com/Lesmiscore)
|
||||||
|
* [nhk] Add support for NHK for School by [Lesmiscore](https://github.com/Lesmiscore)
|
||||||
|
* [niconico:tag] Add support for searching tags
|
||||||
|
* [nrk] Add fallback API
|
||||||
|
* [peekvids] Use JSON-LD by [schn0sch](https://github.com/schn0sch)
|
||||||
|
* [peertube] Add media.fsfe.org by [mxmehl](https://github.com/mxmehl)
|
||||||
|
* [rtvs] Fix extractor by [Bricio](https://github.com/Bricio)
|
||||||
|
* [spiegel] Fix `_VALID_URL`
|
||||||
|
* [ThumbnailsConvertor] Support `webp`
|
||||||
|
* [tiktok] Fix `vm.tiktok`/`vt.tiktok` URLs
|
||||||
|
* [tubitv] Fix/improve TV series extraction by [bbepis](https://github.com/bbepis)
|
||||||
|
* [tumblr] Fix extractor by [foghawk](https://github.com/foghawk)
|
||||||
|
* [twitcasting] Add fallback for finding running live by [Lesmiscore](https://github.com/Lesmiscore)
|
||||||
|
* [TwitCasting] Check for password protection by [Lesmiscore](https://github.com/Lesmiscore)
|
||||||
|
* [twitcasting] Fix extraction by [Lesmiscore](https://github.com/Lesmiscore)
|
||||||
|
* [twitch] Fix field name of `view_count`
|
||||||
|
* [twitter] Fix for private videos by [iphoting](https://github.com/iphoting)
|
||||||
|
* [washingtonpost] Fix extractor by [Bricio](https://github.com/Bricio)
|
||||||
|
* [youtube:tab] Add `approximate_date` extractor-arg
|
||||||
|
* [youtube:tab] Follow redirect to regional channel by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
* [youtube:tab] Reject webpage data if redirected to home page
|
||||||
|
* [youtube] De-prioritize potentially damaged formats
|
||||||
|
* [youtube] Differentiate descriptive audio by language code
|
||||||
|
* [youtube] Ensure subtitle urls are absolute by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
* [youtube] Escape possible `$` in `_extract_n_function_name` regex by [Lesmiscore](https://github.com/Lesmiscore)
|
||||||
|
* [youtube] Fix automatic captions
|
||||||
|
* [youtube] Fix n-sig extraction for phone player JS by [MinePlayersPE](https://github.com/MinePlayersPE)
|
||||||
|
* [youtube] Further de-prioritize 3gp format
|
||||||
|
* [youtube] Label original auto-subs
|
||||||
|
* [youtube] Prefer UTC upload date for videos by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
* [zaq1] Remove dead extractor by [marieell](https://github.com/marieell)
|
||||||
|
* [zee5] Support web-series by [Aniruddh-J](https://github.com/Aniruddh-J)
|
||||||
|
* [zingmp3] Fix extractor by [hatienl0i261299](https://github.com/hatienl0i261299)
|
||||||
|
* [zoom] Add support for screen cast by [Mipsters](https://github.com/Mipsters)
|
||||||
|
|
||||||
|
|
||||||
|
### 2022.02.04
|
||||||
|
|
||||||
|
* [youtube:search] Fix extractor by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
* [youtube:search] Add tests
|
||||||
|
* [twitcasting] Enforce UTF-8 for POST payload by [Lesmiscore](https://github.com/Lesmiscore)
|
||||||
|
* [mediaset] Fix extractor by [nixxo](https://github.com/nixxo)
|
||||||
|
* [websocket] Make syntax error in `websockets` module non-fatal
|
||||||
|
|
||||||
|
### 2022.02.03
|
||||||
|
|
||||||
|
* Merge youtube-dl: Upto [commit/78ce962](https://github.com/ytdl-org/youtube-dl/commit/78ce962f4fe020994c216dd2671546fbe58a5c67)
|
||||||
|
* Add option `--print-to-file`
|
||||||
|
* Make nested --config-locations relative to parent file
|
||||||
|
* Ensure `_type` is present in `info.json`
|
||||||
|
* Fix `--compat-options list-formats`
|
||||||
|
* Fix/improve `InAdvancePagedList`
|
||||||
|
* [downloader/ffmpeg] Handle unknown formats better
|
||||||
|
* [outtmpl] Handle `-o ""` better
|
||||||
|
* [outtmpl] Handle hard-coded file extension better
|
||||||
|
* [extractor] Add convinience function `_yes_playlist`
|
||||||
|
* [extractor] Allow non-fatal `title` extraction
|
||||||
|
* [extractor] Extract video inside `Article` json_ld
|
||||||
|
* [generic] Allow further processing of json_ld URL
|
||||||
|
* [cookies] Fix keyring selection for unsupported desktops
|
||||||
|
* [utils] Strip double spaces in `clean_html` by [dirkf](https://github.com/dirkf)
|
||||||
|
* [aes] Add `unpad_pkcs7`
|
||||||
|
* [test] Fix `test_youtube_playlist_noplaylist`
|
||||||
|
* [docs,cleanup] Misc cleanup
|
||||||
|
* [dplay] Add extractors for site changes by [Sipherdrakon](https://github.com/Sipherdrakon)
|
||||||
|
* [ertgr] Add extractors by [zmousm](https://github.com/zmousm), [dirkf](https://github.com/dirkf)
|
||||||
|
* [Musicdex] Add extractors by [Ashish0804](https://github.com/Ashish0804)
|
||||||
|
* [YandexVideoPreview] Add extractor by [KiberInfinity](https://github.com/KiberInfinity)
|
||||||
|
* [youtube] Add extractor `YoutubeMusicSearchURLIE`
|
||||||
|
* [archive.org] Ignore unnecessary files
|
||||||
|
* [Bilibili] Add 8k support by [u-spec-png](https://github.com/u-spec-png)
|
||||||
|
* [bilibili] Fix extractor, make anthology title non-fatal
|
||||||
|
* [CAM4] Add thumbnail extraction by [alerikaisattera](https://github.com/alerikaisattera)
|
||||||
|
* [cctv] De-prioritize sample format
|
||||||
|
* [crunchyroll:beta] Add cookies support by [tejing1](https://github.com/tejing1)
|
||||||
|
* [crunchyroll] Fix login by [tejing1](https://github.com/tejing1)
|
||||||
|
* [doodstream] Fix extractor
|
||||||
|
* [fc2] Fix extraction by [Lesmiscore](https://github.com/Lesmiscore)
|
||||||
|
* [FFmpegConcat] Abort on --skip-download and download errors
|
||||||
|
* [Fujitv] Extract metadata and support premium by [YuenSzeHong](https://github.com/YuenSzeHong)
|
||||||
|
* [globo] Fix extractor by [Bricio](https://github.com/Bricio)
|
||||||
|
* [glomex] Simplify embed detection
|
||||||
|
* [GoogleSearch] Fix extractor
|
||||||
|
* [Instagram] Fix extraction when logged in by [MinePlayersPE](https://github.com/MinePlayersPE)
|
||||||
|
* [iq.com] Add VIP support by [MinePlayersPE](https://github.com/MinePlayersPE)
|
||||||
|
* [mildom] Fix extractor by [lazypete365](https://github.com/lazypete365)
|
||||||
|
* [MySpass] Fix video url processing by [trassshhub](https://github.com/trassshhub)
|
||||||
|
* [Odnoklassniki] Improve embedded players extraction by [KiberInfinity](https://github.com/KiberInfinity)
|
||||||
|
* [orf:tvthek] Lazy playlist extraction and obey --no-playlist
|
||||||
|
* [Pladform] Fix redirection to external player by [KiberInfinity](https://github.com/KiberInfinity)
|
||||||
|
* [ThisOldHouse] Improve Premium URL check by [Ashish0804](https://github.com/Ashish0804)
|
||||||
|
* [TikTok] Iterate through app versions by [MinePlayersPE](https://github.com/MinePlayersPE)
|
||||||
|
* [tumblr] Fix 403 errors and handle vimeo embeds by [foghawk](https://github.com/foghawk)
|
||||||
|
* [viki] Fix "Bad request" for manifest by [nyuszika7h](https://github.com/nyuszika7h)
|
||||||
|
* [Vimm] add recording extractor by [alerikaisattera](https://github.com/alerikaisattera)
|
||||||
|
* [web.archive:youtube] Add `ytarchive:` prefix and misc cleanup
|
||||||
|
* [youtube:api] Do not use seek when reading HTTPError response by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
* [youtube] Fix n-sig for player e06dea74
|
||||||
|
* [youtube, cleanup] Misc fixes and cleanup
|
||||||
|
|
||||||
|
|
||||||
|
### 2022.01.21
|
||||||
|
|
||||||
|
* Add option `--concat-playlist` to **concat videos in a playlist**
|
||||||
|
* Allow **multiple and nested configuration files**
|
||||||
|
* Add more post-processing stages (`after_video`, `playlist`)
|
||||||
|
* Allow `--exec` to be run at any post-processing stage (Deprecates `--exec-before-download`)
|
||||||
|
* Allow `--print` to be run at any post-processing stage
|
||||||
|
* Allow listing formats, thumbnails, subtitles using `--print` by [pukkandan](https://github.com/pukkandan), [Zirro](https://github.com/Zirro)
|
||||||
|
* Add fields `video_autonumber`, `modified_date`, `modified_timestamp`, `playlist_count`, `channel_follower_count`
|
||||||
|
* Add key `requested_downloads` in the root `info_dict`
|
||||||
|
* Write `download_archive` only after all formats are downloaded
|
||||||
|
* [FfmpegMetadata] Allow setting metadata of individual streams using `meta<n>_` prefix
|
||||||
|
* Add option `--legacy-server-connect` by [xtkoba](https://github.com/xtkoba)
|
||||||
|
* Allow escaped `,` in `--extractor-args`
|
||||||
|
* Allow unicode characters in `info.json`
|
||||||
|
* Check for existing thumbnail/subtitle in final directory
|
||||||
|
* Don't treat empty containers as `None` in `sanitize_info`
|
||||||
|
* Fix `-s --ignore-no-formats --force-write-archive`
|
||||||
|
* Fix live title for multiple formats
|
||||||
|
* List playlist thumbnails in `--list-thumbnails`
|
||||||
|
* Raise error if subtitle download fails
|
||||||
|
* [cookies] Fix bug when keyring is unspecified
|
||||||
|
* [ffmpeg] Ignore unknown streams, standardize use of `-map 0`
|
||||||
|
* [outtmpl] Alternate form for `D` and fix suffix's case
|
||||||
|
* [utils] Add `Sec-Fetch-Mode` to `std_headers`
|
||||||
|
* [utils] Fix `format_bytes` output for Bytes by [pukkandan](https://github.com/pukkandan), [mdawar](https://github.com/mdawar)
|
||||||
|
* [utils] Handle `ss:xxx` in `parse_duration`
|
||||||
|
* [utils] Improve parsing for nested HTML elements by [zmousm](https://github.com/zmousm), [pukkandan](https://github.com/pukkandan)
|
||||||
|
* [utils] Use key `None` in `traverse_obj` to return as-is
|
||||||
|
* [extractor] Detect more subtitle codecs in MPD manifests by [fstirlitz](https://github.com/fstirlitz)
|
||||||
|
* [extractor] Extract chapters from JSON-LD by [iw0nderhow](https://github.com/iw0nderhow), [pukkandan](https://github.com/pukkandan)
|
||||||
|
* [extractor] Extract thumbnails from JSON-LD by [nixxo](https://github.com/nixxo)
|
||||||
|
* [extractor] Improve `url_result` and related
|
||||||
|
* [generic] Improve KVS player extraction by [trassshhub](https://github.com/trassshhub)
|
||||||
|
* [build] Reduce dependency on third party workflows
|
||||||
|
* [extractor,cleanup] Use `_search_nextjs_data`, `format_field`
|
||||||
|
* [cleanup] Minor fixes and cleanup
|
||||||
|
* [docs] Improvements
|
||||||
|
* [test] Fix TestVerboseOutput
|
||||||
|
* [afreecatv] Add livestreams extractor by [wlritchi](https://github.com/wlritchi)
|
||||||
|
* [callin] Add extractor by [foghawk](https://github.com/foghawk)
|
||||||
|
* [CrowdBunker] Add extractors by [Ashish0804](https://github.com/Ashish0804)
|
||||||
|
* [daftsex] Add extractors by [k3ns1n](https://github.com/k3ns1n)
|
||||||
|
* [digitalconcerthall] Add extractor by [teridon](https://github.com/teridon)
|
||||||
|
* [Drooble] Add extractor by [u-spec-png](https://github.com/u-spec-png)
|
||||||
|
* [EuropeanTour] Add extractor by [Ashish0804](https://github.com/Ashish0804)
|
||||||
|
* [iq.com] Add extractors by [MinePlayersPE](https://github.com/MinePlayersPE)
|
||||||
|
* [KelbyOne] Add extractor by [Ashish0804](https://github.com/Ashish0804)
|
||||||
|
* [LnkIE] Add extractor by [Ashish0804](https://github.com/Ashish0804)
|
||||||
|
* [MainStreaming] Add extractor by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
* [megatvcom] Add extractors by [zmousm](https://github.com/zmousm)
|
||||||
|
* [Newsy] Add extractor by [Ashish0804](https://github.com/Ashish0804)
|
||||||
|
* [noodlemagazine] Add extractor by [trassshhub](https://github.com/trassshhub)
|
||||||
|
* [PokerGo] Add extractors by [Ashish0804](https://github.com/Ashish0804)
|
||||||
|
* [Pornez] Add extractor by [mozlima](https://github.com/mozlima)
|
||||||
|
* [PRX] Add Extractors by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
* [RTNews] Add extractor by [Ashish0804](https://github.com/Ashish0804)
|
||||||
|
* [Rule34video] Add extractor by [trassshhub](https://github.com/trassshhub)
|
||||||
|
* [tvopengr] Add extractors by [zmousm](https://github.com/zmousm)
|
||||||
|
* [Vimm] Add extractor by [alerikaisattera](https://github.com/alerikaisattera)
|
||||||
|
* [glomex] Add extractors by [zmousm](https://github.com/zmousm)
|
||||||
|
* [instagram] Add story/highlight extractor by [u-spec-png](https://github.com/u-spec-png)
|
||||||
|
* [openrec] Add movie extractor by [Lesmiscore](https://github.com/Lesmiscore)
|
||||||
|
* [rai] Add Raiplaysound extractors by [nixxo](https://github.com/nixxo), [pukkandan](https://github.com/pukkandan)
|
||||||
|
* [aparat] Fix extractor
|
||||||
|
* [ard] Extract subtitles by [fstirlitz](https://github.com/fstirlitz)
|
||||||
|
* [BiliIntl] Add login by [MinePlayersPE](https://github.com/MinePlayersPE)
|
||||||
|
* [CeskaTelevize] Use `http` for manifests
|
||||||
|
* [CTVNewsIE] Add fallback for video search by [Ashish0804](https://github.com/Ashish0804)
|
||||||
|
* [dplay] Migrate DiscoveryPlusItaly to DiscoveryPlus by [timendum](https://github.com/timendum)
|
||||||
|
* [dplay] Re-structure DiscoveryPlus extractors
|
||||||
|
* [Dropbox] Support password protected files and more formats by [zenerdi0de](https://github.com/zenerdi0de)
|
||||||
|
* [facebook] Fix extraction from groups
|
||||||
|
* [facebook] Improve title and uploader extraction
|
||||||
|
* [facebook] Parse dash manifests
|
||||||
|
* [fox] Extract m3u8 from preview by [ischmidt20](https://github.com/ischmidt20)
|
||||||
|
* [funk] Support origin URLs
|
||||||
|
* [gfycat] Fix `uploader`
|
||||||
|
* [gfycat] Support embeds by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
* [hotstar] Add extractor args to ignore tags by [Ashish0804](https://github.com/Ashish0804)
|
||||||
|
* [hrfernsehen] Fix ardloader extraction by [CreaValix](https://github.com/CreaValix)
|
||||||
|
* [instagram] Fix username extraction for stories and highlights by [nyuszika7h](https://github.com/nyuszika7h)
|
||||||
|
* [kakao] Detect geo-restriction
|
||||||
|
* [line] Remove `tv.line.me` by [sian1468](https://github.com/sian1468)
|
||||||
|
* [mixch] Add `MixchArchiveIE` by [Lesmiscore](https://github.com/Lesmiscore)
|
||||||
|
* [mixcloud] Detect restrictions by [llacb47](https://github.com/llacb47)
|
||||||
|
* [NBCSports] Fix extraction of platform URLs by [ischmidt20](https://github.com/ischmidt20)
|
||||||
|
* [Nexx] Extract more metadata by [MinePlayersPE](https://github.com/MinePlayersPE)
|
||||||
|
* [Nexx] Support 3q CDN by [MinePlayersPE](https://github.com/MinePlayersPE)
|
||||||
|
* [pbs] de-prioritize AD formats
|
||||||
|
* [PornHub,YouTube] Refresh onion addresses by [unit193](https://github.com/unit193)
|
||||||
|
* [RedBullTV] Parse subtitles from manifest by [Ashish0804](https://github.com/Ashish0804)
|
||||||
|
* [streamcz] Fix extractor by [arkamar](https://github.com/arkamar), [pukkandan](https://github.com/pukkandan)
|
||||||
|
* [Ted] Rewrite extractor by [pukkandan](https://github.com/pukkandan), [trassshhub](https://github.com/trassshhub)
|
||||||
|
* [Theta] Fix valid URL by [alerikaisattera](https://github.com/alerikaisattera)
|
||||||
|
* [ThisOldHouseIE] Add support for premium videos by [Ashish0804](https://github.com/Ashish0804)
|
||||||
|
* [TikTok] Fix extraction for sigi-based webpages, add API fallback by [MinePlayersPE](https://github.com/MinePlayersPE)
|
||||||
|
* [TikTok] Pass cookies to formats, and misc fixes by [MinePlayersPE](https://github.com/MinePlayersPE)
|
||||||
|
* [TikTok] Extract captions, user thumbnail by [MinePlayersPE](https://github.com/MinePlayersPE)
|
||||||
|
* [TikTok] Change app version by [MinePlayersPE](https://github.com/MinePlayersPE), [llacb47](https://github.com/llacb47)
|
||||||
|
* [TVer] Extract message for unaired live by [Lesmiscore](https://github.com/Lesmiscore)
|
||||||
|
* [twitcasting] Refactor extractor by [Lesmiscore](https://github.com/Lesmiscore)
|
||||||
|
* [twitter] Fix video in quoted tweets
|
||||||
|
* [veoh] Improve extractor by [foghawk](https://github.com/foghawk)
|
||||||
|
* [vk] Capture `clip` URLs
|
||||||
|
* [vk] Fix VKUserVideosIE by [Ashish0804](https://github.com/Ashish0804)
|
||||||
|
* [vk] Improve `_VALID_URL` by [k3ns1n](https://github.com/k3ns1n)
|
||||||
|
* [VrtNU] Handle empty title by [pgaig](https://github.com/pgaig)
|
||||||
|
* [XVideos] Check HLS formats by [MinePlayersPE](https://github.com/MinePlayersPE)
|
||||||
|
* [yahoo:gyao] Improved playlist handling by [hyano](https://github.com/hyano)
|
||||||
|
* [youtube:tab] Extract more playlist metadata by [coletdjnz](https://github.com/coletdjnz), [pukkandan](https://github.com/pukkandan)
|
||||||
|
* [youtube:tab] Raise error on tab redirect by [krichbanana](https://github.com/krichbanana), [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
* [youtube] Update Innertube clients by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
* [youtube] Detect live-stream embeds
|
||||||
|
* [youtube] Do not return `upload_date` for playlists
|
||||||
|
* [youtube] Extract channel subscriber count by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
* [youtube] Make invalid storyboard URL non-fatal
|
||||||
|
* [youtube] Enforce UTC, update innertube clients and tests by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
* [zdf] Add chapter extraction by [iw0nderhow](https://github.com/iw0nderhow)
|
||||||
|
* [zee5] Add geo-bypass
|
||||||
|
|
||||||
|
|
||||||
### 2021.12.27
|
### 2021.12.27
|
||||||
|
|
||||||
* Avoid recursion error when re-extracting info
|
* Avoid recursion error when re-extracting info
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ You can also find lists of all [contributors of yt-dlp](CONTRIBUTORS) and [autho
|
|||||||
|
|
||||||
* YouTube improvements including: age-gate bypass, private playlists, multiple-clients (to avoid throttling) and a lot of under-the-hood improvements
|
* YouTube improvements including: age-gate bypass, private playlists, multiple-clients (to avoid throttling) and a lot of under-the-hood improvements
|
||||||
* Added support for downloading YoutubeWebArchive videos
|
* Added support for downloading YoutubeWebArchive videos
|
||||||
|
* Added support for new websites MainStreaming, PRX, nzherald, etc
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -41,7 +42,7 @@ You can also find lists of all [contributors of yt-dlp](CONTRIBUTORS) and [autho
|
|||||||
* Improved/fixed support for HiDive, HotStar, Hungama, LBRY, LinkedInLearning, Mxplayer, SonyLiv, TV2, Vimeo, VLive etc
|
* Improved/fixed support for HiDive, HotStar, Hungama, LBRY, LinkedInLearning, Mxplayer, SonyLiv, TV2, Vimeo, VLive etc
|
||||||
|
|
||||||
|
|
||||||
## [Lesmicore](https://github.com/Lesmiscore) (nao20010128nao)
|
## [Lesmiscore](https://github.com/Lesmiscore) (nao20010128nao)
|
||||||
|
|
||||||
**Bitcoin**: bc1qfd02r007cutfdjwjmyy9w23rjvtls6ncve7r3s
|
**Bitcoin**: bc1qfd02r007cutfdjwjmyy9w23rjvtls6ncve7r3s
|
||||||
**Monacoin**: mona1q3tf7dzvshrhfe3md379xtvt2n22duhglv5dskr
|
**Monacoin**: mona1q3tf7dzvshrhfe3md379xtvt2n22duhglv5dskr
|
||||||
|
|||||||
@@ -5,5 +5,6 @@ include README.md
|
|||||||
include completions/*/*
|
include completions/*/*
|
||||||
include supportedsites.md
|
include supportedsites.md
|
||||||
include yt-dlp.1
|
include yt-dlp.1
|
||||||
|
include requirements.txt
|
||||||
recursive-include devscripts *
|
recursive-include devscripts *
|
||||||
recursive-include test *
|
recursive-include test *
|
||||||
|
|||||||
4
Makefile
4
Makefile
@@ -14,9 +14,9 @@ pypi-files: AUTHORS Changelog.md LICENSE README.md README.txt supportedsites com
|
|||||||
.PHONY: all clean install test tar pypi-files completions ot offlinetest codetest supportedsites
|
.PHONY: all clean install test tar pypi-files completions ot offlinetest codetest supportedsites
|
||||||
|
|
||||||
clean-test:
|
clean-test:
|
||||||
rm -rf test/testdata/player-*.js tmp/ *.annotations.xml *.aria2 *.description *.dump *.frag \
|
rm -rf test/testdata/sigs/player-*.js tmp/ *.annotations.xml *.aria2 *.description *.dump *.frag \
|
||||||
*.frag.aria2 *.frag.urls *.info.json *.live_chat.json *.meta *.part* *.tmp *.temp *.unknown_video *.ytdl \
|
*.frag.aria2 *.frag.urls *.info.json *.live_chat.json *.meta *.part* *.tmp *.temp *.unknown_video *.ytdl \
|
||||||
*.3gp *.ape *.avi *.desktop *.flac *.flv *.jpeg *.jpg *.m4a *.m4v *.mhtml *.mkv *.mov *.mp3 \
|
*.3gp *.ape *.ass *.avi *.desktop *.flac *.flv *.jpeg *.jpg *.m4a *.m4v *.mhtml *.mkv *.mov *.mp3 \
|
||||||
*.mp4 *.ogg *.opus *.png *.sbv *.srt *.swf *.swp *.ttml *.url *.vtt *.wav *.webloc *.webm *.webp
|
*.mp4 *.ogg *.opus *.png *.sbv *.srt *.swf *.swp *.ttml *.url *.vtt *.wav *.webloc *.webm *.webp
|
||||||
clean-dist:
|
clean-dist:
|
||||||
rm -rf yt-dlp.1.temp.md yt-dlp.1 README.txt MANIFEST build/ dist/ .coverage cover/ yt-dlp.tar.gz completions/ \
|
rm -rf yt-dlp.1.temp.md yt-dlp.1 README.txt MANIFEST build/ dist/ .coverage cover/ yt-dlp.tar.gz completions/ \
|
||||||
|
|||||||
262
README.md
262
README.md
@@ -3,15 +3,14 @@
|
|||||||
|
|
||||||
[](#readme)
|
[](#readme)
|
||||||
|
|
||||||
[](#release-files "Release")
|
[](#release-files "Release")
|
||||||
[](LICENSE "License")
|
|
||||||
[](Collaborators.md#collaborators "Donate")
|
|
||||||
[](https://readthedocs.org/projects/yt-dlp/ "Docs")
|
|
||||||
[](supportedsites.md "Supported Sites")
|
|
||||||
[](https://pypi.org/project/yt-dlp "PyPi")
|
[](https://pypi.org/project/yt-dlp "PyPi")
|
||||||
[](https://github.com/yt-dlp/yt-dlp/actions "CI Status")
|
[](Collaborators.md#collaborators "Donate")
|
||||||
[](https://discord.gg/H5MNcFW63r "Discord")
|
|
||||||
[](https://matrix.to/#/#yt-dlp:matrix.org "Matrix")
|
[](https://matrix.to/#/#yt-dlp:matrix.org "Matrix")
|
||||||
|
[](https://discord.gg/H5MNcFW63r "Discord")
|
||||||
|
[](supportedsites.md "Supported Sites")
|
||||||
|
[](LICENSE "License")
|
||||||
|
[](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/commits "Commit History")
|
||||||
|
|
||||||
@@ -71,13 +70,13 @@ yt-dlp is a [youtube-dl](https://github.com/ytdl-org/youtube-dl) fork based on t
|
|||||||
|
|
||||||
# NEW FEATURES
|
# NEW FEATURES
|
||||||
|
|
||||||
* Based on **youtube-dl 2021.12.17 [commit/5014bd6](https://github.com/ytdl-org/youtube-dl/commit/5014bd67c22b421207b2650d4dc874b95b36dda1)** and **youtube-dlc 2020.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)
|
* Based on **youtube-dl 2021.12.17 [commit/6508688](https://github.com/ytdl-org/youtube-dl/commit/6508688e88c83bb811653083db9351702cd39a6a)** ([exceptions](https://github.com/yt-dlp/yt-dlp/issues/21)) and **youtube-dlc 2020.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)
|
||||||
|
|
||||||
* **[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
|
||||||
|
|
||||||
* **[Format Sorting](#sorting-formats)**: The default format sorting options have been changed so that higher resolution and better codecs will be now preferred instead of simply using larger bitrate. Furthermore, you can now specify the sort order using `-S`. This allows for much easier format selection than what is possible by simply using `--format` ([examples](#format-selection-examples))
|
* **[Format Sorting](#sorting-formats)**: The default format sorting options have been changed so that higher resolution and better codecs will be now preferred instead of simply using larger bitrate. Furthermore, you can now specify the sort order using `-S`. This allows for much easier format selection than what is possible by simply using `--format` ([examples](#format-selection-examples))
|
||||||
|
|
||||||
* **Merged with animelover1984/youtube-dl**: You get most of the features and improvements from [animelover1984/youtube-dl](https://github.com/animelover1984/youtube-dl) including `--write-comments`, `BiliBiliSearch`, `BilibiliChannel`, Embedding thumbnail in mp4/ogg/opus, playlist infojson etc. Note that the NicoNico improvements are not available. See [#31](https://github.com/yt-dlp/yt-dlp/pull/31) for details.
|
* **Merged with animelover1984/youtube-dl**: You get most of the features and improvements from [animelover1984/youtube-dl](https://github.com/animelover1984/youtube-dl) including `--write-comments`, `BiliBiliSearch`, `BilibiliChannel`, Embedding thumbnail in mp4/ogg/opus, playlist infojson etc. Note that the NicoNico livestreams are not available. See [#31](https://github.com/yt-dlp/yt-dlp/pull/31) for details.
|
||||||
|
|
||||||
* **Youtube improvements**:
|
* **Youtube improvements**:
|
||||||
* All Feeds (`:ytfav`, `:ytwatchlater`, `:ytsubs`, `:ythistory`, `:ytrec`) and private playlists supports downloading multiple pages of content
|
* All Feeds (`:ytfav`, `:ytwatchlater`, `:ytsubs`, `:ythistory`, `:ytrec`) and private playlists supports downloading multiple pages of content
|
||||||
@@ -88,7 +87,7 @@ yt-dlp is a [youtube-dl](https://github.com/ytdl-org/youtube-dl) fork based on t
|
|||||||
* Redirect channel's home URL automatically to `/video` to preserve the old behaviour
|
* Redirect channel's home URL automatically to `/video` to preserve the old behaviour
|
||||||
* `255kbps` audio is extracted (if available) from youtube music when premium cookies are given
|
* `255kbps` audio is extracted (if available) from youtube music when premium cookies are given
|
||||||
* Youtube music Albums, channels etc can be downloaded ([except self-uploaded music](https://github.com/yt-dlp/yt-dlp/issues/723))
|
* Youtube music Albums, channels etc can be downloaded ([except self-uploaded music](https://github.com/yt-dlp/yt-dlp/issues/723))
|
||||||
* Download livestreams from the start using `--live-from-start`
|
* Download livestreams from the start using `--live-from-start` (experimental)
|
||||||
|
|
||||||
* **Cookies from browser**: Cookies can be automatically extracted from all major web browsers using `--cookies-from-browser BROWSER[+KEYRING][:PROFILE]`
|
* **Cookies from browser**: Cookies can be automatically extracted from all major web browsers using `--cookies-from-browser BROWSER[+KEYRING][:PROFILE]`
|
||||||
|
|
||||||
@@ -110,9 +109,9 @@ yt-dlp is a [youtube-dl](https://github.com/ytdl-org/youtube-dl) fork based on t
|
|||||||
|
|
||||||
* **Output template improvements**: Output templates can now have date-time formatting, numeric offsets, object traversal etc. See [output template](#output-template) for details. Even more advanced operations can also be done with the help of `--parse-metadata` and `--replace-in-metadata`
|
* **Output template improvements**: Output templates can now have date-time formatting, numeric offsets, object traversal etc. See [output template](#output-template) for details. Even more advanced operations can also be done with the help of `--parse-metadata` and `--replace-in-metadata`
|
||||||
|
|
||||||
* **Other new options**: Many new options have been added such as `--print`, `--wait-for-video`, `--sleep-requests`, `--convert-thumbnails`, `--write-link`, `--force-download-archive`, `--force-overwrites`, `--break-on-reject` etc
|
* **Other new options**: Many new options have been added such as `--concat-playlist`, `--print`, `--wait-for-video`, `--sleep-requests`, `--convert-thumbnails`, `--write-link`, `--force-download-archive`, `--force-overwrites`, `--break-on-reject` etc
|
||||||
|
|
||||||
* **Improvements**: Regex and other operators in `--match-filter`, multiple `--postprocessor-args` and `--downloader-args`, faster archive checking, more [format selection options](#format-selection), merge multi-video/audio, multiple `--config-locations`, etc
|
* **Improvements**: Regex and other operators in `--format`/`--match-filter`, multiple `--postprocessor-args` and `--downloader-args`, faster archive checking, more [format selection options](#format-selection), merge multi-video/audio, multiple `--config-locations`, `--exec` at different stages, etc
|
||||||
|
|
||||||
* **Plugins**: Extractors and PostProcessors can be loaded from an external file. See [plugins](#plugins) for details
|
* **Plugins**: Extractors and PostProcessors can be loaded from an external file. See [plugins](#plugins) for details
|
||||||
|
|
||||||
@@ -126,11 +125,12 @@ Some of yt-dlp's default options are different from that of youtube-dl and youtu
|
|||||||
|
|
||||||
* The options `--auto-number` (`-A`), `--title` (`-t`) and `--literal` (`-l`), no longer work. See [removed options](#Removed) for details
|
* The options `--auto-number` (`-A`), `--title` (`-t`) and `--literal` (`-l`), no longer work. See [removed options](#Removed) for details
|
||||||
* `avconv` is not supported as an alternative to `ffmpeg`
|
* `avconv` is not supported as an alternative to `ffmpeg`
|
||||||
|
* yt-dlp stores config files in slightly different locations to youtube-dl. See [configuration](#configuration) for a list of correct locations
|
||||||
* The default [output template](#output-template) is `%(title)s [%(id)s].%(ext)s`. There is no real reason for this change. This was changed before yt-dlp was ever made public and now there are no plans to change it back to `%(title)s-%(id)s.%(ext)s`. Instead, you may use `--compat-options filename`
|
* The default [output template](#output-template) is `%(title)s [%(id)s].%(ext)s`. There is no real reason for this change. This was changed before yt-dlp was ever made public and now there are no plans to change it back to `%(title)s-%(id)s.%(ext)s`. Instead, you may use `--compat-options filename`
|
||||||
* The default [format sorting](#sorting-formats) is different from youtube-dl and prefers higher resolution and better codecs rather than higher bitrates. You can use the `--format-sort` option to change this to any order you prefer, or use `--compat-options format-sort` to use youtube-dl's sorting order
|
* The default [format sorting](#sorting-formats) is different from youtube-dl and prefers higher resolution and better codecs rather than higher bitrates. You can use the `--format-sort` option to change this to any order you prefer, or use `--compat-options format-sort` to use youtube-dl's sorting order
|
||||||
* The default format selector is `bv*+ba/b`. This means that if a combined video + audio format that is better than the best video-only format is found, the former will be preferred. Use `-f bv+ba/b` or `--compat-options format-spec` to revert this
|
* The default format selector is `bv*+ba/b`. This means that if a combined video + audio format that is better than the best video-only format is found, the former will be preferred. Use `-f bv+ba/b` or `--compat-options format-spec` to revert this
|
||||||
* Unlike youtube-dlc, yt-dlp does not allow merging multiple audio/video streams into one file by default (since this conflicts with the use of `-f bv*+ba`). If needed, this feature must be enabled using `--audio-multistreams` and `--video-multistreams`. You can also use `--compat-options multistreams` to enable both
|
* Unlike youtube-dlc, yt-dlp does not allow merging multiple audio/video streams into one file by default (since this conflicts with the use of `-f bv*+ba`). If needed, this feature must be enabled using `--audio-multistreams` and `--video-multistreams`. You can also use `--compat-options multistreams` to enable both
|
||||||
* `--ignore-errors` is enabled by default. Use `--abort-on-error` or `--compat-options abort-on-error` to abort on errors instead
|
* `--no-abort-on-error` is enabled by default. Use `--abort-on-error` or `--compat-options abort-on-error` to abort on errors instead
|
||||||
* When writing metadata files such as thumbnails, description or infojson, the same information (if available) is also written for playlists. Use `--no-write-playlist-metafiles` or `--compat-options no-playlist-metafiles` to not write these files
|
* When writing metadata files such as thumbnails, description or infojson, the same information (if available) is also written for playlists. Use `--no-write-playlist-metafiles` or `--compat-options no-playlist-metafiles` to not write these files
|
||||||
* `--add-metadata` attaches the `infojson` to `mkv` files in addition to writing the metadata when used with `--write-info-json`. Use `--no-embed-info-json` or `--compat-options no-attach-info-json` to revert this
|
* `--add-metadata` attaches the `infojson` to `mkv` files in addition to writing the metadata when used with `--write-info-json`. Use `--no-embed-info-json` or `--compat-options no-attach-info-json` to revert this
|
||||||
* Some metadata are embedded into different fields when using `--add-metadata` as compared to youtube-dl. Most notably, `comment` field contains the `webpage_url` and `synopsis` contains the `description`. You can [use `--parse-metadata`](#modifying-metadata) to modify this to your liking or use `--compat-options embed-metadata` to revert this
|
* Some metadata are embedded into different fields when using `--add-metadata` as compared to youtube-dl. Most notably, `comment` field contains the `webpage_url` and `synopsis` contains the `description`. You can [use `--parse-metadata`](#modifying-metadata) to modify this to your liking or use `--compat-options embed-metadata` to revert this
|
||||||
@@ -144,6 +144,8 @@ Some of yt-dlp's default options are different from that of youtube-dl and youtu
|
|||||||
* Thumbnail embedding in `mp4` is done with mutagen if possible. Use `--compat-options embed-thumbnail-atomicparsley` to force the use of AtomicParsley instead
|
* Thumbnail embedding in `mp4` is done with mutagen if possible. Use `--compat-options embed-thumbnail-atomicparsley` to force the use of AtomicParsley instead
|
||||||
* Some private fields such as filenames are removed by default from the infojson. Use `--no-clean-infojson` or `--compat-options no-clean-infojson` to revert this
|
* Some private fields such as filenames are removed by default from the infojson. Use `--no-clean-infojson` or `--compat-options no-clean-infojson` to revert this
|
||||||
* When `--embed-subs` and `--write-subs` are used together, the subtitles are written to disk and also embedded in the media file. You can use just `--embed-subs` to embed the subs and automatically delete the separate file. See [#630 (comment)](https://github.com/yt-dlp/yt-dlp/issues/630#issuecomment-893659460) for more info. `--compat-options no-keep-subs` can be used to revert this
|
* When `--embed-subs` and `--write-subs` are used together, the subtitles are written to disk and also embedded in the media file. You can use just `--embed-subs` to embed the subs and automatically delete the separate file. See [#630 (comment)](https://github.com/yt-dlp/yt-dlp/issues/630#issuecomment-893659460) for more info. `--compat-options no-keep-subs` can be used to revert this
|
||||||
|
* `certifi` will be used for SSL root certificates, if installed. If you want to use system certificates (e.g. self-signed), use `--compat-options no-certifi`
|
||||||
|
* youtube-dl tries to remove some superfluous punctuations from filenames. While this can sometimes be helpfull, it is often undesirable. So yt-dlp tries to keep the fields in the filenames as close to their original values as possible. You can use `--compat-options filename-sanitization` to revert to youtube-dl's behavior
|
||||||
|
|
||||||
For ease of use, a few more compat options are available:
|
For ease of use, a few more compat options are available:
|
||||||
* `--compat-options all`: Use all compat options
|
* `--compat-options all`: Use all compat options
|
||||||
@@ -202,7 +204,7 @@ python3 -m pip install --no-deps -U yt-dlp
|
|||||||
|
|
||||||
If you want to be on the cutting edge, you can also install the master branch with:
|
If you want to be on the cutting edge, you can also install the master branch with:
|
||||||
```
|
```
|
||||||
python3 -m pip install --force-reinstall https://github.com/yt-dlp/yt-dlp/archive/master.zip
|
python3 -m pip install --force-reinstall https://github.com/yt-dlp/yt-dlp/archive/master.tar.gz
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that on some systems, you may need to use `py` or `python` instead of `python3`
|
Note that on some systems, you may need to use `py` or `python` instead of `python3`
|
||||||
@@ -230,14 +232,14 @@ If you [installed using Homebrew](#with-homebrew), run `brew upgrade yt-dlp/taps
|
|||||||
|
|
||||||
File|Description
|
File|Description
|
||||||
:---|:---
|
:---|:---
|
||||||
[yt-dlp](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp)|Platform-independant binary. Needs Python (recommended for **UNIX-like systems**)
|
[yt-dlp](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp)|Platform-independant binary. Needs Python (recommended for **Linux/BSD**)
|
||||||
[yt-dlp.exe](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe)|Windows (Win7 SP1+) standalone x64 binary (recommended for **Windows**)
|
[yt-dlp.exe](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe)|Windows (Win7 SP1+) standalone x64 binary (recommended for **Windows**)
|
||||||
|
[yt-dlp_macos](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_macos)|MacOS (10.15+) standalone executable (recommended for **MacOS**)
|
||||||
|
|
||||||
#### Alternatives
|
#### Alternatives
|
||||||
|
|
||||||
File|Description
|
File|Description
|
||||||
:---|:---
|
:---|:---
|
||||||
[yt-dlp_macos](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_macos)|MacOS (10.15+) standalone executable
|
|
||||||
[yt-dlp_x86.exe](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_x86.exe)|Windows (Vista SP2+) standalone x86 (32-bit) binary
|
[yt-dlp_x86.exe](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_x86.exe)|Windows (Vista SP2+) standalone x86 (32-bit) binary
|
||||||
[yt-dlp_min.exe](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_min.exe)|Windows (Win7 SP1+) standalone x64 binary built with `py2exe`.<br/> Does not contain `pycryptodomex`, needs VC++14
|
[yt-dlp_min.exe](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_min.exe)|Windows (Win7 SP1+) standalone x64 binary built with `py2exe`.<br/> Does not contain `pycryptodomex`, needs VC++14
|
||||||
[yt-dlp_win.zip](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_win.zip)|Unpackaged Windows executable (no auto-update)
|
[yt-dlp_win.zip](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_win.zip)|Unpackaged Windows executable (no auto-update)
|
||||||
@@ -263,28 +265,31 @@ On windows, [Microsoft Visual C++ 2010 SP1 Redistributable Package (x86)](https:
|
|||||||
While all the other dependencies are optional, `ffmpeg` and `ffprobe` are highly recommended
|
While all the other dependencies are optional, `ffmpeg` and `ffprobe` are highly recommended
|
||||||
|
|
||||||
* [**ffmpeg** and **ffprobe**](https://www.ffmpeg.org) - Required for [merging separate video and audio files](#format-selection) as well as for various [post-processing](#post-processing-options) tasks. License [depends on the build](https://www.ffmpeg.org/legal.html)
|
* [**ffmpeg** and **ffprobe**](https://www.ffmpeg.org) - Required for [merging separate video and audio files](#format-selection) as well as for various [post-processing](#post-processing-options) tasks. License [depends on the build](https://www.ffmpeg.org/legal.html)
|
||||||
* [**mutagen**](https://github.com/quodlibet/mutagen) - For embedding thumbnail in certain formats. Licensed under [GPLv2+](https://github.com/quodlibet/mutagen/blob/master/COPYING)
|
* [**mutagen**](https://github.com/quodlibet/mutagen)\* - For embedding thumbnail in certain formats. Licensed under [GPLv2+](https://github.com/quodlibet/mutagen/blob/master/COPYING)
|
||||||
* [**pycryptodomex**](https://github.com/Legrandin/pycryptodome) - For decrypting AES-128 HLS streams and various other data. Licensed under [BSD2](https://github.com/Legrandin/pycryptodome/blob/master/LICENSE.rst)
|
* [**pycryptodomex**](https://github.com/Legrandin/pycryptodome)\* - For decrypting AES-128 HLS streams and various other data. Licensed under [BSD-2-Clause](https://github.com/Legrandin/pycryptodome/blob/master/LICENSE.rst)
|
||||||
* [**websockets**](https://github.com/aaugustin/websockets) - For downloading over websocket. Licensed under [BSD3](https://github.com/aaugustin/websockets/blob/main/LICENSE)
|
* [**websockets**](https://github.com/aaugustin/websockets)\* - For downloading over websocket. Licensed under [BSD-3-Clause](https://github.com/aaugustin/websockets/blob/main/LICENSE)
|
||||||
* [**secretstorage**](https://github.com/mitya57/secretstorage) - For accessing the Gnome keyring while decrypting cookies of Chromium-based browsers on Linux. Licensed under [BSD](https://github.com/mitya57/secretstorage/blob/master/LICENSE)
|
* [**secretstorage**](https://github.com/mitya57/secretstorage)\* - For accessing the Gnome keyring while decrypting cookies of Chromium-based browsers on Linux. Licensed under [BSD-3-Clause](https://github.com/mitya57/secretstorage/blob/master/LICENSE)
|
||||||
* [**AtomicParsley**](https://github.com/wez/atomicparsley) - For embedding thumbnail in mp4/m4a if mutagen is not present. Licensed under [GPLv2+](https://github.com/wez/atomicparsley/blob/master/COPYING)
|
* [**brotli**](https://github.com/google/brotli)\* or [**brotlicffi**](https://github.com/python-hyper/brotlicffi) - [Brotli](https://en.wikipedia.org/wiki/Brotli) content encoding support. Both licensed under MIT <sup>[1](https://github.com/google/brotli/blob/master/LICENSE) [2](https://github.com/python-hyper/brotlicffi/blob/master/LICENSE) </sup>
|
||||||
|
* [**certifi**](https://github.com/certifi/python-certifi)\* - Provides Mozilla's root certificate bundle. Licensed under [MPLv2](https://github.com/certifi/python-certifi/blob/master/LICENSE)
|
||||||
|
* [**AtomicParsley**](https://github.com/wez/atomicparsley) - For embedding thumbnail in mp4/m4a if mutagen/ffmpeg cannot. Licensed under [GPLv2+](https://github.com/wez/atomicparsley/blob/master/COPYING)
|
||||||
* [**rtmpdump**](http://rtmpdump.mplayerhq.hu) - For downloading `rtmp` streams. ffmpeg will be used as a fallback. Licensed under [GPLv2+](http://rtmpdump.mplayerhq.hu)
|
* [**rtmpdump**](http://rtmpdump.mplayerhq.hu) - For downloading `rtmp` streams. ffmpeg will be used as a fallback. Licensed under [GPLv2+](http://rtmpdump.mplayerhq.hu)
|
||||||
* [**mplayer**](http://mplayerhq.hu/design7/info.html) or [**mpv**](https://mpv.io) - For downloading `rstp` streams. ffmpeg will be used as a fallback. Licensed under [GPLv2+](https://github.com/mpv-player/mpv/blob/master/Copyright)
|
* [**mplayer**](http://mplayerhq.hu/design7/info.html) or [**mpv**](https://mpv.io) - For downloading `rstp` streams. ffmpeg will be used as a fallback. Licensed under [GPLv2+](https://github.com/mpv-player/mpv/blob/master/Copyright)
|
||||||
* [**phantomjs**](https://github.com/ariya/phantomjs) - Used in extractors where javascript needs to be run. Licensed under [BSD3](https://github.com/ariya/phantomjs/blob/master/LICENSE.BSD)
|
* [**phantomjs**](https://github.com/ariya/phantomjs) - Used in extractors where javascript needs to be run. Licensed under [BSD-3-Clause](https://github.com/ariya/phantomjs/blob/master/LICENSE.BSD)
|
||||||
* [**sponskrub**](https://github.com/faissaloo/SponSkrub) - For using the now **deprecated** [sponskrub options](#sponskrub-options). Licensed under [GPLv3+](https://github.com/faissaloo/SponSkrub/blob/master/LICENCE.md)
|
* [**sponskrub**](https://github.com/faissaloo/SponSkrub) - For using the now **deprecated** [sponskrub options](#sponskrub-options). Licensed under [GPLv3+](https://github.com/faissaloo/SponSkrub/blob/master/LICENCE.md)
|
||||||
* Any external downloader that you want to use with `--downloader`
|
* Any external downloader that you want to use with `--downloader`
|
||||||
|
|
||||||
To use or redistribute the dependencies, you must agree to their respective licensing terms.
|
To use or redistribute the dependencies, you must agree to their respective licensing terms.
|
||||||
|
|
||||||
The Windows and MacOS standalone release binaries are already built with the python interpreter, mutagen, pycryptodomex and websockets included.
|
The Windows and MacOS standalone release binaries are already built with the python interpreter and all optional python packages (marked with \*) included.
|
||||||
|
|
||||||
|
<!-- TODO: ffmpeg has merged this patch. Remove this note once there is new release -->
|
||||||
**Note**: There are some regressions in newer ffmpeg versions that causes various issues when used alongside yt-dlp. Since ffmpeg is such an important dependency, we provide [custom builds](https://github.com/yt-dlp/FFmpeg-Builds#ffmpeg-static-auto-builds) with patches for these issues at [yt-dlp/FFmpeg-Builds](https://github.com/yt-dlp/FFmpeg-Builds). See [the readme](https://github.com/yt-dlp/FFmpeg-Builds#patches-applied) for details on the specific issues solved by these builds
|
**Note**: There are some regressions in newer ffmpeg versions that causes various issues when used alongside yt-dlp. Since ffmpeg is such an important dependency, we provide [custom builds](https://github.com/yt-dlp/FFmpeg-Builds#ffmpeg-static-auto-builds) with patches for these issues at [yt-dlp/FFmpeg-Builds](https://github.com/yt-dlp/FFmpeg-Builds). See [the readme](https://github.com/yt-dlp/FFmpeg-Builds#patches-applied) for details on the specific issues solved by these builds
|
||||||
|
|
||||||
|
|
||||||
## COMPILE
|
## COMPILE
|
||||||
|
|
||||||
**For Windows**:
|
**For Windows**:
|
||||||
To build the Windows executable, you must have pyinstaller (and optionally mutagen, pycryptodomex, websockets). Once you have all the necessary dependencies installed, (optionally) build lazy extractors using `devscripts/make_lazy_extractors.py`, and then just run `pyinst.py`. The executable will be built for the same architecture (32/64 bit) as the python used to build it.
|
To build the Windows executable, you must have pyinstaller (and any of yt-dlp's optional dependencies if needed). Once you have all the necessary dependencies installed, (optionally) build lazy extractors using `devscripts/make_lazy_extractors.py`, and then just run `pyinst.py`. The executable will be built for the same architecture (32/64 bit) as the python used to build it.
|
||||||
|
|
||||||
py -m pip install -U pyinstaller -r requirements.txt
|
py -m pip install -U pyinstaller -r requirements.txt
|
||||||
py devscripts/make_lazy_extractors.py
|
py devscripts/make_lazy_extractors.py
|
||||||
@@ -365,8 +370,7 @@ You can also fork the project on github and run your fork's [build workflow](.gi
|
|||||||
available. Pass the minimum number of
|
available. Pass the minimum number of
|
||||||
seconds (or range) to wait between retries
|
seconds (or range) to wait between retries
|
||||||
--no-wait-for-video Do not wait for scheduled streams (default)
|
--no-wait-for-video Do not wait for scheduled streams (default)
|
||||||
--mark-watched Mark videos watched (even with --simulate).
|
--mark-watched Mark videos watched (even with --simulate)
|
||||||
Currently only supported for YouTube
|
|
||||||
--no-mark-watched Do not mark videos watched (default)
|
--no-mark-watched Do not mark videos watched (default)
|
||||||
--no-colors Do not emit color codes in output
|
--no-colors Do not emit color codes in output
|
||||||
--compat-options OPTS Options that can help keep compatibility
|
--compat-options OPTS Options that can help keep compatibility
|
||||||
@@ -379,8 +383,9 @@ You can also fork the project on github and run your fork's [build workflow](.gi
|
|||||||
--proxy URL Use the specified HTTP/HTTPS/SOCKS proxy.
|
--proxy URL Use the specified HTTP/HTTPS/SOCKS proxy.
|
||||||
To enable SOCKS proxy, specify a proper
|
To enable SOCKS proxy, specify a proper
|
||||||
scheme. For example
|
scheme. For example
|
||||||
socks5://127.0.0.1:1080/. Pass in an empty
|
socks5://user:pass@127.0.0.1:1080/. Pass in
|
||||||
string (--proxy "") for direct connection
|
an empty string (--proxy "") for direct
|
||||||
|
connection
|
||||||
--socket-timeout SECONDS Time to wait before giving up, in seconds
|
--socket-timeout SECONDS Time to wait before giving up, in seconds
|
||||||
--source-address IP Client-side IP address to bind to
|
--source-address IP Client-side IP address to bind to
|
||||||
-4, --force-ipv4 Make all connections via IPv4
|
-4, --force-ipv4 Make all connections via IPv4
|
||||||
@@ -393,7 +398,7 @@ You can also fork the project on github and run your fork's [build workflow](.gi
|
|||||||
option is not present) is used for the
|
option is not present) is used for the
|
||||||
actual downloading
|
actual downloading
|
||||||
--geo-bypass Bypass geographic restriction via faking
|
--geo-bypass Bypass geographic restriction via faking
|
||||||
X-Forwarded-For HTTP header
|
X-Forwarded-For HTTP header (default)
|
||||||
--no-geo-bypass Do not bypass geographic restriction via
|
--no-geo-bypass Do not bypass geographic restriction via
|
||||||
faking X-Forwarded-For HTTP header
|
faking X-Forwarded-For HTTP header
|
||||||
--geo-bypass-country CODE Force bypass geographic restriction with
|
--geo-bypass-country CODE Force bypass geographic restriction with
|
||||||
@@ -427,24 +432,24 @@ You can also fork the project on github and run your fork's [build workflow](.gi
|
|||||||
--dateafter DATE Download only videos uploaded on or after
|
--dateafter DATE Download only videos uploaded on or after
|
||||||
this date. The date formats accepted is the
|
this date. The date formats accepted is the
|
||||||
same as --date
|
same as --date
|
||||||
--match-filter FILTER Generic video filter. Any field (see
|
--match-filters FILTER Generic video filter. Any field (see
|
||||||
"OUTPUT TEMPLATE") can be compared with a
|
"OUTPUT TEMPLATE") can be compared with a
|
||||||
number or a string using the operators
|
number or a string using the operators
|
||||||
defined in "Filtering formats". You can
|
defined in "Filtering formats". You can
|
||||||
also simply specify a field to match if the
|
also simply specify a field to match if the
|
||||||
field is present and "!field" to check if
|
field is present, use "!field" to check if
|
||||||
the field is not present. In addition,
|
the field is not present, and "&" to check
|
||||||
Python style regular expression matching
|
multiple conditions. Use a "\" to escape
|
||||||
can be done using "~=", and multiple
|
"&" or quotes if needed. If used multiple
|
||||||
filters can be checked with "&". Use a "\"
|
times, the filter matches if atleast one of
|
||||||
to escape "&" or quotes if needed. Eg:
|
the conditions are met. Eg: --match-filter
|
||||||
--match-filter "!is_live & like_count>?100
|
!is_live --match-filter "like_count>?100 &
|
||||||
& description~='(?i)\bcats \& dogs\b'"
|
description~='(?i)\bcats \& dogs\b'"
|
||||||
matches only videos that are not live, has
|
matches only videos that are not live OR
|
||||||
a like count more than 100 (or the like
|
those that have a like count more than 100
|
||||||
field is not available), and also has a
|
(or the like field is not available) and
|
||||||
description that contains the phrase "cats
|
also has a description that contains the
|
||||||
& dogs" (ignoring case)
|
phrase "cats & dogs" (ignoring case)
|
||||||
--no-match-filter Do not use generic video filter (default)
|
--no-match-filter Do not use generic video filter (default)
|
||||||
--no-playlist Download only the video, if the URL refers
|
--no-playlist Download only the video, if the URL refers
|
||||||
to a video and a playlist
|
to a video and a playlist
|
||||||
@@ -604,11 +609,11 @@ You can also fork the project on github and run your fork's [build workflow](.gi
|
|||||||
--write-description etc. (default)
|
--write-description etc. (default)
|
||||||
--no-write-playlist-metafiles Do not write playlist metadata when using
|
--no-write-playlist-metafiles Do not write playlist metadata when using
|
||||||
--write-info-json, --write-description etc.
|
--write-info-json, --write-description etc.
|
||||||
--clean-infojson Remove some private fields such as
|
--clean-info-json Remove some private fields such as
|
||||||
filenames from the infojson. Note that it
|
filenames from the infojson. Note that it
|
||||||
could still contain some personal
|
could still contain some personal
|
||||||
information (default)
|
information (default)
|
||||||
--no-clean-infojson Write all fields to the infojson
|
--no-clean-info-json Write all fields to the infojson
|
||||||
--write-comments Retrieve video comments to be placed in the
|
--write-comments Retrieve video comments to be placed in the
|
||||||
infojson. The comments are fetched even
|
infojson. The comments are fetched even
|
||||||
without this option if the extraction is
|
without this option if the extraction is
|
||||||
@@ -686,6 +691,12 @@ You can also fork the project on github and run your fork's [build workflow](.gi
|
|||||||
Implies --quiet and --simulate (unless
|
Implies --quiet and --simulate (unless
|
||||||
--no-simulate is used). This option can be
|
--no-simulate is used). This option can be
|
||||||
used multiple times
|
used multiple times
|
||||||
|
--print-to-file [WHEN:]TEMPLATE FILE
|
||||||
|
Append given template to the file. The
|
||||||
|
values of WHEN and TEMPLATE are same as
|
||||||
|
that of --print. FILE uses the same syntax
|
||||||
|
as the output template. This option can be
|
||||||
|
used multiple times
|
||||||
-j, --dump-json Quiet, but print JSON information for each
|
-j, --dump-json Quiet, but print JSON information for each
|
||||||
video. Simulate unless --no-simulate is
|
video. Simulate unless --no-simulate is
|
||||||
used. See "OUTPUT TEMPLATE" for a
|
used. See "OUTPUT TEMPLATE" for a
|
||||||
@@ -723,13 +734,13 @@ You can also fork the project on github and run your fork's [build workflow](.gi
|
|||||||
|
|
||||||
## Workarounds:
|
## Workarounds:
|
||||||
--encoding ENCODING Force the specified encoding (experimental)
|
--encoding ENCODING Force the specified encoding (experimental)
|
||||||
|
--legacy-server-connect Explicitly allow HTTPS connection to
|
||||||
|
servers that do not support RFC 5746 secure
|
||||||
|
renegotiation
|
||||||
--no-check-certificates Suppress HTTPS certificate validation
|
--no-check-certificates Suppress HTTPS certificate validation
|
||||||
--prefer-insecure Use an unencrypted connection to retrieve
|
--prefer-insecure Use an unencrypted connection to retrieve
|
||||||
information about the video (Currently
|
information about the video (Currently
|
||||||
supported only for YouTube)
|
supported only for YouTube)
|
||||||
--user-agent UA Specify a custom user agent
|
|
||||||
--referer URL Specify a custom referer, use if the video
|
|
||||||
access is restricted to one domain
|
|
||||||
--add-header FIELD:VALUE Specify a custom HTTP header and its value,
|
--add-header FIELD:VALUE Specify a custom HTTP header and its value,
|
||||||
separated by a colon ":". You can use this
|
separated by a colon ":". You can use this
|
||||||
option multiple times
|
option multiple times
|
||||||
@@ -772,8 +783,8 @@ You can also fork the project on github and run your fork's [build workflow](.gi
|
|||||||
containers irrespective of quality
|
containers irrespective of quality
|
||||||
--no-prefer-free-formats Don't give any special preference to free
|
--no-prefer-free-formats Don't give any special preference to free
|
||||||
containers (default)
|
containers (default)
|
||||||
--check-formats Check that the selected formats are
|
--check-formats Make sure formats are selected only from
|
||||||
actually downloadable
|
those that are actually downloadable
|
||||||
--check-all-formats Check all formats for whether they are
|
--check-all-formats Check all formats for whether they are
|
||||||
actually downloadable
|
actually downloadable
|
||||||
--no-check-formats Do not check that the formats are actually
|
--no-check-formats Do not check that the formats are actually
|
||||||
@@ -830,15 +841,17 @@ You can also fork the project on github and run your fork's [build workflow](.gi
|
|||||||
(requires ffmpeg and ffprobe)
|
(requires ffmpeg and ffprobe)
|
||||||
--audio-format FORMAT Specify audio format to convert the audio
|
--audio-format FORMAT Specify audio format to convert the audio
|
||||||
to when -x is used. Currently supported
|
to when -x is used. Currently supported
|
||||||
formats are: best (default) or one of
|
formats are: best (default) or one of aac,
|
||||||
best|aac|flac|mp3|m4a|opus|vorbis|wav|alac
|
flac, mp3, m4a, opus, vorbis, wav, alac
|
||||||
--audio-quality QUALITY Specify ffmpeg audio quality, insert a
|
--audio-quality QUALITY Specify ffmpeg audio quality to use when
|
||||||
|
converting the audio with -x. Insert a
|
||||||
value between 0 (best) and 10 (worst) for
|
value between 0 (best) and 10 (worst) for
|
||||||
VBR or a specific bitrate like 128K
|
VBR or a specific bitrate like 128K
|
||||||
(default 5)
|
(default 5)
|
||||||
--remux-video FORMAT Remux the video into another container if
|
--remux-video FORMAT Remux the video into another container if
|
||||||
necessary (currently supported: mp4|mkv|flv
|
necessary (currently supported: mp4, mkv,
|
||||||
|webm|mov|avi|mp3|mka|m4a|ogg|opus). If
|
flv, webm, mov, avi, mka, ogg, aac, flac,
|
||||||
|
mp3, m4a, opus, vorbis, wav, alac). If
|
||||||
target container does not support the
|
target container does not support the
|
||||||
video/audio codec, remuxing will fail. You
|
video/audio codec, remuxing will fail. You
|
||||||
can specify multiple rules; Eg.
|
can specify multiple rules; Eg.
|
||||||
@@ -938,10 +951,10 @@ You can also fork the project on github and run your fork's [build workflow](.gi
|
|||||||
option can be used multiple times
|
option can be used multiple times
|
||||||
--no-exec Remove any previously defined --exec
|
--no-exec Remove any previously defined --exec
|
||||||
--convert-subs FORMAT Convert the subtitles to another format
|
--convert-subs FORMAT Convert the subtitles to another format
|
||||||
(currently supported: srt|vtt|ass|lrc)
|
(currently supported: srt, vtt, ass, lrc)
|
||||||
(Alias: --convert-subtitles)
|
(Alias: --convert-subtitles)
|
||||||
--convert-thumbnails FORMAT Convert the thumbnails to another format
|
--convert-thumbnails FORMAT Convert the thumbnails to another format
|
||||||
(currently supported: jpg|png)
|
(currently supported: jpg, png, webp)
|
||||||
--split-chapters Split video into multiple files based on
|
--split-chapters Split video into multiple files based on
|
||||||
internal chapters. The "chapter:" prefix
|
internal chapters. The "chapter:" prefix
|
||||||
can be used with "--paths" and "--output"
|
can be used with "--paths" and "--output"
|
||||||
@@ -972,15 +985,17 @@ You can also fork the project on github and run your fork's [build workflow](.gi
|
|||||||
semicolon ";" delimited list of NAME=VALUE.
|
semicolon ";" delimited list of NAME=VALUE.
|
||||||
The "when" argument determines when the
|
The "when" argument determines when the
|
||||||
postprocessor is invoked. It can be one of
|
postprocessor is invoked. It can be one of
|
||||||
"pre_process" (after extraction),
|
"pre_process" (after video extraction),
|
||||||
"before_dl" (before video download),
|
"after_filter" (after video passes filter),
|
||||||
"post_process" (after video download;
|
"before_dl" (before each video download),
|
||||||
default), "after_move" (after moving file
|
"post_process" (after each video download;
|
||||||
to their final locations), "after_video"
|
default), "after_move" (after moving video
|
||||||
(after downloading and processing all
|
file to it's final locations),
|
||||||
formats of a video), or "playlist" (end of
|
"after_video" (after downloading and
|
||||||
playlist). This option can be used multiple
|
processing all formats of a video), or
|
||||||
times to add different postprocessors
|
"playlist" (at end of playlist). This
|
||||||
|
option can be used multiple times to add
|
||||||
|
different postprocessors
|
||||||
|
|
||||||
## SponsorBlock Options:
|
## SponsorBlock Options:
|
||||||
Make chapter entries for, or remove various segments (sponsor,
|
Make chapter entries for, or remove various segments (sponsor,
|
||||||
@@ -1040,7 +1055,7 @@ You can configure yt-dlp by placing any supported command line option to a confi
|
|||||||
|
|
||||||
1. **Main Configuration**: The file given by `--config-location`
|
1. **Main Configuration**: The file given by `--config-location`
|
||||||
1. **Portable Configuration**: `yt-dlp.conf` in the same directory as the bundled binary. If you are running from source-code (`<root dir>/yt_dlp/__main__.py`), the root directory is used instead.
|
1. **Portable Configuration**: `yt-dlp.conf` in the same directory as the bundled binary. If you are running from source-code (`<root dir>/yt_dlp/__main__.py`), the root directory is used instead.
|
||||||
1. **Home Configuration**: `yt-dlp.conf` in the home path given by `-P "home:<path>"`, or in the current directory if no such path is given
|
1. **Home Configuration**: `yt-dlp.conf` in the home path given by `-P`, or in the current directory if no such path is given
|
||||||
1. **User Configuration**:
|
1. **User Configuration**:
|
||||||
* `%XDG_CONFIG_HOME%/yt-dlp/config` (recommended on Linux/macOS)
|
* `%XDG_CONFIG_HOME%/yt-dlp/config` (recommended on Linux/macOS)
|
||||||
* `%XDG_CONFIG_HOME%/yt-dlp.conf`
|
* `%XDG_CONFIG_HOME%/yt-dlp.conf`
|
||||||
@@ -1127,12 +1142,13 @@ To summarize, the general syntax for a field is:
|
|||||||
%(name[.keys][addition][>strf][,alternate][&replacement][|default])[flags][width][.precision][length]type
|
%(name[.keys][addition][>strf][,alternate][&replacement][|default])[flags][width][.precision][length]type
|
||||||
```
|
```
|
||||||
|
|
||||||
Additionally, you can set different output templates for the various metadata files separately from the general output template by specifying the type of file followed by the template separated by a colon `:`. The different file types supported are `subtitle`, `thumbnail`, `description`, `annotation` (deprecated), `infojson`, `link`, `pl_thumbnail`, `pl_description`, `pl_infojson`, `chapter`, `pl_video`. For example, `-o "%(title)s.%(ext)s" -o "thumbnail:%(title)s\%(title)s.%(ext)s"` will put the thumbnails in a folder with the same name as the video. If any of the templates (except default) is empty, that type of file will not be written. Eg: `--write-thumbnail -o "thumbnail:"` will write thumbnails only for playlists and not for video.
|
Additionally, you can set different output templates for the various metadata files separately from the general output template by specifying the type of file followed by the template separated by a colon `:`. The different file types supported are `subtitle`, `thumbnail`, `description`, `annotation` (deprecated), `infojson`, `link`, `pl_thumbnail`, `pl_description`, `pl_infojson`, `chapter`, `pl_video`. For example, `-o "%(title)s.%(ext)s" -o "thumbnail:%(title)s\%(title)s.%(ext)s"` will put the thumbnails in a folder with the same name as the video. If any of the templates is empty, that type of file will not be written. Eg: `--write-thumbnail -o "thumbnail:"` will write thumbnails only for playlists and not for video.
|
||||||
|
|
||||||
The available fields are:
|
The available fields are:
|
||||||
|
|
||||||
- `id` (string): Video identifier
|
- `id` (string): Video identifier
|
||||||
- `title` (string): Video title
|
- `title` (string): Video title
|
||||||
|
- `fulltitle` (string): Video title ignoring live timestamp and generic title
|
||||||
- `url` (string): Video URL
|
- `url` (string): Video URL
|
||||||
- `ext` (string): Video filename extension
|
- `ext` (string): Video filename extension
|
||||||
- `alt_title` (string): A secondary title of the video
|
- `alt_title` (string): A secondary title of the video
|
||||||
@@ -1142,11 +1158,11 @@ The available fields are:
|
|||||||
- `license` (string): License name the video is licensed under
|
- `license` (string): License name the video is licensed under
|
||||||
- `creator` (string): The creator of the video
|
- `creator` (string): The creator of the video
|
||||||
- `timestamp` (numeric): UNIX timestamp of the moment the video became available
|
- `timestamp` (numeric): UNIX timestamp of the moment the video became available
|
||||||
- `upload_date` (string): Video upload date (YYYYMMDD)
|
- `upload_date` (string): Video upload date in UTC (YYYYMMDD)
|
||||||
- `release_timestamp` (numeric): UNIX timestamp of the moment the video was released
|
- `release_timestamp` (numeric): UNIX timestamp of the moment the video was released
|
||||||
- `release_date` (string): The date (YYYYMMDD) when the video was released
|
- `release_date` (string): The date (YYYYMMDD) when the video was released in UTC
|
||||||
- `modified_timestamp` (numeric): UNIX timestamp of the moment the video was last modified
|
- `modified_timestamp` (numeric): UNIX timestamp of the moment the video was last modified
|
||||||
- `modified_date` (string): The date (YYYYMMDD) when the video was last modified
|
- `modified_date` (string): The date (YYYYMMDD) when the video was last modified in UTC
|
||||||
- `uploader_id` (string): Nickname or id of the video uploader
|
- `uploader_id` (string): Nickname or id of the video uploader
|
||||||
- `channel` (string): Full name of the channel the video is uploaded on
|
- `channel` (string): Full name of the channel the video is uploaded on
|
||||||
- `channel_id` (string): Id of the channel
|
- `channel_id` (string): Id of the channel
|
||||||
@@ -1188,16 +1204,16 @@ The available fields are:
|
|||||||
- `protocol` (string): The protocol that will be used for the actual download
|
- `protocol` (string): The protocol that will be used for the actual download
|
||||||
- `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 when creating the file
|
- `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`
|
||||||
- `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` (string): Name or id of the playlist that contains the video
|
- `playlist_id` (string): Identifier of the playlist that contains the video
|
||||||
|
- `playlist_title` (string): Name of the playlist that contains the video
|
||||||
|
- `playlist` (string): `playlist_id` or `playlist_title`
|
||||||
- `playlist_count` (numeric): Total number of items in the playlist. May not be known if entire playlist is not extracted
|
- `playlist_count` (numeric): Total number of items in the playlist. May not be known if entire playlist is not extracted
|
||||||
- `playlist_index` (numeric): Index of the video in the playlist padded with leading zeros according the final index
|
- `playlist_index` (numeric): Index of the video in the playlist padded with leading zeros according the final index
|
||||||
- `playlist_autonumber` (numeric): Position of the video in the playlist download queue padded with leading zeros according to the total length of the playlist
|
- `playlist_autonumber` (numeric): Position of the video in the playlist download queue padded with leading zeros according to the total length of the playlist
|
||||||
- `playlist_id` (string): Playlist identifier
|
|
||||||
- `playlist_title` (string): Playlist title
|
|
||||||
- `playlist_uploader` (string): Full name of the playlist uploader
|
- `playlist_uploader` (string): Full name of the playlist uploader
|
||||||
- `playlist_uploader_id` (string): Nickname or id of the playlist uploader
|
- `playlist_uploader_id` (string): Nickname or id of the playlist uploader
|
||||||
- `webpage_url` (string): A URL to the video webpage which if given to yt-dlp should allow to get the same result again
|
- `webpage_url` (string): A URL to the video webpage which if given to yt-dlp should allow to get the same result again
|
||||||
@@ -1351,7 +1367,7 @@ You can also use special names to select particular edge case formats:
|
|||||||
- `bv`, `bestvideo`: Select the best quality **video-only** format. Equivalent to `best*[acodec=none]`
|
- `bv`, `bestvideo`: Select the best quality **video-only** format. Equivalent to `best*[acodec=none]`
|
||||||
- `bv*`, `bestvideo*`: Select the best quality format that **contains video**. It may also contain audio. Equivalent to `best*[vcodec!=none]`
|
- `bv*`, `bestvideo*`: Select the best quality format that **contains video**. It may also contain audio. Equivalent to `best*[vcodec!=none]`
|
||||||
- `ba`, `bestaudio`: Select the best quality **audio-only** format. Equivalent to `best*[vcodec=none]`
|
- `ba`, `bestaudio`: Select the best quality **audio-only** format. Equivalent to `best*[vcodec=none]`
|
||||||
- `ba*`, `bestaudio*`: Select the best quality format that **contains audio**. It may also contain video. Equivalent to `best*[acodec!=none]`
|
- `ba*`, `bestaudio*`: Select the best quality format that **contains audio**. It may also contain video. Equivalent to `best*[acodec!=none]` ([Do not use!](https://github.com/yt-dlp/yt-dlp/issues/979#issuecomment-919629354))
|
||||||
- `w*`, `worst*`: Select the worst quality format that contains either a video or an audio
|
- `w*`, `worst*`: Select the worst quality format that contains either a video or an audio
|
||||||
- `w`, `worst`: Select the worst quality format that contains both video and audio. Equivalent to `worst*[vcodec!=none][acodec!=none]`
|
- `w`, `worst`: Select the worst quality format that contains both video and audio. Equivalent to `worst*[vcodec!=none][acodec!=none]`
|
||||||
- `wv`, `worstvideo`: Select the worst quality video-only format. Equivalent to `worst*[acodec=none]`
|
- `wv`, `worstvideo`: Select the worst quality video-only format. Equivalent to `worst*[acodec=none]`
|
||||||
@@ -1359,7 +1375,7 @@ You can also use special names to select particular edge case formats:
|
|||||||
- `wa`, `worstaudio`: Select the worst quality audio-only format. Equivalent to `worst*[vcodec=none]`
|
- `wa`, `worstaudio`: Select the worst quality audio-only format. Equivalent to `worst*[vcodec=none]`
|
||||||
- `wa*`, `worstaudio*`: Select the worst quality format that contains audio. It may also contain video. Equivalent to `worst*[acodec!=none]`
|
- `wa*`, `worstaudio*`: Select the worst quality format that contains audio. It may also contain video. Equivalent to `worst*[acodec!=none]`
|
||||||
|
|
||||||
For example, to download the worst quality video-only format you can use `-f worstvideo`. It is however recommended not to use `worst` and related options. When your format selector is `worst`, the format which is worst in all respects is selected. Most of the time, what you actually want is the video with the smallest filesize instead. So it is generally better to use `-f best -S +size,+br,+res,+fps` instead of `-f worst`. See [sorting formats](#sorting-formats) for more details.
|
For example, to download the worst quality video-only format you can use `-f worstvideo`. It is however recommended not to use `worst` and related options. When your format selector is `worst`, the format which is worst in all respects is selected. Most of the time, what you actually want is the video with the smallest filesize instead. So it is generally better to use `-S +size` or more rigorously, `-S +size,+br,+res,+fps` instead of `-f worst`. See [sorting formats](#sorting-formats) for more details.
|
||||||
|
|
||||||
You can select the n'th best format of a type by using `best<type>.<n>`. For example, `best.2` will select the 2nd best combined format. Similarly, `bv*.3` will select the 3rd best format that contains a video stream.
|
You can select the n'th best format of a type by using `best<type>.<n>`. For example, `best.2` will select the 2nd best combined format. Similarly, `bv*.3` will select the 3rd best format that contains a video stream.
|
||||||
|
|
||||||
@@ -1388,7 +1404,7 @@ The following numeric meta fields can be used with comparisons `<`, `<=`, `>`, `
|
|||||||
- `asr`: Audio sampling rate in Hertz
|
- `asr`: Audio sampling rate in Hertz
|
||||||
- `fps`: Frame rate
|
- `fps`: Frame rate
|
||||||
|
|
||||||
Also filtering work for comparisons `=` (equals), `^=` (starts with), `$=` (ends with), `*=` (contains) and following string meta fields:
|
Also filtering work for comparisons `=` (equals), `^=` (starts with), `$=` (ends with), `*=` (contains), `~=` (matches regex) and following string meta fields:
|
||||||
|
|
||||||
- `ext`: File extension
|
- `ext`: File extension
|
||||||
- `acodec`: Name of the audio codec in use
|
- `acodec`: Name of the audio codec in use
|
||||||
@@ -1398,7 +1414,7 @@ Also filtering work for comparisons `=` (equals), `^=` (starts with), `$=` (ends
|
|||||||
- `format_id`: A short description of the format
|
- `format_id`: A short description of the format
|
||||||
- `language`: Language code
|
- `language`: Language code
|
||||||
|
|
||||||
Any string comparison may be prefixed with negation `!` in order to produce an opposite comparison, e.g. `!*=` (does not contain).
|
Any string comparison may be prefixed with negation `!` in order to produce an opposite comparison, e.g. `!*=` (does not contain). The comparand of a string comparison needs to be quoted with either double or single quotes if it contains spaces or special characters other than `._-`.
|
||||||
|
|
||||||
Note that 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 that 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.
|
||||||
|
|
||||||
@@ -1541,8 +1557,9 @@ $ yt-dlp -S "proto"
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Download the best video with h264 codec, or the best video if there is no such video
|
# Download the best video with either h264 or h265 codec,
|
||||||
$ yt-dlp -f "(bv*+ba/b)[vcodec^=avc1] / (bv*+ba/b)"
|
# or the best video if there is no such video
|
||||||
|
$ yt-dlp -f "(bv*[vcodec~='^((he|a)vc|h26[45])']+ba) / (bv*+ba/b)"
|
||||||
|
|
||||||
# Download the best video with best codec no better than h264,
|
# Download the best video with best codec no better than h264,
|
||||||
# or the best video with worst codec if there is no such video
|
# or the best video with worst codec if there is no such video
|
||||||
@@ -1587,25 +1604,28 @@ This option also has a few special uses:
|
|||||||
* You can download an additional URL based on the metadata of the currently downloaded video. To do this, set the field `additional_urls` to the URL that you want to download. Eg: `--parse-metadata "description:(?P<additional_urls>https?://www\.vimeo\.com/\d+)` will download the first vimeo video found in the description
|
* You can download an additional URL based on the metadata of the currently downloaded video. To do this, set the field `additional_urls` to the URL that you want to download. Eg: `--parse-metadata "description:(?P<additional_urls>https?://www\.vimeo\.com/\d+)` will download the first vimeo video found in the description
|
||||||
* You can use this to change the metadata that is embedded in the media file. To do this, set the value of the corresponding field with a `meta_` prefix. For example, any value you set to `meta_description` field will be added to the `description` field in the file. For example, you can use this to set a different "description" and "synopsis". To modify the metadata of individual streams, use the `meta<n>_` prefix (Eg: `meta1_language`). Any value set to the `meta_` field will overwrite all default values.
|
* You can use this to change the metadata that is embedded in the media file. To do this, set the value of the corresponding field with a `meta_` prefix. For example, any value you set to `meta_description` field will be added to the `description` field in the file. For example, you can use this to set a different "description" and "synopsis". To modify the metadata of individual streams, use the `meta<n>_` prefix (Eg: `meta1_language`). Any value set to the `meta_` field will overwrite all default values.
|
||||||
|
|
||||||
|
**Note**: Metadata modification happens before format selection, post-extraction and other post-processing operations. Some fields may be added or changed during these steps, overriding your changes.
|
||||||
|
|
||||||
For reference, these are the fields yt-dlp adds by default to the file metadata:
|
For reference, these are the fields yt-dlp adds by default to the file metadata:
|
||||||
|
|
||||||
Metadata fields|From
|
Metadata fields | From
|
||||||
:---|:---
|
:--------------------------|:------------------------------------------------
|
||||||
`title`|`track` or `title`
|
`title` | `track` or `title`
|
||||||
`date`|`upload_date`
|
`date` | `upload_date`
|
||||||
`description`, `synopsis`|`description`
|
`description`, `synopsis` | `description`
|
||||||
`purl`, `comment`|`webpage_url`
|
`purl`, `comment` | `webpage_url`
|
||||||
`track`|`track_number`
|
`track` | `track_number`
|
||||||
`artist`|`artist`, `creator`, `uploader` or `uploader_id`
|
`artist` | `artist`, `creator`, `uploader` or `uploader_id`
|
||||||
`genre`|`genre`
|
`genre` | `genre`
|
||||||
`album`|`album`
|
`album` | `album`
|
||||||
`album_artist`|`album_artist`
|
`album_artist` | `album_artist`
|
||||||
`disc`|`disc_number`
|
`disc` | `disc_number`
|
||||||
`show`|`series`
|
`show` | `series`
|
||||||
`season_number`|`season_number`
|
`season_number` | `season_number`
|
||||||
`episode_id`|`episode` or `episode_id`
|
`episode_id` | `episode` or `episode_id`
|
||||||
`episode_sort`|`episode_number`
|
`episode_sort` | `episode_number`
|
||||||
`language` of each stream|From the format's `language`
|
`language` of each stream | the format's `language`
|
||||||
|
|
||||||
**Note**: The file format may not support some of these fields
|
**Note**: The file format may not support some of these fields
|
||||||
|
|
||||||
|
|
||||||
@@ -1621,7 +1641,11 @@ $ yt-dlp --parse-metadata "description:Artist - (?P<artist>.+)"
|
|||||||
# Set title as "Series name S01E05"
|
# Set title as "Series name S01E05"
|
||||||
$ yt-dlp --parse-metadata "%(series)s S%(season_number)02dE%(episode_number)02d:%(title)s"
|
$ yt-dlp --parse-metadata "%(series)s S%(season_number)02dE%(episode_number)02d:%(title)s"
|
||||||
|
|
||||||
# Set "comment" field in video metadata using description instead of webpage_url
|
# Prioritize uploader as the "artist" field in video metadata
|
||||||
|
$ yt-dlp --parse-metadata "%(uploader|)s:%(meta_artist)s" --add-metadata
|
||||||
|
|
||||||
|
# Set "comment" field in video metadata using description instead of webpage_url,
|
||||||
|
# handling multiple lines correctly
|
||||||
$ yt-dlp --parse-metadata "description:(?s)(?P<meta_comment>.+)" --add-metadata
|
$ yt-dlp --parse-metadata "description:(?s)(?P<meta_comment>.+)" --add-metadata
|
||||||
|
|
||||||
# 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
|
||||||
@@ -1634,23 +1658,22 @@ $ yt-dlp --replace-in-metadata "title,uploader" "[ _]" "-"
|
|||||||
|
|
||||||
# EXTRACTOR ARGUMENTS
|
# EXTRACTOR ARGUMENTS
|
||||||
|
|
||||||
Some extractors accept additional arguments which can be passed using `--extractor-args KEY:ARGS`. `ARGS` is a `;` (semicolon) separated string of `ARG=VAL1,VAL2`. Eg: `--extractor-args "youtube:player-client=android_agegate,web;include_live_dash" --extractor-args "funimation:version=uncut"`
|
Some extractors accept additional arguments which can be passed using `--extractor-args KEY:ARGS`. `ARGS` is a `;` (semicolon) separated string of `ARG=VAL1,VAL2`. Eg: `--extractor-args "youtube:player-client=android_embedded,web;include_live_dash" --extractor-args "funimation:version=uncut"`
|
||||||
|
|
||||||
The following extractors use this feature:
|
The following extractors use this feature:
|
||||||
|
|
||||||
#### youtube
|
#### youtube
|
||||||
* `skip`: `hls` or `dash` (or both) to skip download of the respective manifests
|
* `skip`: One or more of `hls`, `dash` or `translated_subs` to skip extraction of the m3u8 manifests, dash manifests and auto-translated subtitles respectively
|
||||||
* `player_client`: Clients to extract video data from. The main clients are `web`, `android`, `ios`, `mweb`. These also have `_music`, `_embedded`, `_agegate`, and `_creator` variants (Eg: `web_embedded`) (`mweb` has only `_agegate`). By default, `android,web` is used, but the agegate and creator variants are added as required for age-gated videos. Similarly the music variants are added for `music.youtube.com` urls. You can also 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` (Eg: `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_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
|
||||||
* `include_live_dash`: Include live dash formats even without `--live-from-start` (These formats don't download properly)
|
* `include_live_dash`: Include live dash formats even without `--live-from-start` (These formats don't download properly)
|
||||||
* `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
|
||||||
* `max_comment_depth` Maximum depth for nested comments. YouTube supports depths 1 or 2 (default)
|
|
||||||
* **Deprecated**: Set `max-replies` to `0` or `all` in `max_comments` instead (e.g. `max_comments=all,all,0` to get no replies)
|
|
||||||
|
|
||||||
#### youtubetab (YouTube playlists, channels, feeds, etc.)
|
#### youtubetab (YouTube playlists, channels, feeds, etc.)
|
||||||
* `skip`: One or more of `webpage` (skip initial webpage download), `authcheck` (allow the download of playlists requiring authentication when no initial webpage is downloaded. This may cause unwanted behavior, see [#1122](https://github.com/yt-dlp/yt-dlp/pull/1122) for more details)
|
* `skip`: One or more of `webpage` (skip initial webpage download), `authcheck` (allow the download of playlists requiring authentication when no initial webpage is downloaded. This may cause unwanted behavior, see [#1122](https://github.com/yt-dlp/yt-dlp/pull/1122) for more details)
|
||||||
|
* `approximate_date`: Extract approximate `upload_date` in flat-playlist. This may cause date-based filters to be slightly off
|
||||||
|
|
||||||
#### funimation
|
#### funimation
|
||||||
* `language`: Languages to extract. Eg: `funimation:language=english,japanese`
|
* `language`: Languages to extract. Eg: `funimation:language=english,japanese`
|
||||||
@@ -1660,9 +1683,17 @@ The following extractors use this feature:
|
|||||||
* `language`: Languages to extract. Eg: `crunchyroll:language=jaJp`
|
* `language`: Languages to extract. Eg: `crunchyroll:language=jaJp`
|
||||||
* `hardsub`: Which hard-sub versions to extract. Eg: `crunchyroll:hardsub=None,enUS`
|
* `hardsub`: Which hard-sub versions to extract. Eg: `crunchyroll:hardsub=None,enUS`
|
||||||
|
|
||||||
|
#### crunchyrollbeta
|
||||||
|
* `format`: Which stream type(s) to extract. Default is `adaptive_hls` Eg: `crunchyrollbeta:format=vo_adaptive_hls`
|
||||||
|
* Potentially useful values include `adaptive_hls`, `adaptive_dash`, `vo_adaptive_hls`, `vo_adaptive_dash`, `download_hls`, `trailer_hls`, `trailer_dash`
|
||||||
|
* `hardsub`: Preference order for which hardsub versions to extract. Default is `None` (no hardsubs). Eg: `crunchyrollbeta:hardsub=en-US,None`
|
||||||
|
|
||||||
#### vikichannel
|
#### vikichannel
|
||||||
* `video_types`: Types of videos to download - one or more of `episodes`, `movies`, `clips`, `trailers`
|
* `video_types`: Types of videos to download - one or more of `episodes`, `movies`, `clips`, `trailers`
|
||||||
|
|
||||||
|
#### niconico
|
||||||
|
* `segment_duration`: Segment duration in milliseconds for HLS-DMC formats. Use it at your own risk since this feature **may result in your account termination.**
|
||||||
|
|
||||||
#### youtubewebarchive
|
#### youtubewebarchive
|
||||||
* `check_all`: Try to check more at the cost of more requests. One or more of `thumbnails`, `captures`
|
* `check_all`: Try to check more at the cost of more requests. One or more of `thumbnails`, `captures`
|
||||||
|
|
||||||
@@ -1674,6 +1705,14 @@ The following extractors use this feature:
|
|||||||
* `vcodec`: vcodec to ignore - one or more of `h264`, `h265`, `dvh265`
|
* `vcodec`: vcodec to ignore - one or more of `h264`, `h265`, `dvh265`
|
||||||
* `dr`: dynamic range to ignore - one or more of `sdr`, `hdr10`, `dv`
|
* `dr`: dynamic range to ignore - one or more of `sdr`, `hdr10`, `dv`
|
||||||
|
|
||||||
|
#### tiktok
|
||||||
|
* `app_version`: App version to call mobile APIs with - should be set along with `manifest_app_version`. (e.g. `20.2.1`)
|
||||||
|
* `manifest_app_version`: Numeric app version to call mobile APIs with. (e.g. `221`)
|
||||||
|
|
||||||
|
#### rokfinchannel
|
||||||
|
* `tab`: Which tab to download. One of `new`, `top`, `videos`, `podcasts`, `streams`, `stacks`. (E.g. `rokfinchannel:tab=streams`)
|
||||||
|
|
||||||
|
|
||||||
NOTE: These options may be changed/removed in the future without concern for backward compatibility
|
NOTE: These options may be changed/removed in the future without concern for backward compatibility
|
||||||
|
|
||||||
<!-- MANPAGE: MOVE "INSTALLATION" SECTION HERE -->
|
<!-- MANPAGE: MOVE "INSTALLATION" SECTION HERE -->
|
||||||
@@ -1709,7 +1748,7 @@ with YoutubeDL(ydl_opts) as ydl:
|
|||||||
ydl.download(['https://www.youtube.com/watch?v=BaW_jenozKc'])
|
ydl.download(['https://www.youtube.com/watch?v=BaW_jenozKc'])
|
||||||
```
|
```
|
||||||
|
|
||||||
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#L191).
|
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#L197).
|
||||||
|
|
||||||
Here's a more complete example demonstrating various functionality:
|
Here's a more complete example demonstrating various functionality:
|
||||||
|
|
||||||
@@ -1790,12 +1829,11 @@ ydl_opts = {
|
|||||||
}],
|
}],
|
||||||
'logger': MyLogger(),
|
'logger': MyLogger(),
|
||||||
'progress_hooks': [my_hook],
|
'progress_hooks': [my_hook],
|
||||||
|
# Add custom headers
|
||||||
|
'http_headers': {'Referer': 'https://www.google.com'}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Add custom headers
|
|
||||||
yt_dlp.utils.std_headers.update({'Referer': 'https://www.google.com'})
|
|
||||||
|
|
||||||
# ℹ️ See the public functions in yt_dlp.YoutubeDL for for other available functions.
|
# ℹ️ See the public functions in yt_dlp.YoutubeDL for for other available functions.
|
||||||
# Eg: "ydl.download", "ydl.download_with_info_file"
|
# Eg: "ydl.download", "ydl.download_with_info_file"
|
||||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||||
@@ -1838,6 +1876,8 @@ While these options are redundant, they are still expected to be used due to the
|
|||||||
--reject-title REGEX --match-filter "title !~= (?i)REGEX"
|
--reject-title REGEX --match-filter "title !~= (?i)REGEX"
|
||||||
--min-views COUNT --match-filter "view_count >=? COUNT"
|
--min-views COUNT --match-filter "view_count >=? COUNT"
|
||||||
--max-views COUNT --match-filter "view_count <=? COUNT"
|
--max-views COUNT --match-filter "view_count <=? COUNT"
|
||||||
|
--user-agent UA --add-header "User-Agent:UA"
|
||||||
|
--referer URL --add-header "Referer:URL"
|
||||||
|
|
||||||
|
|
||||||
#### Not recommended
|
#### Not recommended
|
||||||
@@ -1875,11 +1915,13 @@ These options are not intended to be used by the end-user
|
|||||||
These are aliases that are no longer documented for various reasons
|
These are aliases that are no longer documented for various reasons
|
||||||
|
|
||||||
--avconv-location --ffmpeg-location
|
--avconv-location --ffmpeg-location
|
||||||
|
--clean-infojson --clean-info-json
|
||||||
--cn-verification-proxy URL --geo-verification-proxy URL
|
--cn-verification-proxy URL --geo-verification-proxy URL
|
||||||
--dump-headers --print-traffic
|
--dump-headers --print-traffic
|
||||||
--dump-intermediate-pages --dump-pages
|
--dump-intermediate-pages --dump-pages
|
||||||
--force-write-download-archive --force-write-archive
|
--force-write-download-archive --force-write-archive
|
||||||
--load-info --load-info-json
|
--load-info --load-info-json
|
||||||
|
--no-clean-infojson --no-clean-info-json
|
||||||
--no-split-tracks --no-split-chapters
|
--no-split-tracks --no-split-chapters
|
||||||
--no-write-srt --no-write-subs
|
--no-write-srt --no-write-subs
|
||||||
--prefer-unsecure --prefer-insecure
|
--prefer-unsecure --prefer-insecure
|
||||||
|
|||||||
@@ -24,10 +24,9 @@ def main():
|
|||||||
def gen_ies_md(ies):
|
def gen_ies_md(ies):
|
||||||
for ie in ies:
|
for ie in ies:
|
||||||
ie_md = '**{0}**'.format(ie.IE_NAME)
|
ie_md = '**{0}**'.format(ie.IE_NAME)
|
||||||
ie_desc = getattr(ie, 'IE_DESC', None)
|
if ie.IE_DESC is False:
|
||||||
if ie_desc is False:
|
|
||||||
continue
|
continue
|
||||||
if ie_desc is not None:
|
if ie.IE_DESC is not None:
|
||||||
ie_md += ': {0}'.format(ie.IE_DESC)
|
ie_md += ': {0}'.format(ie.IE_DESC)
|
||||||
search_key = getattr(ie, 'SEARCH_KEY', None)
|
search_key = getattr(ie, 'SEARCH_KEY', None)
|
||||||
if search_key is not None:
|
if search_key is not None:
|
||||||
|
|||||||
@@ -75,21 +75,21 @@ def filter_options(readme):
|
|||||||
section = re.search(r'(?sm)^# USAGE AND OPTIONS\n.+?(?=^# )', readme).group(0)
|
section = re.search(r'(?sm)^# USAGE AND OPTIONS\n.+?(?=^# )', readme).group(0)
|
||||||
options = '# OPTIONS\n'
|
options = '# OPTIONS\n'
|
||||||
for line in section.split('\n')[1:]:
|
for line in section.split('\n')[1:]:
|
||||||
if line.lstrip().startswith('-'):
|
mobj = re.fullmatch(r'''(?x)
|
||||||
split = re.split(r'\s{2,}', line.lstrip())
|
\s{4}(?P<opt>-(?:,\s|[^\s])+)
|
||||||
# Description string may start with `-` as well. If there is
|
(?:\s(?P<meta>(?:[^\s]|\s(?!\s))+))?
|
||||||
# only one piece then it's a description bit not an option.
|
(\s{2,}(?P<desc>.+))?
|
||||||
if len(split) > 1:
|
''', line)
|
||||||
option, description = split
|
if not mobj:
|
||||||
split_option = option.split(' ')
|
options += f'{line.lstrip()}\n'
|
||||||
|
continue
|
||||||
|
option, metavar, description = mobj.group('opt', 'meta', 'desc')
|
||||||
|
|
||||||
if not split_option[-1].startswith('-'): # metavar
|
# Pandoc's definition_lists. See http://pandoc.org/README.html
|
||||||
option = ' '.join(split_option[:-1] + [f'*{split_option[-1]}*'])
|
option = f'{option} *{metavar}*' if metavar else option
|
||||||
|
description = f'{description}\n' if description else ''
|
||||||
# Pandoc's definition_lists. See http://pandoc.org/README.html
|
options += f'\n{option}\n: {description}'
|
||||||
options += f'\n{option}\n: {description}\n'
|
continue
|
||||||
continue
|
|
||||||
options += line.lstrip() + '\n'
|
|
||||||
|
|
||||||
return readme.replace(section, options, 1)
|
return readme.replace(section, options, 1)
|
||||||
|
|
||||||
|
|||||||
1
docs/.gitignore
vendored
1
docs/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
_build/
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
orphan: true
|
|
||||||
---
|
|
||||||
```{include} ../Changelog.md
|
|
||||||
```
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
orphan: true
|
|
||||||
---
|
|
||||||
```{include} ../Collaborators.md
|
|
||||||
```
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
orphan: true
|
|
||||||
---
|
|
||||||
```{include} ../Contributing.md
|
|
||||||
```
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
---
|
|
||||||
orphan: true
|
|
||||||
---
|
|
||||||
# LICENSE
|
|
||||||
```{include} ../LICENSE
|
|
||||||
```
|
|
||||||
177
docs/Makefile
177
docs/Makefile
@@ -1,177 +0,0 @@
|
|||||||
# Makefile for Sphinx documentation
|
|
||||||
#
|
|
||||||
|
|
||||||
# You can set these variables from the command line.
|
|
||||||
SPHINXOPTS =
|
|
||||||
SPHINXBUILD = sphinx-build
|
|
||||||
PAPER =
|
|
||||||
BUILDDIR = _build
|
|
||||||
|
|
||||||
# User-friendly check for sphinx-build
|
|
||||||
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
|
|
||||||
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
|
|
||||||
endif
|
|
||||||
|
|
||||||
# Internal variables.
|
|
||||||
PAPEROPT_a4 = -D latex_paper_size=a4
|
|
||||||
PAPEROPT_letter = -D latex_paper_size=letter
|
|
||||||
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
|
||||||
# the i18n builder cannot share the environment and doctrees with the others
|
|
||||||
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
|
||||||
|
|
||||||
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
|
|
||||||
|
|
||||||
help:
|
|
||||||
@echo "Please use \`make <target>' where <target> is one of"
|
|
||||||
@echo " html to make standalone HTML files"
|
|
||||||
@echo " dirhtml to make HTML files named index.html in directories"
|
|
||||||
@echo " singlehtml to make a single large HTML file"
|
|
||||||
@echo " pickle to make pickle files"
|
|
||||||
@echo " json to make JSON files"
|
|
||||||
@echo " htmlhelp to make HTML files and a HTML help project"
|
|
||||||
@echo " qthelp to make HTML files and a qthelp project"
|
|
||||||
@echo " devhelp to make HTML files and a Devhelp project"
|
|
||||||
@echo " epub to make an epub"
|
|
||||||
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
|
||||||
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
|
||||||
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
|
|
||||||
@echo " text to make text files"
|
|
||||||
@echo " man to make manual pages"
|
|
||||||
@echo " texinfo to make Texinfo files"
|
|
||||||
@echo " info to make Texinfo files and run them through makeinfo"
|
|
||||||
@echo " gettext to make PO message catalogs"
|
|
||||||
@echo " changes to make an overview of all changed/added/deprecated items"
|
|
||||||
@echo " xml to make Docutils-native XML files"
|
|
||||||
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
|
|
||||||
@echo " linkcheck to check all external links for integrity"
|
|
||||||
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
|
||||||
|
|
||||||
clean:
|
|
||||||
rm -rf $(BUILDDIR)/*
|
|
||||||
|
|
||||||
html:
|
|
||||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
|
||||||
@echo
|
|
||||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
|
||||||
|
|
||||||
dirhtml:
|
|
||||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
|
||||||
@echo
|
|
||||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
|
||||||
|
|
||||||
singlehtml:
|
|
||||||
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
|
||||||
@echo
|
|
||||||
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
|
||||||
|
|
||||||
pickle:
|
|
||||||
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
|
||||||
@echo
|
|
||||||
@echo "Build finished; now you can process the pickle files."
|
|
||||||
|
|
||||||
json:
|
|
||||||
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
|
||||||
@echo
|
|
||||||
@echo "Build finished; now you can process the JSON files."
|
|
||||||
|
|
||||||
htmlhelp:
|
|
||||||
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
|
||||||
@echo
|
|
||||||
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
|
||||||
".hhp project file in $(BUILDDIR)/htmlhelp."
|
|
||||||
|
|
||||||
qthelp:
|
|
||||||
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
|
||||||
@echo
|
|
||||||
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
|
||||||
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
|
||||||
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/yt-dlp.qhcp"
|
|
||||||
@echo "To view the help file:"
|
|
||||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/yt-dlp.qhc"
|
|
||||||
|
|
||||||
devhelp:
|
|
||||||
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
|
||||||
@echo
|
|
||||||
@echo "Build finished."
|
|
||||||
@echo "To view the help file:"
|
|
||||||
@echo "# mkdir -p $$HOME/.local/share/devhelp/yt-dlp"
|
|
||||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/yt-dlp"
|
|
||||||
@echo "# devhelp"
|
|
||||||
|
|
||||||
epub:
|
|
||||||
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
|
||||||
@echo
|
|
||||||
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
|
||||||
|
|
||||||
latex:
|
|
||||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
|
||||||
@echo
|
|
||||||
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
|
||||||
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
|
||||||
"(use \`make latexpdf' here to do that automatically)."
|
|
||||||
|
|
||||||
latexpdf:
|
|
||||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
|
||||||
@echo "Running LaTeX files through pdflatex..."
|
|
||||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf
|
|
||||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
|
||||||
|
|
||||||
latexpdfja:
|
|
||||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
|
||||||
@echo "Running LaTeX files through platex and dvipdfmx..."
|
|
||||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
|
|
||||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
|
||||||
|
|
||||||
text:
|
|
||||||
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
|
||||||
@echo
|
|
||||||
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
|
||||||
|
|
||||||
man:
|
|
||||||
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
|
||||||
@echo
|
|
||||||
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
|
||||||
|
|
||||||
texinfo:
|
|
||||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
|
||||||
@echo
|
|
||||||
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
|
||||||
@echo "Run \`make' in that directory to run these through makeinfo" \
|
|
||||||
"(use \`make info' here to do that automatically)."
|
|
||||||
|
|
||||||
info:
|
|
||||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
|
||||||
@echo "Running Texinfo files through makeinfo..."
|
|
||||||
make -C $(BUILDDIR)/texinfo info
|
|
||||||
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
|
||||||
|
|
||||||
gettext:
|
|
||||||
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
|
|
||||||
@echo
|
|
||||||
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
|
|
||||||
|
|
||||||
changes:
|
|
||||||
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
|
||||||
@echo
|
|
||||||
@echo "The overview file is in $(BUILDDIR)/changes."
|
|
||||||
|
|
||||||
linkcheck:
|
|
||||||
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
|
||||||
@echo
|
|
||||||
@echo "Link check complete; look for any errors in the above output " \
|
|
||||||
"or in $(BUILDDIR)/linkcheck/output.txt."
|
|
||||||
|
|
||||||
doctest:
|
|
||||||
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
|
||||||
@echo "Testing of doctests in the sources finished, look at the " \
|
|
||||||
"results in $(BUILDDIR)/doctest/output.txt."
|
|
||||||
|
|
||||||
xml:
|
|
||||||
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
|
|
||||||
@echo
|
|
||||||
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
|
|
||||||
|
|
||||||
pseudoxml:
|
|
||||||
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
|
|
||||||
@echo
|
|
||||||
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
```{include} ../README.md
|
|
||||||
```
|
|
||||||
68
docs/conf.py
68
docs/conf.py
@@ -1,68 +0,0 @@
|
|||||||
# coding: utf-8
|
|
||||||
#
|
|
||||||
# yt-dlp documentation build configuration file
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
# Allows to import yt-dlp
|
|
||||||
sys.path.insert(0, os.path.abspath('..'))
|
|
||||||
|
|
||||||
# -- General configuration ------------------------------------------------
|
|
||||||
|
|
||||||
# Add any Sphinx extension module names here, as strings. They can be
|
|
||||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
|
||||||
# ones.
|
|
||||||
extensions = [
|
|
||||||
'myst_parser',
|
|
||||||
]
|
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
|
||||||
templates_path = ['_templates']
|
|
||||||
|
|
||||||
# The master toctree document.
|
|
||||||
master_doc = 'README'
|
|
||||||
|
|
||||||
# General information about the project.
|
|
||||||
project = u'yt-dlp'
|
|
||||||
author = u'yt-dlp'
|
|
||||||
copyright = u'UNLICENSE'
|
|
||||||
|
|
||||||
# The version info for the project you're documenting, acts as replacement for
|
|
||||||
# |version| and |release|, also used in various other places throughout the
|
|
||||||
# built documents.
|
|
||||||
#
|
|
||||||
# The short X.Y version.
|
|
||||||
from yt_dlp.version import __version__
|
|
||||||
version = __version__
|
|
||||||
# The full version, including alpha/beta/rc tags.
|
|
||||||
release = version
|
|
||||||
|
|
||||||
# List of patterns, relative to source directory, that match files and
|
|
||||||
# directories to ignore when looking for source files.
|
|
||||||
exclude_patterns = ['_build']
|
|
||||||
|
|
||||||
# The name of the Pygments (syntax highlighting) style to use.
|
|
||||||
pygments_style = 'sphinx'
|
|
||||||
|
|
||||||
# -- Options for HTML output ----------------------------------------------
|
|
||||||
|
|
||||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
|
||||||
# a list of builtin themes.
|
|
||||||
html_theme = 'default'
|
|
||||||
|
|
||||||
# Disable highlights
|
|
||||||
highlight_language = 'none'
|
|
||||||
|
|
||||||
# Add any paths that contain custom static files (such as style sheets) here,
|
|
||||||
# relative to this directory. They are copied after the builtin static files,
|
|
||||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
|
||||||
# html_static_path = ['_static']
|
|
||||||
|
|
||||||
# Enable heading anchors
|
|
||||||
myst_heading_anchors = 4
|
|
||||||
|
|
||||||
# Suppress heading warnings
|
|
||||||
suppress_warnings = [
|
|
||||||
'myst.header',
|
|
||||||
]
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
myst-parser
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
orphan: true
|
|
||||||
---
|
|
||||||
```{include} ../supportedsites.md
|
|
||||||
```
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
---
|
|
||||||
orphan: true
|
|
||||||
---
|
|
||||||
# ytdlp_plugins
|
|
||||||
|
|
||||||
See [https://github.com/yt-dlp/yt-dlp/tree/master/ytdlp_plugins](https://github.com/yt-dlp/yt-dlp/tree/master/ytdlp_plugins).
|
|
||||||
@@ -74,7 +74,7 @@ def version_to_list(version):
|
|||||||
|
|
||||||
|
|
||||||
def dependency_options():
|
def dependency_options():
|
||||||
dependencies = [pycryptodome_module(), 'mutagen'] + collect_submodules('websockets')
|
dependencies = [pycryptodome_module(), 'mutagen', 'brotli', 'certifi'] + collect_submodules('websockets')
|
||||||
excluded_modules = ['test', 'ytdlp_plugins', 'youtube-dl', 'youtube-dlc']
|
excluded_modules = ['test', 'ytdlp_plugins', 'youtube-dl', 'youtube-dlc']
|
||||||
|
|
||||||
yield from (f'--hidden-import={module}' for module in dependencies)
|
yield from (f'--hidden-import={module}' for module in dependencies)
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
mutagen
|
mutagen
|
||||||
pycryptodomex
|
pycryptodomex
|
||||||
websockets
|
websockets
|
||||||
|
brotli; platform_python_implementation=='CPython'
|
||||||
|
brotlicffi; platform_python_implementation!='CPython'
|
||||||
|
certifi
|
||||||
4
setup.py
4
setup.py
@@ -21,9 +21,9 @@ DESCRIPTION = 'A youtube-dl fork with additional features and patches'
|
|||||||
LONG_DESCRIPTION = '\n\n'.join((
|
LONG_DESCRIPTION = '\n\n'.join((
|
||||||
'Official repository: <https://github.com/yt-dlp/yt-dlp>',
|
'Official repository: <https://github.com/yt-dlp/yt-dlp>',
|
||||||
'**PS**: Some links in this document will not work since this is a copy of the README.md from Github',
|
'**PS**: Some links in this document will not work since this is a copy of the README.md from Github',
|
||||||
open('README.md', 'r', encoding='utf-8').read()))
|
open('README.md', encoding='utf-8').read()))
|
||||||
|
|
||||||
REQUIREMENTS = ['mutagen', 'pycryptodomex', 'websockets']
|
REQUIREMENTS = open('requirements.txt', encoding='utf-8').read().splitlines()
|
||||||
|
|
||||||
|
|
||||||
if sys.argv[1:2] == ['py2exe']:
|
if sys.argv[1:2] == ['py2exe']:
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
- **17live:clip**
|
- **17live:clip**
|
||||||
- **1tv**: Первый канал
|
- **1tv**: Первый канал
|
||||||
- **20min**
|
- **20min**
|
||||||
- **220.ro**
|
|
||||||
- **23video**
|
- **23video**
|
||||||
- **247sports**
|
- **247sports**
|
||||||
- **24video**
|
- **24video**
|
||||||
@@ -11,7 +10,6 @@
|
|||||||
- **3sat**
|
- **3sat**
|
||||||
- **4tube**
|
- **4tube**
|
||||||
- **56.com**
|
- **56.com**
|
||||||
- **5min**
|
|
||||||
- **6play**
|
- **6play**
|
||||||
- **7plus**
|
- **7plus**
|
||||||
- **8tracks**
|
- **8tracks**
|
||||||
@@ -26,6 +24,8 @@
|
|||||||
- **abcnews:video**
|
- **abcnews:video**
|
||||||
- **abcotvs**: ABC Owned Television Stations
|
- **abcotvs**: ABC Owned Television Stations
|
||||||
- **abcotvs:clips**
|
- **abcotvs:clips**
|
||||||
|
- **AbemaTV**
|
||||||
|
- **AbemaTVTitle**
|
||||||
- **AcademicEarth:Course**
|
- **AcademicEarth:Course**
|
||||||
- **acast**
|
- **acast**
|
||||||
- **acast:channel**
|
- **acast:channel**
|
||||||
@@ -41,11 +41,15 @@
|
|||||||
- **aenetworks:collection**
|
- **aenetworks:collection**
|
||||||
- **aenetworks:show**
|
- **aenetworks:show**
|
||||||
- **afreecatv**: afreecatv.com
|
- **afreecatv**: afreecatv.com
|
||||||
|
- **afreecatv:live**: afreecatv.com
|
||||||
|
- **afreecatv:user**
|
||||||
- **AirMozilla**
|
- **AirMozilla**
|
||||||
- **AliExpressLive**
|
- **AliExpressLive**
|
||||||
- **AlJazeera**
|
- **AlJazeera**
|
||||||
- **Allocine**
|
- **Allocine**
|
||||||
- **AlphaPorno**
|
- **AlphaPorno**
|
||||||
|
- **Alsace20TV**
|
||||||
|
- **Alsace20TVEmbed**
|
||||||
- **Alura**
|
- **Alura**
|
||||||
- **AluraCourse**
|
- **AluraCourse**
|
||||||
- **Amara**
|
- **Amara**
|
||||||
@@ -53,11 +57,15 @@
|
|||||||
- **AMCNetworks**
|
- **AMCNetworks**
|
||||||
- **AmericasTestKitchen**
|
- **AmericasTestKitchen**
|
||||||
- **AmericasTestKitchenSeason**
|
- **AmericasTestKitchenSeason**
|
||||||
|
- **AmHistoryChannel**
|
||||||
- **anderetijden**: npo.nl, ntr.nl, omroepwnl.nl, zapp.nl and npo3.nl
|
- **anderetijden**: npo.nl, ntr.nl, omroepwnl.nl, zapp.nl and npo3.nl
|
||||||
- **AnimalPlanet**
|
- **AnimalPlanet**
|
||||||
- **AnimeLab**
|
- **AnimeLab**
|
||||||
- **AnimeLabShows**
|
- **AnimeLabShows**
|
||||||
- **AnimeOnDemand**
|
- **AnimeOnDemand**
|
||||||
|
- **ant1newsgr:article**: ant1news.gr articles
|
||||||
|
- **ant1newsgr:embed**: ant1news.gr embedded videos
|
||||||
|
- **ant1newsgr:watch**: ant1news.gr videos
|
||||||
- **Anvato**
|
- **Anvato**
|
||||||
- **aol.com**: Yahoo screen and movies
|
- **aol.com**: Yahoo screen and movies
|
||||||
- **APA**
|
- **APA**
|
||||||
@@ -75,6 +83,7 @@
|
|||||||
- **Arkena**
|
- **Arkena**
|
||||||
- **arte.sky.it**
|
- **arte.sky.it**
|
||||||
- **ArteTV**
|
- **ArteTV**
|
||||||
|
- **ArteTVCategory**
|
||||||
- **ArteTVEmbed**
|
- **ArteTVEmbed**
|
||||||
- **ArteTVPlaylist**
|
- **ArteTVPlaylist**
|
||||||
- **AsianCrush**
|
- **AsianCrush**
|
||||||
@@ -96,11 +105,13 @@
|
|||||||
- **awaan:video**
|
- **awaan:video**
|
||||||
- **AZMedien**: AZ Medien videos
|
- **AZMedien**: AZ Medien videos
|
||||||
- **BaiduVideo**: 百度视频
|
- **BaiduVideo**: 百度视频
|
||||||
|
- **BanBye**
|
||||||
|
- **BanByeChannel**
|
||||||
- **bandaichannel**
|
- **bandaichannel**
|
||||||
- **Bandcamp**
|
- **Bandcamp**
|
||||||
- **Bandcamp:album**
|
- **Bandcamp:album**
|
||||||
|
- **Bandcamp:user**
|
||||||
- **Bandcamp:weekly**
|
- **Bandcamp:weekly**
|
||||||
- **BandcampMusic**
|
|
||||||
- **bangumi.bilibili.com**: BiliBili番剧
|
- **bangumi.bilibili.com**: BiliBili番剧
|
||||||
- **BannedVideo**
|
- **BannedVideo**
|
||||||
- **bbc**: BBC
|
- **bbc**: BBC
|
||||||
@@ -122,6 +133,7 @@
|
|||||||
- **bfmtv:live**
|
- **bfmtv:live**
|
||||||
- **BibelTV**
|
- **BibelTV**
|
||||||
- **Bigflix**
|
- **Bigflix**
|
||||||
|
- **Bigo**
|
||||||
- **Bild**: Bild.de
|
- **Bild**: Bild.de
|
||||||
- **BiliBili**
|
- **BiliBili**
|
||||||
- **Bilibili category extractor**
|
- **Bilibili category extractor**
|
||||||
@@ -162,6 +174,8 @@
|
|||||||
- **BuzzFeed**
|
- **BuzzFeed**
|
||||||
- **BYUtv**
|
- **BYUtv**
|
||||||
- **CableAV**
|
- **CableAV**
|
||||||
|
- **Callin**
|
||||||
|
- **Caltrans**
|
||||||
- **CAM4**
|
- **CAM4**
|
||||||
- **Camdemy**
|
- **Camdemy**
|
||||||
- **CamdemyFolder**
|
- **CamdemyFolder**
|
||||||
@@ -225,18 +239,25 @@
|
|||||||
- **ComedyCentralTV**
|
- **ComedyCentralTV**
|
||||||
- **CondeNast**: Condé Nast media group: Allure, Architectural Digest, Ars Technica, Bon Appétit, Brides, Condé Nast, Condé Nast Traveler, Details, Epicurious, GQ, Glamour, Golf Digest, SELF, Teen Vogue, The New Yorker, Vanity Fair, Vogue, W Magazine, WIRED
|
- **CondeNast**: Condé Nast media group: Allure, Architectural Digest, Ars Technica, Bon Appétit, Brides, Condé Nast, Condé Nast Traveler, Details, Epicurious, GQ, Glamour, Golf Digest, SELF, Teen Vogue, The New Yorker, Vanity Fair, Vogue, W Magazine, WIRED
|
||||||
- **CONtv**
|
- **CONtv**
|
||||||
|
- **CookingChannel**
|
||||||
- **Corus**
|
- **Corus**
|
||||||
- **Coub**
|
- **Coub**
|
||||||
- **CozyTV**
|
- **CozyTV**
|
||||||
- **cp24**
|
- **cp24**
|
||||||
|
- **cpac**
|
||||||
|
- **cpac:playlist**
|
||||||
- **Cracked**
|
- **Cracked**
|
||||||
- **Crackle**
|
- **Crackle**
|
||||||
|
- **Craftsy**
|
||||||
- **CrooksAndLiars**
|
- **CrooksAndLiars**
|
||||||
|
- **CrowdBunker**
|
||||||
|
- **CrowdBunkerChannel**
|
||||||
- **crunchyroll**
|
- **crunchyroll**
|
||||||
- **crunchyroll:beta**
|
- **crunchyroll:beta**
|
||||||
- **crunchyroll:playlist**
|
- **crunchyroll:playlist**
|
||||||
- **crunchyroll:playlist:beta**
|
- **crunchyroll:playlist:beta**
|
||||||
- **CSpan**: C-SPAN
|
- **CSpan**: C-SPAN
|
||||||
|
- **CSpanCongress**
|
||||||
- **CtsNews**: 華視新聞
|
- **CtsNews**: 華視新聞
|
||||||
- **CTV**
|
- **CTV**
|
||||||
- **CTVNews**
|
- **CTVNews**
|
||||||
@@ -246,6 +267,9 @@
|
|||||||
- **curiositystream:collections**
|
- **curiositystream:collections**
|
||||||
- **curiositystream:series**
|
- **curiositystream:series**
|
||||||
- **CWTV**
|
- **CWTV**
|
||||||
|
- **Cybrary**
|
||||||
|
- **CybraryCourse**
|
||||||
|
- **Daftsex**
|
||||||
- **DagelijkseKost**: dagelijksekost.een.be
|
- **DagelijkseKost**: dagelijksekost.een.be
|
||||||
- **DailyMail**
|
- **DailyMail**
|
||||||
- **dailymotion**
|
- **dailymotion**
|
||||||
@@ -257,26 +281,27 @@
|
|||||||
- **daum.net:clip**
|
- **daum.net:clip**
|
||||||
- **daum.net:playlist**
|
- **daum.net:playlist**
|
||||||
- **daum.net:user**
|
- **daum.net:user**
|
||||||
|
- **daystar:clip**
|
||||||
- **DBTV**
|
- **DBTV**
|
||||||
- **DctpTv**
|
- **DctpTv**
|
||||||
- **DeezerAlbum**
|
- **DeezerAlbum**
|
||||||
- **DeezerPlaylist**
|
- **DeezerPlaylist**
|
||||||
- **defense.gouv.fr**
|
- **defense.gouv.fr**
|
||||||
- **democracynow**
|
- **democracynow**
|
||||||
|
- **DestinationAmerica**
|
||||||
- **DHM**: Filmarchiv - Deutsches Historisches Museum
|
- **DHM**: Filmarchiv - Deutsches Historisches Museum
|
||||||
- **Digg**
|
- **Digg**
|
||||||
|
- **DigitalConcertHall**: DigitalConcertHall extractor
|
||||||
- **DigitallySpeaking**
|
- **DigitallySpeaking**
|
||||||
- **Digiteka**
|
- **Digiteka**
|
||||||
- **Discovery**
|
- **Discovery**
|
||||||
- **DiscoveryGo**
|
- **DiscoveryLife**
|
||||||
- **DiscoveryGoPlaylist**
|
|
||||||
- **DiscoveryNetworksDe**
|
- **DiscoveryNetworksDe**
|
||||||
- **DiscoveryPlus**
|
- **DiscoveryPlus**
|
||||||
- **DiscoveryPlusIndia**
|
- **DiscoveryPlusIndia**
|
||||||
- **DiscoveryPlusIndiaShow**
|
- **DiscoveryPlusIndiaShow**
|
||||||
- **DiscoveryPlusItaly**
|
- **DiscoveryPlusItaly**
|
||||||
- **DiscoveryPlusItalyShow**
|
- **DiscoveryPlusItalyShow**
|
||||||
- **DiscoveryVR**
|
|
||||||
- **Disney**
|
- **Disney**
|
||||||
- **DIYNetwork**
|
- **DIYNetwork**
|
||||||
- **dlive:stream**
|
- **dlive:stream**
|
||||||
@@ -288,6 +313,7 @@
|
|||||||
- **DouyuTV**: 斗鱼
|
- **DouyuTV**: 斗鱼
|
||||||
- **DPlay**
|
- **DPlay**
|
||||||
- **DRBonanza**
|
- **DRBonanza**
|
||||||
|
- **Drooble**
|
||||||
- **Dropbox**
|
- **Dropbox**
|
||||||
- **Dropout**
|
- **Dropout**
|
||||||
- **DropoutSeason**
|
- **DropoutSeason**
|
||||||
@@ -324,12 +350,16 @@
|
|||||||
- **Eporner**
|
- **Eporner**
|
||||||
- **EroProfile**
|
- **EroProfile**
|
||||||
- **EroProfile:album**
|
- **EroProfile:album**
|
||||||
|
- **ertflix**: ERTFLIX videos
|
||||||
|
- **ertflix:codename**: ERTFLIX videos by codename
|
||||||
|
- **ertwebtv:embed**: ert.gr webtv embedded videos
|
||||||
- **Escapist**
|
- **Escapist**
|
||||||
- **ESPN**
|
- **ESPN**
|
||||||
- **ESPNArticle**
|
- **ESPNArticle**
|
||||||
- **ESPNCricInfo**
|
- **ESPNCricInfo**
|
||||||
- **EsriVideo**
|
- **EsriVideo**
|
||||||
- **Europa**
|
- **Europa**
|
||||||
|
- **EuropeanTour**
|
||||||
- **EUScreen**
|
- **EUScreen**
|
||||||
- **EWETV**
|
- **EWETV**
|
||||||
- **ExpoTV**
|
- **ExpoTV**
|
||||||
@@ -343,6 +373,7 @@
|
|||||||
- **faz.net**
|
- **faz.net**
|
||||||
- **fc2**
|
- **fc2**
|
||||||
- **fc2:embed**
|
- **fc2:embed**
|
||||||
|
- **fc2:live**
|
||||||
- **Fczenit**
|
- **Fczenit**
|
||||||
- **Filmmodu**
|
- **Filmmodu**
|
||||||
- **filmon**
|
- **filmon**
|
||||||
@@ -352,6 +383,7 @@
|
|||||||
- **FiveTV**
|
- **FiveTV**
|
||||||
- **Flickr**
|
- **Flickr**
|
||||||
- **Folketinget**: Folketinget (ft.dk; Danish parliament)
|
- **Folketinget**: Folketinget (ft.dk; Danish parliament)
|
||||||
|
- **FoodNetwork**
|
||||||
- **FootyRoom**
|
- **FootyRoom**
|
||||||
- **Formula1**
|
- **Formula1**
|
||||||
- **FOX**
|
- **FOX**
|
||||||
@@ -361,6 +393,7 @@
|
|||||||
- **foxnews**: Fox News and Fox Business Video
|
- **foxnews**: Fox News and Fox Business Video
|
||||||
- **foxnews:article**
|
- **foxnews:article**
|
||||||
- **FoxSports**
|
- **FoxSports**
|
||||||
|
- **fptplay**: fptplay.vn
|
||||||
- **FranceCulture**
|
- **FranceCulture**
|
||||||
- **FranceInter**
|
- **FranceInter**
|
||||||
- **FranceTV**
|
- **FranceTV**
|
||||||
@@ -368,7 +401,6 @@
|
|||||||
- **FranceTVSite**
|
- **FranceTVSite**
|
||||||
- **Freesound**
|
- **Freesound**
|
||||||
- **freespeech.org**
|
- **freespeech.org**
|
||||||
- **FreshLive**
|
|
||||||
- **FrontendMasters**
|
- **FrontendMasters**
|
||||||
- **FrontendMastersCourse**
|
- **FrontendMastersCourse**
|
||||||
- **FrontendMastersLesson**
|
- **FrontendMastersLesson**
|
||||||
@@ -400,6 +432,7 @@
|
|||||||
- **gem.cbc.ca:playlist**
|
- **gem.cbc.ca:playlist**
|
||||||
- **generic**: Generic downloader that works on some sites
|
- **generic**: Generic downloader that works on some sites
|
||||||
- **Gettr**
|
- **Gettr**
|
||||||
|
- **GettrStreaming**
|
||||||
- **Gfycat**
|
- **Gfycat**
|
||||||
- **GiantBomb**
|
- **GiantBomb**
|
||||||
- **Giga**
|
- **Giga**
|
||||||
@@ -407,7 +440,10 @@
|
|||||||
- **Glide**: Glide mobile video messages (glide.me)
|
- **Glide**: Glide mobile video messages (glide.me)
|
||||||
- **Globo**
|
- **Globo**
|
||||||
- **GloboArticle**
|
- **GloboArticle**
|
||||||
|
- **glomex**: Glomex videos
|
||||||
|
- **glomex:embed**: Glomex embedded videos
|
||||||
- **Go**
|
- **Go**
|
||||||
|
- **GoDiscovery**
|
||||||
- **GodTube**
|
- **GodTube**
|
||||||
- **Gofile**
|
- **Gofile**
|
||||||
- **Golem**
|
- **Golem**
|
||||||
@@ -429,6 +465,7 @@
|
|||||||
- **hetklokhuis**
|
- **hetklokhuis**
|
||||||
- **hgtv.com:show**
|
- **hgtv.com:show**
|
||||||
- **HGTVDe**
|
- **HGTVDe**
|
||||||
|
- **HGTVUsa**
|
||||||
- **HiDive**
|
- **HiDive**
|
||||||
- **HistoricFilms**
|
- **HistoricFilms**
|
||||||
- **history:player**
|
- **history:player**
|
||||||
@@ -437,7 +474,6 @@
|
|||||||
- **hitbox:live**
|
- **hitbox:live**
|
||||||
- **HitRecord**
|
- **HitRecord**
|
||||||
- **hketv**: 香港教育局教育電視 (HKETV) Educational Television, Hong Kong Educational Bureau
|
- **hketv**: 香港教育局教育電視 (HKETV) Educational Television, Hong Kong Educational Bureau
|
||||||
- **HornBunny**
|
|
||||||
- **HotNewHipHop**
|
- **HotNewHipHop**
|
||||||
- **hotstar**
|
- **hotstar**
|
||||||
- **hotstar:playlist**
|
- **hotstar:playlist**
|
||||||
@@ -454,6 +490,7 @@
|
|||||||
- **Hungama**
|
- **Hungama**
|
||||||
- **HungamaAlbumPlaylist**
|
- **HungamaAlbumPlaylist**
|
||||||
- **HungamaSong**
|
- **HungamaSong**
|
||||||
|
- **huya:live**: huya.com
|
||||||
- **Hypem**
|
- **Hypem**
|
||||||
- **ign.com**
|
- **ign.com**
|
||||||
- **IGNArticle**
|
- **IGNArticle**
|
||||||
@@ -470,15 +507,20 @@
|
|||||||
- **IndavideoEmbed**
|
- **IndavideoEmbed**
|
||||||
- **InfoQ**
|
- **InfoQ**
|
||||||
- **Instagram**
|
- **Instagram**
|
||||||
|
- **instagram:story**
|
||||||
- **instagram:tag**: Instagram hashtag search URLs
|
- **instagram:tag**: Instagram hashtag search URLs
|
||||||
- **instagram:user**: Instagram user profile
|
- **instagram:user**: Instagram user profile
|
||||||
- **InstagramIOS**: IOS instagram:// URL
|
- **InstagramIOS**: IOS instagram:// URL
|
||||||
- **Internazionale**
|
- **Internazionale**
|
||||||
- **InternetVideoArchive**
|
- **InternetVideoArchive**
|
||||||
|
- **InvestigationDiscovery**
|
||||||
- **IPrima**
|
- **IPrima**
|
||||||
- **IPrimaCNN**
|
- **IPrimaCNN**
|
||||||
|
- **iq.com**: International version of iQiyi
|
||||||
|
- **iq.com:album**
|
||||||
- **iqiyi**: 爱奇艺
|
- **iqiyi**: 爱奇艺
|
||||||
- **Ir90Tv**
|
- **ITProTV**
|
||||||
|
- **ITProTVCourse**
|
||||||
- **ITTF**
|
- **ITTF**
|
||||||
- **ITV**
|
- **ITV**
|
||||||
- **ITVBTCC**
|
- **ITVBTCC**
|
||||||
@@ -487,6 +529,8 @@
|
|||||||
- **ivideon**: Ivideon TV
|
- **ivideon**: Ivideon TV
|
||||||
- **Iwara**
|
- **Iwara**
|
||||||
- **Izlesene**
|
- **Izlesene**
|
||||||
|
- **Jable**
|
||||||
|
- **JablePlaylist**
|
||||||
- **Jamendo**
|
- **Jamendo**
|
||||||
- **JamendoAlbum**
|
- **JamendoAlbum**
|
||||||
- **JeuxVideo**
|
- **JeuxVideo**
|
||||||
@@ -495,11 +539,11 @@
|
|||||||
- **JWPlatform**
|
- **JWPlatform**
|
||||||
- **Kakao**
|
- **Kakao**
|
||||||
- **Kaltura**
|
- **Kaltura**
|
||||||
- **Kankan**
|
|
||||||
- **Karaoketv**
|
- **Karaoketv**
|
||||||
- **KarriereVideos**
|
- **KarriereVideos**
|
||||||
- **Katsomo**
|
- **Katsomo**
|
||||||
- **KeezMovies**
|
- **KeezMovies**
|
||||||
|
- **KelbyOne**
|
||||||
- **Ketnet**
|
- **Ketnet**
|
||||||
- **khanacademy**
|
- **khanacademy**
|
||||||
- **khanacademy:unit**
|
- **khanacademy:unit**
|
||||||
@@ -522,6 +566,9 @@
|
|||||||
- **la7.it:podcast**
|
- **la7.it:podcast**
|
||||||
- **laola1tv**
|
- **laola1tv**
|
||||||
- **laola1tv:embed**
|
- **laola1tv:embed**
|
||||||
|
- **LastFM**
|
||||||
|
- **LastFMPlaylist**
|
||||||
|
- **LastFMUser**
|
||||||
- **lbry**
|
- **lbry**
|
||||||
- **lbry:channel**
|
- **lbry:channel**
|
||||||
- **LCI**
|
- **LCI**
|
||||||
@@ -545,7 +592,6 @@
|
|||||||
- **limelight:channel_list**
|
- **limelight:channel_list**
|
||||||
- **LineLive**
|
- **LineLive**
|
||||||
- **LineLiveChannel**
|
- **LineLiveChannel**
|
||||||
- **LineTV**
|
|
||||||
- **LinkedIn**
|
- **LinkedIn**
|
||||||
- **linkedin:learning**
|
- **linkedin:learning**
|
||||||
- **linkedin:learning:course**
|
- **linkedin:learning:course**
|
||||||
@@ -554,6 +600,7 @@
|
|||||||
- **LiveJournal**
|
- **LiveJournal**
|
||||||
- **livestream**
|
- **livestream**
|
||||||
- **livestream:original**
|
- **livestream:original**
|
||||||
|
- **Lnk**
|
||||||
- **LnkGo**
|
- **LnkGo**
|
||||||
- **loc**: Library of Congress
|
- **loc**: Library of Congress
|
||||||
- **LocalNews8**
|
- **LocalNews8**
|
||||||
@@ -566,9 +613,11 @@
|
|||||||
- **mailru**: Видео@Mail.Ru
|
- **mailru**: Видео@Mail.Ru
|
||||||
- **mailru:music**: Музыка@Mail.Ru
|
- **mailru:music**: Музыка@Mail.Ru
|
||||||
- **mailru:music:search**: Музыка@Mail.Ru
|
- **mailru:music:search**: Музыка@Mail.Ru
|
||||||
|
- **MainStreaming**: MainStreaming Player
|
||||||
- **MallTV**
|
- **MallTV**
|
||||||
- **mangomolo:live**
|
- **mangomolo:live**
|
||||||
- **mangomolo:video**
|
- **mangomolo:video**
|
||||||
|
- **MangoTV**: 芒果TV
|
||||||
- **ManotoTV**: Manoto TV (Episode)
|
- **ManotoTV**: Manoto TV (Episode)
|
||||||
- **ManotoTVLive**: Manoto TV (Live)
|
- **ManotoTVLive**: Manoto TV (Live)
|
||||||
- **ManotoTVShow**: Manoto TV (Show)
|
- **ManotoTVShow**: Manoto TV (Show)
|
||||||
@@ -592,6 +641,8 @@
|
|||||||
- **MediasiteNamedCatalog**
|
- **MediasiteNamedCatalog**
|
||||||
- **Medici**
|
- **Medici**
|
||||||
- **megaphone.fm**: megaphone.fm embedded players
|
- **megaphone.fm**: megaphone.fm embedded players
|
||||||
|
- **megatvcom**: megatv.com videos
|
||||||
|
- **megatvcom:embed**: megatv.com embedded videos
|
||||||
- **Meipai**: 美拍
|
- **Meipai**: 美拍
|
||||||
- **MelonVOD**
|
- **MelonVOD**
|
||||||
- **META**
|
- **META**
|
||||||
@@ -599,12 +650,12 @@
|
|||||||
- **Metacritic**
|
- **Metacritic**
|
||||||
- **mewatch**
|
- **mewatch**
|
||||||
- **Mgoon**
|
- **Mgoon**
|
||||||
- **MGTV**: 芒果TV
|
|
||||||
- **MiaoPai**
|
- **MiaoPai**
|
||||||
- **microsoftstream**: Microsoft Stream
|
- **microsoftstream**: Microsoft Stream
|
||||||
- **mildom**: Record ongoing live by specific user in Mildom
|
- **mildom**: Record ongoing live by specific user in Mildom
|
||||||
|
- **mildom:clip**: Clip in Mildom
|
||||||
- **mildom:user:vod**: Download all VODs from specific user in Mildom
|
- **mildom:user:vod**: Download all VODs from specific user in Mildom
|
||||||
- **mildom:vod**: Download a VOD in Mildom
|
- **mildom:vod**: VOD in Mildom
|
||||||
- **minds**
|
- **minds**
|
||||||
- **minds:channel**
|
- **minds:channel**
|
||||||
- **minds:group**
|
- **minds:group**
|
||||||
@@ -615,6 +666,7 @@
|
|||||||
- **mirrativ:user**
|
- **mirrativ:user**
|
||||||
- **MiTele**: mitele.es
|
- **MiTele**: mitele.es
|
||||||
- **mixch**
|
- **mixch**
|
||||||
|
- **mixch:archive**
|
||||||
- **mixcloud**
|
- **mixcloud**
|
||||||
- **mixcloud:playlist**
|
- **mixcloud:playlist**
|
||||||
- **mixcloud:user**
|
- **mixcloud:user**
|
||||||
@@ -633,6 +685,7 @@
|
|||||||
- **Motorsport**: motorsport.com
|
- **Motorsport**: motorsport.com
|
||||||
- **MovieClips**
|
- **MovieClips**
|
||||||
- **MovieFap**
|
- **MovieFap**
|
||||||
|
- **Moviepilot**
|
||||||
- **Moviezine**
|
- **Moviezine**
|
||||||
- **MovingImage**
|
- **MovingImage**
|
||||||
- **MSN**
|
- **MSN**
|
||||||
@@ -646,7 +699,13 @@
|
|||||||
- **mtvservices:embedded**
|
- **mtvservices:embedded**
|
||||||
- **MTVUutisetArticle**
|
- **MTVUutisetArticle**
|
||||||
- **MuenchenTV**: münchen.tv
|
- **MuenchenTV**: münchen.tv
|
||||||
|
- **Murrtube**
|
||||||
|
- **MurrtubeUser**: Murrtube user profile
|
||||||
- **MuseScore**
|
- **MuseScore**
|
||||||
|
- **MusicdexAlbum**
|
||||||
|
- **MusicdexArtist**
|
||||||
|
- **MusicdexPlaylist**
|
||||||
|
- **MusicdexSong**
|
||||||
- **mva**: Microsoft Virtual Academy videos
|
- **mva**: Microsoft Virtual Academy videos
|
||||||
- **mva:course**: Microsoft Virtual Academy courses
|
- **mva:course**: Microsoft Virtual Academy courses
|
||||||
- **Mwave**
|
- **Mwave**
|
||||||
@@ -661,7 +720,6 @@
|
|||||||
- **MyVideoGe**
|
- **MyVideoGe**
|
||||||
- **MyVidster**
|
- **MyVidster**
|
||||||
- **MyviEmbed**
|
- **MyviEmbed**
|
||||||
- **MyVisionTV**
|
|
||||||
- **n-tv.de**
|
- **n-tv.de**
|
||||||
- **N1Info:article**
|
- **N1Info:article**
|
||||||
- **N1InfoAsset**
|
- **N1InfoAsset**
|
||||||
@@ -704,14 +762,19 @@
|
|||||||
- **Newgrounds:playlist**
|
- **Newgrounds:playlist**
|
||||||
- **Newgrounds:user**
|
- **Newgrounds:user**
|
||||||
- **Newstube**
|
- **Newstube**
|
||||||
|
- **Newsy**
|
||||||
- **NextMedia**: 蘋果日報
|
- **NextMedia**: 蘋果日報
|
||||||
- **NextMediaActionNews**: 蘋果日報 - 動新聞
|
- **NextMediaActionNews**: 蘋果日報 - 動新聞
|
||||||
- **NextTV**: 壹電視
|
- **NextTV**: 壹電視
|
||||||
- **Nexx**
|
- **Nexx**
|
||||||
- **NexxEmbed**
|
- **NexxEmbed**
|
||||||
|
- **NFB**
|
||||||
- **NFHSNetwork**
|
- **NFHSNetwork**
|
||||||
- **nfl.com** (Currently broken)
|
- **nfl.com** (Currently broken)
|
||||||
- **nfl.com:article** (Currently broken)
|
- **nfl.com:article** (Currently broken)
|
||||||
|
- **NhkForSchoolBangumi**
|
||||||
|
- **NhkForSchoolProgramList**
|
||||||
|
- **NhkForSchoolSubject**: Portal page for each school subjects, like Japanese (kokugo, 国語) or math (sansuu/suugaku or 算数・数学)
|
||||||
- **NhkVod**
|
- **NhkVod**
|
||||||
- **NhkVodProgram**
|
- **NhkVodProgram**
|
||||||
- **nhl.com**
|
- **nhl.com**
|
||||||
@@ -721,7 +784,10 @@
|
|||||||
- **nickelodeonru**
|
- **nickelodeonru**
|
||||||
- **nicknight**
|
- **nicknight**
|
||||||
- **niconico**: ニコニコ動画
|
- **niconico**: ニコニコ動画
|
||||||
- **NiconicoPlaylist**
|
- **niconico:history**: NicoNico user history. Requires cookies.
|
||||||
|
- **niconico:playlist**
|
||||||
|
- **niconico:series**
|
||||||
|
- **niconico:tag**: NicoNico video tag URLs
|
||||||
- **NiconicoUser**
|
- **NiconicoUser**
|
||||||
- **nicovideo:search**: Nico video search; "nicosearch:" prefix
|
- **nicovideo:search**: Nico video search; "nicosearch:" prefix
|
||||||
- **nicovideo:search:date**: Nico video search, newest first; "nicosearchdate:" prefix
|
- **nicovideo:search:date**: Nico video search, newest first; "nicosearchdate:" prefix
|
||||||
@@ -733,6 +799,7 @@
|
|||||||
- **NJPWWorld**: 新日本プロレスワールド
|
- **NJPWWorld**: 新日本プロレスワールド
|
||||||
- **NobelPrize**
|
- **NobelPrize**
|
||||||
- **NonkTube**
|
- **NonkTube**
|
||||||
|
- **NoodleMagazine**
|
||||||
- **Noovo**
|
- **Noovo**
|
||||||
- **Normalboots**
|
- **Normalboots**
|
||||||
- **NosVideo**
|
- **NosVideo**
|
||||||
@@ -785,6 +852,7 @@
|
|||||||
- **OpencastPlaylist**
|
- **OpencastPlaylist**
|
||||||
- **openrec**
|
- **openrec**
|
||||||
- **openrec:capture**
|
- **openrec:capture**
|
||||||
|
- **openrec:movie**
|
||||||
- **OraTV**
|
- **OraTV**
|
||||||
- **orf:burgenland**: Radio Burgenland
|
- **orf:burgenland**: Radio Burgenland
|
||||||
- **orf:fm4**: radio FM4
|
- **orf:fm4**: radio FM4
|
||||||
@@ -809,6 +877,9 @@
|
|||||||
- **PalcoMP3:song**
|
- **PalcoMP3:song**
|
||||||
- **PalcoMP3:video**
|
- **PalcoMP3:video**
|
||||||
- **pandora.tv**: 판도라TV
|
- **pandora.tv**: 판도라TV
|
||||||
|
- **Panopto**
|
||||||
|
- **PanoptoList**
|
||||||
|
- **PanoptoPlaylist**
|
||||||
- **ParamountNetwork**
|
- **ParamountNetwork**
|
||||||
- **ParamountPlus**
|
- **ParamountPlus**
|
||||||
- **ParamountPlusSeries**
|
- **ParamountPlusSeries**
|
||||||
@@ -818,6 +889,7 @@
|
|||||||
- **PatreonUser**
|
- **PatreonUser**
|
||||||
- **pbs**: Public Broadcasting Service (PBS) and member stations: PBS: Public Broadcasting Service, APT - Alabama Public Television (WBIQ), GPB/Georgia Public Broadcasting (WGTV), Mississippi Public Broadcasting (WMPN), Nashville Public Television (WNPT), WFSU-TV (WFSU), WSRE (WSRE), WTCI (WTCI), WPBA/Channel 30 (WPBA), Alaska Public Media (KAKM), Arizona PBS (KAET), KNME-TV/Channel 5 (KNME), Vegas PBS (KLVX), AETN/ARKANSAS ETV NETWORK (KETS), KET (WKLE), WKNO/Channel 10 (WKNO), LPB/LOUISIANA PUBLIC BROADCASTING (WLPB), OETA (KETA), Ozarks Public Television (KOZK), WSIU Public Broadcasting (WSIU), KEET TV (KEET), KIXE/Channel 9 (KIXE), KPBS San Diego (KPBS), KQED (KQED), KVIE Public Television (KVIE), PBS SoCal/KOCE (KOCE), ValleyPBS (KVPT), CONNECTICUT PUBLIC TELEVISION (WEDH), KNPB Channel 5 (KNPB), SOPTV (KSYS), Rocky Mountain PBS (KRMA), KENW-TV3 (KENW), KUED Channel 7 (KUED), Wyoming PBS (KCWC), Colorado Public Television / KBDI 12 (KBDI), KBYU-TV (KBYU), Thirteen/WNET New York (WNET), WGBH/Channel 2 (WGBH), WGBY (WGBY), NJTV Public Media NJ (WNJT), WLIW21 (WLIW), mpt/Maryland Public Television (WMPB), WETA Television and Radio (WETA), WHYY (WHYY), PBS 39 (WLVT), WVPT - Your Source for PBS and More! (WVPT), Howard University Television (WHUT), WEDU PBS (WEDU), WGCU Public Media (WGCU), WPBT2 (WPBT), WUCF TV (WUCF), WUFT/Channel 5 (WUFT), WXEL/Channel 42 (WXEL), WLRN/Channel 17 (WLRN), WUSF Public Broadcasting (WUSF), ETV (WRLK), UNC-TV (WUNC), PBS Hawaii - Oceanic Cable Channel 10 (KHET), Idaho Public Television (KAID), KSPS (KSPS), OPB (KOPB), KWSU/Channel 10 & KTNW/Channel 31 (KWSU), WILL-TV (WILL), Network Knowledge - WSEC/Springfield (WSEC), WTTW11 (WTTW), Iowa Public Television/IPTV (KDIN), Nine Network (KETC), PBS39 Fort Wayne (WFWA), WFYI Indianapolis (WFYI), Milwaukee Public Television (WMVS), WNIN (WNIN), WNIT Public Television (WNIT), WPT (WPNE), WVUT/Channel 22 (WVUT), WEIU/Channel 51 (WEIU), WQPT-TV (WQPT), WYCC PBS Chicago (WYCC), WIPB-TV (WIPB), WTIU (WTIU), CET (WCET), ThinkTVNetwork (WPTD), WBGU-TV (WBGU), WGVU TV (WGVU), NET1 (KUON), Pioneer Public Television (KWCM), SDPB Television (KUSD), TPT (KTCA), KSMQ (KSMQ), KPTS/Channel 8 (KPTS), KTWU/Channel 11 (KTWU), East Tennessee PBS (WSJK), WCTE-TV (WCTE), WLJT, Channel 11 (WLJT), WOSU TV (WOSU), WOUB/WOUC (WOUB), WVPB (WVPB), WKYU-PBS (WKYU), KERA 13 (KERA), MPBN (WCBB), Mountain Lake PBS (WCFE), NHPTV (WENH), Vermont PBS (WETK), witf (WITF), WQED Multimedia (WQED), WMHT Educational Telecommunications (WMHT), Q-TV (WDCQ), WTVS Detroit Public TV (WTVS), CMU Public Television (WCMU), WKAR-TV (WKAR), WNMU-TV Public TV 13 (WNMU), WDSE - WRPT (WDSE), WGTE TV (WGTE), Lakeland Public Television (KAWE), KMOS-TV - Channels 6.1, 6.2 and 6.3 (KMOS), MontanaPBS (KUSM), KRWG/Channel 22 (KRWG), KACV (KACV), KCOS/Channel 13 (KCOS), WCNY/Channel 24 (WCNY), WNED (WNED), WPBS (WPBS), WSKG Public TV (WSKG), WXXI (WXXI), WPSU (WPSU), WVIA Public Media Studios (WVIA), WTVI (WTVI), Western Reserve PBS (WNEO), WVIZ/PBS ideastream (WVIZ), KCTS 9 (KCTS), Basin PBS (KPBT), KUHT / Channel 8 (KUHT), KLRN (KLRN), KLRU (KLRU), WTJX Channel 12 (WTJX), WCVE PBS (WCVE), KBTC Public Television (KBTC)
|
- **pbs**: Public Broadcasting Service (PBS) and member stations: PBS: Public Broadcasting Service, APT - Alabama Public Television (WBIQ), GPB/Georgia Public Broadcasting (WGTV), Mississippi Public Broadcasting (WMPN), Nashville Public Television (WNPT), WFSU-TV (WFSU), WSRE (WSRE), WTCI (WTCI), WPBA/Channel 30 (WPBA), Alaska Public Media (KAKM), Arizona PBS (KAET), KNME-TV/Channel 5 (KNME), Vegas PBS (KLVX), AETN/ARKANSAS ETV NETWORK (KETS), KET (WKLE), WKNO/Channel 10 (WKNO), LPB/LOUISIANA PUBLIC BROADCASTING (WLPB), OETA (KETA), Ozarks Public Television (KOZK), WSIU Public Broadcasting (WSIU), KEET TV (KEET), KIXE/Channel 9 (KIXE), KPBS San Diego (KPBS), KQED (KQED), KVIE Public Television (KVIE), PBS SoCal/KOCE (KOCE), ValleyPBS (KVPT), CONNECTICUT PUBLIC TELEVISION (WEDH), KNPB Channel 5 (KNPB), SOPTV (KSYS), Rocky Mountain PBS (KRMA), KENW-TV3 (KENW), KUED Channel 7 (KUED), Wyoming PBS (KCWC), Colorado Public Television / KBDI 12 (KBDI), KBYU-TV (KBYU), Thirteen/WNET New York (WNET), WGBH/Channel 2 (WGBH), WGBY (WGBY), NJTV Public Media NJ (WNJT), WLIW21 (WLIW), mpt/Maryland Public Television (WMPB), WETA Television and Radio (WETA), WHYY (WHYY), PBS 39 (WLVT), WVPT - Your Source for PBS and More! (WVPT), Howard University Television (WHUT), WEDU PBS (WEDU), WGCU Public Media (WGCU), WPBT2 (WPBT), WUCF TV (WUCF), WUFT/Channel 5 (WUFT), WXEL/Channel 42 (WXEL), WLRN/Channel 17 (WLRN), WUSF Public Broadcasting (WUSF), ETV (WRLK), UNC-TV (WUNC), PBS Hawaii - Oceanic Cable Channel 10 (KHET), Idaho Public Television (KAID), KSPS (KSPS), OPB (KOPB), KWSU/Channel 10 & KTNW/Channel 31 (KWSU), WILL-TV (WILL), Network Knowledge - WSEC/Springfield (WSEC), WTTW11 (WTTW), Iowa Public Television/IPTV (KDIN), Nine Network (KETC), PBS39 Fort Wayne (WFWA), WFYI Indianapolis (WFYI), Milwaukee Public Television (WMVS), WNIN (WNIN), WNIT Public Television (WNIT), WPT (WPNE), WVUT/Channel 22 (WVUT), WEIU/Channel 51 (WEIU), WQPT-TV (WQPT), WYCC PBS Chicago (WYCC), WIPB-TV (WIPB), WTIU (WTIU), CET (WCET), ThinkTVNetwork (WPTD), WBGU-TV (WBGU), WGVU TV (WGVU), NET1 (KUON), Pioneer Public Television (KWCM), SDPB Television (KUSD), TPT (KTCA), KSMQ (KSMQ), KPTS/Channel 8 (KPTS), KTWU/Channel 11 (KTWU), East Tennessee PBS (WSJK), WCTE-TV (WCTE), WLJT, Channel 11 (WLJT), WOSU TV (WOSU), WOUB/WOUC (WOUB), WVPB (WVPB), WKYU-PBS (WKYU), KERA 13 (KERA), MPBN (WCBB), Mountain Lake PBS (WCFE), NHPTV (WENH), Vermont PBS (WETK), witf (WITF), WQED Multimedia (WQED), WMHT Educational Telecommunications (WMHT), Q-TV (WDCQ), WTVS Detroit Public TV (WTVS), CMU Public Television (WCMU), WKAR-TV (WKAR), WNMU-TV Public TV 13 (WNMU), WDSE - WRPT (WDSE), WGTE TV (WGTE), Lakeland Public Television (KAWE), KMOS-TV - Channels 6.1, 6.2 and 6.3 (KMOS), MontanaPBS (KUSM), KRWG/Channel 22 (KRWG), KACV (KACV), KCOS/Channel 13 (KCOS), WCNY/Channel 24 (WCNY), WNED (WNED), WPBS (WPBS), WSKG Public TV (WSKG), WXXI (WXXI), WPSU (WPSU), WVIA Public Media Studios (WVIA), WTVI (WTVI), Western Reserve PBS (WNEO), WVIZ/PBS ideastream (WVIZ), KCTS 9 (KCTS), Basin PBS (KPBT), KUHT / Channel 8 (KUHT), KLRN (KLRN), KLRU (KLRU), WTJX Channel 12 (WTJX), WCVE PBS (WCVE), KBTC Public Television (KBTC)
|
||||||
- **PearVideo**
|
- **PearVideo**
|
||||||
|
- **PeekVids**
|
||||||
- **peer.tv**
|
- **peer.tv**
|
||||||
- **PeerTube**
|
- **PeerTube**
|
||||||
- **PeerTube:Playlist**
|
- **PeerTube:Playlist**
|
||||||
@@ -830,6 +902,7 @@
|
|||||||
- **PhilharmonieDeParis**: Philharmonie de Paris
|
- **PhilharmonieDeParis**: Philharmonie de Paris
|
||||||
- **phoenix.de**
|
- **phoenix.de**
|
||||||
- **Photobucket**
|
- **Photobucket**
|
||||||
|
- **Piapro**
|
||||||
- **Picarto**
|
- **Picarto**
|
||||||
- **PicartoVod**
|
- **PicartoVod**
|
||||||
- **Piksel**
|
- **Piksel**
|
||||||
@@ -849,13 +922,17 @@
|
|||||||
- **PlaysTV**
|
- **PlaysTV**
|
||||||
- **Playtvak**: Playtvak.cz, iDNES.cz and Lidovky.cz
|
- **Playtvak**: Playtvak.cz, iDNES.cz and Lidovky.cz
|
||||||
- **Playvid**
|
- **Playvid**
|
||||||
|
- **PlayVids**
|
||||||
- **Playwire**
|
- **Playwire**
|
||||||
- **pluralsight**
|
- **pluralsight**
|
||||||
- **pluralsight:course**
|
- **pluralsight:course**
|
||||||
- **PlutoTV**
|
- **PlutoTV**
|
||||||
- **podomatic**
|
- **podomatic**
|
||||||
- **Pokemon**
|
- **Pokemon**
|
||||||
|
- **PokemonSoundLibrary**
|
||||||
- **PokemonWatch**
|
- **PokemonWatch**
|
||||||
|
- **PokerGo**
|
||||||
|
- **PokerGoCollection**
|
||||||
- **PolsatGo**
|
- **PolsatGo**
|
||||||
- **PolskieRadio**
|
- **PolskieRadio**
|
||||||
- **polskieradio:kierowcow**
|
- **polskieradio:kierowcow**
|
||||||
@@ -867,6 +944,7 @@
|
|||||||
- **PopcornTV**
|
- **PopcornTV**
|
||||||
- **PornCom**
|
- **PornCom**
|
||||||
- **PornerBros**
|
- **PornerBros**
|
||||||
|
- **Pornez**
|
||||||
- **PornFlip**
|
- **PornFlip**
|
||||||
- **PornHd**
|
- **PornHd**
|
||||||
- **PornHub**: PornHub and Thumbzilla
|
- **PornHub**: PornHub and Thumbzilla
|
||||||
@@ -881,6 +959,11 @@
|
|||||||
- **PressTV**
|
- **PressTV**
|
||||||
- **ProjectVeritas**
|
- **ProjectVeritas**
|
||||||
- **prosiebensat1**: ProSiebenSat.1 Digital
|
- **prosiebensat1**: ProSiebenSat.1 Digital
|
||||||
|
- **PRXAccount**
|
||||||
|
- **PRXSeries**
|
||||||
|
- **prxseries:search**: PRX Series Search; "prxseries:" prefix
|
||||||
|
- **prxstories:search**: PRX Stories Search; "prxstories:" prefix
|
||||||
|
- **PRXStory**
|
||||||
- **puhutv**
|
- **puhutv**
|
||||||
- **puhutv:serie**
|
- **puhutv:serie**
|
||||||
- **Puls4**
|
- **Puls4**
|
||||||
@@ -892,8 +975,6 @@
|
|||||||
- **qqmusic:toplist**: QQ音乐 - 排行榜
|
- **qqmusic:toplist**: QQ音乐 - 排行榜
|
||||||
- **QuantumTV**
|
- **QuantumTV**
|
||||||
- **Qub**
|
- **Qub**
|
||||||
- **Quickline**
|
|
||||||
- **QuicklineLive**
|
|
||||||
- **R7**
|
- **R7**
|
||||||
- **R7Article**
|
- **R7Article**
|
||||||
- **Radiko**
|
- **Radiko**
|
||||||
@@ -914,8 +995,9 @@
|
|||||||
- **RaiPlay**
|
- **RaiPlay**
|
||||||
- **RaiPlayLive**
|
- **RaiPlayLive**
|
||||||
- **RaiPlayPlaylist**
|
- **RaiPlayPlaylist**
|
||||||
- **RaiPlayRadio**
|
- **RaiPlaySound**
|
||||||
- **RaiPlayRadioPlaylist**
|
- **RaiPlaySoundLive**
|
||||||
|
- **RaiPlaySoundPlaylist**
|
||||||
- **RayWenderlich**
|
- **RayWenderlich**
|
||||||
- **RayWenderlichCourse**
|
- **RayWenderlichCourse**
|
||||||
- **RBMARadio**
|
- **RBMARadio**
|
||||||
@@ -944,18 +1026,23 @@
|
|||||||
- **RICE**
|
- **RICE**
|
||||||
- **RMCDecouverte**
|
- **RMCDecouverte**
|
||||||
- **RockstarGames**
|
- **RockstarGames**
|
||||||
|
- **Rokfin**
|
||||||
|
- **rokfin:channel**
|
||||||
|
- **rokfin:stack**
|
||||||
- **RoosterTeeth**
|
- **RoosterTeeth**
|
||||||
- **RoosterTeethSeries**
|
- **RoosterTeethSeries**
|
||||||
- **RottenTomatoes**
|
- **RottenTomatoes**
|
||||||
- **Roxwel**
|
|
||||||
- **Rozhlas**
|
- **Rozhlas**
|
||||||
- **RTBF**
|
- **RTBF**
|
||||||
|
- **RTDocumentry**
|
||||||
|
- **RTDocumentryPlaylist**
|
||||||
- **rte**: Raidió Teilifís Éireann TV
|
- **rte**: Raidió Teilifís Éireann TV
|
||||||
- **rte:radio**: Raidió Teilifís Éireann radio
|
- **rte:radio**: Raidió Teilifís Éireann radio
|
||||||
- **rtl.nl**: rtl.nl and rtlxl.nl
|
- **rtl.nl**: rtl.nl and rtlxl.nl
|
||||||
- **rtl2**
|
- **rtl2**
|
||||||
- **rtl2:you**
|
- **rtl2:you**
|
||||||
- **rtl2:you:series**
|
- **rtl2:you:series**
|
||||||
|
- **RTNews**
|
||||||
- **RTP**
|
- **RTP**
|
||||||
- **RTRFM**
|
- **RTRFM**
|
||||||
- **RTS**: RTS.ch
|
- **RTS**: RTS.ch
|
||||||
@@ -967,8 +1054,10 @@
|
|||||||
- **RTVNH**
|
- **RTVNH**
|
||||||
- **RTVS**
|
- **RTVS**
|
||||||
- **RUHD**
|
- **RUHD**
|
||||||
|
- **Rule34Video**
|
||||||
- **RumbleChannel**
|
- **RumbleChannel**
|
||||||
- **RumbleEmbed**
|
- **RumbleEmbed**
|
||||||
|
- **Ruptly**
|
||||||
- **rutube**: Rutube videos
|
- **rutube**: Rutube videos
|
||||||
- **rutube:channel**: Rutube channel
|
- **rutube:channel**: Rutube channel
|
||||||
- **rutube:embed**: Rutube embedded videos
|
- **rutube:embed**: Rutube embedded videos
|
||||||
@@ -979,6 +1068,7 @@
|
|||||||
- **RUTV**: RUTV.RU
|
- **RUTV**: RUTV.RU
|
||||||
- **Ruutu**
|
- **Ruutu**
|
||||||
- **Ruv**
|
- **Ruv**
|
||||||
|
- **ruv.is:spila**
|
||||||
- **safari**: safaribooksonline.com online video
|
- **safari**: safaribooksonline.com online video
|
||||||
- **safari:api**
|
- **safari:api**
|
||||||
- **safari:course**: safaribooksonline.com online courses
|
- **safari:course**: safaribooksonline.com online courses
|
||||||
@@ -1109,12 +1199,16 @@
|
|||||||
- **TeamTreeHouse**
|
- **TeamTreeHouse**
|
||||||
- **TechTalks**
|
- **TechTalks**
|
||||||
- **techtv.mit.edu**
|
- **techtv.mit.edu**
|
||||||
- **ted**
|
- **TedEmbed**
|
||||||
|
- **TedPlaylist**
|
||||||
|
- **TedSeries**
|
||||||
|
- **TedTalk**
|
||||||
- **Tele13**
|
- **Tele13**
|
||||||
- **Tele5**
|
- **Tele5**
|
||||||
- **TeleBruxelles**
|
- **TeleBruxelles**
|
||||||
- **Telecinco**: telecinco.es, cuatro.com and mediaset.es
|
- **Telecinco**: telecinco.es, cuatro.com and mediaset.es
|
||||||
- **Telegraaf**
|
- **Telegraaf**
|
||||||
|
- **telegram:embed**
|
||||||
- **TeleMB**
|
- **TeleMB**
|
||||||
- **Telemundo**
|
- **Telemundo**
|
||||||
- **TeleQuebec**
|
- **TeleQuebec**
|
||||||
@@ -1131,7 +1225,6 @@
|
|||||||
- **TheIntercept**
|
- **TheIntercept**
|
||||||
- **ThePlatform**
|
- **ThePlatform**
|
||||||
- **ThePlatformFeed**
|
- **ThePlatformFeed**
|
||||||
- **TheScene**
|
|
||||||
- **TheStar**
|
- **TheStar**
|
||||||
- **TheSun**
|
- **TheSun**
|
||||||
- **ThetaStream**
|
- **ThetaStream**
|
||||||
@@ -1148,6 +1241,7 @@
|
|||||||
- **tiktok:tag**
|
- **tiktok:tag**
|
||||||
- **tiktok:user**
|
- **tiktok:user**
|
||||||
- **tinypic**: tinypic.com videos
|
- **tinypic**: tinypic.com videos
|
||||||
|
- **TLC**
|
||||||
- **TMZ**
|
- **TMZ**
|
||||||
- **TNAFlix**
|
- **TNAFlix**
|
||||||
- **TNAFlixNetworkEmbed**
|
- **TNAFlixNetworkEmbed**
|
||||||
@@ -1160,6 +1254,7 @@
|
|||||||
- **Toypics**: Toypics video
|
- **Toypics**: Toypics video
|
||||||
- **ToypicsUser**: Toypics user profile
|
- **ToypicsUser**: Toypics user profile
|
||||||
- **TrailerAddict** (Currently broken)
|
- **TrailerAddict** (Currently broken)
|
||||||
|
- **TravelChannel**
|
||||||
- **Trilulilu**
|
- **Trilulilu**
|
||||||
- **Trovo**
|
- **Trovo**
|
||||||
- **TrovoChannelClip**: All Clips of a trovo.live channel; "trovoclip:" prefix
|
- **TrovoChannelClip**: All Clips of a trovo.live channel; "trovoclip:" prefix
|
||||||
@@ -1207,6 +1302,8 @@
|
|||||||
- **TVNowNew**
|
- **TVNowNew**
|
||||||
- **TVNowSeason**
|
- **TVNowSeason**
|
||||||
- **TVNowShow**
|
- **TVNowShow**
|
||||||
|
- **tvopengr:embed**: tvopen.gr embedded videos
|
||||||
|
- **tvopengr:watch**: tvopen.gr (and ethnos.gr) videos
|
||||||
- **tvp**: Telewizja Polska
|
- **tvp**: Telewizja Polska
|
||||||
- **tvp:embed**: Telewizja Polska
|
- **tvp:embed**: Telewizja Polska
|
||||||
- **tvp:series**
|
- **tvp:series**
|
||||||
@@ -1270,9 +1367,11 @@
|
|||||||
- **Viddler**
|
- **Viddler**
|
||||||
- **Videa**
|
- **Videa**
|
||||||
- **video.arnes.si**: Arnes Video
|
- **video.arnes.si**: Arnes Video
|
||||||
- **video.google:search**: Google Video search; "gvsearch:" prefix (Currently broken)
|
- **video.google:search**: Google Video search; "gvsearch:" prefix
|
||||||
- **video.sky.it**
|
- **video.sky.it**
|
||||||
- **video.sky.it:live**
|
- **video.sky.it:live**
|
||||||
|
- **VideocampusSachsen**
|
||||||
|
- **VideocampusSachsenEmbed**
|
||||||
- **VideoDetective**
|
- **VideoDetective**
|
||||||
- **videofy.me**
|
- **videofy.me**
|
||||||
- **videomore**
|
- **videomore**
|
||||||
@@ -1299,6 +1398,8 @@
|
|||||||
- **vimeo:review**: Review pages on vimeo
|
- **vimeo:review**: Review pages on vimeo
|
||||||
- **vimeo:user**
|
- **vimeo:user**
|
||||||
- **vimeo:watchlater**: Vimeo watch later list, "vimeowatchlater" keyword (requires authentication)
|
- **vimeo:watchlater**: Vimeo watch later list, "vimeowatchlater" keyword (requires authentication)
|
||||||
|
- **Vimm:recording**
|
||||||
|
- **Vimm:stream**
|
||||||
- **Vimple**: Vimple - one-click video hosting
|
- **Vimple**: Vimple - one-click video hosting
|
||||||
- **Vine**
|
- **Vine**
|
||||||
- **vine:user**
|
- **vine:user**
|
||||||
@@ -1313,6 +1414,7 @@
|
|||||||
- **vlive**
|
- **vlive**
|
||||||
- **vlive:channel**
|
- **vlive:channel**
|
||||||
- **vlive:post**
|
- **vlive:post**
|
||||||
|
- **vm.tiktok**
|
||||||
- **Vodlocker**
|
- **Vodlocker**
|
||||||
- **VODPl**
|
- **VODPl**
|
||||||
- **VODPlatform**
|
- **VODPlatform**
|
||||||
@@ -1332,7 +1434,6 @@
|
|||||||
- **VShare**
|
- **VShare**
|
||||||
- **VTM**
|
- **VTM**
|
||||||
- **VTXTV**
|
- **VTXTV**
|
||||||
- **vube**: Vube.com
|
|
||||||
- **VuClip**
|
- **VuClip**
|
||||||
- **Vupload**
|
- **Vupload**
|
||||||
- **VVVVID**
|
- **VVVVID**
|
||||||
@@ -1342,16 +1443,19 @@
|
|||||||
- **Wakanim**
|
- **Wakanim**
|
||||||
- **Walla**
|
- **Walla**
|
||||||
- **WalyTV**
|
- **WalyTV**
|
||||||
|
- **wasdtv:clip**
|
||||||
|
- **wasdtv:record**
|
||||||
|
- **wasdtv:stream**
|
||||||
- **washingtonpost**
|
- **washingtonpost**
|
||||||
- **washingtonpost:article**
|
- **washingtonpost:article**
|
||||||
- **wat.tv**
|
- **wat.tv**
|
||||||
- **WatchBox**
|
- **WatchBox**
|
||||||
- **WatchIndianPorn**: Watch Indian Porn
|
- **WatchIndianPorn**: Watch Indian Porn
|
||||||
- **WDR**
|
- **WDR**
|
||||||
- **wdr:mobile**
|
- **wdr:mobile** (Currently broken)
|
||||||
- **WDRElefant**
|
- **WDRElefant**
|
||||||
- **WDRPage**
|
- **WDRPage**
|
||||||
- **web.archive:youtube**: web.archive.org saved youtube videos
|
- **web.archive:youtube**: web.archive.org saved youtube videos, "ytarchive:" prefix
|
||||||
- **Webcaster**
|
- **Webcaster**
|
||||||
- **WebcasterFeed**
|
- **WebcasterFeed**
|
||||||
- **WebOfStories**
|
- **WebOfStories**
|
||||||
@@ -1383,6 +1487,7 @@
|
|||||||
- **xiami:song**: 虾米音乐
|
- **xiami:song**: 虾米音乐
|
||||||
- **ximalaya**: 喜马拉雅FM
|
- **ximalaya**: 喜马拉雅FM
|
||||||
- **ximalaya:album**: 喜马拉雅FM 专辑
|
- **ximalaya:album**: 喜马拉雅FM 专辑
|
||||||
|
- **xinpianchang**: xinpianchang.com
|
||||||
- **XMinus**
|
- **XMinus**
|
||||||
- **XNXX**
|
- **XNXX**
|
||||||
- **Xstream**
|
- **Xstream**
|
||||||
@@ -1402,6 +1507,7 @@
|
|||||||
- **yandexmusic:playlist**: Яндекс.Музыка - Плейлист
|
- **yandexmusic:playlist**: Яндекс.Музыка - Плейлист
|
||||||
- **yandexmusic:track**: Яндекс.Музыка - Трек
|
- **yandexmusic:track**: Яндекс.Музыка - Трек
|
||||||
- **YandexVideo**
|
- **YandexVideo**
|
||||||
|
- **YandexVideoPreview**
|
||||||
- **YapFiles**
|
- **YapFiles**
|
||||||
- **YesJapan**
|
- **YesJapan**
|
||||||
- **yinyuetai:video**: 音悦Tai
|
- **yinyuetai:video**: 音悦Tai
|
||||||
@@ -1418,6 +1524,7 @@
|
|||||||
- **youtube**: YouTube
|
- **youtube**: YouTube
|
||||||
- **youtube:favorites**: YouTube liked videos; ":ytfav" keyword (requires cookies)
|
- **youtube:favorites**: YouTube liked videos; ":ytfav" keyword (requires cookies)
|
||||||
- **youtube:history**: Youtube watch history; ":ythis" keyword (requires cookies)
|
- **youtube:history**: Youtube watch history; ":ythis" keyword (requires cookies)
|
||||||
|
- **youtube:music:search_url**: YouTube music search URLs with selectable sections (Eg: #songs)
|
||||||
- **youtube:playlist**: YouTube playlists
|
- **youtube:playlist**: YouTube playlists
|
||||||
- **youtube:recommended**: YouTube recommended videos; ":ytrec" keyword
|
- **youtube:recommended**: YouTube recommended videos; ":ytrec" keyword
|
||||||
- **youtube:search**: YouTube search; "ytsearch:" prefix
|
- **youtube:search**: YouTube search; "ytsearch:" prefix
|
||||||
@@ -1425,12 +1532,15 @@
|
|||||||
- **youtube:search_url**: YouTube search URLs with sorting and filter support
|
- **youtube:search_url**: YouTube search URLs with sorting and filter support
|
||||||
- **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:watchlater**: Youtube watch later list; ":ytwatchlater" keyword (requires cookies)
|
- **youtube:watchlater**: Youtube watch later list; ":ytwatchlater" keyword (requires cookies)
|
||||||
|
- **YoutubeLivestreamEmbed**: YouTube livestream embeds
|
||||||
- **YoutubeYtBe**: youtu.be
|
- **YoutubeYtBe**: youtu.be
|
||||||
- **YoutubeYtUser**: YouTube user videos; "ytuser:" prefix
|
|
||||||
- **Zapiks**
|
- **Zapiks**
|
||||||
- **Zattoo**
|
- **Zattoo**
|
||||||
- **ZattooLive**
|
- **ZattooLive**
|
||||||
|
- **ZattooMovies**
|
||||||
|
- **ZattooRecordings**
|
||||||
- **ZDF**
|
- **ZDF**
|
||||||
- **ZDFChannel**
|
- **ZDFChannel**
|
||||||
- **Zee5**
|
- **Zee5**
|
||||||
@@ -1438,7 +1548,7 @@
|
|||||||
- **ZenYandex**
|
- **ZenYandex**
|
||||||
- **ZenYandexChannel**
|
- **ZenYandexChannel**
|
||||||
- **Zhihu**
|
- **Zhihu**
|
||||||
- **zingmp3**: mp3.zing.vn
|
- **zingmp3**: zingmp3.vn
|
||||||
- **zingmp3:album**
|
- **zingmp3:album**
|
||||||
- **zoom**
|
- **zoom**
|
||||||
- **Zype**
|
- **Zype**
|
||||||
|
|||||||
@@ -196,15 +196,7 @@ def expect_dict(self, got_dict, expected_dict):
|
|||||||
|
|
||||||
def sanitize_got_info_dict(got_dict):
|
def sanitize_got_info_dict(got_dict):
|
||||||
IGNORED_FIELDS = (
|
IGNORED_FIELDS = (
|
||||||
# Format keys
|
*YoutubeDL._format_fields,
|
||||||
'url', 'manifest_url', 'format', 'format_id', 'format_note', 'width', 'height', 'resolution',
|
|
||||||
'dynamic_range', 'tbr', 'abr', 'acodec', 'asr', 'vbr', 'fps', 'vcodec', 'container', 'filesize',
|
|
||||||
'filesize_approx', 'player_url', 'protocol', 'fragment_base_url', 'fragments', 'preference',
|
|
||||||
'language', 'language_preference', 'quality', 'source_preference', 'http_headers',
|
|
||||||
'stretched_ratio', 'no_resume', 'has_drm', 'downloader_options',
|
|
||||||
|
|
||||||
# RTMP formats
|
|
||||||
'page_url', 'app', 'play_path', 'tc_url', 'flash_version', 'rtmp_live', 'rtmp_conn', 'rtmp_protocol', 'rtmp_real_time',
|
|
||||||
|
|
||||||
# Lists
|
# Lists
|
||||||
'formats', 'thumbnails', 'subtitles', 'automatic_captions', 'comments', 'entries',
|
'formats', 'thumbnails', 'subtitles', 'automatic_captions', 'comments', 'entries',
|
||||||
@@ -220,7 +212,7 @@ def sanitize_got_info_dict(got_dict):
|
|||||||
IGNORED_PREFIXES = ('', 'playlist', 'requested', 'webpage')
|
IGNORED_PREFIXES = ('', 'playlist', 'requested', 'webpage')
|
||||||
|
|
||||||
def sanitize(key, value):
|
def sanitize(key, value):
|
||||||
if isinstance(value, str) and len(value) > 100:
|
if isinstance(value, str) and len(value) > 100 and key != 'thumbnail':
|
||||||
return f'md5:{md5(value)}'
|
return f'md5:{md5(value)}'
|
||||||
elif isinstance(value, list) and len(value) > 10:
|
elif isinstance(value, list) and len(value) > 10:
|
||||||
return f'count:{len(value)}'
|
return f'count:{len(value)}'
|
||||||
|
|||||||
@@ -30,9 +30,7 @@ class YDL(FakeYDL):
|
|||||||
self.msgs = []
|
self.msgs = []
|
||||||
|
|
||||||
def process_info(self, info_dict):
|
def process_info(self, info_dict):
|
||||||
info_dict = info_dict.copy()
|
self.downloaded_info_dicts.append(info_dict.copy())
|
||||||
info_dict.pop('__original_infodict', None)
|
|
||||||
self.downloaded_info_dicts.append(info_dict)
|
|
||||||
|
|
||||||
def to_screen(self, msg):
|
def to_screen(self, msg):
|
||||||
self.msgs.append(msg)
|
self.msgs.append(msg)
|
||||||
@@ -820,6 +818,8 @@ class TestYoutubeDL(unittest.TestCase):
|
|||||||
test('%(id&foo)s.bar', 'foo.bar')
|
test('%(id&foo)s.bar', 'foo.bar')
|
||||||
test('%(title&foo)s.bar', 'NA.bar')
|
test('%(title&foo)s.bar', 'NA.bar')
|
||||||
test('%(title&foo|baz)s.bar', 'baz.bar')
|
test('%(title&foo|baz)s.bar', 'baz.bar')
|
||||||
|
test('%(x,id&foo|baz)s.bar', 'foo.bar')
|
||||||
|
test('%(x,title&foo|baz)s.bar', 'baz.bar')
|
||||||
|
|
||||||
# Laziness
|
# Laziness
|
||||||
def gen():
|
def gen():
|
||||||
@@ -898,20 +898,6 @@ class TestYoutubeDL(unittest.TestCase):
|
|||||||
os.unlink(filename)
|
os.unlink(filename)
|
||||||
|
|
||||||
def test_match_filter(self):
|
def test_match_filter(self):
|
||||||
class FilterYDL(YDL):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(FilterYDL, self).__init__(*args, **kwargs)
|
|
||||||
self.params['simulate'] = True
|
|
||||||
|
|
||||||
def process_info(self, info_dict):
|
|
||||||
super(YDL, self).process_info(info_dict)
|
|
||||||
|
|
||||||
def _match_entry(self, info_dict, incomplete=False):
|
|
||||||
res = super(FilterYDL, self)._match_entry(info_dict, incomplete)
|
|
||||||
if res is None:
|
|
||||||
self.downloaded_info_dicts.append(info_dict.copy())
|
|
||||||
return res
|
|
||||||
|
|
||||||
first = {
|
first = {
|
||||||
'id': '1',
|
'id': '1',
|
||||||
'url': TEST_URL,
|
'url': TEST_URL,
|
||||||
@@ -939,7 +925,7 @@ class TestYoutubeDL(unittest.TestCase):
|
|||||||
videos = [first, second]
|
videos = [first, second]
|
||||||
|
|
||||||
def get_videos(filter_=None):
|
def get_videos(filter_=None):
|
||||||
ydl = FilterYDL({'match_filter': filter_})
|
ydl = YDL({'match_filter': filter_, 'simulate': True})
|
||||||
for v in videos:
|
for v in videos:
|
||||||
ydl.process_ie_result(v, download=True)
|
ydl.process_ie_result(v, download=True)
|
||||||
return [v['id'] for v in ydl.downloaded_info_dicts]
|
return [v['id'] for v in ydl.downloaded_info_dicts]
|
||||||
@@ -947,7 +933,7 @@ class TestYoutubeDL(unittest.TestCase):
|
|||||||
res = get_videos()
|
res = get_videos()
|
||||||
self.assertEqual(res, ['1', '2'])
|
self.assertEqual(res, ['1', '2'])
|
||||||
|
|
||||||
def f(v):
|
def f(v, incomplete):
|
||||||
if v['id'] == '1':
|
if v['id'] == '1':
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -12,11 +12,6 @@ from test.helper import FakeYDL, is_download_test
|
|||||||
from yt_dlp.extractor import IqiyiIE
|
from yt_dlp.extractor import IqiyiIE
|
||||||
|
|
||||||
|
|
||||||
class IqiyiIEWithCredentials(IqiyiIE):
|
|
||||||
def _get_login_info(self):
|
|
||||||
return 'foo', 'bar'
|
|
||||||
|
|
||||||
|
|
||||||
class WarningLogger(object):
|
class WarningLogger(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.messages = []
|
self.messages = []
|
||||||
@@ -40,8 +35,8 @@ class TestIqiyiSDKInterpreter(unittest.TestCase):
|
|||||||
If `sign` is incorrect, /validate call throws an HTTP 556 error
|
If `sign` is incorrect, /validate call throws an HTTP 556 error
|
||||||
'''
|
'''
|
||||||
logger = WarningLogger()
|
logger = WarningLogger()
|
||||||
ie = IqiyiIEWithCredentials(FakeYDL({'logger': logger}))
|
ie = IqiyiIE(FakeYDL({'logger': logger}))
|
||||||
ie._login()
|
ie._perform_login('foo', 'bar')
|
||||||
self.assertTrue('unable to log in:' in logger.messages[0])
|
self.assertTrue('unable to log in:' in logger.messages[0])
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,18 +7,19 @@ import unittest
|
|||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
|
||||||
from yt_dlp.extractor import (
|
from yt_dlp.extractor import gen_extractor_classes
|
||||||
gen_extractors,
|
from yt_dlp.extractor.common import InfoExtractor
|
||||||
)
|
|
||||||
|
NO_LOGIN = InfoExtractor._perform_login
|
||||||
|
|
||||||
|
|
||||||
class TestNetRc(unittest.TestCase):
|
class TestNetRc(unittest.TestCase):
|
||||||
def test_netrc_present(self):
|
def test_netrc_present(self):
|
||||||
for ie in gen_extractors():
|
for ie in gen_extractor_classes():
|
||||||
if not hasattr(ie, '_login'):
|
if ie._perform_login is NO_LOGIN:
|
||||||
continue
|
continue
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
hasattr(ie, '_NETRC_MACHINE'),
|
ie._NETRC_MACHINE,
|
||||||
'Extractor %s supports login, but is missing a _NETRC_MACHINE property' % ie.IE_NAME)
|
'Extractor %s supports login, but is missing a _NETRC_MACHINE property' % ie.IE_NAME)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ from yt_dlp.utils import (
|
|||||||
is_html,
|
is_html,
|
||||||
js_to_json,
|
js_to_json,
|
||||||
limit_length,
|
limit_length,
|
||||||
|
locked_file,
|
||||||
merge_dicts,
|
merge_dicts,
|
||||||
mimetype2ext,
|
mimetype2ext,
|
||||||
month_by_name,
|
month_by_name,
|
||||||
@@ -160,10 +161,12 @@ class TestUtil(unittest.TestCase):
|
|||||||
sanitize_filename('New World record at 0:12:34'),
|
sanitize_filename('New World record at 0:12:34'),
|
||||||
'New World record at 0_12_34')
|
'New World record at 0_12_34')
|
||||||
|
|
||||||
self.assertEqual(sanitize_filename('--gasdgf'), '_-gasdgf')
|
self.assertEqual(sanitize_filename('--gasdgf'), '--gasdgf')
|
||||||
self.assertEqual(sanitize_filename('--gasdgf', is_id=True), '--gasdgf')
|
self.assertEqual(sanitize_filename('--gasdgf', is_id=True), '--gasdgf')
|
||||||
self.assertEqual(sanitize_filename('.gasdgf'), 'gasdgf')
|
self.assertEqual(sanitize_filename('--gasdgf', is_id=False), '_-gasdgf')
|
||||||
|
self.assertEqual(sanitize_filename('.gasdgf'), '.gasdgf')
|
||||||
self.assertEqual(sanitize_filename('.gasdgf', is_id=True), '.gasdgf')
|
self.assertEqual(sanitize_filename('.gasdgf', is_id=True), '.gasdgf')
|
||||||
|
self.assertEqual(sanitize_filename('.gasdgf', is_id=False), 'gasdgf')
|
||||||
|
|
||||||
forbidden = '"\0\\/'
|
forbidden = '"\0\\/'
|
||||||
for fc in forbidden:
|
for fc in forbidden:
|
||||||
@@ -625,6 +628,8 @@ class TestUtil(unittest.TestCase):
|
|||||||
self.assertEqual(parse_duration('3h 11m 53s'), 11513)
|
self.assertEqual(parse_duration('3h 11m 53s'), 11513)
|
||||||
self.assertEqual(parse_duration('3 hours 11 minutes 53 seconds'), 11513)
|
self.assertEqual(parse_duration('3 hours 11 minutes 53 seconds'), 11513)
|
||||||
self.assertEqual(parse_duration('3 hours 11 mins 53 secs'), 11513)
|
self.assertEqual(parse_duration('3 hours 11 mins 53 secs'), 11513)
|
||||||
|
self.assertEqual(parse_duration('3 hours, 11 minutes, 53 seconds'), 11513)
|
||||||
|
self.assertEqual(parse_duration('3 hours, 11 mins, 53 secs'), 11513)
|
||||||
self.assertEqual(parse_duration('62m45s'), 3765)
|
self.assertEqual(parse_duration('62m45s'), 3765)
|
||||||
self.assertEqual(parse_duration('6m59s'), 419)
|
self.assertEqual(parse_duration('6m59s'), 419)
|
||||||
self.assertEqual(parse_duration('49s'), 49)
|
self.assertEqual(parse_duration('49s'), 49)
|
||||||
@@ -1133,7 +1138,7 @@ class TestUtil(unittest.TestCase):
|
|||||||
|
|
||||||
def test_clean_html(self):
|
def test_clean_html(self):
|
||||||
self.assertEqual(clean_html('a:\nb'), 'a: b')
|
self.assertEqual(clean_html('a:\nb'), 'a: b')
|
||||||
self.assertEqual(clean_html('a:\n "b"'), 'a: "b"')
|
self.assertEqual(clean_html('a:\n "b"'), 'a: "b"')
|
||||||
self.assertEqual(clean_html('a<br>\xa0b'), 'a\nb')
|
self.assertEqual(clean_html('a<br>\xa0b'), 'a\nb')
|
||||||
|
|
||||||
def test_intlist_to_bytes(self):
|
def test_intlist_to_bytes(self):
|
||||||
@@ -1780,6 +1785,7 @@ Line 1
|
|||||||
self.assertEqual(format_bytes(1024**6), '1.00EiB')
|
self.assertEqual(format_bytes(1024**6), '1.00EiB')
|
||||||
self.assertEqual(format_bytes(1024**7), '1.00ZiB')
|
self.assertEqual(format_bytes(1024**7), '1.00ZiB')
|
||||||
self.assertEqual(format_bytes(1024**8), '1.00YiB')
|
self.assertEqual(format_bytes(1024**8), '1.00YiB')
|
||||||
|
self.assertEqual(format_bytes(1024**9), '1024.00YiB')
|
||||||
|
|
||||||
def test_hide_login_info(self):
|
def test_hide_login_info(self):
|
||||||
self.assertEqual(Config.hide_login_info(['-u', 'foo', '-p', 'bar']),
|
self.assertEqual(Config.hide_login_info(['-u', 'foo', '-p', 'bar']),
|
||||||
@@ -1790,6 +1796,36 @@ Line 1
|
|||||||
self.assertEqual(Config.hide_login_info(['--username=foo']),
|
self.assertEqual(Config.hide_login_info(['--username=foo']),
|
||||||
['--username=PRIVATE'])
|
['--username=PRIVATE'])
|
||||||
|
|
||||||
|
def test_locked_file(self):
|
||||||
|
TEXT = 'test_locked_file\n'
|
||||||
|
FILE = 'test_locked_file.ytdl'
|
||||||
|
MODES = 'war' # Order is important
|
||||||
|
|
||||||
|
try:
|
||||||
|
for lock_mode in MODES:
|
||||||
|
with locked_file(FILE, lock_mode, False) as f:
|
||||||
|
if lock_mode == 'r':
|
||||||
|
self.assertEqual(f.read(), TEXT * 2, 'Wrong file content')
|
||||||
|
else:
|
||||||
|
f.write(TEXT)
|
||||||
|
for test_mode in MODES:
|
||||||
|
testing_write = test_mode != 'r'
|
||||||
|
try:
|
||||||
|
with locked_file(FILE, test_mode, False):
|
||||||
|
pass
|
||||||
|
except (BlockingIOError, PermissionError):
|
||||||
|
if not testing_write: # FIXME
|
||||||
|
print(f'Known issue: Exclusive lock ({lock_mode}) blocks read access ({test_mode})')
|
||||||
|
continue
|
||||||
|
self.assertTrue(testing_write, f'{test_mode} is blocked by {lock_mode}')
|
||||||
|
else:
|
||||||
|
self.assertFalse(testing_write, f'{test_mode} is not blocked by {lock_mode}')
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
os.remove(FILE)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -9,11 +9,9 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|||||||
|
|
||||||
from test.helper import FakeYDL, is_download_test
|
from test.helper import FakeYDL, is_download_test
|
||||||
|
|
||||||
|
|
||||||
from yt_dlp.extractor import (
|
from yt_dlp.extractor import (
|
||||||
YoutubePlaylistIE,
|
|
||||||
YoutubeTabIE,
|
|
||||||
YoutubeIE,
|
YoutubeIE,
|
||||||
|
YoutubeTabIE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -27,21 +25,10 @@ class TestYoutubeLists(unittest.TestCase):
|
|||||||
dl = FakeYDL()
|
dl = FakeYDL()
|
||||||
dl.params['noplaylist'] = True
|
dl.params['noplaylist'] = True
|
||||||
ie = YoutubeTabIE(dl)
|
ie = YoutubeTabIE(dl)
|
||||||
result = ie.extract('https://www.youtube.com/watch?v=FXxLjLQi3Fg&list=PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re')
|
result = ie.extract('https://www.youtube.com/watch?v=OmJ-4B-mS-Y&list=PLydZ2Hrp_gPRJViZjLFKaBMgCQOYEEkyp&index=2')
|
||||||
self.assertEqual(result['_type'], 'url')
|
self.assertEqual(result['_type'], 'url')
|
||||||
self.assertEqual(YoutubeIE.extract_id(result['url']), 'FXxLjLQi3Fg')
|
self.assertEqual(result['ie_key'], YoutubeIE.ie_key())
|
||||||
|
self.assertEqual(YoutubeIE.extract_id(result['url']), 'OmJ-4B-mS-Y')
|
||||||
def test_youtube_course(self):
|
|
||||||
print('Skipping: Course URLs no longer exists')
|
|
||||||
return
|
|
||||||
dl = FakeYDL()
|
|
||||||
ie = YoutubePlaylistIE(dl)
|
|
||||||
# TODO find a > 100 (paginating?) videos course
|
|
||||||
result = ie.extract('https://www.youtube.com/course?list=ECUl4u3cNGP61MdtwGTqZA0MreSaDybji8')
|
|
||||||
entries = list(result['entries'])
|
|
||||||
self.assertEqual(YoutubeIE.extract_id(entries[0]['url']), 'j9WZyLZCBzs')
|
|
||||||
self.assertEqual(len(entries), 25)
|
|
||||||
self.assertEqual(YoutubeIE.extract_id(entries[-1]['url']), 'rYefUsYuEp0')
|
|
||||||
|
|
||||||
def test_youtube_mix(self):
|
def test_youtube_mix(self):
|
||||||
dl = FakeYDL()
|
dl = FakeYDL()
|
||||||
@@ -52,15 +39,6 @@ class TestYoutubeLists(unittest.TestCase):
|
|||||||
original_video = entries[0]
|
original_video = entries[0]
|
||||||
self.assertEqual(original_video['id'], 'tyITL_exICo')
|
self.assertEqual(original_video['id'], 'tyITL_exICo')
|
||||||
|
|
||||||
def test_youtube_toptracks(self):
|
|
||||||
print('Skipping: The playlist page gives error 500')
|
|
||||||
return
|
|
||||||
dl = FakeYDL()
|
|
||||||
ie = YoutubePlaylistIE(dl)
|
|
||||||
result = ie.extract('https://www.youtube.com/playlist?list=MCUS')
|
|
||||||
entries = result['entries']
|
|
||||||
self.assertEqual(len(entries), 100)
|
|
||||||
|
|
||||||
def test_youtube_flat_playlist_extraction(self):
|
def test_youtube_flat_playlist_extraction(self):
|
||||||
dl = FakeYDL()
|
dl = FakeYDL()
|
||||||
dl.params['extract_flat'] = True
|
dl.params['extract_flat'] = True
|
||||||
|
|||||||
@@ -86,6 +86,14 @@ _NSIG_TESTS = [
|
|||||||
'https://www.youtube.com/s/player/8040e515/player_ias.vflset/en_US/base.js',
|
'https://www.youtube.com/s/player/8040e515/player_ias.vflset/en_US/base.js',
|
||||||
'wvOFaY-yjgDuIEg5', 'HkfBFDHmgw4rsw',
|
'wvOFaY-yjgDuIEg5', 'HkfBFDHmgw4rsw',
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
'https://www.youtube.com/s/player/e06dea74/player_ias.vflset/en_US/base.js',
|
||||||
|
'AiuodmaDDYw8d3y4bf', 'ankd8eza2T6Qmw',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'https://www.youtube.com/s/player/5dd88d1d/player-plasma-ias-phone-en_US.vflset/base.js',
|
||||||
|
'kSxKFLeqzv_ZyHSAt', 'n8gS8oRlHOxPFA',
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -116,10 +124,17 @@ class TestPlayerInfo(unittest.TestCase):
|
|||||||
class TestSignature(unittest.TestCase):
|
class TestSignature(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
TEST_DIR = os.path.dirname(os.path.abspath(__file__))
|
TEST_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
self.TESTDATA_DIR = os.path.join(TEST_DIR, 'testdata')
|
self.TESTDATA_DIR = os.path.join(TEST_DIR, 'testdata/sigs')
|
||||||
if not os.path.exists(self.TESTDATA_DIR):
|
if not os.path.exists(self.TESTDATA_DIR):
|
||||||
os.mkdir(self.TESTDATA_DIR)
|
os.mkdir(self.TESTDATA_DIR)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
try:
|
||||||
|
for f in os.listdir(self.TESTDATA_DIR):
|
||||||
|
os.remove(f)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def t_factory(name, sig_func, url_pattern):
|
def t_factory(name, sig_func, url_pattern):
|
||||||
def make_tfunc(url, sig_input, expected_sig):
|
def make_tfunc(url, sig_input, expected_sig):
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -2,8 +2,15 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from math import ceil
|
from math import ceil
|
||||||
|
|
||||||
from .compat import compat_b64decode, compat_pycrypto_AES
|
from .compat import (
|
||||||
from .utils import bytes_to_intlist, intlist_to_bytes
|
compat_b64decode,
|
||||||
|
compat_ord,
|
||||||
|
compat_pycrypto_AES,
|
||||||
|
)
|
||||||
|
from .utils import (
|
||||||
|
bytes_to_intlist,
|
||||||
|
intlist_to_bytes,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if compat_pycrypto_AES:
|
if compat_pycrypto_AES:
|
||||||
@@ -25,6 +32,10 @@ else:
|
|||||||
return intlist_to_bytes(aes_gcm_decrypt_and_verify(*map(bytes_to_intlist, (data, key, tag, nonce))))
|
return intlist_to_bytes(aes_gcm_decrypt_and_verify(*map(bytes_to_intlist, (data, key, tag, nonce))))
|
||||||
|
|
||||||
|
|
||||||
|
def unpad_pkcs7(data):
|
||||||
|
return data[:-compat_ord(data[-1])]
|
||||||
|
|
||||||
|
|
||||||
BLOCK_SIZE_BYTES = 16
|
BLOCK_SIZE_BYTES = 16
|
||||||
|
|
||||||
|
|
||||||
@@ -506,5 +517,6 @@ __all__ = [
|
|||||||
'aes_encrypt',
|
'aes_encrypt',
|
||||||
'aes_gcm_decrypt_and_verify',
|
'aes_gcm_decrypt_and_verify',
|
||||||
'aes_gcm_decrypt_and_verify_bytes',
|
'aes_gcm_decrypt_and_verify_bytes',
|
||||||
'key_expansion'
|
'key_expansion',
|
||||||
|
'unpad_pkcs7',
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import base64
|
import base64
|
||||||
|
import collections
|
||||||
import ctypes
|
import ctypes
|
||||||
import getpass
|
import getpass
|
||||||
import html
|
import html
|
||||||
@@ -133,6 +134,16 @@ except AttributeError:
|
|||||||
asyncio.run = compat_asyncio_run
|
asyncio.run = compat_asyncio_run
|
||||||
|
|
||||||
|
|
||||||
|
try: # >= 3.7
|
||||||
|
asyncio.tasks.all_tasks
|
||||||
|
except AttributeError:
|
||||||
|
asyncio.tasks.all_tasks = asyncio.tasks.Task.all_tasks
|
||||||
|
|
||||||
|
try:
|
||||||
|
import websockets as compat_websockets
|
||||||
|
except ImportError:
|
||||||
|
compat_websockets = None
|
||||||
|
|
||||||
# Python 3.8+ does not honor %HOME% on windows, but this breaks compatibility with youtube-dl
|
# Python 3.8+ does not honor %HOME% on windows, but this breaks compatibility with youtube-dl
|
||||||
# See https://github.com/yt-dlp/yt-dlp/issues/792
|
# See https://github.com/yt-dlp/yt-dlp/issues/792
|
||||||
# https://docs.python.org/3/library/os.path.html#os.path.expanduser
|
# https://docs.python.org/3/library/os.path.html#os.path.expanduser
|
||||||
@@ -159,6 +170,13 @@ except ImportError:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
compat_pycrypto_AES = None
|
compat_pycrypto_AES = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
import brotlicffi as compat_brotli
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
import brotli as compat_brotli
|
||||||
|
except ImportError:
|
||||||
|
compat_brotli = None
|
||||||
|
|
||||||
WINDOWS_VT_MODE = False if compat_os_name == 'nt' else None
|
WINDOWS_VT_MODE = False if compat_os_name == 'nt' else None
|
||||||
|
|
||||||
@@ -180,14 +198,17 @@ def windows_enable_vt_mode(): # TODO: Do this the proper way https://bugs.pytho
|
|||||||
|
|
||||||
compat_basestring = str
|
compat_basestring = str
|
||||||
compat_chr = chr
|
compat_chr = chr
|
||||||
|
compat_filter = filter
|
||||||
compat_input = input
|
compat_input = input
|
||||||
compat_integer_types = (int, )
|
compat_integer_types = (int, )
|
||||||
compat_kwargs = lambda kwargs: kwargs
|
compat_kwargs = lambda kwargs: kwargs
|
||||||
|
compat_map = map
|
||||||
compat_numeric_types = (int, float, complex)
|
compat_numeric_types = (int, float, complex)
|
||||||
compat_str = str
|
compat_str = str
|
||||||
compat_xpath = lambda xpath: xpath
|
compat_xpath = lambda xpath: xpath
|
||||||
compat_zip = zip
|
compat_zip = zip
|
||||||
|
|
||||||
|
compat_collections_abc = collections.abc
|
||||||
compat_HTMLParser = html.parser.HTMLParser
|
compat_HTMLParser = html.parser.HTMLParser
|
||||||
compat_HTTPError = urllib.error.HTTPError
|
compat_HTTPError = urllib.error.HTTPError
|
||||||
compat_Struct = struct.Struct
|
compat_Struct = struct.Struct
|
||||||
@@ -244,7 +265,9 @@ __all__ = [
|
|||||||
'compat_asyncio_run',
|
'compat_asyncio_run',
|
||||||
'compat_b64decode',
|
'compat_b64decode',
|
||||||
'compat_basestring',
|
'compat_basestring',
|
||||||
|
'compat_brotli',
|
||||||
'compat_chr',
|
'compat_chr',
|
||||||
|
'compat_collections_abc',
|
||||||
'compat_cookiejar',
|
'compat_cookiejar',
|
||||||
'compat_cookiejar_Cookie',
|
'compat_cookiejar_Cookie',
|
||||||
'compat_cookies',
|
'compat_cookies',
|
||||||
@@ -254,6 +277,7 @@ __all__ = [
|
|||||||
'compat_etree_fromstring',
|
'compat_etree_fromstring',
|
||||||
'compat_etree_register_namespace',
|
'compat_etree_register_namespace',
|
||||||
'compat_expanduser',
|
'compat_expanduser',
|
||||||
|
'compat_filter',
|
||||||
'compat_get_terminal_size',
|
'compat_get_terminal_size',
|
||||||
'compat_getenv',
|
'compat_getenv',
|
||||||
'compat_getpass',
|
'compat_getpass',
|
||||||
@@ -265,6 +289,7 @@ __all__ = [
|
|||||||
'compat_integer_types',
|
'compat_integer_types',
|
||||||
'compat_itertools_count',
|
'compat_itertools_count',
|
||||||
'compat_kwargs',
|
'compat_kwargs',
|
||||||
|
'compat_map',
|
||||||
'compat_numeric_types',
|
'compat_numeric_types',
|
||||||
'compat_ord',
|
'compat_ord',
|
||||||
'compat_os_name',
|
'compat_os_name',
|
||||||
@@ -296,6 +321,7 @@ __all__ = [
|
|||||||
'compat_urllib_response',
|
'compat_urllib_response',
|
||||||
'compat_urlparse',
|
'compat_urlparse',
|
||||||
'compat_urlretrieve',
|
'compat_urlretrieve',
|
||||||
|
'compat_websockets',
|
||||||
'compat_xml_parse_error',
|
'compat_xml_parse_error',
|
||||||
'compat_xpath',
|
'compat_xpath',
|
||||||
'compat_zip',
|
'compat_zip',
|
||||||
|
|||||||
@@ -11,12 +11,17 @@ from datetime import datetime, timedelta, timezone
|
|||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
from hashlib import pbkdf2_hmac
|
from hashlib import pbkdf2_hmac
|
||||||
|
|
||||||
from .aes import aes_cbc_decrypt_bytes, aes_gcm_decrypt_and_verify_bytes
|
from .aes import (
|
||||||
|
aes_cbc_decrypt_bytes,
|
||||||
|
aes_gcm_decrypt_and_verify_bytes,
|
||||||
|
unpad_pkcs7,
|
||||||
|
)
|
||||||
from .compat import (
|
from .compat import (
|
||||||
compat_b64decode,
|
compat_b64decode,
|
||||||
compat_cookiejar_Cookie,
|
compat_cookiejar_Cookie,
|
||||||
)
|
)
|
||||||
from .utils import (
|
from .utils import (
|
||||||
|
error_to_str,
|
||||||
expand_path,
|
expand_path,
|
||||||
Popen,
|
Popen,
|
||||||
YoutubeDLCookieJar,
|
YoutubeDLCookieJar,
|
||||||
@@ -450,7 +455,10 @@ def _extract_safari_cookies(profile, logger):
|
|||||||
cookies_path = os.path.expanduser('~/Library/Cookies/Cookies.binarycookies')
|
cookies_path = os.path.expanduser('~/Library/Cookies/Cookies.binarycookies')
|
||||||
|
|
||||||
if not os.path.isfile(cookies_path):
|
if not os.path.isfile(cookies_path):
|
||||||
raise FileNotFoundError('could not find safari cookies database')
|
logger.debug('Trying secondary cookie location')
|
||||||
|
cookies_path = os.path.expanduser('~/Library/Containers/com.apple.Safari/Data/Library/Cookies/Cookies.binarycookies')
|
||||||
|
if not os.path.isfile(cookies_path):
|
||||||
|
raise FileNotFoundError('could not find safari cookies database')
|
||||||
|
|
||||||
with open(cookies_path, 'rb') as f:
|
with open(cookies_path, 'rb') as f:
|
||||||
cookies_data = f.read()
|
cookies_data = f.read()
|
||||||
@@ -669,8 +677,7 @@ def _get_linux_desktop_environment(env):
|
|||||||
return _LinuxDesktopEnvironment.GNOME
|
return _LinuxDesktopEnvironment.GNOME
|
||||||
elif 'KDE_FULL_SESSION' in env:
|
elif 'KDE_FULL_SESSION' in env:
|
||||||
return _LinuxDesktopEnvironment.KDE
|
return _LinuxDesktopEnvironment.KDE
|
||||||
else:
|
return _LinuxDesktopEnvironment.OTHER
|
||||||
return _LinuxDesktopEnvironment.OTHER
|
|
||||||
|
|
||||||
|
|
||||||
def _choose_linux_keyring(logger):
|
def _choose_linux_keyring(logger):
|
||||||
@@ -715,7 +722,7 @@ def _get_kwallet_network_wallet(logger):
|
|||||||
network_wallet = stdout.decode('utf-8').strip()
|
network_wallet = stdout.decode('utf-8').strip()
|
||||||
logger.debug('NetworkWallet = "{}"'.format(network_wallet))
|
logger.debug('NetworkWallet = "{}"'.format(network_wallet))
|
||||||
return network_wallet
|
return network_wallet
|
||||||
except BaseException as e:
|
except Exception as e:
|
||||||
logger.warning('exception while obtaining NetworkWallet: {}'.format(e))
|
logger.warning('exception while obtaining NetworkWallet: {}'.format(e))
|
||||||
return default_wallet
|
return default_wallet
|
||||||
|
|
||||||
@@ -760,8 +767,8 @@ def _get_kwallet_password(browser_keyring_name, logger):
|
|||||||
if stdout[-1:] == b'\n':
|
if stdout[-1:] == b'\n':
|
||||||
stdout = stdout[:-1]
|
stdout = stdout[:-1]
|
||||||
return stdout
|
return stdout
|
||||||
except BaseException as e:
|
except Exception as e:
|
||||||
logger.warning(f'exception running kwallet-query: {type(e).__name__}({e})')
|
logger.warning(f'exception running kwallet-query: {error_to_str(e)}')
|
||||||
return b''
|
return b''
|
||||||
|
|
||||||
|
|
||||||
@@ -817,8 +824,8 @@ def _get_mac_keyring_password(browser_keyring_name, logger):
|
|||||||
if stdout[-1:] == b'\n':
|
if stdout[-1:] == b'\n':
|
||||||
stdout = stdout[:-1]
|
stdout = stdout[:-1]
|
||||||
return stdout
|
return stdout
|
||||||
except BaseException as e:
|
except Exception as e:
|
||||||
logger.warning(f'exception running find-generic-password: {type(e).__name__}({e})')
|
logger.warning(f'exception running find-generic-password: {error_to_str(e)}')
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -847,10 +854,9 @@ def pbkdf2_sha1(password, salt, iterations, key_length):
|
|||||||
|
|
||||||
|
|
||||||
def _decrypt_aes_cbc(ciphertext, key, logger, initialization_vector=b' ' * 16):
|
def _decrypt_aes_cbc(ciphertext, key, logger, initialization_vector=b' ' * 16):
|
||||||
plaintext = aes_cbc_decrypt_bytes(ciphertext, key, initialization_vector)
|
plaintext = unpad_pkcs7(aes_cbc_decrypt_bytes(ciphertext, key, initialization_vector))
|
||||||
padding_length = plaintext[-1]
|
|
||||||
try:
|
try:
|
||||||
return plaintext[:-padding_length].decode('utf-8')
|
return plaintext.decode('utf-8')
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
logger.warning('failed to decrypt cookie (AES-CBC) because UTF-8 decoding failed. Possibly the key is wrong?', only_once=True)
|
logger.warning('failed to decrypt cookie (AES-CBC) because UTF-8 decoding failed. Possibly the key is wrong?', only_once=True)
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ def get_suitable_downloader(info_dict, params={}, default=NO_DEFAULT, protocol=N
|
|||||||
from .common import FileDownloader
|
from .common import FileDownloader
|
||||||
from .dash import DashSegmentsFD
|
from .dash import DashSegmentsFD
|
||||||
from .f4m import F4mFD
|
from .f4m import F4mFD
|
||||||
|
from .fc2 import FC2LiveFD
|
||||||
from .hls import HlsFD
|
from .hls import HlsFD
|
||||||
from .http import HttpFD
|
from .http import HttpFD
|
||||||
from .rtmp import RtmpFD
|
from .rtmp import RtmpFD
|
||||||
@@ -58,6 +59,7 @@ PROTOCOL_MAP = {
|
|||||||
'ism': IsmFD,
|
'ism': IsmFD,
|
||||||
'mhtml': MhtmlFD,
|
'mhtml': MhtmlFD,
|
||||||
'niconico_dmc': NiconicoDmcFD,
|
'niconico_dmc': NiconicoDmcFD,
|
||||||
|
'fc2_live': FC2LiveFD,
|
||||||
'websocket_frag': WebSocketFragmentFD,
|
'websocket_frag': WebSocketFragmentFD,
|
||||||
'youtube_live_chat': YoutubeLiveChatFD,
|
'youtube_live_chat': YoutubeLiveChatFD,
|
||||||
'youtube_live_chat_replay': YoutubeLiveChatFD,
|
'youtube_live_chat_replay': YoutubeLiveChatFD,
|
||||||
@@ -117,7 +119,7 @@ def _get_suitable_downloader(info_dict, protocol, params, default):
|
|||||||
return FFmpegFD
|
return FFmpegFD
|
||||||
elif (external_downloader or '').lower() == 'native':
|
elif (external_downloader or '').lower() == 'native':
|
||||||
return HlsFD
|
return HlsFD
|
||||||
elif get_suitable_downloader(
|
elif protocol == 'm3u8_native' and get_suitable_downloader(
|
||||||
info_dict, params, None, protocol='m3u8_frag_urls', to_stdout=info_dict['to_stdout']):
|
info_dict, params, None, protocol='m3u8_frag_urls', to_stdout=info_dict['to_stdout']):
|
||||||
return HlsFD
|
return HlsFD
|
||||||
elif params.get('hls_prefer_native') is True:
|
elif params.get('hls_prefer_native') is True:
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from ..utils import (
|
|||||||
encodeFilename,
|
encodeFilename,
|
||||||
error_to_compat_str,
|
error_to_compat_str,
|
||||||
format_bytes,
|
format_bytes,
|
||||||
|
LockingUnsupportedError,
|
||||||
sanitize_open,
|
sanitize_open,
|
||||||
shell_quote,
|
shell_quote,
|
||||||
timeconvert,
|
timeconvert,
|
||||||
@@ -159,7 +160,7 @@ class FileDownloader(object):
|
|||||||
return int(round(number * multiplier))
|
return int(round(number * multiplier))
|
||||||
|
|
||||||
def to_screen(self, *args, **kargs):
|
def to_screen(self, *args, **kargs):
|
||||||
self.ydl.to_stdout(*args, quiet=self.params.get('quiet'), **kargs)
|
self.ydl.to_screen(*args, quiet=self.params.get('quiet'), **kargs)
|
||||||
|
|
||||||
def to_stderr(self, message):
|
def to_stderr(self, message):
|
||||||
self.ydl.to_stderr(message)
|
self.ydl.to_stderr(message)
|
||||||
@@ -210,28 +211,44 @@ class FileDownloader(object):
|
|||||||
def ytdl_filename(self, filename):
|
def ytdl_filename(self, filename):
|
||||||
return filename + '.ytdl'
|
return filename + '.ytdl'
|
||||||
|
|
||||||
def sanitize_open(self, filename, open_mode):
|
def wrap_file_access(action, *, fatal=False):
|
||||||
file_access_retries = self.params.get('file_access_retries', 10)
|
def outer(func):
|
||||||
retry = 0
|
def inner(self, *args, **kwargs):
|
||||||
while True:
|
file_access_retries = self.params.get('file_access_retries', 0)
|
||||||
try:
|
retry = 0
|
||||||
return sanitize_open(filename, open_mode)
|
while True:
|
||||||
except (IOError, OSError) as err:
|
try:
|
||||||
retry = retry + 1
|
return func(self, *args, **kwargs)
|
||||||
if retry > file_access_retries or err.errno not in (errno.EACCES,):
|
except (IOError, OSError) as err:
|
||||||
raise
|
retry = retry + 1
|
||||||
self.to_screen(
|
if retry > file_access_retries or err.errno not in (errno.EACCES, errno.EINVAL):
|
||||||
'[download] Got file access error. Retrying (attempt %d of %s) ...'
|
if not fatal:
|
||||||
% (retry, self.format_retries(file_access_retries)))
|
self.report_error(f'unable to {action} file: {err}')
|
||||||
time.sleep(0.01)
|
return
|
||||||
|
raise
|
||||||
|
self.to_screen(
|
||||||
|
f'[download] Unable to {action} file due to file access error. '
|
||||||
|
f'Retrying (attempt {retry} of {self.format_retries(file_access_retries)}) ...')
|
||||||
|
time.sleep(0.01)
|
||||||
|
return inner
|
||||||
|
return outer
|
||||||
|
|
||||||
|
@wrap_file_access('open', fatal=True)
|
||||||
|
def sanitize_open(self, filename, open_mode):
|
||||||
|
f, filename = sanitize_open(filename, open_mode)
|
||||||
|
if not getattr(f, 'locked', None):
|
||||||
|
self.write_debug(f'{LockingUnsupportedError.msg}. Proceeding without locking', only_once=True)
|
||||||
|
return f, filename
|
||||||
|
|
||||||
|
@wrap_file_access('remove')
|
||||||
|
def try_remove(self, filename):
|
||||||
|
os.remove(filename)
|
||||||
|
|
||||||
|
@wrap_file_access('rename')
|
||||||
def try_rename(self, old_filename, new_filename):
|
def try_rename(self, old_filename, new_filename):
|
||||||
if old_filename == new_filename:
|
if old_filename == new_filename:
|
||||||
return
|
return
|
||||||
try:
|
os.replace(old_filename, new_filename)
|
||||||
os.replace(old_filename, new_filename)
|
|
||||||
except (IOError, OSError) as err:
|
|
||||||
self.report_error(f'unable to rename file: {err}')
|
|
||||||
|
|
||||||
def try_utime(self, filename, last_modified_hdr):
|
def try_utime(self, filename, last_modified_hdr):
|
||||||
"""Try to set the last-modified time of the given file."""
|
"""Try to set the last-modified time of the given file."""
|
||||||
@@ -264,9 +281,9 @@ class FileDownloader(object):
|
|||||||
elif self.ydl.params.get('logger'):
|
elif self.ydl.params.get('logger'):
|
||||||
self._multiline = MultilineLogger(self.ydl.params['logger'], lines)
|
self._multiline = MultilineLogger(self.ydl.params['logger'], lines)
|
||||||
elif self.params.get('progress_with_newline'):
|
elif self.params.get('progress_with_newline'):
|
||||||
self._multiline = BreaklineStatusPrinter(self.ydl._screen_file, lines)
|
self._multiline = BreaklineStatusPrinter(self.ydl._out_files['screen'], lines)
|
||||||
else:
|
else:
|
||||||
self._multiline = MultilinePrinter(self.ydl._screen_file, lines, not self.params.get('quiet'))
|
self._multiline = MultilinePrinter(self.ydl._out_files['screen'], lines, not self.params.get('quiet'))
|
||||||
self._multiline.allow_colors = self._multiline._HAVE_FULLCAP and not self.params.get('no_color')
|
self._multiline.allow_colors = self._multiline._HAVE_FULLCAP and not self.params.get('no_color')
|
||||||
|
|
||||||
def _finish_multiline_status(self):
|
def _finish_multiline_status(self):
|
||||||
|
|||||||
@@ -13,15 +13,18 @@ from ..compat import (
|
|||||||
)
|
)
|
||||||
from ..postprocessor.ffmpeg import FFmpegPostProcessor, EXT_TO_OUT_FORMATS
|
from ..postprocessor.ffmpeg import FFmpegPostProcessor, EXT_TO_OUT_FORMATS
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
|
classproperty,
|
||||||
cli_option,
|
cli_option,
|
||||||
cli_valueless_option,
|
cli_valueless_option,
|
||||||
cli_bool_option,
|
cli_bool_option,
|
||||||
_configuration_args,
|
_configuration_args,
|
||||||
|
determine_ext,
|
||||||
encodeFilename,
|
encodeFilename,
|
||||||
encodeArgument,
|
encodeArgument,
|
||||||
handle_youtubedl_headers,
|
handle_youtubedl_headers,
|
||||||
check_executable,
|
check_executable,
|
||||||
Popen,
|
Popen,
|
||||||
|
remove_end,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -71,17 +74,23 @@ class ExternalFD(FragmentFD):
|
|||||||
def get_basename(cls):
|
def get_basename(cls):
|
||||||
return cls.__name__[:-2].lower()
|
return cls.__name__[:-2].lower()
|
||||||
|
|
||||||
|
@classproperty
|
||||||
|
def EXE_NAME(cls):
|
||||||
|
return cls.get_basename()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def exe(self):
|
def exe(self):
|
||||||
return self.get_basename()
|
return self.EXE_NAME
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def available(cls, path=None):
|
def available(cls, path=None):
|
||||||
path = check_executable(path or cls.get_basename(), [cls.AVAILABLE_OPT])
|
path = check_executable(
|
||||||
if path:
|
cls.EXE_NAME if path in (None, cls.get_basename()) else path,
|
||||||
cls.exe = path
|
[cls.AVAILABLE_OPT])
|
||||||
return path
|
if not path:
|
||||||
return False
|
return False
|
||||||
|
cls.exe = path
|
||||||
|
return path
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def supports(cls, info_dict):
|
def supports(cls, info_dict):
|
||||||
@@ -104,7 +113,7 @@ class ExternalFD(FragmentFD):
|
|||||||
|
|
||||||
def _configuration_args(self, keys=None, *args, **kwargs):
|
def _configuration_args(self, keys=None, *args, **kwargs):
|
||||||
return _configuration_args(
|
return _configuration_args(
|
||||||
self.get_basename(), self.params.get('external_downloader_args'), self.get_basename(),
|
self.get_basename(), self.params.get('external_downloader_args'), self.EXE_NAME,
|
||||||
keys, *args, **kwargs)
|
keys, *args, **kwargs)
|
||||||
|
|
||||||
def _call_downloader(self, tmpfilename, info_dict):
|
def _call_downloader(self, tmpfilename, info_dict):
|
||||||
@@ -157,9 +166,9 @@ class ExternalFD(FragmentFD):
|
|||||||
dest.write(decrypt_fragment(fragment, src.read()))
|
dest.write(decrypt_fragment(fragment, src.read()))
|
||||||
src.close()
|
src.close()
|
||||||
if not self.params.get('keep_fragments', False):
|
if not self.params.get('keep_fragments', False):
|
||||||
os.remove(encodeFilename(fragment_filename))
|
self.try_remove(encodeFilename(fragment_filename))
|
||||||
dest.close()
|
dest.close()
|
||||||
os.remove(encodeFilename('%s.frag.urls' % tmpfilename))
|
self.try_remove(encodeFilename('%s.frag.urls' % tmpfilename))
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
@@ -167,7 +176,7 @@ class CurlFD(ExternalFD):
|
|||||||
AVAILABLE_OPT = '-V'
|
AVAILABLE_OPT = '-V'
|
||||||
|
|
||||||
def _make_cmd(self, tmpfilename, info_dict):
|
def _make_cmd(self, tmpfilename, info_dict):
|
||||||
cmd = [self.exe, '--location', '-o', tmpfilename]
|
cmd = [self.exe, '--location', '-o', tmpfilename, '--compressed']
|
||||||
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', '%s: %s' % (key, val)]
|
cmd += ['--header', '%s: %s' % (key, val)]
|
||||||
@@ -217,7 +226,7 @@ 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']
|
cmd = [self.exe, '-O', tmpfilename, '-nv', '--no-cookies', '--compression=auto']
|
||||||
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', '%s: %s' % (key, val)]
|
cmd += ['--header', '%s: %s' % (key, val)]
|
||||||
@@ -228,7 +237,10 @@ class WgetFD(ExternalFD):
|
|||||||
retry[1] = '0'
|
retry[1] = '0'
|
||||||
cmd += retry
|
cmd += retry
|
||||||
cmd += self._option('--bind-address', 'source_address')
|
cmd += self._option('--bind-address', 'source_address')
|
||||||
cmd += self._option('--proxy', 'proxy')
|
proxy = self.params.get('proxy')
|
||||||
|
if proxy:
|
||||||
|
for var in ('http_proxy', 'https_proxy'):
|
||||||
|
cmd += ['--execute', '%s=%s' % (var, proxy)]
|
||||||
cmd += self._valueless_option('--no-check-certificate', 'nocheckcertificate')
|
cmd += self._valueless_option('--no-check-certificate', 'nocheckcertificate')
|
||||||
cmd += self._configuration_args()
|
cmd += self._configuration_args()
|
||||||
cmd += ['--', info_dict['url']]
|
cmd += ['--', info_dict['url']]
|
||||||
@@ -251,7 +263,7 @@ class Aria2cFD(ExternalFD):
|
|||||||
def _make_cmd(self, tmpfilename, info_dict):
|
def _make_cmd(self, tmpfilename, info_dict):
|
||||||
cmd = [self.exe, '-c',
|
cmd = [self.exe, '-c',
|
||||||
'--console-log-level=warn', '--summary-interval=0', '--download-result=hide',
|
'--console-log-level=warn', '--summary-interval=0', '--download-result=hide',
|
||||||
'--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:
|
||||||
cmd += ['--allow-overwrite=true', '--allow-piece-length-change=true']
|
cmd += ['--allow-overwrite=true', '--allow-piece-length-change=true']
|
||||||
else:
|
else:
|
||||||
@@ -301,10 +313,7 @@ class Aria2cFD(ExternalFD):
|
|||||||
|
|
||||||
class HttpieFD(ExternalFD):
|
class HttpieFD(ExternalFD):
|
||||||
AVAILABLE_OPT = '--version'
|
AVAILABLE_OPT = '--version'
|
||||||
|
EXE_NAME = 'http'
|
||||||
@classmethod
|
|
||||||
def available(cls, path=None):
|
|
||||||
return super().available(path or 'http')
|
|
||||||
|
|
||||||
def _make_cmd(self, tmpfilename, info_dict):
|
def _make_cmd(self, tmpfilename, info_dict):
|
||||||
cmd = ['http', '--download', '--output', tmpfilename, info_dict['url']]
|
cmd = ['http', '--download', '--output', tmpfilename, info_dict['url']]
|
||||||
@@ -463,6 +472,15 @@ class FFmpegFD(ExternalFD):
|
|||||||
args += ['-f', 'flv']
|
args += ['-f', 'flv']
|
||||||
elif ext == 'mp4' and tmpfilename == '-':
|
elif ext == 'mp4' and tmpfilename == '-':
|
||||||
args += ['-f', 'mpegts']
|
args += ['-f', 'mpegts']
|
||||||
|
elif ext == 'unknown_video':
|
||||||
|
ext = determine_ext(remove_end(tmpfilename, '.part'))
|
||||||
|
if ext == 'unknown_video':
|
||||||
|
self.report_warning(
|
||||||
|
'The video format is unknown and cannot be downloaded by ffmpeg. '
|
||||||
|
'Explicitly set the extension in the filename to attempt download in that format')
|
||||||
|
else:
|
||||||
|
self.report_warning(f'The video format is unknown. Trying to download as {ext} according to the filename')
|
||||||
|
args += ['-f', EXT_TO_OUT_FORMATS.get(ext, ext)]
|
||||||
else:
|
else:
|
||||||
args += ['-f', EXT_TO_OUT_FORMATS.get(ext, ext)]
|
args += ['-f', EXT_TO_OUT_FORMATS.get(ext, ext)]
|
||||||
|
|
||||||
@@ -496,11 +514,13 @@ class AVconvFD(FFmpegFD):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
_BY_NAME = dict(
|
_BY_NAME = {
|
||||||
(klass.get_basename(), klass)
|
klass.get_basename(): klass
|
||||||
for name, klass in globals().items()
|
for name, klass in globals().items()
|
||||||
if name.endswith('FD') and name not in ('ExternalFD', 'FragmentFD')
|
if name.endswith('FD') and name not in ('ExternalFD', 'FragmentFD')
|
||||||
)
|
}
|
||||||
|
|
||||||
|
_BY_EXE = {klass.EXE_NAME: klass for klass in _BY_NAME.values()}
|
||||||
|
|
||||||
|
|
||||||
def list_external_downloaders():
|
def list_external_downloaders():
|
||||||
@@ -512,4 +532,4 @@ def get_external_downloader(external_downloader):
|
|||||||
downloader . """
|
downloader . """
|
||||||
# Drop .exe extension on Windows
|
# Drop .exe extension on Windows
|
||||||
bn = os.path.splitext(os.path.basename(external_downloader))[0]
|
bn = os.path.splitext(os.path.basename(external_downloader))[0]
|
||||||
return _BY_NAME.get(bn)
|
return _BY_NAME.get(bn, _BY_EXE.get(bn))
|
||||||
|
|||||||
41
yt_dlp/downloader/fc2.py
Normal file
41
yt_dlp/downloader/fc2.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
from __future__ import division, unicode_literals
|
||||||
|
|
||||||
|
import threading
|
||||||
|
|
||||||
|
from .common import FileDownloader
|
||||||
|
from .external import FFmpegFD
|
||||||
|
|
||||||
|
|
||||||
|
class FC2LiveFD(FileDownloader):
|
||||||
|
"""
|
||||||
|
Downloads FC2 live without being stopped. <br>
|
||||||
|
Note, this is not a part of public API, and will be removed without notice.
|
||||||
|
DO NOT USE
|
||||||
|
"""
|
||||||
|
|
||||||
|
def real_download(self, filename, info_dict):
|
||||||
|
ws = info_dict['ws']
|
||||||
|
|
||||||
|
heartbeat_lock = threading.Lock()
|
||||||
|
heartbeat_state = [None, 1]
|
||||||
|
|
||||||
|
def heartbeat():
|
||||||
|
try:
|
||||||
|
heartbeat_state[1] += 1
|
||||||
|
ws.send('{"name":"heartbeat","arguments":{},"id":%d}' % heartbeat_state[1])
|
||||||
|
except Exception:
|
||||||
|
self.to_screen('[fc2:live] Heartbeat failed')
|
||||||
|
|
||||||
|
with heartbeat_lock:
|
||||||
|
heartbeat_state[0] = threading.Timer(30, heartbeat)
|
||||||
|
heartbeat_state[0]._daemonic = True
|
||||||
|
heartbeat_state[0].start()
|
||||||
|
|
||||||
|
heartbeat()
|
||||||
|
|
||||||
|
new_info_dict = info_dict.copy()
|
||||||
|
new_info_dict.update({
|
||||||
|
'ws': None,
|
||||||
|
'protocol': 'live_ffmpeg',
|
||||||
|
})
|
||||||
|
return FFmpegFD(self.ydl, self.params or {}).download(filename, new_info_dict)
|
||||||
@@ -14,7 +14,7 @@ except ImportError:
|
|||||||
|
|
||||||
from .common import FileDownloader
|
from .common import FileDownloader
|
||||||
from .http import HttpFD
|
from .http import HttpFD
|
||||||
from ..aes import aes_cbc_decrypt_bytes
|
from ..aes import aes_cbc_decrypt_bytes, unpad_pkcs7
|
||||||
from ..compat import (
|
from ..compat import (
|
||||||
compat_os_name,
|
compat_os_name,
|
||||||
compat_urllib_error,
|
compat_urllib_error,
|
||||||
@@ -25,6 +25,7 @@ from ..utils import (
|
|||||||
error_to_compat_str,
|
error_to_compat_str,
|
||||||
encodeFilename,
|
encodeFilename,
|
||||||
sanitized_Request,
|
sanitized_Request,
|
||||||
|
traverse_obj,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -132,14 +133,19 @@ class FragmentFD(FileDownloader):
|
|||||||
}
|
}
|
||||||
success = ctx['dl'].download(fragment_filename, fragment_info_dict)
|
success = ctx['dl'].download(fragment_filename, fragment_info_dict)
|
||||||
if not success:
|
if not success:
|
||||||
return False, None
|
return False
|
||||||
if fragment_info_dict.get('filetime'):
|
if fragment_info_dict.get('filetime'):
|
||||||
ctx['fragment_filetime'] = fragment_info_dict.get('filetime')
|
ctx['fragment_filetime'] = fragment_info_dict.get('filetime')
|
||||||
ctx['fragment_filename_sanitized'] = fragment_filename
|
ctx['fragment_filename_sanitized'] = fragment_filename
|
||||||
return True, self._read_fragment(ctx)
|
return True
|
||||||
|
|
||||||
def _read_fragment(self, ctx):
|
def _read_fragment(self, ctx):
|
||||||
down, frag_sanitized = self.sanitize_open(ctx['fragment_filename_sanitized'], 'rb')
|
try:
|
||||||
|
down, frag_sanitized = self.sanitize_open(ctx['fragment_filename_sanitized'], 'rb')
|
||||||
|
except FileNotFoundError:
|
||||||
|
if ctx.get('live'):
|
||||||
|
return None
|
||||||
|
raise
|
||||||
ctx['fragment_filename_sanitized'] = frag_sanitized
|
ctx['fragment_filename_sanitized'] = frag_sanitized
|
||||||
frag_content = down.read()
|
frag_content = down.read()
|
||||||
down.close()
|
down.close()
|
||||||
@@ -153,7 +159,7 @@ class FragmentFD(FileDownloader):
|
|||||||
if self.__do_ytdl_file(ctx):
|
if self.__do_ytdl_file(ctx):
|
||||||
self._write_ytdl_file(ctx)
|
self._write_ytdl_file(ctx)
|
||||||
if not self.params.get('keep_fragments', False):
|
if not self.params.get('keep_fragments', False):
|
||||||
os.remove(encodeFilename(ctx['fragment_filename_sanitized']))
|
self.try_remove(encodeFilename(ctx['fragment_filename_sanitized']))
|
||||||
del ctx['fragment_filename_sanitized']
|
del ctx['fragment_filename_sanitized']
|
||||||
|
|
||||||
def _prepare_frag_download(self, ctx):
|
def _prepare_frag_download(self, ctx):
|
||||||
@@ -172,7 +178,7 @@ class FragmentFD(FileDownloader):
|
|||||||
dl = HttpQuietDownloader(
|
dl = HttpQuietDownloader(
|
||||||
self.ydl,
|
self.ydl,
|
||||||
{
|
{
|
||||||
'continuedl': True,
|
'continuedl': self.params.get('continuedl', True),
|
||||||
'quiet': self.params.get('quiet'),
|
'quiet': self.params.get('quiet'),
|
||||||
'noprogress': True,
|
'noprogress': True,
|
||||||
'ratelimit': self.params.get('ratelimit'),
|
'ratelimit': self.params.get('ratelimit'),
|
||||||
@@ -299,7 +305,7 @@ class FragmentFD(FileDownloader):
|
|||||||
if self.__do_ytdl_file(ctx):
|
if self.__do_ytdl_file(ctx):
|
||||||
ytdl_filename = encodeFilename(self.ytdl_filename(ctx['filename']))
|
ytdl_filename = encodeFilename(self.ytdl_filename(ctx['filename']))
|
||||||
if os.path.isfile(ytdl_filename):
|
if os.path.isfile(ytdl_filename):
|
||||||
os.remove(ytdl_filename)
|
self.try_remove(ytdl_filename)
|
||||||
elapsed = time.time() - ctx['started']
|
elapsed = time.time() - ctx['started']
|
||||||
|
|
||||||
if ctx['tmpfilename'] == '-':
|
if ctx['tmpfilename'] == '-':
|
||||||
@@ -366,8 +372,7 @@ class FragmentFD(FileDownloader):
|
|||||||
# not what it decrypts to.
|
# not what it decrypts to.
|
||||||
if self.params.get('test', False):
|
if self.params.get('test', False):
|
||||||
return frag_content
|
return frag_content
|
||||||
decrypted_data = aes_cbc_decrypt_bytes(frag_content, decrypt_info['KEY'], iv)
|
return unpad_pkcs7(aes_cbc_decrypt_bytes(frag_content, decrypt_info['KEY'], iv))
|
||||||
return decrypted_data[:-decrypted_data[-1]]
|
|
||||||
|
|
||||||
return decrypt_fragment
|
return decrypt_fragment
|
||||||
|
|
||||||
@@ -383,6 +388,7 @@ class FragmentFD(FileDownloader):
|
|||||||
max_workers = self.params.get('concurrent_fragment_downloads', 1)
|
max_workers = self.params.get('concurrent_fragment_downloads', 1)
|
||||||
if max_progress > 1:
|
if max_progress > 1:
|
||||||
self._prepare_multiline_status(max_progress)
|
self._prepare_multiline_status(max_progress)
|
||||||
|
is_live = any(traverse_obj(args, (..., 2, 'is_live'), default=[]))
|
||||||
|
|
||||||
def thread_func(idx, ctx, fragments, info_dict, tpe):
|
def thread_func(idx, ctx, fragments, info_dict, tpe):
|
||||||
ctx['max_progress'] = max_progress
|
ctx['max_progress'] = max_progress
|
||||||
@@ -396,25 +402,43 @@ class FragmentFD(FileDownloader):
|
|||||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
spins = []
|
|
||||||
if compat_os_name == 'nt':
|
if compat_os_name == 'nt':
|
||||||
self.report_warning('Ctrl+C does not work on Windows when used with parallel threads. '
|
def future_result(future):
|
||||||
'This is a known issue and patches are welcome')
|
while True:
|
||||||
|
try:
|
||||||
|
return future.result(0.1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
raise
|
||||||
|
except concurrent.futures.TimeoutError:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
def future_result(future):
|
||||||
|
return future.result()
|
||||||
|
|
||||||
|
def interrupt_trigger_iter(fg):
|
||||||
|
for f in fg:
|
||||||
|
if not interrupt_trigger[0]:
|
||||||
|
break
|
||||||
|
yield f
|
||||||
|
|
||||||
|
spins = []
|
||||||
for idx, (ctx, fragments, info_dict) in enumerate(args):
|
for idx, (ctx, fragments, info_dict) in enumerate(args):
|
||||||
tpe = FTPE(math.ceil(max_workers / max_progress))
|
tpe = FTPE(math.ceil(max_workers / max_progress))
|
||||||
job = tpe.submit(thread_func, idx, ctx, fragments, info_dict, tpe)
|
job = tpe.submit(thread_func, idx, ctx, interrupt_trigger_iter(fragments), info_dict, tpe)
|
||||||
spins.append((tpe, job))
|
spins.append((tpe, job))
|
||||||
|
|
||||||
result = True
|
result = True
|
||||||
for tpe, job in spins:
|
for tpe, job in spins:
|
||||||
try:
|
try:
|
||||||
result = result and job.result()
|
result = result and future_result(job)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
interrupt_trigger[0] = False
|
interrupt_trigger[0] = False
|
||||||
finally:
|
finally:
|
||||||
tpe.shutdown(wait=True)
|
tpe.shutdown(wait=True)
|
||||||
if not interrupt_trigger[0]:
|
if not interrupt_trigger[0] and not is_live:
|
||||||
raise KeyboardInterrupt()
|
raise KeyboardInterrupt()
|
||||||
|
# we expect the user wants to stop and DO WANT the preceding postprocessors to run;
|
||||||
|
# so returning a intermediate result here instead of KeyboardInterrupt on live
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def download_and_append_fragments(
|
def download_and_append_fragments(
|
||||||
@@ -432,24 +456,23 @@ class FragmentFD(FileDownloader):
|
|||||||
pack_func = lambda frag_content, _: frag_content
|
pack_func = lambda frag_content, _: frag_content
|
||||||
|
|
||||||
def download_fragment(fragment, ctx):
|
def download_fragment(fragment, ctx):
|
||||||
|
if not interrupt_trigger[0]:
|
||||||
|
return
|
||||||
|
|
||||||
frag_index = ctx['fragment_index'] = fragment['frag_index']
|
frag_index = ctx['fragment_index'] = fragment['frag_index']
|
||||||
ctx['last_error'] = None
|
ctx['last_error'] = None
|
||||||
if not interrupt_trigger[0]:
|
|
||||||
return False, frag_index
|
|
||||||
headers = info_dict.get('http_headers', {}).copy()
|
headers = info_dict.get('http_headers', {}).copy()
|
||||||
byte_range = fragment.get('byte_range')
|
byte_range = fragment.get('byte_range')
|
||||||
if byte_range:
|
if byte_range:
|
||||||
headers['Range'] = 'bytes=%d-%d' % (byte_range['start'], byte_range['end'] - 1)
|
headers['Range'] = 'bytes=%d-%d' % (byte_range['start'], byte_range['end'] - 1)
|
||||||
|
|
||||||
# Never skip the first fragment
|
# Never skip the first fragment
|
||||||
fatal = is_fatal(fragment.get('index') or (frag_index - 1))
|
fatal, count = is_fatal(fragment.get('index') or (frag_index - 1)), 0
|
||||||
count, frag_content = 0, None
|
|
||||||
while count <= fragment_retries:
|
while count <= fragment_retries:
|
||||||
try:
|
try:
|
||||||
success, frag_content = self._download_fragment(ctx, fragment['url'], info_dict, headers)
|
if self._download_fragment(ctx, fragment['url'], info_dict, headers):
|
||||||
if not success:
|
break
|
||||||
return False, frag_index
|
return
|
||||||
break
|
|
||||||
except (compat_urllib_error.HTTPError, http.client.IncompleteRead) as err:
|
except (compat_urllib_error.HTTPError, http.client.IncompleteRead) as err:
|
||||||
# Unavailable (possibly temporary) fragments may be served.
|
# Unavailable (possibly temporary) fragments may be served.
|
||||||
# First we try to retry then either skip or abort.
|
# First we try to retry then either skip or abort.
|
||||||
@@ -466,25 +489,19 @@ class FragmentFD(FileDownloader):
|
|||||||
break
|
break
|
||||||
raise
|
raise
|
||||||
|
|
||||||
if count > fragment_retries:
|
if count > fragment_retries and fatal:
|
||||||
if not fatal:
|
|
||||||
return False, frag_index
|
|
||||||
ctx['dest_stream'].close()
|
ctx['dest_stream'].close()
|
||||||
self.report_error('Giving up after %s fragment retries' % fragment_retries)
|
self.report_error('Giving up after %s fragment retries' % fragment_retries)
|
||||||
return False, frag_index
|
|
||||||
return frag_content, frag_index
|
|
||||||
|
|
||||||
def append_fragment(frag_content, frag_index, ctx):
|
def append_fragment(frag_content, frag_index, ctx):
|
||||||
if not frag_content:
|
if frag_content:
|
||||||
if not is_fatal(frag_index - 1):
|
self._append_fragment(ctx, pack_func(frag_content, frag_index))
|
||||||
self.report_skip_fragment(frag_index, 'fragment not found')
|
elif not is_fatal(frag_index - 1):
|
||||||
return True
|
self.report_skip_fragment(frag_index, 'fragment not found')
|
||||||
else:
|
else:
|
||||||
ctx['dest_stream'].close()
|
ctx['dest_stream'].close()
|
||||||
self.report_error(
|
self.report_error(f'fragment {frag_index} not found, unable to continue')
|
||||||
'fragment %s not found, unable to continue' % frag_index)
|
return False
|
||||||
return False
|
|
||||||
self._append_fragment(ctx, pack_func(frag_content, frag_index))
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
decrypt_fragment = self.decrypter(info_dict)
|
decrypt_fragment = self.decrypter(info_dict)
|
||||||
@@ -495,25 +512,23 @@ class FragmentFD(FileDownloader):
|
|||||||
|
|
||||||
def _download_fragment(fragment):
|
def _download_fragment(fragment):
|
||||||
ctx_copy = ctx.copy()
|
ctx_copy = ctx.copy()
|
||||||
frag_content, frag_index = download_fragment(fragment, ctx_copy)
|
download_fragment(fragment, ctx_copy)
|
||||||
return fragment, frag_content, frag_index, ctx_copy.get('fragment_filename_sanitized')
|
return fragment, fragment['frag_index'], ctx_copy.get('fragment_filename_sanitized')
|
||||||
|
|
||||||
self.report_warning('The download speed shown is only of one thread. This is a known issue and patches are welcome')
|
self.report_warning('The download speed shown is only of one thread. This is a known issue and patches are welcome')
|
||||||
with tpe or concurrent.futures.ThreadPoolExecutor(max_workers) as pool:
|
with tpe or concurrent.futures.ThreadPoolExecutor(max_workers) as pool:
|
||||||
for fragment, frag_content, frag_index, frag_filename in pool.map(_download_fragment, fragments):
|
for fragment, frag_index, frag_filename in pool.map(_download_fragment, fragments):
|
||||||
if not interrupt_trigger[0]:
|
|
||||||
break
|
|
||||||
ctx['fragment_filename_sanitized'] = frag_filename
|
ctx['fragment_filename_sanitized'] = frag_filename
|
||||||
ctx['fragment_index'] = frag_index
|
ctx['fragment_index'] = frag_index
|
||||||
result = append_fragment(decrypt_fragment(fragment, frag_content), frag_index, ctx)
|
result = append_fragment(decrypt_fragment(fragment, self._read_fragment(ctx)), frag_index, ctx)
|
||||||
if not result:
|
if not result:
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
for fragment in fragments:
|
for fragment in fragments:
|
||||||
if not interrupt_trigger[0]:
|
if not interrupt_trigger[0]:
|
||||||
break
|
break
|
||||||
frag_content, frag_index = download_fragment(fragment, ctx)
|
download_fragment(fragment, ctx)
|
||||||
result = append_fragment(decrypt_fragment(fragment, frag_content), frag_index, ctx)
|
result = append_fragment(decrypt_fragment(fragment, self._read_fragment(ctx)), fragment['frag_index'], ctx)
|
||||||
if not result:
|
if not result:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,30 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import errno
|
|
||||||
import os
|
import os
|
||||||
import socket
|
import ssl
|
||||||
import time
|
import time
|
||||||
import random
|
import random
|
||||||
import re
|
|
||||||
|
|
||||||
from .common import FileDownloader
|
from .common import FileDownloader
|
||||||
from ..compat import (
|
from ..compat import (
|
||||||
compat_str,
|
|
||||||
compat_urllib_error,
|
compat_urllib_error,
|
||||||
|
compat_http_client
|
||||||
)
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ContentTooShortError,
|
ContentTooShortError,
|
||||||
encodeFilename,
|
encodeFilename,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
|
parse_http_range,
|
||||||
sanitized_Request,
|
sanitized_Request,
|
||||||
ThrottledDownload,
|
ThrottledDownload,
|
||||||
|
try_call,
|
||||||
write_xattr,
|
write_xattr,
|
||||||
XAttrMetadataError,
|
XAttrMetadataError,
|
||||||
XAttrUnavailableError,
|
XAttrUnavailableError,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
RESPONSE_READ_EXCEPTIONS = (TimeoutError, ConnectionError, ssl.SSLError, compat_http_client.HTTPException)
|
||||||
|
|
||||||
|
|
||||||
class HttpFD(FileDownloader):
|
class HttpFD(FileDownloader):
|
||||||
def real_download(self, filename, info_dict):
|
def real_download(self, filename, info_dict):
|
||||||
@@ -53,11 +55,11 @@ class HttpFD(FileDownloader):
|
|||||||
|
|
||||||
ctx.open_mode = 'wb'
|
ctx.open_mode = 'wb'
|
||||||
ctx.resume_len = 0
|
ctx.resume_len = 0
|
||||||
ctx.data_len = None
|
|
||||||
ctx.block_size = self.params.get('buffersize', 1024)
|
ctx.block_size = self.params.get('buffersize', 1024)
|
||||||
ctx.start_time = time.time()
|
ctx.start_time = time.time()
|
||||||
ctx.chunk_size = None
|
|
||||||
throttle_start = None
|
# parse given Range
|
||||||
|
req_start, req_end, _ = parse_http_range(headers.get('Range'))
|
||||||
|
|
||||||
if self.params.get('continuedl', True):
|
if self.params.get('continuedl', True):
|
||||||
# Establish possible resume length
|
# Establish possible resume length
|
||||||
@@ -80,43 +82,50 @@ class HttpFD(FileDownloader):
|
|||||||
class NextFragment(Exception):
|
class NextFragment(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def set_range(req, start, end):
|
|
||||||
range_header = 'bytes=%d-' % start
|
|
||||||
if end:
|
|
||||||
range_header += compat_str(end)
|
|
||||||
req.add_header('Range', range_header)
|
|
||||||
|
|
||||||
def establish_connection():
|
def establish_connection():
|
||||||
ctx.chunk_size = (random.randint(int(chunk_size * 0.95), chunk_size)
|
ctx.chunk_size = (random.randint(int(chunk_size * 0.95), chunk_size)
|
||||||
if not is_test and chunk_size else chunk_size)
|
if not is_test and chunk_size else chunk_size)
|
||||||
if ctx.resume_len > 0:
|
if ctx.resume_len > 0:
|
||||||
range_start = ctx.resume_len
|
range_start = ctx.resume_len
|
||||||
|
if req_start is not None:
|
||||||
|
# offset the beginning of Range to be within request
|
||||||
|
range_start += req_start
|
||||||
if ctx.is_resume:
|
if ctx.is_resume:
|
||||||
self.report_resuming_byte(ctx.resume_len)
|
self.report_resuming_byte(ctx.resume_len)
|
||||||
ctx.open_mode = 'ab'
|
ctx.open_mode = 'ab'
|
||||||
|
elif req_start is not None:
|
||||||
|
range_start = req_start
|
||||||
elif ctx.chunk_size > 0:
|
elif ctx.chunk_size > 0:
|
||||||
range_start = 0
|
range_start = 0
|
||||||
else:
|
else:
|
||||||
range_start = None
|
range_start = None
|
||||||
ctx.is_resume = False
|
ctx.is_resume = False
|
||||||
range_end = range_start + ctx.chunk_size - 1 if ctx.chunk_size else None
|
|
||||||
if range_end and ctx.data_len is not None and range_end >= ctx.data_len:
|
if ctx.chunk_size:
|
||||||
range_end = ctx.data_len - 1
|
chunk_aware_end = range_start + ctx.chunk_size - 1
|
||||||
has_range = range_start is not None
|
# we're not allowed to download outside Range
|
||||||
ctx.has_range = has_range
|
range_end = chunk_aware_end if req_end is None else min(chunk_aware_end, req_end)
|
||||||
|
elif req_end is not None:
|
||||||
|
# there's no need for chunked downloads, so download until the end of Range
|
||||||
|
range_end = req_end
|
||||||
|
else:
|
||||||
|
range_end = None
|
||||||
|
|
||||||
|
if try_call(lambda: range_start > range_end):
|
||||||
|
ctx.resume_len = 0
|
||||||
|
ctx.open_mode = 'wb'
|
||||||
|
raise RetryDownload(Exception(f'Conflicting range. (start={range_start} > end={range_end})'))
|
||||||
|
|
||||||
|
if try_call(lambda: range_end >= ctx.content_len):
|
||||||
|
range_end = ctx.content_len - 1
|
||||||
|
|
||||||
request = sanitized_Request(url, request_data, headers)
|
request = sanitized_Request(url, request_data, headers)
|
||||||
|
has_range = range_start is not None
|
||||||
if has_range:
|
if has_range:
|
||||||
set_range(request, range_start, range_end)
|
request.add_header('Range', f'bytes={int(range_start)}-{int_or_none(range_end) or ""}')
|
||||||
# Establish connection
|
# Establish connection
|
||||||
try:
|
try:
|
||||||
try:
|
ctx.data = self.ydl.urlopen(request)
|
||||||
ctx.data = self.ydl.urlopen(request)
|
|
||||||
except (compat_urllib_error.URLError, ) as err:
|
|
||||||
# reason may not be available, e.g. for urllib2.HTTPError on python 2.6
|
|
||||||
reason = getattr(err, 'reason', None)
|
|
||||||
if isinstance(reason, socket.timeout):
|
|
||||||
raise RetryDownload(err)
|
|
||||||
raise err
|
|
||||||
# When trying to resume, Content-Range HTTP header of response has to be checked
|
# When trying to resume, Content-Range HTTP header of response has to be checked
|
||||||
# to match the value of requested Range HTTP header. This is due to a webservers
|
# to match the value of requested Range HTTP header. This is due to a webservers
|
||||||
# that don't support resuming and serve a whole file with no Content-Range
|
# that don't support resuming and serve a whole file with no Content-Range
|
||||||
@@ -124,31 +133,28 @@ class HttpFD(FileDownloader):
|
|||||||
# https://github.com/ytdl-org/youtube-dl/issues/6057#issuecomment-126129799)
|
# https://github.com/ytdl-org/youtube-dl/issues/6057#issuecomment-126129799)
|
||||||
if has_range:
|
if has_range:
|
||||||
content_range = ctx.data.headers.get('Content-Range')
|
content_range = ctx.data.headers.get('Content-Range')
|
||||||
if content_range:
|
content_range_start, content_range_end, content_len = parse_http_range(content_range)
|
||||||
content_range_m = re.search(r'bytes (\d+)-(\d+)?(?:/(\d+))?', content_range)
|
if content_range_start is not None and range_start == content_range_start:
|
||||||
# Content-Range is present and matches requested Range, resume is possible
|
# Content-Range is present and matches requested Range, resume is possible
|
||||||
if content_range_m:
|
accept_content_len = (
|
||||||
if range_start == int(content_range_m.group(1)):
|
# Non-chunked download
|
||||||
content_range_end = int_or_none(content_range_m.group(2))
|
not ctx.chunk_size
|
||||||
content_len = int_or_none(content_range_m.group(3))
|
# Chunked download and requested piece or
|
||||||
accept_content_len = (
|
# its part is promised to be served
|
||||||
# Non-chunked download
|
or content_range_end == range_end
|
||||||
not ctx.chunk_size
|
or content_len < range_end)
|
||||||
# Chunked download and requested piece or
|
if accept_content_len:
|
||||||
# its part is promised to be served
|
ctx.content_len = content_len
|
||||||
or content_range_end == range_end
|
if content_len or req_end:
|
||||||
or content_len < range_end)
|
ctx.data_len = min(content_len or req_end, req_end or content_len) - (req_start or 0)
|
||||||
if accept_content_len:
|
return
|
||||||
ctx.data_len = content_len
|
|
||||||
return
|
|
||||||
# Content-Range is either not present or invalid. Assuming remote webserver is
|
# Content-Range is either not present or invalid. Assuming remote webserver is
|
||||||
# trying to send the whole file, resume is not possible, so wiping the local file
|
# trying to send the whole file, resume is not possible, so wiping the local file
|
||||||
# and performing entire redownload
|
# and performing entire redownload
|
||||||
self.report_unable_to_resume()
|
self.report_unable_to_resume()
|
||||||
ctx.resume_len = 0
|
ctx.resume_len = 0
|
||||||
ctx.open_mode = 'wb'
|
ctx.open_mode = 'wb'
|
||||||
ctx.data_len = int_or_none(ctx.data.info().get('Content-length', None))
|
ctx.data_len = ctx.content_len = int_or_none(ctx.data.info().get('Content-length', None))
|
||||||
return
|
|
||||||
except (compat_urllib_error.HTTPError, ) as err:
|
except (compat_urllib_error.HTTPError, ) as err:
|
||||||
if err.code == 416:
|
if err.code == 416:
|
||||||
# Unable to resume (requested range not satisfiable)
|
# Unable to resume (requested range not satisfiable)
|
||||||
@@ -190,16 +196,16 @@ class HttpFD(FileDownloader):
|
|||||||
# Unexpected HTTP error
|
# Unexpected HTTP error
|
||||||
raise
|
raise
|
||||||
raise RetryDownload(err)
|
raise RetryDownload(err)
|
||||||
except socket.timeout as err:
|
except compat_urllib_error.URLError as err:
|
||||||
|
if isinstance(err.reason, ssl.CertificateError):
|
||||||
|
raise
|
||||||
|
raise RetryDownload(err)
|
||||||
|
# In urllib.request.AbstractHTTPHandler, the response is partially read on request.
|
||||||
|
# Any errors that occur during this will not be wrapped by URLError
|
||||||
|
except RESPONSE_READ_EXCEPTIONS as err:
|
||||||
raise RetryDownload(err)
|
raise RetryDownload(err)
|
||||||
except socket.error as err:
|
|
||||||
if err.errno in (errno.ECONNRESET, errno.ETIMEDOUT):
|
|
||||||
# Connection reset is no problem, just retry
|
|
||||||
raise RetryDownload(err)
|
|
||||||
raise
|
|
||||||
|
|
||||||
def download():
|
def download():
|
||||||
nonlocal throttle_start
|
|
||||||
data_len = ctx.data.info().get('Content-length', None)
|
data_len = ctx.data.info().get('Content-length', None)
|
||||||
|
|
||||||
# Range HTTP header may be ignored/unsupported by a webserver
|
# Range HTTP header may be ignored/unsupported by a webserver
|
||||||
@@ -242,16 +248,8 @@ class HttpFD(FileDownloader):
|
|||||||
try:
|
try:
|
||||||
# Download and write
|
# Download and write
|
||||||
data_block = ctx.data.read(block_size if not is_test else min(block_size, data_len - byte_counter))
|
data_block = ctx.data.read(block_size if not is_test else min(block_size, data_len - byte_counter))
|
||||||
# socket.timeout is a subclass of socket.error but may not have
|
except RESPONSE_READ_EXCEPTIONS as err:
|
||||||
# errno set
|
retry(err)
|
||||||
except socket.timeout as e:
|
|
||||||
retry(e)
|
|
||||||
except socket.error as e:
|
|
||||||
# SSLError on python 2 (inherits socket.error) may have
|
|
||||||
# no errno set but this error message
|
|
||||||
if e.errno in (errno.ECONNRESET, errno.ETIMEDOUT) or getattr(e, 'message', None) == 'The read operation timed out':
|
|
||||||
retry(e)
|
|
||||||
raise
|
|
||||||
|
|
||||||
byte_counter += len(data_block)
|
byte_counter += len(data_block)
|
||||||
|
|
||||||
@@ -322,16 +320,16 @@ class HttpFD(FileDownloader):
|
|||||||
if speed and speed < (self.params.get('throttledratelimit') or 0):
|
if speed and speed < (self.params.get('throttledratelimit') or 0):
|
||||||
# The speed must stay below the limit for 3 seconds
|
# The speed must stay below the limit for 3 seconds
|
||||||
# This prevents raising error when the speed temporarily goes down
|
# This prevents raising error when the speed temporarily goes down
|
||||||
if throttle_start is None:
|
if ctx.throttle_start is None:
|
||||||
throttle_start = now
|
ctx.throttle_start = now
|
||||||
elif now - throttle_start > 3:
|
elif now - ctx.throttle_start > 3:
|
||||||
if ctx.stream is not None and ctx.tmpfilename != '-':
|
if ctx.stream is not None and ctx.tmpfilename != '-':
|
||||||
ctx.stream.close()
|
ctx.stream.close()
|
||||||
raise ThrottledDownload()
|
raise ThrottledDownload()
|
||||||
elif speed:
|
elif speed:
|
||||||
throttle_start = None
|
ctx.throttle_start = None
|
||||||
|
|
||||||
if not is_test and ctx.chunk_size and ctx.data_len is not None and byte_counter < ctx.data_len:
|
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.resume_len = byte_counter
|
||||||
# ctx.block_size = block_size
|
# ctx.block_size = block_size
|
||||||
raise NextFragment()
|
raise NextFragment()
|
||||||
|
|||||||
@@ -263,9 +263,11 @@ class IsmFD(FragmentFD):
|
|||||||
count = 0
|
count = 0
|
||||||
while count <= fragment_retries:
|
while count <= fragment_retries:
|
||||||
try:
|
try:
|
||||||
success, frag_content = self._download_fragment(ctx, segment['url'], info_dict)
|
success = self._download_fragment(ctx, segment['url'], info_dict)
|
||||||
if not success:
|
if not success:
|
||||||
return False
|
return False
|
||||||
|
frag_content = self._read_fragment(ctx)
|
||||||
|
|
||||||
if not extra_state['ism_track_written']:
|
if not extra_state['ism_track_written']:
|
||||||
tfhd_data = extract_box_data(frag_content, [b'moof', b'traf', b'tfhd'])
|
tfhd_data = extract_box_data(frag_content, [b'moof', b'traf', b'tfhd'])
|
||||||
info_dict['_download_params']['track_id'] = u32.unpack(tfhd_data[4:8])[0]
|
info_dict['_download_params']['track_id'] = u32.unpack(tfhd_data[4:8])[0]
|
||||||
|
|||||||
@@ -166,10 +166,15 @@ body > figure > img {
|
|||||||
if (i + 1) <= ctx['fragment_index']:
|
if (i + 1) <= ctx['fragment_index']:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
fragment_url = urljoin(fragment_base_url, fragment['path'])
|
fragment_url = fragment.get('url')
|
||||||
success, frag_content = self._download_fragment(ctx, fragment_url, info_dict)
|
if not fragment_url:
|
||||||
|
assert fragment_base_url
|
||||||
|
fragment_url = urljoin(fragment_base_url, fragment['path'])
|
||||||
|
|
||||||
|
success = self._download_fragment(ctx, fragment_url, info_dict)
|
||||||
if not success:
|
if not success:
|
||||||
continue
|
continue
|
||||||
|
frag_content = self._read_fragment(ctx)
|
||||||
|
|
||||||
mime_type = b'image/jpeg'
|
mime_type = b'image/jpeg'
|
||||||
if frag_content.startswith(b'\x89PNG\r\n\x1a\n'):
|
if frag_content.startswith(b'\x89PNG\r\n\x1a\n'):
|
||||||
|
|||||||
@@ -5,9 +5,12 @@ import threading
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
import websockets
|
import websockets
|
||||||
has_websockets = True
|
except (ImportError, SyntaxError):
|
||||||
except ImportError:
|
# websockets 3.10 on python 3.6 causes SyntaxError
|
||||||
|
# See https://github.com/yt-dlp/yt-dlp/issues/2633
|
||||||
has_websockets = False
|
has_websockets = False
|
||||||
|
else:
|
||||||
|
has_websockets = True
|
||||||
|
|
||||||
from .common import FileDownloader
|
from .common import FileDownloader
|
||||||
from .external import FFmpegFD
|
from .external import FFmpegFD
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ class YoutubeLiveChatFD(FragmentFD):
|
|||||||
def real_download(self, filename, info_dict):
|
def real_download(self, filename, info_dict):
|
||||||
video_id = info_dict['video_id']
|
video_id = info_dict['video_id']
|
||||||
self.to_screen('[%s] Downloading live chat' % self.FD_NAME)
|
self.to_screen('[%s] Downloading live chat' % self.FD_NAME)
|
||||||
|
if not self.params.get('skip_download') and info_dict['protocol'] == 'youtube_live_chat':
|
||||||
|
self.report_warning('Live chat download runs until the livestream ends. '
|
||||||
|
'If you wish to download the video simultaneously, run a separate yt-dlp instance')
|
||||||
|
|
||||||
fragment_retries = self.params.get('fragment_retries', 0)
|
fragment_retries = self.params.get('fragment_retries', 0)
|
||||||
test = self.params.get('test', False)
|
test = self.params.get('test', False)
|
||||||
@@ -112,9 +115,10 @@ class YoutubeLiveChatFD(FragmentFD):
|
|||||||
count = 0
|
count = 0
|
||||||
while count <= fragment_retries:
|
while count <= fragment_retries:
|
||||||
try:
|
try:
|
||||||
success, raw_fragment = dl_fragment(url, request_data, headers)
|
success = dl_fragment(url, request_data, headers)
|
||||||
if not success:
|
if not success:
|
||||||
return False, None, None, None
|
return False, None, None, None
|
||||||
|
raw_fragment = self._read_fragment(ctx)
|
||||||
try:
|
try:
|
||||||
data = ie.extract_yt_initial_data(video_id, raw_fragment.decode('utf-8', 'replace'))
|
data = ie.extract_yt_initial_data(video_id, raw_fragment.decode('utf-8', 'replace'))
|
||||||
except RegexNotFoundError:
|
except RegexNotFoundError:
|
||||||
@@ -142,9 +146,10 @@ class YoutubeLiveChatFD(FragmentFD):
|
|||||||
|
|
||||||
self._prepare_and_start_frag_download(ctx, info_dict)
|
self._prepare_and_start_frag_download(ctx, info_dict)
|
||||||
|
|
||||||
success, raw_fragment = dl_fragment(info_dict['url'])
|
success = dl_fragment(info_dict['url'])
|
||||||
if not success:
|
if not success:
|
||||||
return False
|
return False
|
||||||
|
raw_fragment = self._read_fragment(ctx)
|
||||||
try:
|
try:
|
||||||
data = ie.extract_yt_initial_data(video_id, raw_fragment.decode('utf-8', 'replace'))
|
data = ie.extract_yt_initial_data(video_id, raw_fragment.decode('utf-8', 'replace'))
|
||||||
except RegexNotFoundError:
|
except RegexNotFoundError:
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ class ABCIViewIE(InfoExtractor):
|
|||||||
'hdnea': token,
|
'hdnea': token,
|
||||||
})
|
})
|
||||||
|
|
||||||
for sd in ('720', 'sd', 'sd-low'):
|
for sd in ('1080', '720', 'sd', 'sd-low'):
|
||||||
sd_url = try_get(
|
sd_url = try_get(
|
||||||
stream, lambda x: x['streams']['hls'][sd], compat_str)
|
stream, lambda x: x['streams']['hls'][sd], compat_str)
|
||||||
if not sd_url:
|
if not sd_url:
|
||||||
@@ -300,11 +300,10 @@ class ABCIViewShowSeriesIE(InfoExtractor):
|
|||||||
unescapeHTML(webpage_data).encode('utf-8').decode('unicode_escape'), show_id)
|
unescapeHTML(webpage_data).encode('utf-8').decode('unicode_escape'), show_id)
|
||||||
video_data = video_data['route']['pageData']['_embedded']
|
video_data = video_data['route']['pageData']['_embedded']
|
||||||
|
|
||||||
if self.get_param('noplaylist') and 'highlightVideo' in video_data:
|
highlight = try_get(video_data, lambda x: x['highlightVideo']['shareUrl'])
|
||||||
self.to_screen('Downloading just the highlight video because of --no-playlist')
|
if not self._yes_playlist(show_id, bool(highlight), video_label='highlight video'):
|
||||||
return self.url_result(video_data['highlightVideo']['shareUrl'], ie=ABCIViewIE.ie_key())
|
return self.url_result(highlight, ie=ABCIViewIE.ie_key())
|
||||||
|
|
||||||
self.to_screen(f'Downloading playlist {show_id} - add --no-playlist to just download the highlight video')
|
|
||||||
series = video_data['selectedSeries']
|
series = video_data['selectedSeries']
|
||||||
return {
|
return {
|
||||||
'_type': 'playlist',
|
'_type': 'playlist',
|
||||||
|
|||||||
476
yt_dlp/extractor/abematv.py
Normal file
476
yt_dlp/extractor/abematv.py
Normal file
@@ -0,0 +1,476 @@
|
|||||||
|
import io
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
import re
|
||||||
|
import struct
|
||||||
|
from base64 import urlsafe_b64encode
|
||||||
|
from binascii import unhexlify
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..aes import aes_ecb_decrypt
|
||||||
|
from ..compat import (
|
||||||
|
compat_urllib_response,
|
||||||
|
compat_urllib_parse_urlparse,
|
||||||
|
compat_urllib_request,
|
||||||
|
)
|
||||||
|
from ..utils import (
|
||||||
|
ExtractorError,
|
||||||
|
decode_base,
|
||||||
|
int_or_none,
|
||||||
|
random_uuidv4,
|
||||||
|
request_to_url,
|
||||||
|
time_seconds,
|
||||||
|
update_url_query,
|
||||||
|
traverse_obj,
|
||||||
|
intlist_to_bytes,
|
||||||
|
bytes_to_intlist,
|
||||||
|
urljoin,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# NOTE: network handler related code is temporary thing until network stack overhaul PRs are merged (#2861/#2862)
|
||||||
|
|
||||||
|
def add_opener(ydl, handler):
|
||||||
|
''' Add a handler for opening URLs, like _download_webpage '''
|
||||||
|
# https://github.com/python/cpython/blob/main/Lib/urllib/request.py#L426
|
||||||
|
# https://github.com/python/cpython/blob/main/Lib/urllib/request.py#L605
|
||||||
|
assert isinstance(ydl._opener, compat_urllib_request.OpenerDirector)
|
||||||
|
ydl._opener.add_handler(handler)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_opener(ydl, handler):
|
||||||
|
'''
|
||||||
|
Remove handler(s) for opening URLs
|
||||||
|
@param handler Either handler object itself or handler type.
|
||||||
|
Specifying handler type will remove all handler which isinstance returns True.
|
||||||
|
'''
|
||||||
|
# https://github.com/python/cpython/blob/main/Lib/urllib/request.py#L426
|
||||||
|
# https://github.com/python/cpython/blob/main/Lib/urllib/request.py#L605
|
||||||
|
opener = ydl._opener
|
||||||
|
assert isinstance(ydl._opener, compat_urllib_request.OpenerDirector)
|
||||||
|
if isinstance(handler, (type, tuple)):
|
||||||
|
find_cp = lambda x: isinstance(x, handler)
|
||||||
|
else:
|
||||||
|
find_cp = lambda x: x is handler
|
||||||
|
|
||||||
|
removed = []
|
||||||
|
for meth in dir(handler):
|
||||||
|
if meth in ["redirect_request", "do_open", "proxy_open"]:
|
||||||
|
# oops, coincidental match
|
||||||
|
continue
|
||||||
|
|
||||||
|
i = meth.find("_")
|
||||||
|
protocol = meth[:i]
|
||||||
|
condition = meth[i + 1:]
|
||||||
|
|
||||||
|
if condition.startswith("error"):
|
||||||
|
j = condition.find("_") + i + 1
|
||||||
|
kind = meth[j + 1:]
|
||||||
|
try:
|
||||||
|
kind = int(kind)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
lookup = opener.handle_error.get(protocol, {})
|
||||||
|
opener.handle_error[protocol] = lookup
|
||||||
|
elif condition == "open":
|
||||||
|
kind = protocol
|
||||||
|
lookup = opener.handle_open
|
||||||
|
elif condition == "response":
|
||||||
|
kind = protocol
|
||||||
|
lookup = opener.process_response
|
||||||
|
elif condition == "request":
|
||||||
|
kind = protocol
|
||||||
|
lookup = opener.process_request
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
handlers = lookup.setdefault(kind, [])
|
||||||
|
if handlers:
|
||||||
|
handlers[:] = [x for x in handlers if not find_cp(x)]
|
||||||
|
|
||||||
|
removed.append(x for x in handlers if find_cp(x))
|
||||||
|
|
||||||
|
if removed:
|
||||||
|
for x in opener.handlers:
|
||||||
|
if find_cp(x):
|
||||||
|
x.add_parent(None)
|
||||||
|
opener.handlers[:] = [x for x in opener.handlers if not find_cp(x)]
|
||||||
|
|
||||||
|
|
||||||
|
class AbemaLicenseHandler(compat_urllib_request.BaseHandler):
|
||||||
|
handler_order = 499
|
||||||
|
STRTABLE = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
|
||||||
|
HKEY = b'3AF0298C219469522A313570E8583005A642E73EDD58E3EA2FB7339D3DF1597E'
|
||||||
|
|
||||||
|
def __init__(self, ie: 'AbemaTVIE'):
|
||||||
|
# the protcol that this should really handle is 'abematv-license://'
|
||||||
|
# abematv_license_open is just a placeholder for development purposes
|
||||||
|
# ref. https://github.com/python/cpython/blob/f4c03484da59049eb62a9bf7777b963e2267d187/Lib/urllib/request.py#L510
|
||||||
|
setattr(self, 'abematv-license_open', getattr(self, 'abematv_license_open'))
|
||||||
|
self.ie = ie
|
||||||
|
|
||||||
|
def _get_videokey_from_ticket(self, ticket):
|
||||||
|
to_show = self.ie._downloader.params.get('verbose', False)
|
||||||
|
media_token = self.ie._get_media_token(to_show=to_show)
|
||||||
|
|
||||||
|
license_response = self.ie._download_json(
|
||||||
|
'https://license.abema.io/abematv-hls', None, note='Requesting playback license' if to_show else False,
|
||||||
|
query={'t': media_token},
|
||||||
|
data=json.dumps({
|
||||||
|
'kv': 'a',
|
||||||
|
'lt': ticket
|
||||||
|
}).encode('utf-8'),
|
||||||
|
headers={
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
})
|
||||||
|
|
||||||
|
res = decode_base(license_response['k'], self.STRTABLE)
|
||||||
|
encvideokey = bytes_to_intlist(struct.pack('>QQ', res >> 64, res & 0xffffffffffffffff))
|
||||||
|
|
||||||
|
h = hmac.new(
|
||||||
|
unhexlify(self.HKEY),
|
||||||
|
(license_response['cid'] + self.ie._DEVICE_ID).encode('utf-8'),
|
||||||
|
digestmod=hashlib.sha256)
|
||||||
|
enckey = bytes_to_intlist(h.digest())
|
||||||
|
|
||||||
|
return intlist_to_bytes(aes_ecb_decrypt(encvideokey, enckey))
|
||||||
|
|
||||||
|
def abematv_license_open(self, url):
|
||||||
|
url = request_to_url(url)
|
||||||
|
ticket = compat_urllib_parse_urlparse(url).netloc
|
||||||
|
response_data = self._get_videokey_from_ticket(ticket)
|
||||||
|
return compat_urllib_response.addinfourl(io.BytesIO(response_data), headers={
|
||||||
|
'Content-Length': len(response_data),
|
||||||
|
}, url=url, code=200)
|
||||||
|
|
||||||
|
|
||||||
|
class AbemaTVBaseIE(InfoExtractor):
|
||||||
|
def _extract_breadcrumb_list(self, webpage, video_id):
|
||||||
|
for jld in re.finditer(
|
||||||
|
r'(?is)</span></li></ul><script[^>]+type=(["\']?)application/ld\+json\1[^>]*>(?P<json_ld>.+?)</script>',
|
||||||
|
webpage):
|
||||||
|
jsonld = self._parse_json(jld.group('json_ld'), video_id, fatal=False)
|
||||||
|
if jsonld:
|
||||||
|
if jsonld.get('@type') != 'BreadcrumbList':
|
||||||
|
continue
|
||||||
|
trav = traverse_obj(jsonld, ('itemListElement', ..., 'name'))
|
||||||
|
if trav:
|
||||||
|
return trav
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class AbemaTVIE(AbemaTVBaseIE):
|
||||||
|
_VALID_URL = r'https?://abema\.tv/(?P<type>now-on-air|video/episode|channels/.+?/slots)/(?P<id>[^?/]+)'
|
||||||
|
_NETRC_MACHINE = 'abematv'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://abema.tv/video/episode/194-25_s2_p1',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '194-25_s2_p1',
|
||||||
|
'title': '第1話 「チーズケーキ」 「モーニング再び」',
|
||||||
|
'series': '異世界食堂2',
|
||||||
|
'series_number': 2,
|
||||||
|
'episode': '第1話 「チーズケーキ」 「モーニング再び」',
|
||||||
|
'episode_number': 1,
|
||||||
|
},
|
||||||
|
'skip': 'expired',
|
||||||
|
}, {
|
||||||
|
'url': 'https://abema.tv/channels/anime-live2/slots/E8tvAnMJ7a9a5d',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'E8tvAnMJ7a9a5d',
|
||||||
|
'title': 'ゆるキャン△ SEASON2 全話一挙【無料ビデオ72時間】',
|
||||||
|
'series': 'ゆるキャン△ SEASON2',
|
||||||
|
'episode': 'ゆるキャン△ SEASON2 全話一挙【無料ビデオ72時間】',
|
||||||
|
'series_number': 2,
|
||||||
|
'episode_number': 1,
|
||||||
|
'description': 'md5:9c5a3172ae763278f9303922f0ea5b17',
|
||||||
|
},
|
||||||
|
'skip': 'expired',
|
||||||
|
}, {
|
||||||
|
'url': 'https://abema.tv/video/episode/87-877_s1282_p31047',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'E8tvAnMJ7a9a5d',
|
||||||
|
'title': '第5話『光射す』',
|
||||||
|
'description': 'md5:56d4fc1b4f7769ded5f923c55bb4695d',
|
||||||
|
'thumbnail': r're:https://hayabusa\.io/.+',
|
||||||
|
'series': '相棒',
|
||||||
|
'episode': '第5話『光射す』',
|
||||||
|
},
|
||||||
|
'skip': 'expired',
|
||||||
|
}, {
|
||||||
|
'url': 'https://abema.tv/now-on-air/abema-anime',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'abema-anime',
|
||||||
|
# this varies
|
||||||
|
# 'title': '女子高生の無駄づかい 全話一挙【無料ビデオ72時間】',
|
||||||
|
'description': 'md5:55f2e61f46a17e9230802d7bcc913d5f',
|
||||||
|
'is_live': True,
|
||||||
|
},
|
||||||
|
'skip': 'Not supported until yt-dlp implements native live downloader OR AbemaTV can start a local HTTP server',
|
||||||
|
}]
|
||||||
|
_USERTOKEN = None
|
||||||
|
_DEVICE_ID = None
|
||||||
|
_TIMETABLE = None
|
||||||
|
_MEDIATOKEN = None
|
||||||
|
|
||||||
|
_SECRETKEY = b'v+Gjs=25Aw5erR!J8ZuvRrCx*rGswhB&qdHd_SYerEWdU&a?3DzN9BRbp5KwY4hEmcj5#fykMjJ=AuWz5GSMY-d@H7DMEh3M@9n2G552Us$$k9cD=3TxwWe86!x#Zyhe'
|
||||||
|
|
||||||
|
def _generate_aks(self, deviceid):
|
||||||
|
deviceid = deviceid.encode('utf-8')
|
||||||
|
# add 1 hour and then drop minute and secs
|
||||||
|
ts_1hour = int((time_seconds(hours=9) // 3600 + 1) * 3600)
|
||||||
|
time_struct = time.gmtime(ts_1hour)
|
||||||
|
ts_1hour_str = str(ts_1hour).encode('utf-8')
|
||||||
|
|
||||||
|
tmp = None
|
||||||
|
|
||||||
|
def mix_once(nonce):
|
||||||
|
nonlocal tmp
|
||||||
|
h = hmac.new(self._SECRETKEY, digestmod=hashlib.sha256)
|
||||||
|
h.update(nonce)
|
||||||
|
tmp = h.digest()
|
||||||
|
|
||||||
|
def mix_tmp(count):
|
||||||
|
nonlocal tmp
|
||||||
|
for i in range(count):
|
||||||
|
mix_once(tmp)
|
||||||
|
|
||||||
|
def mix_twist(nonce):
|
||||||
|
nonlocal tmp
|
||||||
|
mix_once(urlsafe_b64encode(tmp).rstrip(b'=') + nonce)
|
||||||
|
|
||||||
|
mix_once(self._SECRETKEY)
|
||||||
|
mix_tmp(time_struct.tm_mon)
|
||||||
|
mix_twist(deviceid)
|
||||||
|
mix_tmp(time_struct.tm_mday % 5)
|
||||||
|
mix_twist(ts_1hour_str)
|
||||||
|
mix_tmp(time_struct.tm_hour % 5)
|
||||||
|
|
||||||
|
return urlsafe_b64encode(tmp).rstrip(b'=').decode('utf-8')
|
||||||
|
|
||||||
|
def _get_device_token(self):
|
||||||
|
if self._USERTOKEN:
|
||||||
|
return self._USERTOKEN
|
||||||
|
|
||||||
|
self._DEVICE_ID = random_uuidv4()
|
||||||
|
aks = self._generate_aks(self._DEVICE_ID)
|
||||||
|
user_data = self._download_json(
|
||||||
|
'https://api.abema.io/v1/users', None, note='Authorizing',
|
||||||
|
data=json.dumps({
|
||||||
|
'deviceId': self._DEVICE_ID,
|
||||||
|
'applicationKeySecret': aks,
|
||||||
|
}).encode('utf-8'),
|
||||||
|
headers={
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
})
|
||||||
|
self._USERTOKEN = user_data['token']
|
||||||
|
|
||||||
|
# don't allow adding it 2 times or more, though it's guarded
|
||||||
|
remove_opener(self._downloader, AbemaLicenseHandler)
|
||||||
|
add_opener(self._downloader, AbemaLicenseHandler(self))
|
||||||
|
|
||||||
|
return self._USERTOKEN
|
||||||
|
|
||||||
|
def _get_media_token(self, invalidate=False, to_show=True):
|
||||||
|
if not invalidate and self._MEDIATOKEN:
|
||||||
|
return self._MEDIATOKEN
|
||||||
|
|
||||||
|
self._MEDIATOKEN = self._download_json(
|
||||||
|
'https://api.abema.io/v1/media/token', None, note='Fetching media token' if to_show else False,
|
||||||
|
query={
|
||||||
|
'osName': 'android',
|
||||||
|
'osVersion': '6.0.1',
|
||||||
|
'osLang': 'ja_JP',
|
||||||
|
'osTimezone': 'Asia/Tokyo',
|
||||||
|
'appId': 'tv.abema',
|
||||||
|
'appVersion': '3.27.1'
|
||||||
|
}, headers={
|
||||||
|
'Authorization': 'bearer ' + self._get_device_token()
|
||||||
|
})['token']
|
||||||
|
|
||||||
|
return self._MEDIATOKEN
|
||||||
|
|
||||||
|
def _perform_login(self, username, password):
|
||||||
|
if '@' in username: # don't strictly check if it's email address or not
|
||||||
|
ep, method = 'user/email', 'email'
|
||||||
|
else:
|
||||||
|
ep, method = 'oneTimePassword', 'userId'
|
||||||
|
|
||||||
|
login_response = self._download_json(
|
||||||
|
f'https://api.abema.io/v1/auth/{ep}', None, note='Logging in',
|
||||||
|
data=json.dumps({
|
||||||
|
method: username,
|
||||||
|
'password': password
|
||||||
|
}).encode('utf-8'), headers={
|
||||||
|
'Authorization': 'bearer ' + self._get_device_token(),
|
||||||
|
'Origin': 'https://abema.tv',
|
||||||
|
'Referer': 'https://abema.tv/',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
})
|
||||||
|
|
||||||
|
self._USERTOKEN = login_response['token']
|
||||||
|
self._get_media_token(True)
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
# starting download using infojson from this extractor is undefined behavior,
|
||||||
|
# and never be fixed in the future; you must trigger downloads by directly specifing URL.
|
||||||
|
# (unless there's a way to hook before downloading by extractor)
|
||||||
|
video_id, video_type = self._match_valid_url(url).group('id', 'type')
|
||||||
|
headers = {
|
||||||
|
'Authorization': 'Bearer ' + self._get_device_token(),
|
||||||
|
}
|
||||||
|
video_type = video_type.split('/')[-1]
|
||||||
|
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
canonical_url = self._search_regex(
|
||||||
|
r'<link\s+rel="canonical"\s*href="(.+?)"', webpage, 'canonical URL',
|
||||||
|
default=url)
|
||||||
|
info = self._search_json_ld(webpage, video_id, default={})
|
||||||
|
|
||||||
|
title = self._search_regex(
|
||||||
|
r'<span\s*class=".+?EpisodeTitleBlock__title">(.+?)</span>', webpage, 'title', default=None)
|
||||||
|
if not title:
|
||||||
|
jsonld = None
|
||||||
|
for jld in re.finditer(
|
||||||
|
r'(?is)<span\s*class="com-m-Thumbnail__image">(?:</span>)?<script[^>]+type=(["\']?)application/ld\+json\1[^>]*>(?P<json_ld>.+?)</script>',
|
||||||
|
webpage):
|
||||||
|
jsonld = self._parse_json(jld.group('json_ld'), video_id, fatal=False)
|
||||||
|
if jsonld:
|
||||||
|
break
|
||||||
|
if jsonld:
|
||||||
|
title = jsonld.get('caption')
|
||||||
|
if not title and video_type == 'now-on-air':
|
||||||
|
if not self._TIMETABLE:
|
||||||
|
# cache the timetable because it goes to 5MiB in size (!!)
|
||||||
|
self._TIMETABLE = self._download_json(
|
||||||
|
'https://api.abema.io/v1/timetable/dataSet?debug=false', video_id,
|
||||||
|
headers=headers)
|
||||||
|
now = time_seconds(hours=9)
|
||||||
|
for slot in self._TIMETABLE.get('slots', []):
|
||||||
|
if slot.get('channelId') != video_id:
|
||||||
|
continue
|
||||||
|
if slot['startAt'] <= now and now < slot['endAt']:
|
||||||
|
title = slot['title']
|
||||||
|
break
|
||||||
|
|
||||||
|
# read breadcrumb on top of page
|
||||||
|
breadcrumb = self._extract_breadcrumb_list(webpage, video_id)
|
||||||
|
if breadcrumb:
|
||||||
|
# breadcrumb list translates to: (example is 1st test for this IE)
|
||||||
|
# Home > Anime (genre) > Isekai Shokudo 2 (series name) > Episode 1 "Cheese cakes" "Morning again" (episode title)
|
||||||
|
# hence this works
|
||||||
|
info['series'] = breadcrumb[-2]
|
||||||
|
info['episode'] = breadcrumb[-1]
|
||||||
|
if not title:
|
||||||
|
title = info['episode']
|
||||||
|
|
||||||
|
description = self._html_search_regex(
|
||||||
|
(r'<p\s+class="com-video-EpisodeDetailsBlock__content"><span\s+class=".+?">(.+?)</span></p><div',
|
||||||
|
r'<span\s+class=".+?SlotSummary.+?">(.+?)</span></div><div',),
|
||||||
|
webpage, 'description', default=None, group=1)
|
||||||
|
if not description:
|
||||||
|
og_desc = self._html_search_meta(
|
||||||
|
('description', 'og:description', 'twitter:description'), webpage)
|
||||||
|
if og_desc:
|
||||||
|
description = re.sub(r'''(?sx)
|
||||||
|
^(.+?)(?:
|
||||||
|
アニメの動画を無料で見るならABEMA!| # anime
|
||||||
|
等、.+ # applies for most of categories
|
||||||
|
)?
|
||||||
|
''', r'\1', og_desc)
|
||||||
|
|
||||||
|
# canonical URL may contain series and episode number
|
||||||
|
mobj = re.search(r's(\d+)_p(\d+)$', canonical_url)
|
||||||
|
if mobj:
|
||||||
|
seri = int_or_none(mobj.group(1), default=float('inf'))
|
||||||
|
epis = int_or_none(mobj.group(2), default=float('inf'))
|
||||||
|
info['series_number'] = seri if seri < 100 else None
|
||||||
|
# some anime like Detective Conan (though not available in AbemaTV)
|
||||||
|
# has more than 1000 episodes (1026 as of 2021/11/15)
|
||||||
|
info['episode_number'] = epis if epis < 2000 else None
|
||||||
|
|
||||||
|
is_live, m3u8_url = False, None
|
||||||
|
if video_type == 'now-on-air':
|
||||||
|
is_live = True
|
||||||
|
channel_url = 'https://api.abema.io/v1/channels'
|
||||||
|
if video_id == 'news-global':
|
||||||
|
channel_url = update_url_query(channel_url, {'division': '1'})
|
||||||
|
onair_channels = self._download_json(channel_url, video_id)
|
||||||
|
for ch in onair_channels['channels']:
|
||||||
|
if video_id == ch['id']:
|
||||||
|
m3u8_url = ch['playback']['hls']
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise ExtractorError(f'Cannot find on-air {video_id} channel.', expected=True)
|
||||||
|
elif video_type == 'episode':
|
||||||
|
api_response = self._download_json(
|
||||||
|
f'https://api.abema.io/v1/video/programs/{video_id}', video_id,
|
||||||
|
note='Checking playability',
|
||||||
|
headers=headers)
|
||||||
|
ondemand_types = traverse_obj(api_response, ('terms', ..., 'onDemandType'), default=[])
|
||||||
|
if 3 not in ondemand_types:
|
||||||
|
# cannot acquire decryption key for these streams
|
||||||
|
self.report_warning('This is a premium-only stream')
|
||||||
|
|
||||||
|
m3u8_url = f'https://vod-abematv.akamaized.net/program/{video_id}/playlist.m3u8'
|
||||||
|
elif video_type == 'slots':
|
||||||
|
api_response = self._download_json(
|
||||||
|
f'https://api.abema.io/v1/media/slots/{video_id}', video_id,
|
||||||
|
note='Checking playability',
|
||||||
|
headers=headers)
|
||||||
|
if not traverse_obj(api_response, ('slot', 'flags', 'timeshiftFree'), default=False):
|
||||||
|
self.report_warning('This is a premium-only stream')
|
||||||
|
|
||||||
|
m3u8_url = f'https://vod-abematv.akamaized.net/slot/{video_id}/playlist.m3u8'
|
||||||
|
else:
|
||||||
|
raise ExtractorError('Unreachable')
|
||||||
|
|
||||||
|
if is_live:
|
||||||
|
self.report_warning("This is a livestream; yt-dlp doesn't support downloading natively, but FFmpeg cannot handle m3u8 manifests from AbemaTV")
|
||||||
|
self.report_warning('Please consider using Streamlink to download these streams (https://github.com/streamlink/streamlink)')
|
||||||
|
formats = self._extract_m3u8_formats(
|
||||||
|
m3u8_url, video_id, ext='mp4', live=is_live)
|
||||||
|
|
||||||
|
info.update({
|
||||||
|
'id': video_id,
|
||||||
|
'title': title,
|
||||||
|
'description': description,
|
||||||
|
'formats': formats,
|
||||||
|
'is_live': is_live,
|
||||||
|
})
|
||||||
|
return info
|
||||||
|
|
||||||
|
|
||||||
|
class AbemaTVTitleIE(AbemaTVBaseIE):
|
||||||
|
_VALID_URL = r'https?://abema\.tv/video/title/(?P<id>[^?/]+)'
|
||||||
|
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://abema.tv/video/title/90-1597',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '90-1597',
|
||||||
|
'title': 'シャッフルアイランド',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 2,
|
||||||
|
}, {
|
||||||
|
'url': 'https://abema.tv/video/title/193-132',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '193-132',
|
||||||
|
'title': '真心が届く~僕とスターのオフィス・ラブ!?~',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 16,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
|
playlist_title, breadcrumb = None, self._extract_breadcrumb_list(webpage, video_id)
|
||||||
|
if breadcrumb:
|
||||||
|
playlist_title = breadcrumb[-1]
|
||||||
|
|
||||||
|
playlist = [
|
||||||
|
self.url_result(urljoin('https://abema.tv/', mobj.group(1)))
|
||||||
|
for mobj in re.finditer(r'<li\s*class=".+?EpisodeList.+?"><a\s*href="(/[^"]+?)"', webpage)]
|
||||||
|
|
||||||
|
return self.playlist_result(playlist, playlist_title=playlist_title, playlist_id=video_id)
|
||||||
@@ -8,11 +8,10 @@ import os
|
|||||||
import random
|
import random
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..aes import aes_cbc_decrypt
|
from ..aes import aes_cbc_decrypt_bytes, unpad_pkcs7
|
||||||
from ..compat import (
|
from ..compat import (
|
||||||
compat_HTTPError,
|
compat_HTTPError,
|
||||||
compat_b64decode,
|
compat_b64decode,
|
||||||
compat_ord,
|
|
||||||
)
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ass_subtitles_timecode,
|
ass_subtitles_timecode,
|
||||||
@@ -84,14 +83,11 @@ class ADNIE(InfoExtractor):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# http://animedigitalnetwork.fr/components/com_vodvideo/videojs/adn-vjs.min.js
|
# http://animedigitalnetwork.fr/components/com_vodvideo/videojs/adn-vjs.min.js
|
||||||
dec_subtitles = intlist_to_bytes(aes_cbc_decrypt(
|
dec_subtitles = unpad_pkcs7(aes_cbc_decrypt_bytes(
|
||||||
bytes_to_intlist(compat_b64decode(enc_subtitles[24:])),
|
compat_b64decode(enc_subtitles[24:]),
|
||||||
bytes_to_intlist(binascii.unhexlify(self._K + 'ab9f52f5baae7c72')),
|
binascii.unhexlify(self._K + 'ab9f52f5baae7c72'),
|
||||||
bytes_to_intlist(compat_b64decode(enc_subtitles[:24]))
|
compat_b64decode(enc_subtitles[:24])))
|
||||||
))
|
subtitles_json = self._parse_json(dec_subtitles.decode(), None, fatal=False)
|
||||||
subtitles_json = self._parse_json(
|
|
||||||
dec_subtitles[:-compat_ord(dec_subtitles[-1])].decode(),
|
|
||||||
None, fatal=False)
|
|
||||||
if not subtitles_json:
|
if not subtitles_json:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -130,10 +126,7 @@ Format: Marked,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text'''
|
|||||||
}])
|
}])
|
||||||
return subtitles
|
return subtitles
|
||||||
|
|
||||||
def _real_initialize(self):
|
def _perform_login(self, username, password):
|
||||||
username, password = self._get_login_info()
|
|
||||||
if not username:
|
|
||||||
return
|
|
||||||
try:
|
try:
|
||||||
access_token = (self._download_json(
|
access_token = (self._download_json(
|
||||||
self._API_BASE_URL + 'authentication/login', None,
|
self._API_BASE_URL + 'authentication/login', None,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class AdobeConnectIE(InfoExtractor):
|
|||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
webpage = self._download_webpage(url, video_id)
|
webpage = self._download_webpage(url, video_id)
|
||||||
title = self._html_search_regex(r'<title>(.+?)</title>', webpage, 'title')
|
title = self._html_extract_title(webpage)
|
||||||
qs = compat_parse_qs(self._search_regex(r"swfUrl\s*=\s*'([^']+)'", webpage, 'swf url').split('?')[1])
|
qs = compat_parse_qs(self._search_regex(r"swfUrl\s*=\s*'([^']+)'", webpage, 'swf url').split('?')[1])
|
||||||
is_live = qs.get('isLive', ['false'])[0] == 'true'
|
is_live = qs.get('isLive', ['false'])[0] == 'true'
|
||||||
formats = []
|
formats = []
|
||||||
|
|||||||
@@ -1345,6 +1345,11 @@ MSO_INFO = {
|
|||||||
'username_field': 'username',
|
'username_field': 'username',
|
||||||
'password_field': 'password',
|
'password_field': 'password',
|
||||||
},
|
},
|
||||||
|
'Suddenlink': {
|
||||||
|
'name': 'Suddenlink',
|
||||||
|
'username_field': 'username',
|
||||||
|
'password_field': 'password',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1635,6 +1640,58 @@ class AdobePassIE(InfoExtractor):
|
|||||||
urlh.geturl(), video_id, 'Sending final bookend',
|
urlh.geturl(), video_id, 'Sending final bookend',
|
||||||
query=hidden_data)
|
query=hidden_data)
|
||||||
|
|
||||||
|
post_form(mvpd_confirm_page_res, 'Confirming Login')
|
||||||
|
elif mso_id == 'Suddenlink':
|
||||||
|
# Suddenlink is similar to SlingTV in using a tab history count and a meta refresh,
|
||||||
|
# but they also do a dynmaic redirect using javascript that has to be followed as well
|
||||||
|
first_bookend_page, urlh = post_form(
|
||||||
|
provider_redirect_page_res, 'Pressing Continue...')
|
||||||
|
|
||||||
|
hidden_data = self._hidden_inputs(first_bookend_page)
|
||||||
|
hidden_data['history_val'] = 1
|
||||||
|
|
||||||
|
provider_login_redirect_page_res = self._download_webpage_handle(
|
||||||
|
urlh.geturl(), video_id, 'Sending First Bookend',
|
||||||
|
query=hidden_data)
|
||||||
|
|
||||||
|
provider_login_redirect_page, urlh = provider_login_redirect_page_res
|
||||||
|
|
||||||
|
# Some website partners seem to not have the extra ajaxurl redirect step, so we check if we already
|
||||||
|
# have the login prompt or not
|
||||||
|
if 'id="password" type="password" name="password"' in provider_login_redirect_page:
|
||||||
|
provider_login_page_res = provider_login_redirect_page_res
|
||||||
|
else:
|
||||||
|
provider_tryauth_url = self._html_search_regex(
|
||||||
|
r'url:\s*[\'"]([^\'"]+)', provider_login_redirect_page, 'ajaxurl')
|
||||||
|
provider_tryauth_page = self._download_webpage(
|
||||||
|
provider_tryauth_url, video_id, 'Submitting TryAuth',
|
||||||
|
query=hidden_data)
|
||||||
|
|
||||||
|
provider_login_page_res = self._download_webpage_handle(
|
||||||
|
f'https://authorize.suddenlink.net/saml/module.php/authSynacor/login.php?AuthState={provider_tryauth_page}',
|
||||||
|
video_id, 'Getting Login Page',
|
||||||
|
query=hidden_data)
|
||||||
|
|
||||||
|
provider_association_redirect, urlh = post_form(
|
||||||
|
provider_login_page_res, 'Logging in', {
|
||||||
|
mso_info['username_field']: username,
|
||||||
|
mso_info['password_field']: password
|
||||||
|
})
|
||||||
|
|
||||||
|
provider_refresh_redirect_url = extract_redirect_url(
|
||||||
|
provider_association_redirect, url=urlh.geturl())
|
||||||
|
|
||||||
|
last_bookend_page, urlh = self._download_webpage_handle(
|
||||||
|
provider_refresh_redirect_url, video_id,
|
||||||
|
'Downloading Auth Association Redirect Page')
|
||||||
|
|
||||||
|
hidden_data = self._hidden_inputs(last_bookend_page)
|
||||||
|
hidden_data['history_val'] = 3
|
||||||
|
|
||||||
|
mvpd_confirm_page_res = self._download_webpage_handle(
|
||||||
|
urlh.geturl(), video_id, 'Sending Final Bookend',
|
||||||
|
query=hidden_data)
|
||||||
|
|
||||||
post_form(mvpd_confirm_page_res, 'Confirming Login')
|
post_form(mvpd_confirm_page_res, 'Confirming Login')
|
||||||
else:
|
else:
|
||||||
# Some providers (e.g. DIRECTV NOW) have another meta refresh
|
# Some providers (e.g. DIRECTV NOW) have another meta refresh
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import functools
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..compat import compat_xpath
|
from ..compat import compat_xpath
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
|
ExtractorError,
|
||||||
|
OnDemandPagedList,
|
||||||
date_from_str,
|
date_from_str,
|
||||||
determine_ext,
|
determine_ext,
|
||||||
ExtractorError,
|
|
||||||
int_or_none,
|
int_or_none,
|
||||||
qualities,
|
qualities,
|
||||||
traverse_obj,
|
traverse_obj,
|
||||||
@@ -32,7 +34,7 @@ class AfreecaTVIE(InfoExtractor):
|
|||||||
/app/(?:index|read_ucc_bbs)\.cgi|
|
/app/(?:index|read_ucc_bbs)\.cgi|
|
||||||
/player/[Pp]layer\.(?:swf|html)
|
/player/[Pp]layer\.(?:swf|html)
|
||||||
)\?.*?\bnTitleNo=|
|
)\?.*?\bnTitleNo=|
|
||||||
vod\.afreecatv\.com/PLAYER/STATION/
|
vod\.afreecatv\.com/(PLAYER/STATION|player)/
|
||||||
)
|
)
|
||||||
(?P<id>\d+)
|
(?P<id>\d+)
|
||||||
'''
|
'''
|
||||||
@@ -170,6 +172,9 @@ class AfreecaTVIE(InfoExtractor):
|
|||||||
}, {
|
}, {
|
||||||
'url': 'http://vod.afreecatv.com/PLAYER/STATION/15055030',
|
'url': 'http://vod.afreecatv.com/PLAYER/STATION/15055030',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'http://vod.afreecatv.com/player/15055030',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -181,14 +186,7 @@ class AfreecaTVIE(InfoExtractor):
|
|||||||
video_key['part'] = int(m.group('part'))
|
video_key['part'] = int(m.group('part'))
|
||||||
return video_key
|
return video_key
|
||||||
|
|
||||||
def _real_initialize(self):
|
def _perform_login(self, username, password):
|
||||||
self._login()
|
|
||||||
|
|
||||||
def _login(self):
|
|
||||||
username, password = self._get_login_info()
|
|
||||||
if username is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
login_form = {
|
login_form = {
|
||||||
'szWork': 'login',
|
'szWork': 'login',
|
||||||
'szType': 'json',
|
'szType': 'json',
|
||||||
@@ -416,26 +414,35 @@ class AfreecaTVLiveIE(AfreecaTVIE):
|
|||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
broadcaster_id, broadcast_no = self._match_valid_url(url).group('id', 'bno')
|
broadcaster_id, broadcast_no = self._match_valid_url(url).group('id', 'bno')
|
||||||
|
password = self.get_param('videopassword')
|
||||||
|
|
||||||
info = self._download_json(self._LIVE_API_URL, broadcaster_id, fatal=False,
|
info = self._download_json(self._LIVE_API_URL, broadcaster_id, fatal=False,
|
||||||
data=urlencode_postdata({'bid': broadcaster_id})) or {}
|
data=urlencode_postdata({'bid': broadcaster_id})) or {}
|
||||||
channel_info = info.get('CHANNEL') or {}
|
channel_info = info.get('CHANNEL') or {}
|
||||||
broadcaster_id = channel_info.get('BJID') or broadcaster_id
|
broadcaster_id = channel_info.get('BJID') or broadcaster_id
|
||||||
broadcast_no = channel_info.get('BNO') or broadcast_no
|
broadcast_no = channel_info.get('BNO') or broadcast_no
|
||||||
|
password_protected = channel_info.get('BPWD')
|
||||||
if not broadcast_no:
|
if not broadcast_no:
|
||||||
raise ExtractorError(f'Unable to extract broadcast number ({broadcaster_id} may not be live)', expected=True)
|
raise ExtractorError(f'Unable to extract broadcast number ({broadcaster_id} may not be live)', expected=True)
|
||||||
|
if password_protected == 'Y' and password is None:
|
||||||
|
raise ExtractorError(
|
||||||
|
'This livestream is protected by a password, use the --video-password option',
|
||||||
|
expected=True)
|
||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
quality_key = qualities(self._QUALITIES)
|
quality_key = qualities(self._QUALITIES)
|
||||||
for quality_str in self._QUALITIES:
|
for quality_str in self._QUALITIES:
|
||||||
|
params = {
|
||||||
|
'bno': broadcast_no,
|
||||||
|
'stream_type': 'common',
|
||||||
|
'type': 'aid',
|
||||||
|
'quality': quality_str,
|
||||||
|
}
|
||||||
|
if password is not None:
|
||||||
|
params['pwd'] = password
|
||||||
aid_response = self._download_json(
|
aid_response = self._download_json(
|
||||||
self._LIVE_API_URL, broadcast_no, fatal=False,
|
self._LIVE_API_URL, broadcast_no, fatal=False,
|
||||||
data=urlencode_postdata({
|
data=urlencode_postdata(params),
|
||||||
'bno': broadcast_no,
|
|
||||||
'stream_type': 'common',
|
|
||||||
'type': 'aid',
|
|
||||||
'quality': quality_str,
|
|
||||||
}),
|
|
||||||
note=f'Downloading access token for {quality_str} stream',
|
note=f'Downloading access token for {quality_str} stream',
|
||||||
errnote=f'Unable to download access token for {quality_str} stream')
|
errnote=f'Unable to download access token for {quality_str} stream')
|
||||||
aid = traverse_obj(aid_response, ('CHANNEL', 'AID'))
|
aid = traverse_obj(aid_response, ('CHANNEL', 'AID'))
|
||||||
@@ -477,3 +484,57 @@ class AfreecaTVLiveIE(AfreecaTVIE):
|
|||||||
'formats': formats,
|
'formats': formats,
|
||||||
'is_live': True,
|
'is_live': True,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AfreecaTVUserIE(InfoExtractor):
|
||||||
|
IE_NAME = 'afreecatv:user'
|
||||||
|
_VALID_URL = r'https?://bj\.afreeca(?:tv)?\.com/(?P<id>[^/]+)/vods/?(?P<slug_type>[^/]+)?'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://bj.afreecatv.com/ryuryu24/vods/review',
|
||||||
|
'info_dict': {
|
||||||
|
'_type': 'playlist',
|
||||||
|
'id': 'ryuryu24',
|
||||||
|
'title': 'ryuryu24 - review',
|
||||||
|
},
|
||||||
|
'playlist_count': 218,
|
||||||
|
}, {
|
||||||
|
'url': 'https://bj.afreecatv.com/parang1995/vods/highlight',
|
||||||
|
'info_dict': {
|
||||||
|
'_type': 'playlist',
|
||||||
|
'id': 'parang1995',
|
||||||
|
'title': 'parang1995 - highlight',
|
||||||
|
},
|
||||||
|
'playlist_count': 997,
|
||||||
|
}, {
|
||||||
|
'url': 'https://bj.afreecatv.com/ryuryu24/vods',
|
||||||
|
'info_dict': {
|
||||||
|
'_type': 'playlist',
|
||||||
|
'id': 'ryuryu24',
|
||||||
|
'title': 'ryuryu24 - all',
|
||||||
|
},
|
||||||
|
'playlist_count': 221,
|
||||||
|
}, {
|
||||||
|
'url': 'https://bj.afreecatv.com/ryuryu24/vods/balloonclip',
|
||||||
|
'info_dict': {
|
||||||
|
'_type': 'playlist',
|
||||||
|
'id': 'ryuryu24',
|
||||||
|
'title': 'ryuryu24 - balloonclip',
|
||||||
|
},
|
||||||
|
'playlist_count': 0,
|
||||||
|
}]
|
||||||
|
_PER_PAGE = 60
|
||||||
|
|
||||||
|
def _fetch_page(self, user_id, user_type, page):
|
||||||
|
page += 1
|
||||||
|
info = self._download_json(f'https://bjapi.afreecatv.com/api/{user_id}/vods/{user_type}', user_id,
|
||||||
|
query={'page': page, 'per_page': self._PER_PAGE, 'orderby': 'reg_date'},
|
||||||
|
note=f'Downloading {user_type} video page {page}')
|
||||||
|
for item in info['data']:
|
||||||
|
yield self.url_result(
|
||||||
|
f'https://vod.afreecatv.com/player/{item["title_no"]}/', AfreecaTVIE, item['title_no'])
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
user_id, user_type = self._match_valid_url(url).group('id', 'slug_type')
|
||||||
|
user_type = user_type or 'all'
|
||||||
|
entries = OnDemandPagedList(functools.partial(self._fetch_page, user_id, user_type), self._PER_PAGE)
|
||||||
|
return self.playlist_result(entries, user_id, f'{user_id} - {user_type}')
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class AliExpressLiveIE(InfoExtractor):
|
|||||||
'id': '2800002704436634',
|
'id': '2800002704436634',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'CASIMA7.22',
|
'title': 'CASIMA7.22',
|
||||||
'thumbnail': r're:http://.*\.jpg',
|
'thumbnail': r're:https?://.*\.jpg',
|
||||||
'uploader': 'CASIMA Official Store',
|
'uploader': 'CASIMA Official Store',
|
||||||
'timestamp': 1500717600,
|
'timestamp': 1500717600,
|
||||||
'upload_date': '20170722',
|
'upload_date': '20170722',
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from ..utils import (
|
|||||||
int_or_none,
|
int_or_none,
|
||||||
qualities,
|
qualities,
|
||||||
remove_end,
|
remove_end,
|
||||||
|
strip_or_none,
|
||||||
try_get,
|
try_get,
|
||||||
unified_timestamp,
|
unified_timestamp,
|
||||||
url_basename,
|
url_basename,
|
||||||
@@ -102,10 +103,7 @@ class AllocineIE(InfoExtractor):
|
|||||||
video_id = display_id
|
video_id = display_id
|
||||||
media_data = self._download_json(
|
media_data = self._download_json(
|
||||||
'http://www.allocine.fr/ws/AcVisiondataV5.ashx?media=%s' % video_id, display_id)
|
'http://www.allocine.fr/ws/AcVisiondataV5.ashx?media=%s' % video_id, display_id)
|
||||||
title = remove_end(
|
title = remove_end(strip_or_none(self._html_extract_title(webpage), ' - AlloCiné'))
|
||||||
self._html_search_regex(
|
|
||||||
r'(?s)<title>(.+?)</title>', webpage, 'title').strip(),
|
|
||||||
' - AlloCiné')
|
|
||||||
for key, value in media_data['video'].items():
|
for key, value in media_data['video'].items():
|
||||||
if not key.endswith('Path'):
|
if not key.endswith('Path'):
|
||||||
continue
|
continue
|
||||||
|
|||||||
87
yt_dlp/extractor/alsace20tv.py
Normal file
87
yt_dlp/extractor/alsace20tv.py
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
clean_html,
|
||||||
|
dict_get,
|
||||||
|
get_element_by_class,
|
||||||
|
int_or_none,
|
||||||
|
unified_strdate,
|
||||||
|
url_or_none,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Alsace20TVBaseIE(InfoExtractor):
|
||||||
|
def _extract_video(self, video_id, url=None):
|
||||||
|
info = self._download_json(
|
||||||
|
'https://www.alsace20.tv/visionneuse/visio_v9_js.php?key=%s&habillage=0&mode=html' % (video_id, ),
|
||||||
|
video_id) or {}
|
||||||
|
title = info.get('titre')
|
||||||
|
|
||||||
|
formats = []
|
||||||
|
for res, fmt_url in (info.get('files') or {}).items():
|
||||||
|
formats.extend(
|
||||||
|
self._extract_smil_formats(fmt_url, video_id, fatal=False)
|
||||||
|
if '/smil:_' in fmt_url
|
||||||
|
else self._extract_mpd_formats(fmt_url, video_id, mpd_id=res, fatal=False))
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
webpage = (url and self._download_webpage(url, video_id, fatal=False)) or ''
|
||||||
|
thumbnail = url_or_none(dict_get(info, ('image', 'preview', )) or self._og_search_thumbnail(webpage))
|
||||||
|
upload_date = self._search_regex(r'/(\d{6})_', thumbnail, 'upload_date', default=None)
|
||||||
|
upload_date = unified_strdate('20%s-%s-%s' % (upload_date[:2], upload_date[2:4], upload_date[4:])) if upload_date else None
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': title,
|
||||||
|
'formats': formats,
|
||||||
|
'description': clean_html(get_element_by_class('wysiwyg', webpage)),
|
||||||
|
'upload_date': upload_date,
|
||||||
|
'thumbnail': thumbnail,
|
||||||
|
'duration': int_or_none(self._og_search_property('video:duration', webpage) if webpage else None),
|
||||||
|
'view_count': int_or_none(info.get('nb_vues')),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Alsace20TVIE(Alsace20TVBaseIE):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?alsace20\.tv/(?:[\w-]+/)+[\w-]+-(?P<id>[\w]+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.alsace20.tv/VOD/Actu/JT/Votre-JT-jeudi-3-fevrier-lyNHCXpYJh.html',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'lyNHCXpYJh',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'description': 'md5:fc0bc4a0692d3d2dba4524053de4c7b7',
|
||||||
|
'title': 'Votre JT du jeudi 3 février',
|
||||||
|
'upload_date': '20220203',
|
||||||
|
'thumbnail': r're:https?://.+\.jpg',
|
||||||
|
'duration': 1073,
|
||||||
|
'view_count': int,
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
return self._extract_video(video_id, url)
|
||||||
|
|
||||||
|
|
||||||
|
class Alsace20TVEmbedIE(Alsace20TVBaseIE):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?alsace20\.tv/emb/(?P<id>[\w]+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.alsace20.tv/emb/lyNHCXpYJh',
|
||||||
|
# 'md5': 'd91851bf9af73c0ad9b2cdf76c127fbb',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'lyNHCXpYJh',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Votre JT du jeudi 3 février',
|
||||||
|
'upload_date': '20220203',
|
||||||
|
'thumbnail': r're:https?://.+\.jpg',
|
||||||
|
'view_count': int,
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'format': 'bestvideo',
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
return self._extract_video(video_id)
|
||||||
@@ -74,14 +74,7 @@ class AluraIE(InfoExtractor):
|
|||||||
"formats": formats
|
"formats": formats
|
||||||
}
|
}
|
||||||
|
|
||||||
def _real_initialize(self):
|
def _perform_login(self, username, password):
|
||||||
self._login()
|
|
||||||
|
|
||||||
def _login(self):
|
|
||||||
username, password = self._get_login_info()
|
|
||||||
if username is None:
|
|
||||||
return
|
|
||||||
pass
|
|
||||||
|
|
||||||
login_page = self._download_webpage(
|
login_page = self._download_webpage(
|
||||||
self._LOGIN_URL, None, 'Downloading login popup')
|
self._LOGIN_URL, None, 'Downloading login popup')
|
||||||
|
|||||||
@@ -15,25 +15,21 @@ from ..compat import compat_HTTPError
|
|||||||
|
|
||||||
|
|
||||||
class AnimeLabBaseIE(InfoExtractor):
|
class AnimeLabBaseIE(InfoExtractor):
|
||||||
_LOGIN_REQUIRED = True
|
|
||||||
_LOGIN_URL = 'https://www.animelab.com/login'
|
_LOGIN_URL = 'https://www.animelab.com/login'
|
||||||
_NETRC_MACHINE = 'animelab'
|
_NETRC_MACHINE = 'animelab'
|
||||||
|
_LOGGED_IN = False
|
||||||
|
|
||||||
def _login(self):
|
def _is_logged_in(self, login_page=None):
|
||||||
def is_logged_in(login_webpage):
|
if not self._LOGGED_IN:
|
||||||
return 'Sign In' not in login_webpage
|
if not login_page:
|
||||||
|
login_page = self._download_webpage(self._LOGIN_URL, None, 'Downloading login page')
|
||||||
|
AnimeLabBaseIE._LOGGED_IN = 'Sign In' not in login_page
|
||||||
|
return self._LOGGED_IN
|
||||||
|
|
||||||
login_page = self._download_webpage(
|
def _perform_login(self, username, password):
|
||||||
self._LOGIN_URL, None, 'Downloading login page')
|
if self._is_logged_in():
|
||||||
|
|
||||||
# Check if already logged in
|
|
||||||
if is_logged_in(login_page):
|
|
||||||
return
|
return
|
||||||
|
|
||||||
(username, password) = self._get_login_info()
|
|
||||||
if username is None and self._LOGIN_REQUIRED:
|
|
||||||
self.raise_login_required('Login is required to access any AnimeLab content')
|
|
||||||
|
|
||||||
login_form = {
|
login_form = {
|
||||||
'email': username,
|
'email': username,
|
||||||
'password': password,
|
'password': password,
|
||||||
@@ -47,17 +43,14 @@ class AnimeLabBaseIE(InfoExtractor):
|
|||||||
except ExtractorError as e:
|
except ExtractorError as e:
|
||||||
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 400:
|
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 400:
|
||||||
raise ExtractorError('Unable to log in (wrong credentials?)', expected=True)
|
raise ExtractorError('Unable to log in (wrong credentials?)', expected=True)
|
||||||
else:
|
raise
|
||||||
raise
|
|
||||||
|
|
||||||
# if login was successful
|
if not self._is_logged_in(response):
|
||||||
if is_logged_in(response):
|
raise ExtractorError('Unable to login (cannot verify if logged in)')
|
||||||
return
|
|
||||||
|
|
||||||
raise ExtractorError('Unable to login (cannot verify if logged in)')
|
|
||||||
|
|
||||||
def _real_initialize(self):
|
def _real_initialize(self):
|
||||||
self._login()
|
if not self._is_logged_in():
|
||||||
|
self.raise_login_required('Login is required to access any AnimeLab content')
|
||||||
|
|
||||||
|
|
||||||
class AnimeLabIE(AnimeLabBaseIE):
|
class AnimeLabIE(AnimeLabBaseIE):
|
||||||
|
|||||||
@@ -53,11 +53,7 @@ class AnimeOnDemandIE(InfoExtractor):
|
|||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _login(self):
|
def _perform_login(self, username, password):
|
||||||
username, password = self._get_login_info()
|
|
||||||
if username is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
login_page = self._download_webpage(
|
login_page = self._download_webpage(
|
||||||
self._LOGIN_URL, None, 'Downloading login page')
|
self._LOGIN_URL, None, 'Downloading login page')
|
||||||
|
|
||||||
@@ -93,9 +89,6 @@ class AnimeOnDemandIE(InfoExtractor):
|
|||||||
raise ExtractorError('Unable to login: %s' % error, expected=True)
|
raise ExtractorError('Unable to login: %s' % error, expected=True)
|
||||||
raise ExtractorError('Unable to log in')
|
raise ExtractorError('Unable to log in')
|
||||||
|
|
||||||
def _real_initialize(self):
|
|
||||||
self._login()
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
anime_id = self._match_id(url)
|
anime_id = self._match_id(url)
|
||||||
|
|
||||||
|
|||||||
143
yt_dlp/extractor/ant1newsgr.py
Normal file
143
yt_dlp/extractor/ant1newsgr.py
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
HEADRequest,
|
||||||
|
ExtractorError,
|
||||||
|
determine_ext,
|
||||||
|
scale_thumbnails_to_max_format_width,
|
||||||
|
unescapeHTML,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Ant1NewsGrBaseIE(InfoExtractor):
|
||||||
|
def _download_and_extract_api_data(self, video_id, netloc, cid=None):
|
||||||
|
url = f'{self.http_scheme()}//{netloc}{self._API_PATH}'
|
||||||
|
info = self._download_json(url, video_id, query={'cid': cid or video_id})
|
||||||
|
try:
|
||||||
|
source = info['url']
|
||||||
|
except KeyError:
|
||||||
|
raise ExtractorError('no source found for %s' % video_id)
|
||||||
|
formats, subs = (self._extract_m3u8_formats_and_subtitles(source, video_id, 'mp4')
|
||||||
|
if determine_ext(source) == 'm3u8' else ([{'url': source}], {}))
|
||||||
|
self._sort_formats(formats)
|
||||||
|
thumbnails = scale_thumbnails_to_max_format_width(
|
||||||
|
formats, [{'url': info['thumb']}], r'(?<=/imgHandler/)\d+')
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': info.get('title'),
|
||||||
|
'thumbnails': thumbnails,
|
||||||
|
'formats': formats,
|
||||||
|
'subtitles': subs,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Ant1NewsGrWatchIE(Ant1NewsGrBaseIE):
|
||||||
|
IE_NAME = 'ant1newsgr:watch'
|
||||||
|
IE_DESC = 'ant1news.gr videos'
|
||||||
|
_VALID_URL = r'https?://(?P<netloc>(?:www\.)?ant1news\.gr)/watch/(?P<id>\d+)/'
|
||||||
|
_API_PATH = '/templates/data/player'
|
||||||
|
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.ant1news.gr/watch/1506168/ant1-news-09112021-stis-18-45',
|
||||||
|
'md5': '95925e6b32106754235f2417e0d2dfab',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '1506168',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'md5:0ad00fa66ecf8aa233d26ab0dba7514a',
|
||||||
|
'description': 'md5:18665af715a6dcfeac1d6153a44f16b0',
|
||||||
|
'thumbnail': 'https://ant1media.azureedge.net/imgHandler/640/26d46bf6-8158-4f02-b197-7096c714b2de.jpg',
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id, netloc = self._match_valid_url(url).group('id', 'netloc')
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
info = self._download_and_extract_api_data(video_id, netloc)
|
||||||
|
info['description'] = self._og_search_description(webpage)
|
||||||
|
return info
|
||||||
|
|
||||||
|
|
||||||
|
class Ant1NewsGrArticleIE(Ant1NewsGrBaseIE):
|
||||||
|
IE_NAME = 'ant1newsgr:article'
|
||||||
|
IE_DESC = 'ant1news.gr articles'
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?ant1news\.gr/[^/]+/article/(?P<id>\d+)/'
|
||||||
|
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.ant1news.gr/afieromata/article/549468/o-tzeims-mpont-sta-meteora-oi-apeiles-kai-o-xesikomos-ton-kalogeron',
|
||||||
|
'md5': '294f18331bb516539d72d85a82887dcc',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '_xvg/m_cmbatw=',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'md5:a93e8ecf2e4073bfdffcb38f59945411',
|
||||||
|
'timestamp': 1603092840,
|
||||||
|
'upload_date': '20201019',
|
||||||
|
'thumbnail': 'https://ant1media.azureedge.net/imgHandler/640/756206d2-d640-40e2-b201-3555abdfc0db.jpg',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://ant1news.gr/Society/article/620286/symmoria-anilikon-dikigoros-thymaton-ithelan-na-toys-apoteleiosoyn',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '620286',
|
||||||
|
'title': 'md5:91fe569e952e4d146485740ae927662b',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 2,
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
info = self._search_json_ld(webpage, video_id, expected_type='NewsArticle')
|
||||||
|
embed_urls = list(Ant1NewsGrEmbedIE._extract_urls(webpage))
|
||||||
|
if not embed_urls:
|
||||||
|
raise ExtractorError('no videos found for %s' % video_id, expected=True)
|
||||||
|
return self.playlist_from_matches(
|
||||||
|
embed_urls, video_id, info.get('title'), ie=Ant1NewsGrEmbedIE.ie_key(),
|
||||||
|
video_kwargs={'url_transparent': True, 'timestamp': info.get('timestamp')})
|
||||||
|
|
||||||
|
|
||||||
|
class Ant1NewsGrEmbedIE(Ant1NewsGrBaseIE):
|
||||||
|
IE_NAME = 'ant1newsgr:embed'
|
||||||
|
IE_DESC = 'ant1news.gr embedded videos'
|
||||||
|
_BASE_PLAYER_URL_RE = r'(?:https?:)?//(?:[a-zA-Z0-9\-]+\.)?(?:antenna|ant1news)\.gr/templates/pages/player'
|
||||||
|
_VALID_URL = rf'{_BASE_PLAYER_URL_RE}\?([^#]+&)?cid=(?P<id>[^#&]+)'
|
||||||
|
_API_PATH = '/news/templates/data/jsonPlayer'
|
||||||
|
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.antenna.gr/templates/pages/player?cid=3f_li_c_az_jw_y_u=&w=670&h=377',
|
||||||
|
'md5': 'dfc58c3a11a5a9aad2ba316ed447def3',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '3f_li_c_az_jw_y_u=',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'md5:a30c93332455f53e1e84ae0724f0adf7',
|
||||||
|
'thumbnail': 'https://ant1media.azureedge.net/imgHandler/640/bbe31201-3f09-4a4e-87f5-8ad2159fffe2.jpg',
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _extract_urls(cls, webpage):
|
||||||
|
_EMBED_URL_RE = rf'{cls._BASE_PLAYER_URL_RE}\?(?:(?!(?P=_q1)).)+'
|
||||||
|
_EMBED_RE = rf'<iframe[^>]+?src=(?P<_q1>["\'])(?P<url>{_EMBED_URL_RE})(?P=_q1)'
|
||||||
|
for mobj in re.finditer(_EMBED_RE, webpage):
|
||||||
|
url = unescapeHTML(mobj.group('url'))
|
||||||
|
if not cls.suitable(url):
|
||||||
|
continue
|
||||||
|
yield url
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
|
||||||
|
canonical_url = self._request_webpage(
|
||||||
|
HEADRequest(url), video_id,
|
||||||
|
note='Resolve canonical player URL',
|
||||||
|
errnote='Could not resolve canonical player URL').geturl()
|
||||||
|
_, netloc, _, _, query, _ = urllib.parse.urlparse(canonical_url)
|
||||||
|
cid = urllib.parse.parse_qs(query)['cid'][0]
|
||||||
|
|
||||||
|
return self._download_and_extract_api_data(video_id, netloc, cid=cid)
|
||||||
@@ -3,7 +3,9 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
|
clean_html,
|
||||||
clean_podcast_url,
|
clean_podcast_url,
|
||||||
|
get_element_by_class,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
try_get,
|
try_get,
|
||||||
@@ -14,16 +16,17 @@ class ApplePodcastsIE(InfoExtractor):
|
|||||||
_VALID_URL = r'https?://podcasts\.apple\.com/(?:[^/]+/)?podcast(?:/[^/]+){1,2}.*?\bi=(?P<id>\d+)'
|
_VALID_URL = r'https?://podcasts\.apple\.com/(?:[^/]+/)?podcast(?:/[^/]+){1,2}.*?\bi=(?P<id>\d+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://podcasts.apple.com/us/podcast/207-whitney-webb-returns/id1135137367?i=1000482637777',
|
'url': 'https://podcasts.apple.com/us/podcast/207-whitney-webb-returns/id1135137367?i=1000482637777',
|
||||||
'md5': 'df02e6acb11c10e844946a39e7222b08',
|
'md5': '41dc31cd650143e530d9423b6b5a344f',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '1000482637777',
|
'id': '1000482637777',
|
||||||
'ext': 'mp3',
|
'ext': 'mp3',
|
||||||
'title': '207 - Whitney Webb Returns',
|
'title': '207 - Whitney Webb Returns',
|
||||||
'description': 'md5:13a73bade02d2e43737751e3987e1399',
|
'description': 'md5:75ef4316031df7b41ced4e7b987f79c6',
|
||||||
'upload_date': '20200705',
|
'upload_date': '20200705',
|
||||||
'timestamp': 1593921600,
|
'timestamp': 1593932400,
|
||||||
'duration': 6425,
|
'duration': 6454,
|
||||||
'series': 'The Tim Dillon Show',
|
'series': 'The Tim Dillon Show',
|
||||||
|
'thumbnail': 're:.+[.](png|jpe?g|webp)',
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://podcasts.apple.com/podcast/207-whitney-webb-returns/id1135137367?i=1000482637777',
|
'url': 'https://podcasts.apple.com/podcast/207-whitney-webb-returns/id1135137367?i=1000482637777',
|
||||||
@@ -39,24 +42,47 @@ class ApplePodcastsIE(InfoExtractor):
|
|||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
episode_id = self._match_id(url)
|
episode_id = self._match_id(url)
|
||||||
webpage = self._download_webpage(url, episode_id)
|
webpage = self._download_webpage(url, episode_id)
|
||||||
ember_data = self._parse_json(self._search_regex(
|
episode_data = {}
|
||||||
r'id="shoebox-ember-data-store"[^>]*>\s*({.+?})\s*<',
|
ember_data = {}
|
||||||
webpage, 'ember data'), episode_id)
|
# new page type 2021-11
|
||||||
ember_data = ember_data.get(episode_id) or ember_data
|
amp_data = self._parse_json(self._search_regex(
|
||||||
episode = ember_data['data']['attributes']
|
r'(?s)id="shoebox-media-api-cache-amp-podcasts"[^>]*>\s*({.+?})\s*<',
|
||||||
|
webpage, 'AMP data', default='{}'), episode_id, fatal=False) or {}
|
||||||
|
amp_data = try_get(amp_data,
|
||||||
|
lambda a: self._parse_json(
|
||||||
|
next(a[x] for x in iter(a) if episode_id in x),
|
||||||
|
episode_id),
|
||||||
|
dict) or {}
|
||||||
|
amp_data = amp_data.get('d') or []
|
||||||
|
episode_data = try_get(
|
||||||
|
amp_data,
|
||||||
|
lambda a: next(x for x in a
|
||||||
|
if x['type'] == 'podcast-episodes' and x['id'] == episode_id),
|
||||||
|
dict)
|
||||||
|
if not episode_data:
|
||||||
|
# try pre 2021-11 page type: TODO: consider deleting if no longer used
|
||||||
|
ember_data = self._parse_json(self._search_regex(
|
||||||
|
r'(?s)id="shoebox-ember-data-store"[^>]*>\s*({.+?})\s*<',
|
||||||
|
webpage, 'ember data'), episode_id) or {}
|
||||||
|
ember_data = ember_data.get(episode_id) or ember_data
|
||||||
|
episode_data = try_get(ember_data, lambda x: x['data'], dict)
|
||||||
|
episode = episode_data['attributes']
|
||||||
description = episode.get('description') or {}
|
description = episode.get('description') or {}
|
||||||
|
|
||||||
series = None
|
series = None
|
||||||
for inc in (ember_data.get('included') or []):
|
for inc in (amp_data or ember_data.get('included') or []):
|
||||||
if inc.get('type') == 'media/podcast':
|
if inc.get('type') == 'media/podcast':
|
||||||
series = try_get(inc, lambda x: x['attributes']['name'])
|
series = try_get(inc, lambda x: x['attributes']['name'])
|
||||||
|
series = series or clean_html(get_element_by_class('podcast-header__identity', webpage))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': episode_id,
|
'id': episode_id,
|
||||||
'title': episode['name'],
|
'title': episode.get('name'),
|
||||||
'url': clean_podcast_url(episode['assetUrl']),
|
'url': clean_podcast_url(episode['assetUrl']),
|
||||||
'description': description.get('standard') or description.get('short'),
|
'description': description.get('standard') or description.get('short'),
|
||||||
'timestamp': parse_iso8601(episode.get('releaseDateTime')),
|
'timestamp': parse_iso8601(episode.get('releaseDateTime')),
|
||||||
'duration': int_or_none(episode.get('durationInMilliseconds'), 1000),
|
'duration': int_or_none(episode.get('durationInMilliseconds'), 1000),
|
||||||
'series': series,
|
'series': series,
|
||||||
|
'thumbnail': self._og_search_thumbnail(webpage),
|
||||||
|
'vcodec': 'none',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ from ..utils import (
|
|||||||
get_element_by_id,
|
get_element_by_id,
|
||||||
HEADRequest,
|
HEADRequest,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
|
join_nonempty,
|
||||||
KNOWN_EXTENSIONS,
|
KNOWN_EXTENSIONS,
|
||||||
merge_dicts,
|
merge_dicts,
|
||||||
mimetype2ext,
|
mimetype2ext,
|
||||||
@@ -64,7 +65,7 @@ class ArchiveOrgIE(InfoExtractor):
|
|||||||
'description': 'md5:43a603fd6c5b4b90d12a96b921212b9c',
|
'description': 'md5:43a603fd6c5b4b90d12a96b921212b9c',
|
||||||
'uploader': 'yorkmba99@hotmail.com',
|
'uploader': 'yorkmba99@hotmail.com',
|
||||||
'timestamp': 1387699629,
|
'timestamp': 1387699629,
|
||||||
'upload_date': "20131222",
|
'upload_date': '20131222',
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://archive.org/embed/XD300-23_68HighlightsAResearchCntAugHumanIntellect',
|
'url': 'http://archive.org/embed/XD300-23_68HighlightsAResearchCntAugHumanIntellect',
|
||||||
@@ -150,8 +151,7 @@ class ArchiveOrgIE(InfoExtractor):
|
|||||||
|
|
||||||
# Archive.org metadata API doesn't clearly demarcate playlist entries
|
# Archive.org metadata API doesn't clearly demarcate playlist entries
|
||||||
# or subtitle tracks, so we get them from the embeddable player.
|
# or subtitle tracks, so we get them from the embeddable player.
|
||||||
embed_page = self._download_webpage(
|
embed_page = self._download_webpage(f'https://archive.org/embed/{identifier}', identifier)
|
||||||
'https://archive.org/embed/' + identifier, identifier)
|
|
||||||
playlist = self._playlist_data(embed_page)
|
playlist = self._playlist_data(embed_page)
|
||||||
|
|
||||||
entries = {}
|
entries = {}
|
||||||
@@ -166,17 +166,17 @@ class ArchiveOrgIE(InfoExtractor):
|
|||||||
'thumbnails': [],
|
'thumbnails': [],
|
||||||
'artist': p.get('artist'),
|
'artist': p.get('artist'),
|
||||||
'track': p.get('title'),
|
'track': p.get('title'),
|
||||||
'subtitles': {}}
|
'subtitles': {},
|
||||||
|
}
|
||||||
|
|
||||||
for track in p.get('tracks', []):
|
for track in p.get('tracks', []):
|
||||||
if track['kind'] != 'subtitles':
|
if track['kind'] != 'subtitles':
|
||||||
continue
|
continue
|
||||||
|
|
||||||
entries[p['orig']][track['label']] = {
|
entries[p['orig']][track['label']] = {
|
||||||
'url': 'https://archive.org/' + track['file'].lstrip('/')}
|
'url': 'https://archive.org/' + track['file'].lstrip('/')
|
||||||
|
}
|
||||||
|
|
||||||
metadata = self._download_json(
|
metadata = self._download_json('http://archive.org/metadata/' + identifier, identifier)
|
||||||
'http://archive.org/metadata/' + identifier, identifier)
|
|
||||||
m = metadata['metadata']
|
m = metadata['metadata']
|
||||||
identifier = m['identifier']
|
identifier = m['identifier']
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@ class ArchiveOrgIE(InfoExtractor):
|
|||||||
'license': m.get('licenseurl'),
|
'license': m.get('licenseurl'),
|
||||||
'release_date': unified_strdate(m.get('date')),
|
'release_date': unified_strdate(m.get('date')),
|
||||||
'timestamp': unified_timestamp(dict_get(m, ['publicdate', 'addeddate'])),
|
'timestamp': unified_timestamp(dict_get(m, ['publicdate', 'addeddate'])),
|
||||||
'webpage_url': 'https://archive.org/details/' + identifier,
|
'webpage_url': f'https://archive.org/details/{identifier}',
|
||||||
'location': m.get('venue'),
|
'location': m.get('venue'),
|
||||||
'release_year': int_or_none(m.get('year'))}
|
'release_year': int_or_none(m.get('year'))}
|
||||||
|
|
||||||
@@ -207,7 +207,7 @@ class ArchiveOrgIE(InfoExtractor):
|
|||||||
'discnumber': int_or_none(f.get('disc')),
|
'discnumber': int_or_none(f.get('disc')),
|
||||||
'release_year': int_or_none(f.get('year'))})
|
'release_year': int_or_none(f.get('year'))})
|
||||||
entry = entries[f['name']]
|
entry = entries[f['name']]
|
||||||
elif f.get('original') in entries:
|
elif traverse_obj(f, 'original', expected_type=str) in entries:
|
||||||
entry = entries[f['original']]
|
entry = entries[f['original']]
|
||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
@@ -230,13 +230,12 @@ class ArchiveOrgIE(InfoExtractor):
|
|||||||
'filesize': int_or_none(f.get('size')),
|
'filesize': int_or_none(f.get('size')),
|
||||||
'protocol': 'https'})
|
'protocol': 'https'})
|
||||||
|
|
||||||
# Sort available formats by filesize
|
|
||||||
for entry in entries.values():
|
for entry in entries.values():
|
||||||
entry['formats'] = list(sorted(entry['formats'], key=lambda x: x.get('filesize', -1)))
|
self._sort_formats(entry['formats'])
|
||||||
|
|
||||||
if len(entries) == 1:
|
if len(entries) == 1:
|
||||||
# If there's only one item, use it as the main info dict
|
# If there's only one item, use it as the main info dict
|
||||||
only_video = entries[list(entries.keys())[0]]
|
only_video = next(iter(entries.values()))
|
||||||
if entry_id:
|
if entry_id:
|
||||||
info = merge_dicts(only_video, info)
|
info = merge_dicts(only_video, info)
|
||||||
else:
|
else:
|
||||||
@@ -261,19 +260,19 @@ class ArchiveOrgIE(InfoExtractor):
|
|||||||
|
|
||||||
class YoutubeWebArchiveIE(InfoExtractor):
|
class YoutubeWebArchiveIE(InfoExtractor):
|
||||||
IE_NAME = 'web.archive:youtube'
|
IE_NAME = 'web.archive:youtube'
|
||||||
IE_DESC = 'web.archive.org saved youtube videos'
|
IE_DESC = 'web.archive.org saved youtube videos, "ytarchive:" prefix'
|
||||||
_VALID_URL = r"""(?x)^
|
_VALID_URL = r'''(?x)(?:(?P<prefix>ytarchive:)|
|
||||||
(?:https?://)?web\.archive\.org/
|
(?:https?://)?web\.archive\.org/
|
||||||
(?:web/)?
|
(?:web/)?(?:(?P<date>[0-9]{14})?[0-9A-Za-z_*]*/)? # /web and the version index is optional
|
||||||
(?:(?P<date>[0-9]{14})?[0-9A-Za-z_*]*/)? # /web and the version index is optional
|
(?:https?(?::|%3[Aa])//)?(?:
|
||||||
|
(?:\w+\.)?youtube\.com(?::(?:80|443))?/watch(?:\.php)?(?:\?|%3[fF])(?:[^\#]+(?:&|%26))?v(?:=|%3[dD]) # Youtube URL
|
||||||
(?:https?(?::|%3[Aa])//)?
|
|(?:wayback-fakeurl\.archive\.org/yt/) # Or the internal fake url
|
||||||
(?:
|
)
|
||||||
(?:\w+\.)?youtube\.com(?::(?:80|443))?/watch(?:\.php)?(?:\?|%3[fF])(?:[^\#]+(?:&|%26))?v(?:=|%3[dD]) # Youtube URL
|
)(?P<id>[0-9A-Za-z_-]{11})
|
||||||
|(?:wayback-fakeurl\.archive\.org/yt/) # Or the internal fake url
|
(?(prefix)
|
||||||
)
|
(?::(?P<date2>[0-9]{14}))?$|
|
||||||
(?P<id>[0-9A-Za-z_-]{11})(?:%26|\#|&|$)
|
(?:%26|[#&]|$)
|
||||||
"""
|
)'''
|
||||||
|
|
||||||
_TESTS = [
|
_TESTS = [
|
||||||
{
|
{
|
||||||
@@ -438,7 +437,13 @@ class YoutubeWebArchiveIE(InfoExtractor):
|
|||||||
}, {
|
}, {
|
||||||
'url': 'https://web.archive.org/http://www.youtube.com:80/watch?v=-05VVye-ffg',
|
'url': 'https://web.archive.org/http://www.youtube.com:80/watch?v=-05VVye-ffg',
|
||||||
'only_matching': True
|
'only_matching': True
|
||||||
}
|
}, {
|
||||||
|
'url': 'ytarchive:BaW_jenozKc:20050214000000',
|
||||||
|
'only_matching': True
|
||||||
|
}, {
|
||||||
|
'url': 'ytarchive:BaW_jenozKc',
|
||||||
|
'only_matching': True
|
||||||
|
},
|
||||||
]
|
]
|
||||||
_YT_INITIAL_DATA_RE = r'(?:(?:(?:window\s*\[\s*["\']ytInitialData["\']\s*\]|ytInitialData)\s*=\s*({.+?})\s*;)|%s)' % YoutubeBaseInfoExtractor._YT_INITIAL_DATA_RE
|
_YT_INITIAL_DATA_RE = r'(?:(?:(?:window\s*\[\s*["\']ytInitialData["\']\s*\]|ytInitialData)\s*=\s*({.+?})\s*;)|%s)' % YoutubeBaseInfoExtractor._YT_INITIAL_DATA_RE
|
||||||
_YT_INITIAL_PLAYER_RESPONSE_RE = r'(?:(?:(?:window\s*\[\s*["\']ytInitialPlayerResponse["\']\s*\]|ytInitialPlayerResponse)\s*=[(\s]*({.+?})[)\s]*;)|%s)' % YoutubeBaseInfoExtractor._YT_INITIAL_PLAYER_RESPONSE_RE
|
_YT_INITIAL_PLAYER_RESPONSE_RE = r'(?:(?:(?:window\s*\[\s*["\']ytInitialPlayerResponse["\']\s*\]|ytInitialPlayerResponse)\s*=[(\s]*({.+?})[)\s]*;)|%s)' % YoutubeBaseInfoExtractor._YT_INITIAL_PLAYER_RESPONSE_RE
|
||||||
@@ -452,7 +457,7 @@ class YoutubeWebArchiveIE(InfoExtractor):
|
|||||||
_OLDEST_CAPTURE_DATE = 20050214000000
|
_OLDEST_CAPTURE_DATE = 20050214000000
|
||||||
_NEWEST_CAPTURE_DATE = 20500101000000
|
_NEWEST_CAPTURE_DATE = 20500101000000
|
||||||
|
|
||||||
def _call_cdx_api(self, item_id, url, filters: list = None, collapse: list = None, query: dict = None, note='Downloading CDX API JSON'):
|
def _call_cdx_api(self, item_id, url, filters: list = None, collapse: list = None, query: dict = None, note=None, fatal=False):
|
||||||
# CDX docs: https://github.com/internetarchive/wayback/blob/master/wayback-cdx-server/README.md
|
# CDX docs: https://github.com/internetarchive/wayback/blob/master/wayback-cdx-server/README.md
|
||||||
query = {
|
query = {
|
||||||
'url': url,
|
'url': url,
|
||||||
@@ -463,7 +468,9 @@ class YoutubeWebArchiveIE(InfoExtractor):
|
|||||||
'collapse': collapse or [],
|
'collapse': collapse or [],
|
||||||
**(query or {})
|
**(query or {})
|
||||||
}
|
}
|
||||||
res = self._download_json('https://web.archive.org/cdx/search/cdx', item_id, note, query=query)
|
res = self._download_json(
|
||||||
|
'https://web.archive.org/cdx/search/cdx', item_id,
|
||||||
|
note or 'Downloading CDX API JSON', query=query, fatal=fatal)
|
||||||
if isinstance(res, list) and len(res) >= 2:
|
if isinstance(res, list) and len(res) >= 2:
|
||||||
# format response to make it easier to use
|
# format response to make it easier to use
|
||||||
return list(dict(zip(res[0], v)) for v in res[1:])
|
return list(dict(zip(res[0], v)) for v in res[1:])
|
||||||
@@ -476,15 +483,13 @@ class YoutubeWebArchiveIE(InfoExtractor):
|
|||||||
regex), webpage, name, default='{}'), video_id, fatal=False)
|
regex), webpage, name, default='{}'), video_id, fatal=False)
|
||||||
|
|
||||||
def _extract_webpage_title(self, webpage):
|
def _extract_webpage_title(self, webpage):
|
||||||
page_title = self._html_search_regex(
|
page_title = self._html_extract_title(webpage, default='')
|
||||||
r'<title>([^<]*)</title>', webpage, 'title', default='')
|
|
||||||
# YouTube video pages appear to always have either 'YouTube -' as prefix or '- YouTube' as suffix.
|
# YouTube video pages appear to always have either 'YouTube -' as prefix or '- YouTube' as suffix.
|
||||||
return self._html_search_regex(
|
return self._html_search_regex(
|
||||||
r'(?:YouTube\s*-\s*(.*)$)|(?:(.*)\s*-\s*YouTube$)',
|
r'(?:YouTube\s*-\s*(.*)$)|(?:(.*)\s*-\s*YouTube$)',
|
||||||
page_title, 'title', default='')
|
page_title, 'title', default='')
|
||||||
|
|
||||||
def _extract_metadata(self, video_id, webpage):
|
def _extract_metadata(self, video_id, webpage):
|
||||||
|
|
||||||
search_meta = ((lambda x: self._html_search_meta(x, webpage, default=None)) if webpage else (lambda x: None))
|
search_meta = ((lambda x: self._html_search_meta(x, webpage, default=None)) if webpage else (lambda x: None))
|
||||||
player_response = self._extract_yt_initial_variable(
|
player_response = self._extract_yt_initial_variable(
|
||||||
webpage, self._YT_INITIAL_PLAYER_RESPONSE_RE, video_id, 'initial player response') or {}
|
webpage, self._YT_INITIAL_PLAYER_RESPONSE_RE, video_id, 'initial player response') or {}
|
||||||
@@ -596,7 +601,7 @@ class YoutubeWebArchiveIE(InfoExtractor):
|
|||||||
|
|
||||||
# Prefer the new polymer UI captures as we support extracting more metadata from them
|
# Prefer the new polymer UI captures as we support extracting more metadata from them
|
||||||
# WBM captures seem to all switch to this layout ~July 2020
|
# WBM captures seem to all switch to this layout ~July 2020
|
||||||
modern_captures = list(filter(lambda x: x >= 20200701000000, all_captures))
|
modern_captures = [x for x in all_captures if x >= 20200701000000]
|
||||||
if modern_captures:
|
if modern_captures:
|
||||||
capture_dates.append(modern_captures[0])
|
capture_dates.append(modern_captures[0])
|
||||||
capture_dates.append(url_date)
|
capture_dates.append(url_date)
|
||||||
@@ -608,11 +613,11 @@ class YoutubeWebArchiveIE(InfoExtractor):
|
|||||||
|
|
||||||
# Fallbacks if any of the above fail
|
# Fallbacks if any of the above fail
|
||||||
capture_dates.extend([self._OLDEST_CAPTURE_DATE, self._NEWEST_CAPTURE_DATE])
|
capture_dates.extend([self._OLDEST_CAPTURE_DATE, self._NEWEST_CAPTURE_DATE])
|
||||||
return orderedSet(capture_dates)
|
return orderedSet(filter(None, capture_dates))
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
|
video_id, url_date, url_date_2 = self._match_valid_url(url).group('id', 'date', 'date2')
|
||||||
url_date, video_id = self._match_valid_url(url).groups()
|
url_date = url_date or url_date_2
|
||||||
|
|
||||||
urlh = None
|
urlh = None
|
||||||
try:
|
try:
|
||||||
@@ -629,11 +634,9 @@ class YoutubeWebArchiveIE(InfoExtractor):
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
capture_dates = self._get_capture_dates(video_id, int_or_none(url_date))
|
capture_dates = self._get_capture_dates(video_id, int_or_none(url_date))
|
||||||
self.write_debug('Captures to try: ' + ', '.join(str(i) for i in capture_dates if i is not None))
|
self.write_debug('Captures to try: ' + join_nonempty(*capture_dates, delim=', '))
|
||||||
info = {'id': video_id}
|
info = {'id': video_id}
|
||||||
for capture in capture_dates:
|
for capture in capture_dates:
|
||||||
if not capture:
|
|
||||||
continue
|
|
||||||
webpage = self._download_webpage(
|
webpage = self._download_webpage(
|
||||||
(self._WAYBACK_BASE_URL + 'http://www.youtube.com/watch?v=%s') % (capture, video_id),
|
(self._WAYBACK_BASE_URL + 'http://www.youtube.com/watch?v=%s') % (capture, video_id),
|
||||||
video_id=video_id, fatal=False, errnote='unable to download capture webpage (it may not be archived)',
|
video_id=video_id, fatal=False, errnote='unable to download capture webpage (it may not be archived)',
|
||||||
@@ -648,7 +651,7 @@ class YoutubeWebArchiveIE(InfoExtractor):
|
|||||||
info['thumbnails'] = self._extract_thumbnails(video_id)
|
info['thumbnails'] = self._extract_thumbnails(video_id)
|
||||||
|
|
||||||
if urlh:
|
if urlh:
|
||||||
url = compat_urllib_parse_unquote(urlh.url)
|
url = compat_urllib_parse_unquote(urlh.geturl())
|
||||||
video_file_url_qs = parse_qs(url)
|
video_file_url_qs = parse_qs(url)
|
||||||
# Attempt to recover any ext & format info from playback url & response headers
|
# Attempt to recover any ext & format info from playback url & response headers
|
||||||
format = {'url': url, 'filesize': int_or_none(urlh.headers.get('x-archive-orig-content-length'))}
|
format = {'url': url, 'filesize': int_or_none(urlh.headers.get('x-archive-orig-content-length'))}
|
||||||
|
|||||||
@@ -124,8 +124,7 @@ class ArcPublishingIE(InfoExtractor):
|
|||||||
formats.extend(smil_formats)
|
formats.extend(smil_formats)
|
||||||
elif stream_type in ('ts', 'hls'):
|
elif stream_type in ('ts', 'hls'):
|
||||||
m3u8_formats = self._extract_m3u8_formats(
|
m3u8_formats = self._extract_m3u8_formats(
|
||||||
s_url, uuid, 'mp4', 'm3u8' if is_live else 'm3u8_native',
|
s_url, uuid, 'mp4', live=is_live, m3u8_id='hls', fatal=False)
|
||||||
m3u8_id='hls', fatal=False)
|
|
||||||
if all([f.get('acodec') == 'none' for f in m3u8_formats]):
|
if all([f.get('acodec') == 'none' for f in m3u8_formats]):
|
||||||
continue
|
continue
|
||||||
for f in m3u8_formats:
|
for f in m3u8_formats:
|
||||||
|
|||||||
@@ -407,8 +407,9 @@ class ARDBetaMediathekIE(ARDMediathekBaseIE):
|
|||||||
(?:(?:beta|www)\.)?ardmediathek\.de/
|
(?:(?:beta|www)\.)?ardmediathek\.de/
|
||||||
(?:(?P<client>[^/]+)/)?
|
(?:(?P<client>[^/]+)/)?
|
||||||
(?:player|live|video|(?P<playlist>sendung|sammlung))/
|
(?:player|live|video|(?P<playlist>sendung|sammlung))/
|
||||||
(?:(?P<display_id>[^?#]+)/)?
|
(?:(?P<display_id>(?(playlist)[^?#]+?|[^?#]+))/)?
|
||||||
(?P<id>(?(playlist)|Y3JpZDovL)[a-zA-Z0-9]+)'''
|
(?P<id>(?(playlist)|Y3JpZDovL)[a-zA-Z0-9]+)
|
||||||
|
(?(playlist)/(?P<season>\d+)?/?(?:[?#]|$))'''
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.ardmediathek.de/mdr/video/die-robuste-roswita/Y3JpZDovL21kci5kZS9iZWl0cmFnL2Ntcy84MWMxN2MzZC0wMjkxLTRmMzUtODk4ZS0wYzhlOWQxODE2NGI/',
|
'url': 'https://www.ardmediathek.de/mdr/video/die-robuste-roswita/Y3JpZDovL21kci5kZS9iZWl0cmFnL2Ntcy84MWMxN2MzZC0wMjkxLTRmMzUtODk4ZS0wYzhlOWQxODE2NGI/',
|
||||||
@@ -436,6 +437,13 @@ class ARDBetaMediathekIE(ARDMediathekBaseIE):
|
|||||||
'description': 'md5:39578c7b96c9fe50afdf5674ad985e6b',
|
'description': 'md5:39578c7b96c9fe50afdf5674ad985e6b',
|
||||||
'upload_date': '20211108',
|
'upload_date': '20211108',
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.ardmediathek.de/sendung/beforeigners/beforeigners/staffel-1/Y3JpZDovL2Rhc2Vyc3RlLmRlL2JlZm9yZWlnbmVycw/1',
|
||||||
|
'playlist_count': 6,
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'Y3JpZDovL2Rhc2Vyc3RlLmRlL2JlZm9yZWlnbmVycw',
|
||||||
|
'title': 'beforeigners/beforeigners/staffel-1',
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://beta.ardmediathek.de/ard/video/Y3JpZDovL2Rhc2Vyc3RlLmRlL3RhdG9ydC9mYmM4NGM1NC0xNzU4LTRmZGYtYWFhZS0wYzcyZTIxNGEyMDE',
|
'url': 'https://beta.ardmediathek.de/ard/video/Y3JpZDovL2Rhc2Vyc3RlLmRlL3RhdG9ydC9mYmM4NGM1NC0xNzU4LTRmZGYtYWFhZS0wYzcyZTIxNGEyMDE',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
@@ -561,14 +569,15 @@ class ARDBetaMediathekIE(ARDMediathekBaseIE):
|
|||||||
break
|
break
|
||||||
pageNumber = pageNumber + 1
|
pageNumber = pageNumber + 1
|
||||||
|
|
||||||
return self.playlist_result(entries, playlist_title=display_id)
|
return self.playlist_result(entries, playlist_id, playlist_title=display_id)
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id, display_id, playlist_type, client = self._match_valid_url(url).group(
|
video_id, display_id, playlist_type, client, season_number = self._match_valid_url(url).group(
|
||||||
'id', 'display_id', 'playlist', 'client')
|
'id', 'display_id', 'playlist', 'client', 'season')
|
||||||
display_id, client = display_id or video_id, client or 'ard'
|
display_id, client = display_id or video_id, client or 'ard'
|
||||||
|
|
||||||
if playlist_type:
|
if playlist_type:
|
||||||
|
# TODO: Extract only specified season
|
||||||
return self._ARD_extract_playlist(url, video_id, display_id, client, playlist_type)
|
return self._ARD_extract_playlist(url, video_id, display_id, client, playlist_type)
|
||||||
|
|
||||||
player_page = self._download_json(
|
player_page = self._download_json(
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from ..utils import (
|
|||||||
int_or_none,
|
int_or_none,
|
||||||
parse_qs,
|
parse_qs,
|
||||||
qualities,
|
qualities,
|
||||||
|
strip_or_none,
|
||||||
try_get,
|
try_get,
|
||||||
unified_strdate,
|
unified_strdate,
|
||||||
url_or_none,
|
url_or_none,
|
||||||
@@ -137,6 +138,7 @@ class ArteTVIE(ArteTVBaseIE):
|
|||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
lang_pref = -1
|
lang_pref = -1
|
||||||
|
format_note = '%s, %s' % (f.get('versionCode'), f.get('versionLibelle'))
|
||||||
|
|
||||||
media_type = f.get('mediaType')
|
media_type = f.get('mediaType')
|
||||||
if media_type == 'hls':
|
if media_type == 'hls':
|
||||||
@@ -144,14 +146,17 @@ class ArteTVIE(ArteTVBaseIE):
|
|||||||
format_url, video_id, 'mp4', entry_protocol='m3u8_native',
|
format_url, video_id, 'mp4', entry_protocol='m3u8_native',
|
||||||
m3u8_id=format_id, fatal=False)
|
m3u8_id=format_id, fatal=False)
|
||||||
for m3u8_format in m3u8_formats:
|
for m3u8_format in m3u8_formats:
|
||||||
m3u8_format['language_preference'] = lang_pref
|
m3u8_format.update({
|
||||||
|
'language_preference': lang_pref,
|
||||||
|
'format_note': format_note,
|
||||||
|
})
|
||||||
formats.extend(m3u8_formats)
|
formats.extend(m3u8_formats)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
format = {
|
format = {
|
||||||
'format_id': format_id,
|
'format_id': format_id,
|
||||||
'language_preference': lang_pref,
|
'language_preference': lang_pref,
|
||||||
'format_note': '%s, %s' % (f.get('versionCode'), f.get('versionLibelle')),
|
'format_note': format_note,
|
||||||
'width': int_or_none(f.get('width')),
|
'width': int_or_none(f.get('width')),
|
||||||
'height': int_or_none(f.get('height')),
|
'height': int_or_none(f.get('height')),
|
||||||
'tbr': int_or_none(f.get('bitrate')),
|
'tbr': int_or_none(f.get('bitrate')),
|
||||||
@@ -253,3 +258,44 @@ class ArteTVPlaylistIE(ArteTVBaseIE):
|
|||||||
title = collection.get('title')
|
title = collection.get('title')
|
||||||
description = collection.get('shortDescription') or collection.get('teaserText')
|
description = collection.get('shortDescription') or collection.get('teaserText')
|
||||||
return self.playlist_result(entries, playlist_id, title, description)
|
return self.playlist_result(entries, playlist_id, title, description)
|
||||||
|
|
||||||
|
|
||||||
|
class ArteTVCategoryIE(ArteTVBaseIE):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?arte\.tv/(?P<lang>%s)/videos/(?P<id>[\w-]+(?:/[\w-]+)*)/?\s*$' % ArteTVBaseIE._ARTE_LANGUAGES
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.arte.tv/en/videos/politics-and-society/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'politics-and-society',
|
||||||
|
'title': 'Politics and society',
|
||||||
|
'description': 'Investigative documentary series, geopolitical analysis, and international commentary',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 13,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def suitable(cls, url):
|
||||||
|
return (
|
||||||
|
not any(ie.suitable(url) for ie in (ArteTVIE, ArteTVPlaylistIE, ))
|
||||||
|
and super(ArteTVCategoryIE, cls).suitable(url))
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
lang, playlist_id = self._match_valid_url(url).groups()
|
||||||
|
webpage = self._download_webpage(url, playlist_id)
|
||||||
|
|
||||||
|
items = []
|
||||||
|
for video in re.finditer(
|
||||||
|
r'<a\b[^>]*?href\s*=\s*(?P<q>"|\'|\b)(?P<url>https?://www\.arte\.tv/%s/videos/[\w/-]+)(?P=q)' % lang,
|
||||||
|
webpage):
|
||||||
|
video = video.group('url')
|
||||||
|
if video == url:
|
||||||
|
continue
|
||||||
|
if any(ie.suitable(video) for ie in (ArteTVIE, ArteTVPlaylistIE, )):
|
||||||
|
items.append(video)
|
||||||
|
|
||||||
|
title = (self._og_search_title(webpage, default=None)
|
||||||
|
or self._html_search_regex(r'<title\b[^>]*>([^<]+)</title>', default=None))
|
||||||
|
title = strip_or_none(title.rsplit('|', 1)[0]) or self._generic_title(url)
|
||||||
|
|
||||||
|
return self.playlist_from_matches(items, playlist_id=playlist_id, playlist_title=title,
|
||||||
|
description=self._og_search_description(webpage, default=None))
|
||||||
|
|||||||
@@ -181,8 +181,7 @@ class AsianCrushPlaylistIE(AsianCrushBaseIE):
|
|||||||
'title', default=None) or self._og_search_title(
|
'title', default=None) or self._og_search_title(
|
||||||
webpage, default=None) or self._html_search_meta(
|
webpage, default=None) or self._html_search_meta(
|
||||||
'twitter:title', webpage, 'title',
|
'twitter:title', webpage, 'title',
|
||||||
default=None) or self._search_regex(
|
default=None) or self._html_extract_title(webpage)
|
||||||
r'<title>([^<]+)</title>', webpage, 'title', fatal=False)
|
|
||||||
if title:
|
if title:
|
||||||
title = re.sub(r'\s*\|\s*.+?$', '', title)
|
title = re.sub(r'\s*\|\s*.+?$', '', title)
|
||||||
|
|
||||||
|
|||||||
@@ -37,9 +37,6 @@ class AtresPlayerIE(InfoExtractor):
|
|||||||
]
|
]
|
||||||
_API_BASE = 'https://api.atresplayer.com/'
|
_API_BASE = 'https://api.atresplayer.com/'
|
||||||
|
|
||||||
def _real_initialize(self):
|
|
||||||
self._login()
|
|
||||||
|
|
||||||
def _handle_error(self, e, code):
|
def _handle_error(self, e, code):
|
||||||
if isinstance(e.cause, compat_HTTPError) and e.cause.code == code:
|
if isinstance(e.cause, compat_HTTPError) and e.cause.code == code:
|
||||||
error = self._parse_json(e.cause.read(), None)
|
error = self._parse_json(e.cause.read(), None)
|
||||||
@@ -48,11 +45,7 @@ class AtresPlayerIE(InfoExtractor):
|
|||||||
raise ExtractorError(error['error_description'], expected=True)
|
raise ExtractorError(error['error_description'], expected=True)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def _login(self):
|
def _perform_login(self, username, password):
|
||||||
username, password = self._get_login_info()
|
|
||||||
if username is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._request_webpage(
|
self._request_webpage(
|
||||||
self._API_BASE + 'login', None, 'Downloading login page')
|
self._API_BASE + 'login', None, 'Downloading login page')
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from ..utils import (
|
|||||||
float_or_none,
|
float_or_none,
|
||||||
jwt_encode_hs256,
|
jwt_encode_hs256,
|
||||||
try_get,
|
try_get,
|
||||||
|
ExtractorError,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -94,6 +95,11 @@ class ATVAtIE(InfoExtractor):
|
|||||||
})
|
})
|
||||||
|
|
||||||
video_id, videos_data = list(videos['data'].items())[0]
|
video_id, videos_data = list(videos['data'].items())[0]
|
||||||
|
error_msg = try_get(videos_data, lambda x: x['error']['title'])
|
||||||
|
if error_msg == 'Geo check failed':
|
||||||
|
self.raise_geo_restricted(error_msg)
|
||||||
|
elif error_msg:
|
||||||
|
raise ExtractorError(error_msg)
|
||||||
entries = [
|
entries = [
|
||||||
self._extract_video_info(url, contentResource[video['id']], video)
|
self._extract_video_info(url, contentResource[video['id']], video)
|
||||||
for video in videos_data]
|
for video in videos_data]
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ class AudiomackIE(InfoExtractor):
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
# audiomack wrapper around soundcloud song
|
# audiomack wrapper around soundcloud song
|
||||||
|
# Needs new test URL.
|
||||||
{
|
{
|
||||||
'add_ie': ['Soundcloud'],
|
'add_ie': ['Soundcloud'],
|
||||||
'url': 'http://www.audiomack.com/song/hip-hop-daily/black-mamba-freestyle',
|
'url': 'http://www.audiomack.com/song/hip-hop-daily/black-mamba-freestyle',
|
||||||
|
|||||||
@@ -11,11 +11,12 @@ class AZMedienIE(InfoExtractor):
|
|||||||
IE_DESC = 'AZ Medien videos'
|
IE_DESC = 'AZ Medien videos'
|
||||||
_VALID_URL = r'''(?x)
|
_VALID_URL = r'''(?x)
|
||||||
https?://
|
https?://
|
||||||
(?:www\.)?
|
(?:www\.|tv\.)?
|
||||||
(?P<host>
|
(?P<host>
|
||||||
telezueri\.ch|
|
telezueri\.ch|
|
||||||
telebaern\.tv|
|
telebaern\.tv|
|
||||||
telem1\.ch
|
telem1\.ch|
|
||||||
|
tvo-online\.ch
|
||||||
)/
|
)/
|
||||||
[^/]+/
|
[^/]+/
|
||||||
(?P<id>
|
(?P<id>
|
||||||
@@ -30,7 +31,7 @@ class AZMedienIE(InfoExtractor):
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.telezueri.ch/sonntalk/bundesrats-vakanzen-eu-rahmenabkommen-133214569',
|
'url': 'https://tv.telezueri.ch/sonntalk/bundesrats-vakanzen-eu-rahmenabkommen-133214569',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '1_anruz3wy',
|
'id': '1_anruz3wy',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
@@ -38,6 +39,9 @@ class AZMedienIE(InfoExtractor):
|
|||||||
'uploader_id': 'TVOnline',
|
'uploader_id': 'TVOnline',
|
||||||
'upload_date': '20180930',
|
'upload_date': '20180930',
|
||||||
'timestamp': 1538328802,
|
'timestamp': 1538328802,
|
||||||
|
'view_count': int,
|
||||||
|
'thumbnail': 'http://cfvod.kaltura.com/p/1719221/sp/171922100/thumbnail/entry_id/1_anruz3wy/version/100031',
|
||||||
|
'duration': 1930
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
|
|||||||
153
yt_dlp/extractor/banbye.py
Normal file
153
yt_dlp/extractor/banbye.py
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import math
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..compat import (
|
||||||
|
compat_urllib_parse_urlparse,
|
||||||
|
compat_parse_qs,
|
||||||
|
)
|
||||||
|
from ..utils import (
|
||||||
|
format_field,
|
||||||
|
InAdvancePagedList,
|
||||||
|
traverse_obj,
|
||||||
|
unified_timestamp,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BanByeBaseIE(InfoExtractor):
|
||||||
|
_API_BASE = 'https://api.banbye.com'
|
||||||
|
_CDN_BASE = 'https://cdn.banbye.com'
|
||||||
|
_VIDEO_BASE = 'https://banbye.com/watch'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_playlist_id(url, param='playlist'):
|
||||||
|
return compat_parse_qs(
|
||||||
|
compat_urllib_parse_urlparse(url).query).get(param, [None])[0]
|
||||||
|
|
||||||
|
def _extract_playlist(self, playlist_id):
|
||||||
|
data = self._download_json(f'{self._API_BASE}/playlists/{playlist_id}', playlist_id)
|
||||||
|
return self.playlist_result([
|
||||||
|
self.url_result(f'{self._VIDEO_BASE}/{video_id}', BanByeIE)
|
||||||
|
for video_id in data['videoIds']], playlist_id, data.get('name'))
|
||||||
|
|
||||||
|
|
||||||
|
class BanByeIE(BanByeBaseIE):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?banbye.com/(?:en/)?watch/(?P<id>\w+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://banbye.com/watch/v_ytfmvkVYLE8T',
|
||||||
|
'md5': '2f4ea15c5ca259a73d909b2cfd558eb5',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'v_ytfmvkVYLE8T',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'md5:5ec098f88a0d796f987648de6322ba0f',
|
||||||
|
'description': 'md5:4d94836e73396bc18ef1fa0f43e5a63a',
|
||||||
|
'uploader': 'wRealu24',
|
||||||
|
'channel_id': 'ch_wrealu24',
|
||||||
|
'channel_url': 'https://banbye.com/channel/ch_wrealu24',
|
||||||
|
'timestamp': 1647604800,
|
||||||
|
'upload_date': '20220318',
|
||||||
|
'duration': 1931,
|
||||||
|
'thumbnail': r're:https?://.*\.webp',
|
||||||
|
'tags': 'count:5',
|
||||||
|
'like_count': int,
|
||||||
|
'dislike_count': int,
|
||||||
|
'view_count': int,
|
||||||
|
'comment_count': int,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://banbye.com/watch/v_2JjQtqjKUE_F?playlistId=p_Ld82N6gBw_OJ',
|
||||||
|
'info_dict': {
|
||||||
|
'title': 'Krzysztof Karoń',
|
||||||
|
'id': 'p_Ld82N6gBw_OJ',
|
||||||
|
},
|
||||||
|
'playlist_count': 9,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
playlist_id = self._extract_playlist_id(url, 'playlistId')
|
||||||
|
|
||||||
|
if self._yes_playlist(playlist_id, video_id):
|
||||||
|
return self._extract_playlist(playlist_id)
|
||||||
|
|
||||||
|
data = self._download_json(f'{self._API_BASE}/videos/{video_id}', video_id)
|
||||||
|
thumbnails = [{
|
||||||
|
'id': f'{quality}p',
|
||||||
|
'url': f'{self._CDN_BASE}/video/{video_id}/{quality}.webp',
|
||||||
|
} for quality in [48, 96, 144, 240, 512, 1080]]
|
||||||
|
formats = [{
|
||||||
|
'format_id': f'http-{quality}p',
|
||||||
|
'quality': quality,
|
||||||
|
'url': f'{self._CDN_BASE}/video/{video_id}/{quality}.mp4',
|
||||||
|
} for quality in data['quality']]
|
||||||
|
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': data.get('title'),
|
||||||
|
'description': data.get('desc'),
|
||||||
|
'uploader': traverse_obj(data, ('channel', 'name')),
|
||||||
|
'channel_id': data.get('channelId'),
|
||||||
|
'channel_url': format_field(data, 'channelId', 'https://banbye.com/channel/%s'),
|
||||||
|
'timestamp': unified_timestamp(data.get('publishedAt')),
|
||||||
|
'duration': data.get('duration'),
|
||||||
|
'tags': data.get('tags'),
|
||||||
|
'formats': formats,
|
||||||
|
'thumbnails': thumbnails,
|
||||||
|
'like_count': data.get('likes'),
|
||||||
|
'dislike_count': data.get('dislikes'),
|
||||||
|
'view_count': data.get('views'),
|
||||||
|
'comment_count': data.get('commentCount'),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class BanByeChannelIE(BanByeBaseIE):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?banbye.com/(?:en/)?channel/(?P<id>\w+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://banbye.com/channel/ch_wrealu24',
|
||||||
|
'info_dict': {
|
||||||
|
'title': 'wRealu24',
|
||||||
|
'id': 'ch_wrealu24',
|
||||||
|
'description': 'md5:da54e48416b74dfdde20a04867c0c2f6',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 791,
|
||||||
|
}, {
|
||||||
|
'url': 'https://banbye.com/channel/ch_wrealu24?playlist=p_Ld82N6gBw_OJ',
|
||||||
|
'info_dict': {
|
||||||
|
'title': 'Krzysztof Karoń',
|
||||||
|
'id': 'p_Ld82N6gBw_OJ',
|
||||||
|
},
|
||||||
|
'playlist_count': 9,
|
||||||
|
}]
|
||||||
|
_PAGE_SIZE = 100
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
channel_id = self._match_id(url)
|
||||||
|
playlist_id = self._extract_playlist_id(url)
|
||||||
|
|
||||||
|
if playlist_id:
|
||||||
|
return self._extract_playlist(playlist_id)
|
||||||
|
|
||||||
|
def page_func(page_num):
|
||||||
|
data = self._download_json(f'{self._API_BASE}/videos', channel_id, query={
|
||||||
|
'channelId': channel_id,
|
||||||
|
'sort': 'new',
|
||||||
|
'limit': self._PAGE_SIZE,
|
||||||
|
'offset': page_num * self._PAGE_SIZE,
|
||||||
|
}, note=f'Downloading page {page_num+1}')
|
||||||
|
return [
|
||||||
|
self.url_result(f"{self._VIDEO_BASE}/{video['_id']}", BanByeIE)
|
||||||
|
for video in data['items']
|
||||||
|
]
|
||||||
|
|
||||||
|
channel_data = self._download_json(f'{self._API_BASE}/channels/{channel_id}', channel_id)
|
||||||
|
entries = InAdvancePagedList(
|
||||||
|
page_func,
|
||||||
|
math.ceil(channel_data['videoCount'] / self._PAGE_SIZE),
|
||||||
|
self._PAGE_SIZE)
|
||||||
|
|
||||||
|
return self.playlist_result(
|
||||||
|
entries, channel_id, channel_data.get('name'), channel_data.get('description'))
|
||||||
@@ -183,6 +183,7 @@ class BandcampIE(InfoExtractor):
|
|||||||
'format_note': f.get('description'),
|
'format_note': f.get('description'),
|
||||||
'filesize': parse_filesize(f.get('size_mb')),
|
'filesize': parse_filesize(f.get('size_mb')),
|
||||||
'vcodec': 'none',
|
'vcodec': 'none',
|
||||||
|
'acodec': format_id.split('-')[0],
|
||||||
})
|
})
|
||||||
|
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
@@ -212,7 +213,7 @@ class BandcampIE(InfoExtractor):
|
|||||||
|
|
||||||
class BandcampAlbumIE(BandcampIE):
|
class BandcampAlbumIE(BandcampIE):
|
||||||
IE_NAME = 'Bandcamp:album'
|
IE_NAME = 'Bandcamp:album'
|
||||||
_VALID_URL = r'https?://(?:(?P<subdomain>[^.]+)\.)?bandcamp\.com(?!/music)(?:/album/(?P<id>[^/?#&]+))?'
|
_VALID_URL = r'https?://(?:(?P<subdomain>[^.]+)\.)?bandcamp\.com/album/(?P<id>[^/?#&]+)'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://blazo.bandcamp.com/album/jazz-format-mixtape-vol-1',
|
'url': 'http://blazo.bandcamp.com/album/jazz-format-mixtape-vol-1',
|
||||||
@@ -257,14 +258,6 @@ class BandcampAlbumIE(BandcampIE):
|
|||||||
'id': 'hierophany-of-the-open-grave',
|
'id': 'hierophany-of-the-open-grave',
|
||||||
},
|
},
|
||||||
'playlist_mincount': 9,
|
'playlist_mincount': 9,
|
||||||
}, {
|
|
||||||
'url': 'http://dotscale.bandcamp.com',
|
|
||||||
'info_dict': {
|
|
||||||
'title': 'Loom',
|
|
||||||
'id': 'dotscale',
|
|
||||||
'uploader_id': 'dotscale',
|
|
||||||
},
|
|
||||||
'playlist_mincount': 7,
|
|
||||||
}, {
|
}, {
|
||||||
# with escaped quote in title
|
# with escaped quote in title
|
||||||
'url': 'https://jstrecords.bandcamp.com/album/entropy-ep',
|
'url': 'https://jstrecords.bandcamp.com/album/entropy-ep',
|
||||||
@@ -391,41 +384,63 @@ class BandcampWeeklyIE(BandcampIE):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class BandcampMusicIE(InfoExtractor):
|
class BandcampUserIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?P<id>[^/]+)\.bandcamp\.com/music'
|
IE_NAME = 'Bandcamp:user'
|
||||||
|
_VALID_URL = r'https?://(?!www\.)(?P<id>[^.]+)\.bandcamp\.com(?:/music)?/?(?:[#?]|$)'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
|
# Type 1 Bandcamp user page.
|
||||||
|
'url': 'https://adrianvonziegler.bandcamp.com',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'adrianvonziegler',
|
||||||
|
'title': 'Discography of adrianvonziegler',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 23,
|
||||||
|
}, {
|
||||||
|
# Bandcamp user page with only one album
|
||||||
|
'url': 'http://dotscale.bandcamp.com',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'dotscale',
|
||||||
|
'title': 'Discography of dotscale'
|
||||||
|
},
|
||||||
|
'playlist_count': 1,
|
||||||
|
}, {
|
||||||
|
# Type 2 Bandcamp user page.
|
||||||
|
'url': 'https://nightcallofficial.bandcamp.com',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'nightcallofficial',
|
||||||
|
'title': 'Discography of nightcallofficial',
|
||||||
|
},
|
||||||
|
'playlist_count': 4,
|
||||||
|
}, {
|
||||||
'url': 'https://steviasphere.bandcamp.com/music',
|
'url': 'https://steviasphere.bandcamp.com/music',
|
||||||
'playlist_mincount': 47,
|
'playlist_mincount': 47,
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'steviasphere',
|
'id': 'steviasphere',
|
||||||
|
'title': 'Discography of steviasphere',
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://coldworldofficial.bandcamp.com/music',
|
'url': 'https://coldworldofficial.bandcamp.com/music',
|
||||||
'playlist_mincount': 10,
|
'playlist_mincount': 10,
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'coldworldofficial',
|
'id': 'coldworldofficial',
|
||||||
|
'title': 'Discography of coldworldofficial',
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://nuclearwarnowproductions.bandcamp.com/music',
|
'url': 'https://nuclearwarnowproductions.bandcamp.com/music',
|
||||||
'playlist_mincount': 399,
|
'playlist_mincount': 399,
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'nuclearwarnowproductions',
|
'id': 'nuclearwarnowproductions',
|
||||||
|
'title': 'Discography of nuclearwarnowproductions',
|
||||||
},
|
},
|
||||||
}
|
}]
|
||||||
]
|
|
||||||
|
|
||||||
_TYPE_IE_DICT = {
|
|
||||||
'album': BandcampAlbumIE.ie_key(),
|
|
||||||
'track': BandcampIE.ie_key()
|
|
||||||
}
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
id = self._match_id(url)
|
uploader = self._match_id(url)
|
||||||
webpage = self._download_webpage(url, id)
|
webpage = self._download_webpage(url, uploader)
|
||||||
items = re.findall(r'href\=\"\/(?P<path>(?P<type>album|track)+/[^\"]+)', webpage)
|
|
||||||
entries = [
|
discography_data = (re.findall(r'<li data-item-id=["\'][^>]+>\s*<a href=["\']([^"\']+)', webpage)
|
||||||
self.url_result(
|
or re.findall(r'<div[^>]+trackTitle["\'][^"\']+["\']([^"\']+)', webpage))
|
||||||
f'https://{id}.bandcamp.com/{item[0]}',
|
|
||||||
ie=self._TYPE_IE_DICT[item[1]])
|
return self.playlist_from_matches(
|
||||||
for item in items]
|
discography_data, uploader, f'Discography of {uploader}', getter=lambda x: urljoin(url, x))
|
||||||
return self.playlist_result(entries, id)
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from ..compat import (
|
|||||||
compat_etree_Element,
|
compat_etree_Element,
|
||||||
compat_HTTPError,
|
compat_HTTPError,
|
||||||
compat_str,
|
compat_str,
|
||||||
|
compat_urllib_error,
|
||||||
compat_urlparse,
|
compat_urlparse,
|
||||||
)
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
@@ -38,7 +39,7 @@ from ..utils import (
|
|||||||
class BBCCoUkIE(InfoExtractor):
|
class BBCCoUkIE(InfoExtractor):
|
||||||
IE_NAME = 'bbc.co.uk'
|
IE_NAME = 'bbc.co.uk'
|
||||||
IE_DESC = 'BBC iPlayer'
|
IE_DESC = 'BBC iPlayer'
|
||||||
_ID_REGEX = r'(?:[pbm][\da-z]{7}|w[\da-z]{7,14})'
|
_ID_REGEX = r'(?:[pbml][\da-z]{7}|w[\da-z]{7,14})'
|
||||||
_VALID_URL = r'''(?x)
|
_VALID_URL = r'''(?x)
|
||||||
https?://
|
https?://
|
||||||
(?:www\.)?bbc\.co\.uk/
|
(?:www\.)?bbc\.co\.uk/
|
||||||
@@ -263,11 +264,7 @@ class BBCCoUkIE(InfoExtractor):
|
|||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _login(self):
|
def _perform_login(self, username, password):
|
||||||
username, password = self._get_login_info()
|
|
||||||
if username is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
login_page = self._download_webpage(
|
login_page = self._download_webpage(
|
||||||
self._LOGIN_URL, None, 'Downloading signin page')
|
self._LOGIN_URL, None, 'Downloading signin page')
|
||||||
|
|
||||||
@@ -293,9 +290,6 @@ class BBCCoUkIE(InfoExtractor):
|
|||||||
'Unable to login: %s' % error, expected=True)
|
'Unable to login: %s' % error, expected=True)
|
||||||
raise ExtractorError('Unable to log in')
|
raise ExtractorError('Unable to log in')
|
||||||
|
|
||||||
def _real_initialize(self):
|
|
||||||
self._login()
|
|
||||||
|
|
||||||
class MediaSelectionError(Exception):
|
class MediaSelectionError(Exception):
|
||||||
def __init__(self, id):
|
def __init__(self, id):
|
||||||
self.id = id
|
self.id = id
|
||||||
@@ -394,9 +388,17 @@ class BBCCoUkIE(InfoExtractor):
|
|||||||
formats.extend(self._extract_mpd_formats(
|
formats.extend(self._extract_mpd_formats(
|
||||||
href, programme_id, mpd_id=format_id, fatal=False))
|
href, programme_id, mpd_id=format_id, fatal=False))
|
||||||
elif transfer_format == 'hls':
|
elif transfer_format == 'hls':
|
||||||
formats.extend(self._extract_m3u8_formats(
|
# TODO: let expected_status be passed into _extract_xxx_formats() instead
|
||||||
href, programme_id, ext='mp4', entry_protocol='m3u8_native',
|
try:
|
||||||
m3u8_id=format_id, fatal=False))
|
fmts = self._extract_m3u8_formats(
|
||||||
|
href, programme_id, ext='mp4', entry_protocol='m3u8_native',
|
||||||
|
m3u8_id=format_id, fatal=False)
|
||||||
|
except ExtractorError as e:
|
||||||
|
if not (isinstance(e.exc_info[1], compat_urllib_error.HTTPError)
|
||||||
|
and e.exc_info[1].code in (403, 404)):
|
||||||
|
raise
|
||||||
|
fmts = []
|
||||||
|
formats.extend(fmts)
|
||||||
elif transfer_format == 'hds':
|
elif transfer_format == 'hds':
|
||||||
formats.extend(self._extract_f4m_formats(
|
formats.extend(self._extract_f4m_formats(
|
||||||
href, programme_id, f4m_id=format_id, fatal=False))
|
href, programme_id, f4m_id=format_id, fatal=False))
|
||||||
@@ -784,21 +786,33 @@ class BBCIE(BBCCoUkIE):
|
|||||||
'timestamp': 1437785037,
|
'timestamp': 1437785037,
|
||||||
'upload_date': '20150725',
|
'upload_date': '20150725',
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
# video with window.__INITIAL_DATA__ and value as JSON string
|
||||||
|
'url': 'https://www.bbc.com/news/av/world-europe-59468682',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'p0b71qth',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Why France is making this woman a national hero',
|
||||||
|
'description': 'md5:7affdfab80e9c3a1f976230a1ff4d5e4',
|
||||||
|
'thumbnail': r're:https?://.+/.+\.jpg',
|
||||||
|
'timestamp': 1638230731,
|
||||||
|
'upload_date': '20211130',
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
# single video article embedded with data-media-vpid
|
# single video article embedded with data-media-vpid
|
||||||
'url': 'http://www.bbc.co.uk/sport/rowing/35908187',
|
'url': 'http://www.bbc.co.uk/sport/rowing/35908187',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}, {
|
}, {
|
||||||
|
# bbcthreeConfig
|
||||||
'url': 'https://www.bbc.co.uk/bbcthree/clip/73d0bbd0-abc3-4cea-b3c0-cdae21905eb1',
|
'url': 'https://www.bbc.co.uk/bbcthree/clip/73d0bbd0-abc3-4cea-b3c0-cdae21905eb1',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'p06556y7',
|
'id': 'p06556y7',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Transfers: Cristiano Ronaldo to Man Utd, Arsenal to spend?',
|
'title': 'Things Not To Say to people that live on council estates',
|
||||||
'description': 'md5:4b7dfd063d5a789a1512e99662be3ddd',
|
'description': "From being labelled a 'chav', to the presumption that they're 'scroungers', people who live on council estates encounter all kinds of prejudices and false assumptions about themselves, their families, and their lifestyles. Here, eight people discuss the common statements, misconceptions, and clichés that they're tired of hearing.",
|
||||||
|
'duration': 360,
|
||||||
|
'thumbnail': r're:https?://.+/.+\.jpg',
|
||||||
},
|
},
|
||||||
'params': {
|
|
||||||
'skip_download': True,
|
|
||||||
}
|
|
||||||
}, {
|
}, {
|
||||||
# window.__PRELOADED_STATE__
|
# window.__PRELOADED_STATE__
|
||||||
'url': 'https://www.bbc.co.uk/radio/play/b0b9z4yl',
|
'url': 'https://www.bbc.co.uk/radio/play/b0b9z4yl',
|
||||||
@@ -892,9 +906,8 @@ class BBCIE(BBCCoUkIE):
|
|||||||
|
|
||||||
playlist_title = json_ld_info.get('title')
|
playlist_title = json_ld_info.get('title')
|
||||||
if not playlist_title:
|
if not playlist_title:
|
||||||
playlist_title = self._og_search_title(
|
playlist_title = (self._og_search_title(webpage, default=None)
|
||||||
webpage, default=None) or self._html_search_regex(
|
or self._html_extract_title(webpage, 'playlist title', default=None))
|
||||||
r'<title>(.+?)</title>', webpage, 'playlist title', default=None)
|
|
||||||
if playlist_title:
|
if playlist_title:
|
||||||
playlist_title = re.sub(r'(.+)\s*-\s*BBC.*?$', r'\1', playlist_title).strip()
|
playlist_title = re.sub(r'(.+)\s*-\s*BBC.*?$', r'\1', playlist_title).strip()
|
||||||
|
|
||||||
@@ -1171,9 +1184,16 @@ class BBCIE(BBCCoUkIE):
|
|||||||
return self.playlist_result(
|
return self.playlist_result(
|
||||||
entries, playlist_id, playlist_title, playlist_description)
|
entries, playlist_id, playlist_title, playlist_description)
|
||||||
|
|
||||||
initial_data = self._parse_json(self._search_regex(
|
initial_data = self._search_regex(
|
||||||
r'window\.__INITIAL_DATA__\s*=\s*({.+?});', webpage,
|
r'window\.__INITIAL_DATA__\s*=\s*("{.+?}")\s*;', webpage,
|
||||||
'preload state', default='{}'), playlist_id, fatal=False)
|
'quoted preload state', default=None)
|
||||||
|
if initial_data is None:
|
||||||
|
initial_data = self._search_regex(
|
||||||
|
r'window\.__INITIAL_DATA__\s*=\s*({.+?})\s*;', webpage,
|
||||||
|
'preload state', default={})
|
||||||
|
else:
|
||||||
|
initial_data = self._parse_json(initial_data or '"{}"', playlist_id, fatal=False)
|
||||||
|
initial_data = self._parse_json(initial_data, playlist_id, fatal=False)
|
||||||
if initial_data:
|
if initial_data:
|
||||||
def parse_media(media):
|
def parse_media(media):
|
||||||
if not media:
|
if not media:
|
||||||
@@ -1214,7 +1234,10 @@ class BBCIE(BBCCoUkIE):
|
|||||||
if name == 'media-experience':
|
if name == 'media-experience':
|
||||||
parse_media(try_get(resp, lambda x: x['data']['initialItem']['mediaItem'], dict))
|
parse_media(try_get(resp, lambda x: x['data']['initialItem']['mediaItem'], dict))
|
||||||
elif name == 'article':
|
elif name == 'article':
|
||||||
for block in (try_get(resp, lambda x: x['data']['blocks'], list) or []):
|
for block in (try_get(resp,
|
||||||
|
(lambda x: x['data']['blocks'],
|
||||||
|
lambda x: x['data']['content']['model']['blocks'],),
|
||||||
|
list) or []):
|
||||||
if block.get('type') != 'media':
|
if block.get('type') != 'media':
|
||||||
continue
|
continue
|
||||||
parse_media(block.get('model'))
|
parse_media(block.get('model'))
|
||||||
|
|||||||
@@ -1,32 +1,45 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..compat import (
|
|
||||||
compat_str,
|
|
||||||
)
|
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
int_or_none,
|
int_or_none,
|
||||||
parse_qs,
|
traverse_obj,
|
||||||
|
try_get,
|
||||||
unified_timestamp,
|
unified_timestamp,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class BeegIE(InfoExtractor):
|
class BeegIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?beeg\.(?:com|porn(?:/video)?)/(?P<id>\d+)'
|
_VALID_URL = r'https?://(?:www\.)?beeg\.(?:com(?:/video)?)/-?(?P<id>\d+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
# api/v6 v1
|
'url': 'https://beeg.com/-0983946056129650',
|
||||||
'url': 'http://beeg.com/5416503',
|
'md5': '51d235147c4627cfce884f844293ff88',
|
||||||
'md5': 'a1a1b1a8bc70a89e49ccfd113aed0820',
|
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '5416503',
|
'id': '0983946056129650',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Sultry Striptease',
|
'title': 'sucked cock and fucked in a private plane',
|
||||||
'description': 'md5:d22219c09da287c14bed3d6c37ce4bc2',
|
'duration': 927,
|
||||||
'timestamp': 1391813355,
|
|
||||||
'upload_date': '20140207',
|
|
||||||
'duration': 383,
|
|
||||||
'tags': list,
|
'tags': list,
|
||||||
'age_limit': 18,
|
'age_limit': 18,
|
||||||
|
'upload_date': '20220131',
|
||||||
|
'timestamp': 1643656455,
|
||||||
|
'display_id': 2540839,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
'url': 'https://beeg.com/-0599050563103750?t=4-861',
|
||||||
|
'md5': 'bd8b5ea75134f7f07fad63008db2060e',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '0599050563103750',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Bad Relatives',
|
||||||
|
'duration': 2060,
|
||||||
|
'tags': list,
|
||||||
|
'age_limit': 18,
|
||||||
|
'description': 'md5:b4fc879a58ae6c604f8f259155b7e3b9',
|
||||||
|
'timestamp': 1643623200,
|
||||||
|
'display_id': 2569965,
|
||||||
|
'upload_date': '20220131',
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
# api/v6 v2
|
# api/v6 v2
|
||||||
@@ -36,12 +49,6 @@ class BeegIE(InfoExtractor):
|
|||||||
# api/v6 v2 w/o t
|
# api/v6 v2 w/o t
|
||||||
'url': 'https://beeg.com/1277207756',
|
'url': 'https://beeg.com/1277207756',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}, {
|
|
||||||
'url': 'https://beeg.porn/video/5416503',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'https://beeg.porn/5416503',
|
|
||||||
'only_matching': True,
|
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
@@ -49,68 +56,38 @@ class BeegIE(InfoExtractor):
|
|||||||
|
|
||||||
webpage = self._download_webpage(url, video_id)
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
beeg_version = self._search_regex(
|
video = self._download_json(
|
||||||
r'beeg_version\s*=\s*([\da-zA-Z_-]+)', webpage, 'beeg version',
|
'https://store.externulls.com/facts/file/%s' % video_id,
|
||||||
default='1546225636701')
|
video_id, 'Downloading JSON for %s' % video_id)
|
||||||
|
|
||||||
if len(video_id) >= 10:
|
fc_facts = video.get('fc_facts')
|
||||||
query = {
|
first_fact = {}
|
||||||
'v': 2,
|
for fact in fc_facts:
|
||||||
}
|
if not first_fact or try_get(fact, lambda x: x['id'] < first_fact['id']):
|
||||||
qs = parse_qs(url)
|
first_fact = fact
|
||||||
t = qs.get('t', [''])[0].split('-')
|
|
||||||
if len(t) > 1:
|
|
||||||
query.update({
|
|
||||||
's': t[0],
|
|
||||||
'e': t[1],
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
query = {'v': 1}
|
|
||||||
|
|
||||||
for api_path in ('', 'api.'):
|
resources = traverse_obj(video, ('file', 'hls_resources')) or first_fact.get('hls_resources')
|
||||||
video = self._download_json(
|
|
||||||
'https://%sbeeg.com/api/v6/%s/video/%s'
|
|
||||||
% (api_path, beeg_version, video_id), video_id,
|
|
||||||
fatal=api_path == 'api.', query=query)
|
|
||||||
if video:
|
|
||||||
break
|
|
||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
for format_id, video_url in video.items():
|
for format_id, video_uri in resources.items():
|
||||||
if not video_url:
|
if not video_uri:
|
||||||
continue
|
continue
|
||||||
height = self._search_regex(
|
height = int_or_none(self._search_regex(r'fl_cdn_(\d+)', format_id, 'height', default=None))
|
||||||
r'^(\d+)[pP]$', format_id, 'height', default=None)
|
current_formats = self._extract_m3u8_formats(f'https://video.beeg.com/{video_uri}', video_id, ext='mp4', m3u8_id=str(height))
|
||||||
if not height:
|
for f in current_formats:
|
||||||
continue
|
f['height'] = height
|
||||||
formats.append({
|
formats.extend(current_formats)
|
||||||
'url': self._proto_relative_url(
|
|
||||||
video_url.replace('{DATA_MARKERS}', 'data=pc_XX__%s_0' % beeg_version), 'https:'),
|
|
||||||
'format_id': format_id,
|
|
||||||
'height': int(height),
|
|
||||||
})
|
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
title = video['title']
|
|
||||||
video_id = compat_str(video.get('id') or video_id)
|
|
||||||
display_id = video.get('code')
|
|
||||||
description = video.get('desc')
|
|
||||||
series = video.get('ps_name')
|
|
||||||
|
|
||||||
timestamp = unified_timestamp(video.get('date'))
|
|
||||||
duration = int_or_none(video.get('duration'))
|
|
||||||
|
|
||||||
tags = [tag.strip() for tag in video['tags'].split(',')] if video.get('tags') else None
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'display_id': display_id,
|
'display_id': first_fact.get('id'),
|
||||||
'title': title,
|
'title': traverse_obj(video, ('file', 'stuff', 'sf_name')),
|
||||||
'description': description,
|
'description': traverse_obj(video, ('file', 'stuff', 'sf_story')),
|
||||||
'series': series,
|
'timestamp': unified_timestamp(first_fact.get('fc_created')),
|
||||||
'timestamp': timestamp,
|
'duration': int_or_none(traverse_obj(video, ('file', 'fl_duration'))),
|
||||||
'duration': duration,
|
'tags': traverse_obj(video, ('tags', ..., 'tg_name')),
|
||||||
'tags': tags,
|
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
'age_limit': self._rta_search(webpage),
|
'age_limit': self._rta_search(webpage),
|
||||||
}
|
}
|
||||||
|
|||||||
59
yt_dlp/extractor/bigo.py
Normal file
59
yt_dlp/extractor/bigo.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import ExtractorError, urlencode_postdata
|
||||||
|
|
||||||
|
|
||||||
|
class BigoIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?bigo\.tv/(?:[a-z]{2,}/)?(?P<id>[^/]+)'
|
||||||
|
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.bigo.tv/ja/221338632',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '6576287577575737440',
|
||||||
|
'title': '土よ〜💁♂️ 休憩室/REST room',
|
||||||
|
'thumbnail': r're:https?://.+',
|
||||||
|
'uploader': '✨Shin💫',
|
||||||
|
'uploader_id': '221338632',
|
||||||
|
'is_live': True,
|
||||||
|
},
|
||||||
|
'skip': 'livestream',
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.bigo.tv/th/Tarlerm1304',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://bigo.tv/115976881',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
user_id = self._match_id(url)
|
||||||
|
|
||||||
|
info_raw = self._download_json(
|
||||||
|
'https://bigo.tv/studio/getInternalStudioInfo',
|
||||||
|
user_id, data=urlencode_postdata({'siteId': user_id}))
|
||||||
|
|
||||||
|
if not isinstance(info_raw, dict):
|
||||||
|
raise ExtractorError('Received invalid JSON data')
|
||||||
|
if info_raw.get('code'):
|
||||||
|
raise ExtractorError(
|
||||||
|
'Bigo says: %s (code %s)' % (info_raw.get('msg'), info_raw.get('code')), expected=True)
|
||||||
|
info = info_raw.get('data') or {}
|
||||||
|
|
||||||
|
if not info.get('alive'):
|
||||||
|
raise ExtractorError('This user is offline.', expected=True)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': info.get('roomId') or user_id,
|
||||||
|
'title': info.get('roomTopic') or info.get('nick_name') or user_id,
|
||||||
|
'formats': [{
|
||||||
|
'url': info.get('hls_src'),
|
||||||
|
'ext': 'mp4',
|
||||||
|
'protocol': 'm3u8',
|
||||||
|
}],
|
||||||
|
'thumbnail': info.get('snapshot'),
|
||||||
|
'uploader': info.get('nick_name'),
|
||||||
|
'uploader_id': user_id,
|
||||||
|
'is_live': True,
|
||||||
|
}
|
||||||
@@ -15,11 +15,12 @@ from ..compat import (
|
|||||||
)
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
|
filter_dict,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
float_or_none,
|
float_or_none,
|
||||||
|
mimetype2ext,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
traverse_obj,
|
traverse_obj,
|
||||||
try_get,
|
|
||||||
parse_count,
|
parse_count,
|
||||||
smuggle_url,
|
smuggle_url,
|
||||||
srt_subtitles_timecode,
|
srt_subtitles_timecode,
|
||||||
@@ -50,18 +51,21 @@ class BiliBiliIE(InfoExtractor):
|
|||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.bilibili.com/video/av1074402/',
|
'url': 'http://www.bilibili.com/video/av1074402/',
|
||||||
'md5': '5f7d29e1a2872f3df0cf76b1f87d3788',
|
'md5': '7ac275ec84a99a6552c5d229659a0fe1',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '1074402',
|
'id': '1074402_part1',
|
||||||
'ext': 'flv',
|
'ext': 'mp4',
|
||||||
'title': '【金坷垃】金泡沫',
|
'title': '【金坷垃】金泡沫',
|
||||||
'description': 'md5:ce18c2a2d2193f0df2917d270f2e5923',
|
|
||||||
'duration': 308.067,
|
|
||||||
'timestamp': 1398012678,
|
|
||||||
'upload_date': '20140420',
|
|
||||||
'thumbnail': r're:^https?://.+\.jpg',
|
|
||||||
'uploader': '菊子桑',
|
|
||||||
'uploader_id': '156160',
|
'uploader_id': '156160',
|
||||||
|
'uploader': '菊子桑',
|
||||||
|
'upload_date': '20140420',
|
||||||
|
'description': 'md5:ce18c2a2d2193f0df2917d270f2e5923',
|
||||||
|
'timestamp': 1398012678,
|
||||||
|
'tags': ['顶上去报复社会', '该来的总会来的', '金克拉是检验歌曲的唯一标准', '坷垃教主', '金坷垃', '邓紫棋', '治愈系坷垃'],
|
||||||
|
'bv_id': 'BV11x411K7CN',
|
||||||
|
'cid': '1554319',
|
||||||
|
'thumbnail': 'http://i2.hdslb.com/bfs/archive/c79a8cf0347cd7a897c53a2f756e96aead128e8c.jpg',
|
||||||
|
'duration': 308.36,
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
# Tested in BiliBiliBangumiIE
|
# Tested in BiliBiliBangumiIE
|
||||||
@@ -75,49 +79,32 @@ class BiliBiliIE(InfoExtractor):
|
|||||||
'url': 'http://bangumi.bilibili.com/anime/5802/play#100643',
|
'url': 'http://bangumi.bilibili.com/anime/5802/play#100643',
|
||||||
'md5': '3f721ad1e75030cc06faf73587cfec57',
|
'md5': '3f721ad1e75030cc06faf73587cfec57',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '100643',
|
'id': '100643_part1',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'CHAOS;CHILD',
|
'title': 'CHAOS;CHILD',
|
||||||
'description': '如果你是神明,并且能够让妄想成为现实。那你会进行怎么样的妄想?是淫靡的世界?独裁社会?毁灭性的制裁?还是……2015年,涩谷。从6年前发生的大灾害“涩谷地震”之后复兴了的这个街区里新设立的私立高中...',
|
'description': '如果你是神明,并且能够让妄想成为现实。那你会进行怎么样的妄想?是淫靡的世界?独裁社会?毁灭性的制裁?还是……2015年,涩谷。从6年前发生的大灾害“涩谷地震”之后复兴了的这个街区里新设立的私立高中...',
|
||||||
},
|
},
|
||||||
'skip': 'Geo-restricted to China',
|
'skip': 'Geo-restricted to China',
|
||||||
}, {
|
}, {
|
||||||
# Title with double quotes
|
|
||||||
'url': 'http://www.bilibili.com/video/av8903802/',
|
'url': 'http://www.bilibili.com/video/av8903802/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '8903802',
|
'id': '8903802_part1',
|
||||||
|
'ext': 'mp4',
|
||||||
'title': '阿滴英文|英文歌分享#6 "Closer',
|
'title': '阿滴英文|英文歌分享#6 "Closer',
|
||||||
|
'upload_date': '20170301',
|
||||||
'description': '滴妹今天唱Closer給你聽! 有史以来,被推最多次也是最久的歌曲,其实歌词跟我原本想像差蛮多的,不过还是好听! 微博@阿滴英文',
|
'description': '滴妹今天唱Closer給你聽! 有史以来,被推最多次也是最久的歌曲,其实歌词跟我原本想像差蛮多的,不过还是好听! 微博@阿滴英文',
|
||||||
|
'timestamp': 1488382634,
|
||||||
|
'uploader_id': '65880958',
|
||||||
|
'uploader': '阿滴英文',
|
||||||
|
'thumbnail': 'http://i2.hdslb.com/bfs/archive/49267ce20bc246be6304bf369a3ded0256854c23.jpg',
|
||||||
|
'cid': '14694589',
|
||||||
|
'duration': 554.117,
|
||||||
|
'bv_id': 'BV13x41117TL',
|
||||||
|
'tags': ['人文', '英语', '文化', '公开课', '阿滴英文'],
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
'playlist': [{
|
|
||||||
'info_dict': {
|
|
||||||
'id': '8903802_part1',
|
|
||||||
'ext': 'flv',
|
|
||||||
'title': '阿滴英文|英文歌分享#6 "Closer',
|
|
||||||
'description': 'md5:3b1b9e25b78da4ef87e9b548b88ee76a',
|
|
||||||
'uploader': '阿滴英文',
|
|
||||||
'uploader_id': '65880958',
|
|
||||||
'timestamp': 1488382634,
|
|
||||||
'upload_date': '20170301',
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
'skip_download': True,
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'info_dict': {
|
|
||||||
'id': '8903802_part2',
|
|
||||||
'ext': 'flv',
|
|
||||||
'title': '阿滴英文|英文歌分享#6 "Closer',
|
|
||||||
'description': 'md5:3b1b9e25b78da4ef87e9b548b88ee76a',
|
|
||||||
'uploader': '阿滴英文',
|
|
||||||
'uploader_id': '65880958',
|
|
||||||
'timestamp': 1488382634,
|
|
||||||
'upload_date': '20170301',
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
'skip_download': True,
|
|
||||||
},
|
|
||||||
}]
|
|
||||||
}, {
|
}, {
|
||||||
# new BV video id format
|
# new BV video id format
|
||||||
'url': 'https://www.bilibili.com/video/BV1JE411F741',
|
'url': 'https://www.bilibili.com/video/BV1JE411F741',
|
||||||
@@ -130,6 +117,27 @@ class BiliBiliIE(InfoExtractor):
|
|||||||
'title': '物语中的人物是如何吐槽自己的OP的'
|
'title': '物语中的人物是如何吐槽自己的OP的'
|
||||||
},
|
},
|
||||||
'playlist_count': 17,
|
'playlist_count': 17,
|
||||||
|
}, {
|
||||||
|
# Correct matching of single and double quotes in title
|
||||||
|
'url': 'https://www.bilibili.com/video/BV1NY411E7Rx/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '255513412_part1',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Vid"eo" Te\'st',
|
||||||
|
'cid': '570602418',
|
||||||
|
'thumbnail': 'http://i2.hdslb.com/bfs/archive/0c0de5a90b6d5b991b8dcc6cde0afbf71d564791.jpg',
|
||||||
|
'upload_date': '20220408',
|
||||||
|
'timestamp': 1649436552,
|
||||||
|
'description': 'Vid"eo" Te\'st',
|
||||||
|
'uploader_id': '1630758804',
|
||||||
|
'bv_id': 'BV1NY411E7Rx',
|
||||||
|
'duration': 60.394,
|
||||||
|
'uploader': 'bili_31244483705',
|
||||||
|
'tags': ['VLOG'],
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
}]
|
}]
|
||||||
|
|
||||||
_APP_KEY = 'iVGUTjsxvpLeuDCf'
|
_APP_KEY = 'iVGUTjsxvpLeuDCf'
|
||||||
@@ -152,6 +160,7 @@ class BiliBiliIE(InfoExtractor):
|
|||||||
av_id, bv_id = self._get_video_id_set(video_id, mobj.group('id_bv') is not None)
|
av_id, bv_id = self._get_video_id_set(video_id, mobj.group('id_bv') is not None)
|
||||||
video_id = av_id
|
video_id = av_id
|
||||||
|
|
||||||
|
info = {}
|
||||||
anime_id = mobj.group('anime_id')
|
anime_id = mobj.group('anime_id')
|
||||||
page_id = mobj.group('page')
|
page_id = mobj.group('page')
|
||||||
webpage = self._download_webpage(url, video_id)
|
webpage = self._download_webpage(url, video_id)
|
||||||
@@ -203,66 +212,96 @@ class BiliBiliIE(InfoExtractor):
|
|||||||
}
|
}
|
||||||
headers.update(self.geo_verification_headers())
|
headers.update(self.geo_verification_headers())
|
||||||
|
|
||||||
|
video_info = self._parse_json(
|
||||||
|
self._search_regex(r'window.__playinfo__\s*=\s*({.+?})</script>', webpage, 'video info', default=None) or '{}',
|
||||||
|
video_id, fatal=False)
|
||||||
|
video_info = video_info.get('data') or {}
|
||||||
|
|
||||||
|
durl = traverse_obj(video_info, ('dash', 'video'))
|
||||||
|
audios = traverse_obj(video_info, ('dash', 'audio')) or []
|
||||||
entries = []
|
entries = []
|
||||||
|
|
||||||
RENDITIONS = ('qn=80&quality=80&type=', 'quality=2&type=mp4')
|
RENDITIONS = ('qn=80&quality=80&type=', 'quality=2&type=mp4')
|
||||||
for num, rendition in enumerate(RENDITIONS, start=1):
|
for num, rendition in enumerate(RENDITIONS, start=1):
|
||||||
payload = 'appkey=%s&cid=%s&otype=json&%s' % (self._APP_KEY, cid, rendition)
|
payload = 'appkey=%s&cid=%s&otype=json&%s' % (self._APP_KEY, cid, rendition)
|
||||||
sign = hashlib.md5((payload + self._BILIBILI_KEY).encode('utf-8')).hexdigest()
|
sign = hashlib.md5((payload + self._BILIBILI_KEY).encode('utf-8')).hexdigest()
|
||||||
|
|
||||||
video_info = self._download_json(
|
|
||||||
'http://interface.bilibili.com/v2/playurl?%s&sign=%s' % (payload, sign),
|
|
||||||
video_id, note='Downloading video info page',
|
|
||||||
headers=headers, fatal=num == len(RENDITIONS))
|
|
||||||
|
|
||||||
if not video_info:
|
if not video_info:
|
||||||
continue
|
video_info = self._download_json(
|
||||||
|
'http://interface.bilibili.com/v2/playurl?%s&sign=%s' % (payload, sign),
|
||||||
|
video_id, note='Downloading video info page',
|
||||||
|
headers=headers, fatal=num == len(RENDITIONS))
|
||||||
|
if not video_info:
|
||||||
|
continue
|
||||||
|
|
||||||
if 'durl' not in video_info:
|
if not durl and 'durl' not in video_info:
|
||||||
if num < len(RENDITIONS):
|
if num < len(RENDITIONS):
|
||||||
continue
|
continue
|
||||||
self._report_error(video_info)
|
self._report_error(video_info)
|
||||||
|
|
||||||
for idx, durl in enumerate(video_info['durl']):
|
formats = []
|
||||||
formats = [{
|
for idx, durl in enumerate(durl or video_info['durl']):
|
||||||
'url': durl['url'],
|
formats.append({
|
||||||
'filesize': int_or_none(durl['size']),
|
'url': durl.get('baseUrl') or durl.get('base_url') or durl.get('url'),
|
||||||
}]
|
'ext': mimetype2ext(durl.get('mimeType') or durl.get('mime_type')),
|
||||||
for backup_url in durl.get('backup_url', []):
|
'fps': int_or_none(durl.get('frameRate') or durl.get('frame_rate')),
|
||||||
|
'width': int_or_none(durl.get('width')),
|
||||||
|
'height': int_or_none(durl.get('height')),
|
||||||
|
'vcodec': durl.get('codecs'),
|
||||||
|
'acodec': 'none' if audios else None,
|
||||||
|
'tbr': float_or_none(durl.get('bandwidth'), scale=1000),
|
||||||
|
'filesize': int_or_none(durl.get('size')),
|
||||||
|
})
|
||||||
|
for backup_url in traverse_obj(durl, 'backup_url', expected_type=list) or []:
|
||||||
formats.append({
|
formats.append({
|
||||||
'url': backup_url,
|
'url': backup_url,
|
||||||
# backup URLs have lower priorities
|
|
||||||
'quality': -2 if 'hd.mp4' in backup_url else -3,
|
'quality': -2 if 'hd.mp4' in backup_url else -3,
|
||||||
})
|
})
|
||||||
|
|
||||||
for a_format in formats:
|
for audio in audios:
|
||||||
a_format.setdefault('http_headers', {}).update({
|
formats.append({
|
||||||
'Referer': url,
|
'url': audio.get('baseUrl') or audio.get('base_url') or audio.get('url'),
|
||||||
|
'ext': mimetype2ext(audio.get('mimeType') or audio.get('mime_type')),
|
||||||
|
'fps': int_or_none(audio.get('frameRate') or audio.get('frame_rate')),
|
||||||
|
'width': int_or_none(audio.get('width')),
|
||||||
|
'height': int_or_none(audio.get('height')),
|
||||||
|
'acodec': audio.get('codecs'),
|
||||||
|
'vcodec': 'none',
|
||||||
|
'tbr': float_or_none(audio.get('bandwidth'), scale=1000),
|
||||||
|
'filesize': int_or_none(audio.get('size'))
|
||||||
|
})
|
||||||
|
for backup_url in traverse_obj(audio, 'backup_url', expected_type=list) or []:
|
||||||
|
formats.append({
|
||||||
|
'url': backup_url,
|
||||||
|
# backup URLs have lower priorities
|
||||||
|
'quality': -3,
|
||||||
})
|
})
|
||||||
|
|
||||||
self._sort_formats(formats)
|
info.update({
|
||||||
|
'id': video_id,
|
||||||
entries.append({
|
'duration': float_or_none(durl.get('length'), 1000),
|
||||||
'id': '%s_part%s' % (video_id, idx),
|
'formats': formats,
|
||||||
'duration': float_or_none(durl.get('length'), 1000),
|
'http_headers': {
|
||||||
'formats': formats,
|
'Referer': url,
|
||||||
})
|
},
|
||||||
|
})
|
||||||
break
|
break
|
||||||
|
|
||||||
title = self._html_search_regex(
|
self._sort_formats(formats)
|
||||||
(r'<h1[^>]+\btitle=(["\'])(?P<title>(?:(?!\1).)+)\1',
|
|
||||||
r'(?s)<h1[^>]*>(?P<title>.+?)</h1>'), webpage, 'title',
|
title = self._html_search_regex((
|
||||||
group='title')
|
r'<h1[^>]+title=(["])(?P<content>[^"]+)',
|
||||||
|
r'<h1[^>]+title=([\'])(?P<content>[^\']+)',
|
||||||
|
r'(?s)<h1[^>]*>(?P<content>.+?)</h1>',
|
||||||
|
self._meta_regex('title')
|
||||||
|
), webpage, 'title', group='content', fatal=False)
|
||||||
|
|
||||||
# Get part title for anthologies
|
# Get part title for anthologies
|
||||||
if page_id is not None:
|
if page_id is not None:
|
||||||
# TODO: The json is already downloaded by _extract_anthology_entries. Don't redownload for each video
|
# TODO: The json is already downloaded by _extract_anthology_entries. Don't redownload for each video.
|
||||||
part_title = try_get(
|
part_info = traverse_obj(self._download_json(
|
||||||
self._download_json(
|
f'https://api.bilibili.com/x/player/pagelist?bvid={bv_id}&jsonp=jsonp',
|
||||||
f'https://api.bilibili.com/x/player/pagelist?bvid={bv_id}&jsonp=jsonp',
|
video_id, note='Extracting videos in anthology'), 'data', expected_type=list)
|
||||||
video_id, note='Extracting videos in anthology'),
|
title = title if len(part_info) == 1 else traverse_obj(part_info, (int(page_id) - 1, 'part')) or title
|
||||||
lambda x: x['data'][int(page_id) - 1]['part'])
|
|
||||||
title = part_title or title
|
|
||||||
|
|
||||||
description = self._html_search_meta('description', webpage)
|
description = self._html_search_meta('description', webpage)
|
||||||
timestamp = unified_timestamp(self._html_search_regex(
|
timestamp = unified_timestamp(self._html_search_regex(
|
||||||
@@ -272,15 +311,15 @@ class BiliBiliIE(InfoExtractor):
|
|||||||
thumbnail = self._html_search_meta(['og:image', 'thumbnailUrl'], webpage)
|
thumbnail = self._html_search_meta(['og:image', 'thumbnailUrl'], webpage)
|
||||||
|
|
||||||
# TODO 'view_count' requires deobfuscating Javascript
|
# TODO 'view_count' requires deobfuscating Javascript
|
||||||
info = {
|
info.update({
|
||||||
'id': str(video_id) if page_id is None else '%s_part%s' % (video_id, page_id),
|
'id': f'{video_id}_part{page_id or 1}',
|
||||||
'cid': cid,
|
'cid': cid,
|
||||||
'title': title,
|
'title': title,
|
||||||
'description': description,
|
'description': description,
|
||||||
'timestamp': timestamp,
|
'timestamp': timestamp,
|
||||||
'thumbnail': thumbnail,
|
'thumbnail': thumbnail,
|
||||||
'duration': float_or_none(video_info.get('timelength'), scale=1000),
|
'duration': float_or_none(video_info.get('timelength'), scale=1000),
|
||||||
}
|
})
|
||||||
|
|
||||||
uploader_mobj = re.search(
|
uploader_mobj = re.search(
|
||||||
r'<a[^>]+href="(?:https?:)?//space\.bilibili\.com/(?P<id>\d+)"[^>]*>\s*(?P<name>[^<]+?)\s*<',
|
r'<a[^>]+href="(?:https?:)?//space\.bilibili\.com/(?P<id>\d+)"[^>]*>\s*(?P<name>[^<]+?)\s*<',
|
||||||
@@ -301,7 +340,7 @@ class BiliBiliIE(InfoExtractor):
|
|||||||
video_id, fatal=False, note='Downloading tags'), ('data', ..., 'tag_name')),
|
video_id, fatal=False, note='Downloading tags'), ('data', ..., 'tag_name')),
|
||||||
}
|
}
|
||||||
|
|
||||||
entries[0]['subtitles'] = {
|
info['subtitles'] = {
|
||||||
'danmaku': [{
|
'danmaku': [{
|
||||||
'ext': 'xml',
|
'ext': 'xml',
|
||||||
'url': f'https://comment.bilibili.com/{cid}.xml',
|
'url': f'https://comment.bilibili.com/{cid}.xml',
|
||||||
@@ -336,12 +375,10 @@ class BiliBiliIE(InfoExtractor):
|
|||||||
entry['id'] = '%s_part%d' % (video_id, (idx + 1))
|
entry['id'] = '%s_part%d' % (video_id, (idx + 1))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'_type': 'multi_video',
|
|
||||||
'id': str(video_id),
|
'id': str(video_id),
|
||||||
'bv_id': bv_id,
|
'bv_id': bv_id,
|
||||||
'title': title,
|
'title': title,
|
||||||
'description': description,
|
'description': description,
|
||||||
'entries': entries,
|
|
||||||
**info, **top_level_info
|
**info, **top_level_info
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -482,9 +519,9 @@ class BilibiliChannelIE(InfoExtractor):
|
|||||||
data = self._download_json(
|
data = self._download_json(
|
||||||
self._API_URL % (list_id, page_num), list_id, note=f'Downloading page {page_num}')['data']
|
self._API_URL % (list_id, page_num), list_id, note=f'Downloading page {page_num}')['data']
|
||||||
|
|
||||||
max_count = max_count or try_get(data, lambda x: x['page']['count'])
|
max_count = max_count or traverse_obj(data, ('page', 'count'))
|
||||||
|
|
||||||
entries = try_get(data, lambda x: x['list']['vlist'])
|
entries = traverse_obj(data, ('list', 'vlist'))
|
||||||
if not entries:
|
if not entries:
|
||||||
return
|
return
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
@@ -522,7 +559,7 @@ class BilibiliCategoryIE(InfoExtractor):
|
|||||||
api_url, query, query={'Search_key': query, 'pn': page_num},
|
api_url, query, query={'Search_key': query, 'pn': page_num},
|
||||||
note='Extracting results from page %s of %s' % (page_num, num_pages))
|
note='Extracting results from page %s of %s' % (page_num, num_pages))
|
||||||
|
|
||||||
video_list = try_get(parsed_json, lambda x: x['data']['archives'], list)
|
video_list = traverse_obj(parsed_json, ('data', 'archives'), expected_type=list)
|
||||||
if not video_list:
|
if not video_list:
|
||||||
raise ExtractorError('Failed to retrieve video list for page %d' % page_num)
|
raise ExtractorError('Failed to retrieve video list for page %d' % page_num)
|
||||||
|
|
||||||
@@ -552,7 +589,7 @@ class BilibiliCategoryIE(InfoExtractor):
|
|||||||
|
|
||||||
api_url = 'https://api.bilibili.com/x/web-interface/newlist?rid=%d&type=1&ps=20&jsonp=jsonp' % rid_value
|
api_url = 'https://api.bilibili.com/x/web-interface/newlist?rid=%d&type=1&ps=20&jsonp=jsonp' % rid_value
|
||||||
page_json = self._download_json(api_url, query, query={'Search_key': query, 'pn': '1'})
|
page_json = self._download_json(api_url, query, query={'Search_key': query, 'pn': '1'})
|
||||||
page_data = try_get(page_json, lambda x: x['data']['page'], dict)
|
page_data = traverse_obj(page_json, ('data', 'page'), expected_type=dict)
|
||||||
count, size = int_or_none(page_data.get('count')), int_or_none(page_data.get('size'))
|
count, size = int_or_none(page_data.get('count')), int_or_none(page_data.get('size'))
|
||||||
if count is None or not size:
|
if count is None or not size:
|
||||||
raise ExtractorError('Failed to calculate either page count or size')
|
raise ExtractorError('Failed to calculate either page count or size')
|
||||||
@@ -751,15 +788,21 @@ class BiliIntlBaseIE(InfoExtractor):
|
|||||||
for i, line in enumerate(json['body']) if line.get('content'))
|
for i, line in enumerate(json['body']) if line.get('content'))
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def _get_subtitles(self, ep_id):
|
def _get_subtitles(self, *, ep_id=None, aid=None):
|
||||||
sub_json = self._call_api(f'/web/v2/subtitle?episode_id={ep_id}&platform=web', ep_id)
|
sub_json = self._call_api(
|
||||||
|
'/web/v2/subtitle', ep_id or aid, note='Downloading subtitles list',
|
||||||
|
errnote='Unable to download subtitles list', query=filter_dict({
|
||||||
|
'platform': 'web',
|
||||||
|
'episode_id': ep_id,
|
||||||
|
'aid': aid,
|
||||||
|
}))
|
||||||
subtitles = {}
|
subtitles = {}
|
||||||
for sub in sub_json.get('subtitles') or []:
|
for sub in sub_json.get('subtitles') or []:
|
||||||
sub_url = sub.get('url')
|
sub_url = sub.get('url')
|
||||||
if not sub_url:
|
if not sub_url:
|
||||||
continue
|
continue
|
||||||
sub_data = self._download_json(
|
sub_data = self._download_json(
|
||||||
sub_url, ep_id, errnote='Unable to download subtitles', fatal=False,
|
sub_url, ep_id or aid, errnote='Unable to download subtitles', fatal=False,
|
||||||
note='Downloading subtitles%s' % f' for {sub["lang"]}' if sub.get('lang') else '')
|
note='Downloading subtitles%s' % f' for {sub["lang"]}' if sub.get('lang') else '')
|
||||||
if not sub_data:
|
if not sub_data:
|
||||||
continue
|
continue
|
||||||
@@ -769,9 +812,14 @@ class BiliIntlBaseIE(InfoExtractor):
|
|||||||
})
|
})
|
||||||
return subtitles
|
return subtitles
|
||||||
|
|
||||||
def _get_formats(self, ep_id):
|
def _get_formats(self, *, ep_id=None, aid=None):
|
||||||
video_json = self._call_api(f'/web/playurl?ep_id={ep_id}&platform=web', ep_id,
|
video_json = self._call_api(
|
||||||
note='Downloading video formats', errnote='Unable to download video formats')
|
'/web/playurl', ep_id or aid, note='Downloading video formats',
|
||||||
|
errnote='Unable to download video formats', query=filter_dict({
|
||||||
|
'platform': 'web',
|
||||||
|
'ep_id': ep_id,
|
||||||
|
'aid': aid,
|
||||||
|
}))
|
||||||
video_json = video_json['playurl']
|
video_json = video_json['playurl']
|
||||||
formats = []
|
formats = []
|
||||||
for vid in video_json.get('video') or []:
|
for vid in video_json.get('video') or []:
|
||||||
@@ -805,23 +853,19 @@ class BiliIntlBaseIE(InfoExtractor):
|
|||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
return formats
|
return formats
|
||||||
|
|
||||||
def _extract_ep_info(self, episode_data, ep_id):
|
def _extract_video_info(self, video_data, *, ep_id=None, aid=None):
|
||||||
return {
|
return {
|
||||||
'id': ep_id,
|
'id': ep_id or aid,
|
||||||
'title': episode_data.get('title_display') or episode_data['title'],
|
'title': video_data.get('title_display') or video_data.get('title'),
|
||||||
'thumbnail': episode_data.get('cover'),
|
'thumbnail': video_data.get('cover'),
|
||||||
'episode_number': int_or_none(self._search_regex(
|
'episode_number': int_or_none(self._search_regex(
|
||||||
r'^E(\d+)(?:$| - )', episode_data.get('title_display'), 'episode number', default=None)),
|
r'^E(\d+)(?:$| - )', video_data.get('title_display') or '', 'episode number', default=None)),
|
||||||
'formats': self._get_formats(ep_id),
|
'formats': self._get_formats(ep_id=ep_id, aid=aid),
|
||||||
'subtitles': self._get_subtitles(ep_id),
|
'subtitles': self._get_subtitles(ep_id=ep_id, aid=aid),
|
||||||
'extractor_key': BiliIntlIE.ie_key(),
|
'extractor_key': BiliIntlIE.ie_key(),
|
||||||
}
|
}
|
||||||
|
|
||||||
def _login(self):
|
def _perform_login(self, username, password):
|
||||||
username, password = self._get_login_info()
|
|
||||||
if username is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from Cryptodome.PublicKey import RSA
|
from Cryptodome.PublicKey import RSA
|
||||||
from Cryptodome.Cipher import PKCS1_v1_5
|
from Cryptodome.Cipher import PKCS1_v1_5
|
||||||
@@ -852,12 +896,9 @@ class BiliIntlBaseIE(InfoExtractor):
|
|||||||
else:
|
else:
|
||||||
raise ExtractorError('Unable to log in')
|
raise ExtractorError('Unable to log in')
|
||||||
|
|
||||||
def _real_initialize(self):
|
|
||||||
self._login()
|
|
||||||
|
|
||||||
|
|
||||||
class BiliIntlIE(BiliIntlBaseIE):
|
class BiliIntlIE(BiliIntlBaseIE):
|
||||||
_VALID_URL = r'https?://(?:www\.)?bili(?:bili\.tv|intl\.com)/(?:[a-z]{2}/)?play/(?P<season_id>\d+)/(?P<id>\d+)'
|
_VALID_URL = r'https?://(?:www\.)?bili(?:bili\.tv|intl\.com)/(?:[a-z]{2}/)?(play/(?P<season_id>\d+)/(?P<ep_id>\d+)|video/(?P<aid>\d+))'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
# Bstation page
|
# Bstation page
|
||||||
'url': 'https://www.bilibili.tv/en/play/34613/341736',
|
'url': 'https://www.bilibili.tv/en/play/34613/341736',
|
||||||
@@ -892,24 +933,35 @@ class BiliIntlIE(BiliIntlBaseIE):
|
|||||||
}, {
|
}, {
|
||||||
'url': 'https://www.biliintl.com/en/play/34613/341736',
|
'url': 'https://www.biliintl.com/en/play/34613/341736',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
# User-generated content (as opposed to a series licensed from a studio)
|
||||||
|
'url': 'https://bilibili.tv/en/video/2019955076',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
# No language in URL
|
||||||
|
'url': 'https://www.bilibili.tv/video/2019955076',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
season_id, video_id = self._match_valid_url(url).groups()
|
season_id, ep_id, aid = self._match_valid_url(url).group('season_id', 'ep_id', 'aid')
|
||||||
|
video_id = ep_id or aid
|
||||||
webpage = self._download_webpage(url, video_id)
|
webpage = self._download_webpage(url, video_id)
|
||||||
# Bstation layout
|
# Bstation layout
|
||||||
initial_data = self._parse_json(self._search_regex(
|
initial_data = self._parse_json(self._search_regex(
|
||||||
r'window\.__INITIAL_DATA__\s*=\s*({.+?});', webpage,
|
r'window\.__INITIAL_(?:DATA|STATE)__\s*=\s*({.+?});', webpage,
|
||||||
'preload state', default='{}'), video_id, fatal=False) or {}
|
'preload state', default='{}'), video_id, fatal=False) or {}
|
||||||
episode_data = traverse_obj(initial_data, ('OgvVideo', 'epDetail'), expected_type=dict)
|
video_data = (
|
||||||
|
traverse_obj(initial_data, ('OgvVideo', 'epDetail'), expected_type=dict)
|
||||||
|
or traverse_obj(initial_data, ('UgcVideo', 'videoData'), expected_type=dict) or {})
|
||||||
|
|
||||||
if not episode_data:
|
if season_id and not video_data:
|
||||||
# Non-Bstation layout, read through episode list
|
# Non-Bstation layout, read through episode list
|
||||||
season_json = self._call_api(f'/web/v2/ogv/play/episodes?season_id={season_id}&platform=web', video_id)
|
season_json = self._call_api(f'/web/v2/ogv/play/episodes?season_id={season_id}&platform=web', video_id)
|
||||||
episode_data = next(
|
video_data = traverse_obj(season_json,
|
||||||
episode for episode in traverse_obj(season_json, ('sections', ..., 'episodes', ...), expected_type=dict)
|
('sections', ..., 'episodes', lambda _, v: str(v['episode_id']) == ep_id),
|
||||||
if str(episode.get('episode_id')) == video_id)
|
expected_type=dict, get_all=False)
|
||||||
return self._extract_ep_info(episode_data, video_id)
|
return self._extract_video_info(video_data, ep_id=ep_id, aid=aid)
|
||||||
|
|
||||||
|
|
||||||
class BiliIntlSeriesIE(BiliIntlBaseIE):
|
class BiliIntlSeriesIE(BiliIntlBaseIE):
|
||||||
@@ -937,7 +989,7 @@ class BiliIntlSeriesIE(BiliIntlBaseIE):
|
|||||||
series_json = self._call_api(f'/web/v2/ogv/play/episodes?season_id={series_id}&platform=web', series_id)
|
series_json = self._call_api(f'/web/v2/ogv/play/episodes?season_id={series_id}&platform=web', series_id)
|
||||||
for episode in traverse_obj(series_json, ('sections', ..., 'episodes', ...), expected_type=dict, default=[]):
|
for episode in traverse_obj(series_json, ('sections', ..., 'episodes', ...), expected_type=dict, default=[]):
|
||||||
episode_id = str(episode.get('episode_id'))
|
episode_id = str(episode.get('episode_id'))
|
||||||
yield self._extract_ep_info(episode, episode_id)
|
yield self._extract_video_info(episode, ep_id=episode_id)
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
series_id = self._match_id(url)
|
series_id = self._match_id(url)
|
||||||
|
|||||||
@@ -3,27 +3,28 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from .vk import VKIE
|
from .vk import VKIE
|
||||||
from ..compat import (
|
from ..compat import compat_b64decode
|
||||||
compat_b64decode,
|
from ..utils import (
|
||||||
compat_urllib_parse_unquote,
|
int_or_none,
|
||||||
|
js_to_json,
|
||||||
|
traverse_obj,
|
||||||
|
unified_timestamp,
|
||||||
)
|
)
|
||||||
from ..utils import int_or_none
|
|
||||||
|
|
||||||
|
|
||||||
class BIQLEIE(InfoExtractor):
|
class BIQLEIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?biqle\.(?:com|org|ru)/watch/(?P<id>-?\d+_\d+)'
|
_VALID_URL = r'https?://(?:www\.)?biqle\.(?:com|org|ru)/watch/(?P<id>-?\d+_\d+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
# Youtube embed
|
'url': 'https://biqle.ru/watch/-2000421746_85421746',
|
||||||
'url': 'https://biqle.ru/watch/-115995369_456239081',
|
'md5': 'ae6ef4f04d19ac84e4658046d02c151c',
|
||||||
'md5': '97af5a06ee4c29bbf9c001bdb1cf5c06',
|
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '8v4f-avW-VI',
|
'id': '-2000421746_85421746',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': "PASSE-PARTOUT - L'ete c'est fait pour jouer",
|
'title': 'Forsaken By Hope Studio Clip',
|
||||||
'description': 'Passe-Partout',
|
'description': 'Forsaken By Hope Studio Clip — Смотреть онлайн',
|
||||||
'uploader_id': 'mrsimpsonstef3',
|
'upload_date': '19700101',
|
||||||
'uploader': 'Phanolito',
|
'thumbnail': r're:https://[^/]+/impf/7vN3ACwSTgChP96OdOfzFjUCzFR6ZglDQgWsIw/KPaACiVJJxM\.jpg\?size=800x450&quality=96&keep_aspect_ratio=1&background=000000&sign=b48ea459c4d33dbcba5e26d63574b1cb&type=video_thumb',
|
||||||
'upload_date': '20120822',
|
'timestamp': 0,
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://biqle.org/watch/-44781847_168547604',
|
'url': 'http://biqle.org/watch/-44781847_168547604',
|
||||||
@@ -32,53 +33,62 @@ class BIQLEIE(InfoExtractor):
|
|||||||
'id': '-44781847_168547604',
|
'id': '-44781847_168547604',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Ребенок в шоке от автоматической мойки',
|
'title': 'Ребенок в шоке от автоматической мойки',
|
||||||
|
'description': 'Ребенок в шоке от автоматической мойки — Смотреть онлайн',
|
||||||
'timestamp': 1396633454,
|
'timestamp': 1396633454,
|
||||||
'uploader': 'Dmitry Kotov',
|
|
||||||
'upload_date': '20140404',
|
'upload_date': '20140404',
|
||||||
'uploader_id': '47850140',
|
'thumbnail': r're:https://[^/]+/c535507/u190034692/video/l_b84df002\.jpg',
|
||||||
},
|
},
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
webpage = self._download_webpage(url, video_id)
|
webpage = self._download_webpage(url, video_id)
|
||||||
embed_url = self._proto_relative_url(self._search_regex(
|
|
||||||
r'<iframe.+?src="((?:https?:)?//(?:daxab\.com|dxb\.to|[^/]+/player)/[^"]+)".*?></iframe>',
|
title = self._html_search_meta('name', webpage, 'Title', fatal=False)
|
||||||
webpage, 'embed url'))
|
timestamp = unified_timestamp(self._html_search_meta('uploadDate', webpage, 'Upload Date', default=None))
|
||||||
|
description = self._html_search_meta('description', webpage, 'Description', default=None)
|
||||||
|
|
||||||
|
global_embed_url = self._search_regex(
|
||||||
|
r'<script[^<]+?window.globEmbedUrl\s*=\s*\'((?:https?:)?//(?:daxab\.com|dxb\.to|[^/]+/player)/[^\']+)\'',
|
||||||
|
webpage, 'global Embed url')
|
||||||
|
hash = self._search_regex(
|
||||||
|
r'<script id="data-embed-video[^<]+?hash: "([^"]+)"[^<]*</script>', webpage, 'Hash')
|
||||||
|
|
||||||
|
embed_url = global_embed_url + hash
|
||||||
|
|
||||||
if VKIE.suitable(embed_url):
|
if VKIE.suitable(embed_url):
|
||||||
return self.url_result(embed_url, VKIE.ie_key(), video_id)
|
return self.url_result(embed_url, VKIE.ie_key(), video_id)
|
||||||
|
|
||||||
embed_page = self._download_webpage(
|
embed_page = self._download_webpage(
|
||||||
embed_url, video_id, headers={'Referer': url})
|
embed_url, video_id, 'Downloading embed webpage', headers={'Referer': url})
|
||||||
video_ext = self._get_cookies(embed_url).get('video_ext')
|
|
||||||
if video_ext:
|
glob_params = self._parse_json(self._search_regex(
|
||||||
video_ext = compat_urllib_parse_unquote(video_ext.value)
|
r'<script id="globParams">[^<]*window.globParams = ([^;]+);[^<]+</script>',
|
||||||
if not video_ext:
|
embed_page, 'Global Parameters'), video_id, transform_source=js_to_json)
|
||||||
video_ext = compat_b64decode(self._search_regex(
|
host_name = compat_b64decode(glob_params['server'][::-1]).decode()
|
||||||
r'video_ext\s*:\s*[\'"]([A-Za-z0-9+/=]+)',
|
|
||||||
embed_page, 'video_ext')).decode()
|
|
||||||
video_id, sig, _, access_token = video_ext.split(':')
|
|
||||||
item = self._download_json(
|
item = self._download_json(
|
||||||
'https://api.vk.com/method/video.get', video_id,
|
f'https://{host_name}/method/video.get/{video_id}', video_id,
|
||||||
headers={'User-Agent': 'okhttp/3.4.1'}, query={
|
headers={'Referer': url}, query={
|
||||||
'access_token': access_token,
|
'token': glob_params['video']['access_token'],
|
||||||
'sig': sig,
|
|
||||||
'v': 5.44,
|
|
||||||
'videos': video_id,
|
'videos': video_id,
|
||||||
|
'ckey': glob_params['c_key'],
|
||||||
|
'credentials': glob_params['video']['credentials'],
|
||||||
})['response']['items'][0]
|
})['response']['items'][0]
|
||||||
title = item['title']
|
|
||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
for f_id, f_url in item.get('files', {}).items():
|
for f_id, f_url in item.get('files', {}).items():
|
||||||
if f_id == 'external':
|
if f_id == 'external':
|
||||||
return self.url_result(f_url)
|
return self.url_result(f_url)
|
||||||
ext, height = f_id.split('_')
|
ext, height = f_id.split('_')
|
||||||
formats.append({
|
height_extra_key = traverse_obj(glob_params, ('video', 'partial', 'quality', height))
|
||||||
'format_id': height + 'p',
|
if height_extra_key:
|
||||||
'url': f_url,
|
formats.append({
|
||||||
'height': int_or_none(height),
|
'format_id': f'{height}p',
|
||||||
'ext': ext,
|
'url': f'https://{host_name}/{f_url[8:]}&videos={video_id}&extra_key={height_extra_key}',
|
||||||
})
|
'height': int_or_none(height),
|
||||||
|
'ext': ext,
|
||||||
|
})
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
thumbnails = []
|
thumbnails = []
|
||||||
@@ -96,10 +106,9 @@ class BIQLEIE(InfoExtractor):
|
|||||||
'title': title,
|
'title': title,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
'comment_count': int_or_none(item.get('comments')),
|
'comment_count': int_or_none(item.get('comments')),
|
||||||
'description': item.get('description'),
|
'description': description,
|
||||||
'duration': int_or_none(item.get('duration')),
|
'duration': int_or_none(item.get('duration')),
|
||||||
'thumbnails': thumbnails,
|
'thumbnails': thumbnails,
|
||||||
'timestamp': int_or_none(item.get('date')),
|
'timestamp': timestamp,
|
||||||
'uploader': item.get('owner_id'),
|
|
||||||
'view_count': int_or_none(item.get('views')),
|
'view_count': int_or_none(item.get('views')),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ class BRIE(InfoExtractor):
|
|||||||
|
|
||||||
class BRMediathekIE(InfoExtractor):
|
class BRMediathekIE(InfoExtractor):
|
||||||
IE_DESC = 'Bayerischer Rundfunk Mediathek'
|
IE_DESC = 'Bayerischer Rundfunk Mediathek'
|
||||||
_VALID_URL = r'https?://(?:www\.)?br\.de/mediathek/video/[^/?&#]*?-(?P<id>av:[0-9a-f]{24})'
|
_VALID_URL = r'https?://(?:www\.)?br\.de/mediathek//?video/(?:[^/?&#]+?-)?(?P<id>av:[0-9a-f]{24})'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.br.de/mediathek/video/gesundheit-die-sendung-vom-28112017-av:5a1e6a6e8fce6d001871cc8e',
|
'url': 'https://www.br.de/mediathek/video/gesundheit-die-sendung-vom-28112017-av:5a1e6a6e8fce6d001871cc8e',
|
||||||
@@ -188,6 +188,9 @@ class BRMediathekIE(InfoExtractor):
|
|||||||
'timestamp': 1511942766,
|
'timestamp': 1511942766,
|
||||||
'upload_date': '20171129',
|
'upload_date': '20171129',
|
||||||
}
|
}
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.br.de/mediathek//video/av:61b0db581aed360007558c12',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
|
|||||||
@@ -29,9 +29,8 @@ class BreitBartIE(InfoExtractor):
|
|||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'title': self._og_search_title(
|
'title': (self._og_search_title(webpage, default=None)
|
||||||
webpage, default=None) or self._html_search_regex(
|
or self._html_extract_title(webpage, 'video title')),
|
||||||
r'(?s)<title>(.*?)</title>', webpage, 'video title'),
|
|
||||||
'description': self._og_search_description(webpage),
|
'description': self._og_search_description(webpage),
|
||||||
'thumbnail': self._og_search_thumbnail(webpage),
|
'thumbnail': self._og_search_thumbnail(webpage),
|
||||||
'age_limit': self._rta_search(webpage),
|
'age_limit': self._rta_search(webpage),
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class CallinIE(InfoExtractor):
|
|||||||
id = episode['id']
|
id = episode['id']
|
||||||
title = (episode.get('title')
|
title = (episode.get('title')
|
||||||
or self._og_search_title(webpage, fatal=False)
|
or self._og_search_title(webpage, fatal=False)
|
||||||
or self._html_search_regex('<title>(.*?)</title>', webpage, 'title'))
|
or self._html_extract_title(webpage))
|
||||||
url = episode['m3u8']
|
url = episode['m3u8']
|
||||||
formats = self._extract_m3u8_formats(url, display_id, ext='ts')
|
formats = self._extract_m3u8_formats(url, display_id, ext='ts')
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|||||||
41
yt_dlp/extractor/caltrans.py
Normal file
41
yt_dlp/extractor/caltrans.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
|
||||||
|
|
||||||
|
class CaltransIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:[^/]+\.)?ca\.gov/vm/loc/[^/]+/(?P<id>[a-z0-9_]+)\.htm'
|
||||||
|
_TEST = {
|
||||||
|
'url': 'https://cwwp2.dot.ca.gov/vm/loc/d3/hwy50at24th.htm',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'hwy50at24th',
|
||||||
|
'ext': 'ts',
|
||||||
|
'title': 'US-50 : Sacramento : Hwy 50 at 24th',
|
||||||
|
'live_status': 'is_live',
|
||||||
|
'thumbnail': 'https://cwwp2.dot.ca.gov/data/d3/cctv/image/hwy50at24th/hwy50at24th.jpg',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
|
global_vars = self._search_regex(
|
||||||
|
r'<script[^<]+?([^<]+\.m3u8[^<]+)</script>',
|
||||||
|
webpage, 'Global Vars')
|
||||||
|
route_place = self._search_regex(r'routePlace\s*=\s*"([^"]+)"', global_vars, 'Route Place', fatal=False)
|
||||||
|
location_name = self._search_regex(r'locationName\s*=\s*"([^"]+)"', global_vars, 'Location Name', fatal=False)
|
||||||
|
poster_url = self._search_regex(r'posterURL\s*=\s*"([^"]+)"', global_vars, 'Poster Url', fatal=False)
|
||||||
|
video_stream = self._search_regex(r'videoStreamURL\s*=\s*"([^"]+)"', global_vars, 'Video Stream URL', fatal=False)
|
||||||
|
|
||||||
|
formats = self._extract_m3u8_formats(video_stream, video_id, 'ts', live=True)
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': f'{route_place} : {location_name}',
|
||||||
|
'is_live': True,
|
||||||
|
'formats': formats,
|
||||||
|
'thumbnail': poster_url,
|
||||||
|
}
|
||||||
@@ -13,6 +13,8 @@ class CAM4IE(InfoExtractor):
|
|||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 're:^foxynesss [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
|
'title': 're:^foxynesss [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$',
|
||||||
'age_limit': 18,
|
'age_limit': 18,
|
||||||
|
'live_status': 'is_live',
|
||||||
|
'thumbnail': 'https://snapshots.xcdnpro.com/thumbnails/foxynesss',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,4 +31,5 @@ class CAM4IE(InfoExtractor):
|
|||||||
'is_live': True,
|
'is_live': True,
|
||||||
'age_limit': 18,
|
'age_limit': 18,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
|
'thumbnail': f'https://snapshots.xcdnpro.com/thumbnails/{channel_id}',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -245,10 +245,6 @@ class VrtNUIE(GigyaBaseIE):
|
|||||||
'upload_date': '20200727',
|
'upload_date': '20200727',
|
||||||
},
|
},
|
||||||
'skip': 'This video is only available for registered users',
|
'skip': 'This video is only available for registered users',
|
||||||
'params': {
|
|
||||||
'username': '<snip>',
|
|
||||||
'password': '<snip>',
|
|
||||||
},
|
|
||||||
'expected_warnings': ['is not a supported codec'],
|
'expected_warnings': ['is not a supported codec'],
|
||||||
}, {
|
}, {
|
||||||
# Only available via new API endpoint
|
# Only available via new API endpoint
|
||||||
@@ -264,24 +260,13 @@ class VrtNUIE(GigyaBaseIE):
|
|||||||
'episode_number': 5,
|
'episode_number': 5,
|
||||||
},
|
},
|
||||||
'skip': 'This video is only available for registered users',
|
'skip': 'This video is only available for registered users',
|
||||||
'params': {
|
|
||||||
'username': '<snip>',
|
|
||||||
'password': '<snip>',
|
|
||||||
},
|
|
||||||
'expected_warnings': ['Unable to download asset JSON', 'is not a supported codec', 'Unknown MIME type'],
|
'expected_warnings': ['Unable to download asset JSON', 'is not a supported codec', 'Unknown MIME type'],
|
||||||
}]
|
}]
|
||||||
_NETRC_MACHINE = 'vrtnu'
|
_NETRC_MACHINE = 'vrtnu'
|
||||||
_APIKEY = '3_0Z2HujMtiWq_pkAjgnS2Md2E11a1AwZjYiBETtwNE-EoEHDINgtnvcAOpNgmrVGy'
|
_APIKEY = '3_0Z2HujMtiWq_pkAjgnS2Md2E11a1AwZjYiBETtwNE-EoEHDINgtnvcAOpNgmrVGy'
|
||||||
_CONTEXT_ID = 'R3595707040'
|
_CONTEXT_ID = 'R3595707040'
|
||||||
|
|
||||||
def _real_initialize(self):
|
def _perform_login(self, username, password):
|
||||||
self._login()
|
|
||||||
|
|
||||||
def _login(self):
|
|
||||||
username, password = self._get_login_info()
|
|
||||||
if username is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
auth_info = self._gigya_login({
|
auth_info = self._gigya_login({
|
||||||
'APIKey': self._APIKEY,
|
'APIKey': self._APIKEY,
|
||||||
'targetEnv': 'jssdk',
|
'targetEnv': 'jssdk',
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user