mirror of
https://github.com/yt-dlp/yt-dlp.git
synced 2026-08-08 21:28:38 +03:00
Compare commits
304
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
459aea84c3 | ||
|
|
87e0499624 | ||
|
|
0f86a1cd59 | ||
|
|
d80d98e7d4 | ||
|
|
352d5da812 | ||
|
|
d43de6821c | ||
|
|
070f6a85ea | ||
|
|
4b4b7f746c | ||
|
|
e9efb99f66 | ||
|
|
a709d87335 | ||
|
|
774a46c53d | ||
|
|
c8b80b9643 | ||
|
|
4e260d1a56 | ||
|
|
4f3fa23e5a | ||
|
|
b28bac93ab | ||
|
|
37893bb0c9 | ||
|
|
c25de59cf7 | ||
|
|
205a0654c0 | ||
|
|
663949f825 | ||
|
|
b69fd25c25 | ||
|
|
e0fd95737d | ||
|
|
4ac5b94807 | ||
|
|
4273cc776d | ||
|
|
fa9f30b802 | ||
|
|
1cefca9e44 | ||
|
|
5edb8dfec2 | ||
|
|
0fcba15d57 | ||
|
|
adbc4ec4bb | ||
|
|
c031b0414c | ||
|
|
f3aa3c3f98 | ||
|
|
ae43a4b986 | ||
|
|
ca5db158ae | ||
|
|
5f549d4959 | ||
|
|
6839d02cb6 | ||
|
|
2aae2c91ff | ||
|
|
c2dedf12e8 | ||
|
|
e75bb0d6c3 | ||
|
|
dd0228ce1f | ||
|
|
37e57a9fd4 | ||
|
|
940a67a3e2 | ||
|
|
e6ae51c123 | ||
|
|
75ad33572b | ||
|
|
aab41cdd33 | ||
|
|
b3a5115ff1 | ||
|
|
d76d15a669 | ||
|
|
e978789f0f | ||
|
|
ec2e44fc57 | ||
|
|
375d9360bf | ||
|
|
d5c3254889 | ||
|
|
fed1309651 | ||
|
|
fe69f52e5c | ||
|
|
3116be32b4 | ||
|
|
a8549f19e7 | ||
|
|
39ca3b5c7f | ||
|
|
46383212b3 | ||
|
|
0bb322b9c0 | ||
|
|
ff9f925b63 | ||
|
|
5bfc8bee5a | ||
|
|
19188702ef | ||
|
|
d984a98def | ||
|
|
069c6ccf02 | ||
|
|
53dad39e30 | ||
|
|
db77c49c84 | ||
|
|
abc07b554c | ||
|
|
86f3d52f8c | ||
|
|
8b688881ba | ||
|
|
13debc86e7 | ||
|
|
b5f94e4fa1 | ||
|
|
61882afdc5 | ||
|
|
aa4b054512 | ||
|
|
487c5b3389 | ||
|
|
8157a09d22 | ||
|
|
b1aaf1c07f | ||
|
|
5f9aaac8c2 | ||
|
|
54c2521ca6 | ||
|
|
2814f12ba4 | ||
|
|
1619836cb7 | ||
|
|
e3c7d49571 | ||
|
|
ddd24c9949 | ||
|
|
443b21dc4e | ||
|
|
66f4c04e50 | ||
|
|
93864403ea | ||
|
|
b5475f1145 | ||
|
|
38d79fd16c | ||
|
|
acc0d6a411 | ||
|
|
146cc4114a | ||
|
|
818faa3a86 | ||
|
|
aa5ecf082c | ||
|
|
d2b2fca53f | ||
|
|
63ccf4ff1a | ||
|
|
43b2290658 | ||
|
|
99148c6a33 | ||
|
|
9bdd99cf39 | ||
|
|
2c4aaaddc9 | ||
|
|
5f7cb91ae9 | ||
|
|
3efb96a6d1 | ||
|
|
3262f8abf2 | ||
|
|
bdbafb3913 | ||
|
|
a804f6d89c | ||
|
|
814dfb7e25 | ||
|
|
91f071af60 | ||
|
|
2aa5e2cc01 | ||
|
|
1bad50eced | ||
|
|
ac0efabf12 | ||
|
|
73f035e1fe | ||
|
|
0cbed930c8 | ||
|
|
5118d2ec58 | ||
|
|
717216b093 | ||
|
|
5c22c63da3 | ||
|
|
ee8dd27a73 | ||
|
|
f304da8a29 | ||
|
|
06dfe0a0a2 | ||
|
|
75b725a7cc | ||
|
|
13ab5fa586 | ||
|
|
36eaf3039a | ||
|
|
f2ebc5c7be | ||
|
|
b222c27145 | ||
|
|
5e5be0c0b2 | ||
|
|
7578d77d8c | ||
|
|
b29165267f | ||
|
|
bc104778d6 | ||
|
|
d298d33fe6 | ||
|
|
bf57cfa8b7 | ||
|
|
3c2208f82d | ||
|
|
93e597ba28 | ||
|
|
b28cdcc0e4 | ||
|
|
a33c0d9c5d | ||
|
|
75689fe59b | ||
|
|
5ce1d13eba | ||
|
|
e04b003e64 | ||
|
|
909b0d66f4 | ||
|
|
dfd78699f5 | ||
|
|
639f80c1f9 | ||
|
|
896a88c5c6 | ||
|
|
4e4ba1d75f | ||
|
|
2abf081554 | ||
|
|
359df0fc42 | ||
|
|
3938a9212c | ||
|
|
cf1f13b817 | ||
|
|
18d6dd4e01 | ||
|
|
883ecd5494 | ||
|
|
eb56d132d2 | ||
|
|
17b4540662 | ||
|
|
da27aeea5c | ||
|
|
fec41d17a5 | ||
|
|
a61fd4cf6f | ||
|
|
a6213a4925 | ||
|
|
9941a1e127 | ||
|
|
ff51ed588f | ||
|
|
57dbe8077f | ||
|
|
e5d731f35d | ||
|
|
d52cd2f5cd | ||
|
|
bc8ab44ea0 | ||
|
|
8f122fa070 | ||
|
|
14a086058a | ||
|
|
0e6b018a10 | ||
|
|
f7b558df4d | ||
|
|
1ee34c76bb | ||
|
|
234416e4bf | ||
|
|
c98d4df23b | ||
|
|
849d699a8b | ||
|
|
77fcc65158 | ||
|
|
545ad64988 | ||
|
|
d76991ab07 | ||
|
|
282f570918 | ||
|
|
c07a39ae8e | ||
|
|
c5e3f84972 | ||
|
|
c45b87419f | ||
|
|
7333296ff5 | ||
|
|
a04e005521 | ||
|
|
6b993ca765 | ||
|
|
dd2a987d3f | ||
|
|
9222c38182 | ||
|
|
467b6b8387 | ||
|
|
8863c8f09e | ||
|
|
e16fefd869 | ||
|
|
c6118ca2cc | ||
|
|
764f5de2f4 | ||
|
|
cfcaf64a4b | ||
|
|
402cd603a4 | ||
|
|
22a510ff44 | ||
|
|
61be785a67 | ||
|
|
11852843e7 | ||
|
|
525d9e0c7d | ||
|
|
9d63137eac | ||
|
|
266a1b5d52 | ||
|
|
450bdf69bc | ||
|
|
720c309932 | ||
|
|
d8cf8d97a8 | ||
|
|
d0d012d4e7 | ||
|
|
013b50b794 | ||
|
|
dac5df5a98 | ||
|
|
f279aaee8e | ||
|
|
d0e6121adf | ||
|
|
9ac24e235e | ||
|
|
7c7f7161fc | ||
|
|
e339d25a0d | ||
|
|
39c04074e7 | ||
|
|
92775d8a40 | ||
|
|
df03de2c02 | ||
|
|
48e9310660 | ||
|
|
c1dc0ee56e | ||
|
|
bf5f605e76 | ||
|
|
e08a85d865 | ||
|
|
093a17107e | ||
|
|
44bcb8d122 | ||
|
|
013ae2e503 | ||
|
|
b47d236d72 | ||
|
|
9ebf3c6ab9 | ||
|
|
7144b697fc | ||
|
|
2e9a445bc3 | ||
|
|
86c1a8aae4 | ||
|
|
ebfab36fca | ||
|
|
c15de6ffe6 | ||
|
|
56bb56f3cf | ||
|
|
c0599d4fe4 | ||
|
|
3f771f75d7 | ||
|
|
ed76230b3f | ||
|
|
89fcdff5d8 | ||
|
|
f98709af31 | ||
|
|
c586f9e8de | ||
|
|
59a7a13ef9 | ||
|
|
4476d2c764 | ||
|
|
aa9369a2d8 | ||
|
|
d54c6003ab | ||
|
|
1ee316a34a | ||
|
|
358247ed2a | ||
|
|
9b12e9a573 | ||
|
|
a109acbf82 | ||
|
|
a49891c761 | ||
|
|
582fad70f5 | ||
|
|
aeec0e44e2 | ||
|
|
d9190e4467 | ||
|
|
e1b7c54d78 | ||
|
|
244644c02c | ||
|
|
34921b4345 | ||
|
|
a331949df3 | ||
|
|
2c5e8a961e | ||
|
|
b515b37cc4 | ||
|
|
3c4eebf772 | ||
|
|
fb2d1ee6cc | ||
|
|
9cb070f9c0 | ||
|
|
2a6f8475ac | ||
|
|
73673ccff3 | ||
|
|
aeb2a9ad27 | ||
|
|
df6c409d1f | ||
|
|
a9d4da606d | ||
|
|
c18d4482b1 | ||
|
|
0f6518938d | ||
|
|
22cd06c452 | ||
|
|
a4211baff5 | ||
|
|
8913ef74d7 | ||
|
|
832e9000c7 | ||
|
|
673c0057e8 | ||
|
|
9af98e17bd | ||
|
|
31c49255bf | ||
|
|
bd93fd5d45 | ||
|
|
d89257f398 | ||
|
|
9bd979ca40 | ||
|
|
a1fc7ca074 | ||
|
|
c588b602d3 | ||
|
|
f0ffaa1621 | ||
|
|
0930b11fda | ||
|
|
a0bb6ce58d | ||
|
|
da48320075 | ||
|
|
5b6cb56207 | ||
|
|
b2f25dc242 | ||
|
|
2f9e021299 | ||
|
|
8dcf65c92e | ||
|
|
92592bd305 | ||
|
|
404f611f1c | ||
|
|
cd9ea4104b | ||
|
|
652fb0d446 | ||
|
|
6b301aaa34 | ||
|
|
fa0b816e37 | ||
|
|
5e7bbac305 | ||
|
|
10beccc980 | ||
|
|
e6ff66efc0 | ||
|
|
aeaf3b2b92 | ||
|
|
7b5f3f7c3d | ||
|
|
3783b5f1d1 | ||
|
|
ab630a57b9 | ||
|
|
16b0d7e621 | ||
|
|
5be76d1ab7 | ||
|
|
b7b186e7de | ||
|
|
bd1c792327 | ||
|
|
dc88e9be03 | ||
|
|
673944b001 | ||
|
|
0c873df3a8 | ||
|
|
c35ada3360 | ||
|
|
0db3bae879 | ||
|
|
48f796874d | ||
|
|
abad800058 | ||
|
|
08438d2ca5 | ||
|
|
7de837a5e3 | ||
|
|
7e59ca440a | ||
|
|
8e7ab2cf08 | ||
|
|
ad64a2323f | ||
|
|
f2fe69c7b0 | ||
|
|
fccf502118 | ||
|
|
9f1a1c36e6 | ||
|
|
96565c7e55 | ||
|
|
ec11a9f4a2 | ||
|
|
93c7f3398d |
@@ -1,6 +1,6 @@
|
|||||||
name: Broken site support
|
name: Broken site support
|
||||||
description: Report broken or misfunctioning site
|
description: Report broken or misfunctioning site
|
||||||
labels: [triage, extractor-bug]
|
labels: [triage, site-bug]
|
||||||
body:
|
body:
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: checklist
|
id: checklist
|
||||||
@@ -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.10.22**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
|
- label: I've verified that I'm running yt-dlp version **2021.12.25**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
|
||||||
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
|
||||||
@@ -43,7 +43,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Verbose log
|
label: Verbose log
|
||||||
description: |
|
description: |
|
||||||
Provide the complete verbose output of yt-dlp that clearly demonstrates the problem.
|
Provide the complete verbose output of yt-dlp **that clearly demonstrates the problem**.
|
||||||
Add the `-Uv` flag to your command line you run yt-dlp with (`yt-dlp -Uv <your command line>`), copy the WHOLE output and insert it below.
|
Add the `-Uv` flag to your command line you run yt-dlp with (`yt-dlp -Uv <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: |
|
||||||
@@ -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.10.22 (exe)
|
[debug] yt-dlp version 2021.12.25 (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.10.22)
|
yt-dlp is up to date (2021.12.25)
|
||||||
<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.10.22**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
|
- label: I've verified that I'm running yt-dlp version **2021.12.25**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
|
||||||
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
|
||||||
@@ -34,7 +34,7 @@ body:
|
|||||||
label: Example URLs
|
label: Example URLs
|
||||||
description: |
|
description: |
|
||||||
Provide all kinds of example URLs for which support should be added
|
Provide all kinds of example URLs for which support should be added
|
||||||
value: |
|
placeholder: |
|
||||||
- Single video: https://www.youtube.com/watch?v=BaW_jenozKc
|
- Single video: https://www.youtube.com/watch?v=BaW_jenozKc
|
||||||
- Single video: https://youtu.be/BaW_jenozKc
|
- Single video: https://youtu.be/BaW_jenozKc
|
||||||
- Playlist: https://www.youtube.com/playlist?list=PL4lCao7KL_QFVb7Iudeipvc2BCavECqzc
|
- Playlist: https://www.youtube.com/playlist?list=PL4lCao7KL_QFVb7Iudeipvc2BCavECqzc
|
||||||
@@ -54,7 +54,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Verbose log
|
label: Verbose log
|
||||||
description: |
|
description: |
|
||||||
Provide the complete verbose output using one of the example URLs provided above.
|
Provide the complete verbose output **using one of the example URLs provided above**.
|
||||||
Add the `-Uv` flag to your command line you run yt-dlp with (`yt-dlp -Uv <your command line>`), copy the WHOLE output and insert it below.
|
Add the `-Uv` flag to your command line you run yt-dlp with (`yt-dlp -Uv <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: |
|
||||||
@@ -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.10.22 (exe)
|
[debug] yt-dlp version 2021.12.25 (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.10.22)
|
yt-dlp is up to date (2021.12.25)
|
||||||
<more lines>
|
<more lines>
|
||||||
render: shell
|
render: shell
|
||||||
validations:
|
validations:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
name: Site feature request
|
name: Site feature request
|
||||||
description: Request a new functionality for a site
|
description: Request a new functionality for a supported site
|
||||||
labels: [triage, site-enhancement]
|
labels: [triage, site-enhancement]
|
||||||
body:
|
body:
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
@@ -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.10.22**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
|
- label: I've verified that I'm running yt-dlp version **2021.12.25**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
|
||||||
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
|
||||||
@@ -47,3 +47,26 @@ body:
|
|||||||
placeholder: WRITE DESCRIPTION HERE
|
placeholder: WRITE DESCRIPTION HERE
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: log
|
||||||
|
attributes:
|
||||||
|
label: Verbose log
|
||||||
|
description: |
|
||||||
|
Provide the complete verbose output of yt-dlp that demonstrates the need for the enhancement.
|
||||||
|
Add the `-Uv` flag to your command line you run yt-dlp with (`yt-dlp -Uv <your command line>`), copy the WHOLE output and insert it below.
|
||||||
|
It should look similar to this:
|
||||||
|
placeholder: |
|
||||||
|
[debug] Command-line config: ['-Uv', '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.25 (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.25)
|
||||||
|
<more lines>
|
||||||
|
render: shell
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
name: Bug report
|
name: Bug report
|
||||||
description: Report a bug unrelated to any particular site or extractor
|
description: Report a bug unrelated to any particular site or extractor
|
||||||
labels: [triage,bug]
|
labels: [triage, bug]
|
||||||
body:
|
body:
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: checklist
|
id: checklist
|
||||||
@@ -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.10.22**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
|
- label: I've verified that I'm running yt-dlp version **2021.12.25**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
|
||||||
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
|
||||||
@@ -37,20 +37,20 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Verbose log
|
label: Verbose log
|
||||||
description: |
|
description: |
|
||||||
Provide the complete verbose output of yt-dlp that clearly demonstrates the problem.
|
Provide the complete verbose output of yt-dlp **that clearly demonstrates the problem**.
|
||||||
Add the `-Uv` flag to your command line you run yt-dlp with (`yt-dlp -Uv <your command line>`), copy the WHOLE output and insert it below.
|
Add the `-Uv` flag to **your** command line you run yt-dlp with (`yt-dlp -Uv <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: |
|
||||||
[debug] Command-line config: ['-Uv', 'http://www.youtube.com/watch?v=BaW_jenozKc']
|
[debug] Command-line config: ['-Uv', 'http://www.youtube.com/watch?v=BaW_jenozKc']
|
||||||
[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.10.22 (exe)
|
[debug] yt-dlp version 2021.12.25 (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.10.22)
|
yt-dlp is up to date (2021.12.25)
|
||||||
<more lines>
|
<more lines>
|
||||||
render: shell
|
render: shell
|
||||||
validations:
|
validations:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Feature request request
|
name: Feature request
|
||||||
description: Request a new functionality unrelated to any particular site or extractor
|
description: Request a new functionality unrelated to any particular site or extractor
|
||||||
labels: [triage, enhancement]
|
labels: [triage, enhancement]
|
||||||
body:
|
body:
|
||||||
@@ -11,7 +11,7 @@ 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.10.22**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
|
- label: I've verified that I'm running yt-dlp version **2021.12.25**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update))
|
||||||
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
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ body:
|
|||||||
description: |
|
description: |
|
||||||
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
|
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
|
||||||
options:
|
options:
|
||||||
- label: I'm asking a question and not reporting a bug/feature request
|
- label: I'm asking a question and **not** reporting a bug/feature request
|
||||||
required: true
|
required: true
|
||||||
- label: I've looked through the [README](https://github.com/yt-dlp/yt-dlp#readme)
|
- label: I've looked through the [README](https://github.com/yt-dlp/yt-dlp#readme)
|
||||||
required: true
|
required: true
|
||||||
@@ -24,7 +24,29 @@ body:
|
|||||||
description: |
|
description: |
|
||||||
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
|
||||||
placeholder: WRITE QUESTION HERE
|
placeholder: WRITE QUESTION HERE
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: log
|
||||||
|
attributes:
|
||||||
|
label: Verbose log
|
||||||
|
description: |
|
||||||
|
If your question involes a yt-dlp command, provide the complete verbose output of that command.
|
||||||
|
Add the `-Uv` flag to **your** command line you run yt-dlp with (`yt-dlp -Uv <your command line>`), copy the WHOLE output and insert it below.
|
||||||
|
It should look similar to this:
|
||||||
|
placeholder: |
|
||||||
|
[debug] Command-line config: ['-Uv', '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
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ blank_issues_enabled: false
|
|||||||
contact_links:
|
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!
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
name: Broken site support
|
name: Broken site support
|
||||||
description: Report broken or misfunctioning site
|
description: Report broken or misfunctioning site
|
||||||
labels: [triage, extractor-bug]
|
labels: [triage, site-bug]
|
||||||
body:
|
body:
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: checklist
|
id: checklist
|
||||||
@@ -43,7 +43,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Verbose log
|
label: Verbose log
|
||||||
description: |
|
description: |
|
||||||
Provide the complete verbose output of yt-dlp that clearly demonstrates the problem.
|
Provide the complete verbose output of yt-dlp **that clearly demonstrates the problem**.
|
||||||
Add the `-Uv` flag to your command line you run yt-dlp with (`yt-dlp -Uv <your command line>`), copy the WHOLE output and insert it below.
|
Add the `-Uv` flag to your command line you run yt-dlp with (`yt-dlp -Uv <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: |
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ body:
|
|||||||
label: Example URLs
|
label: Example URLs
|
||||||
description: |
|
description: |
|
||||||
Provide all kinds of example URLs for which support should be added
|
Provide all kinds of example URLs for which support should be added
|
||||||
value: |
|
placeholder: |
|
||||||
- Single video: https://www.youtube.com/watch?v=BaW_jenozKc
|
- Single video: https://www.youtube.com/watch?v=BaW_jenozKc
|
||||||
- Single video: https://youtu.be/BaW_jenozKc
|
- Single video: https://youtu.be/BaW_jenozKc
|
||||||
- Playlist: https://www.youtube.com/playlist?list=PL4lCao7KL_QFVb7Iudeipvc2BCavECqzc
|
- Playlist: https://www.youtube.com/playlist?list=PL4lCao7KL_QFVb7Iudeipvc2BCavECqzc
|
||||||
@@ -54,7 +54,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Verbose log
|
label: Verbose log
|
||||||
description: |
|
description: |
|
||||||
Provide the complete verbose output using one of the example URLs provided above.
|
Provide the complete verbose output **using one of the example URLs provided above**.
|
||||||
Add the `-Uv` flag to your command line you run yt-dlp with (`yt-dlp -Uv <your command line>`), copy the WHOLE output and insert it below.
|
Add the `-Uv` flag to your command line you run yt-dlp with (`yt-dlp -Uv <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: |
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
name: Site feature request
|
name: Site feature request
|
||||||
description: Request a new functionality for a site
|
description: Request a new functionality for a supported site
|
||||||
labels: [triage, site-enhancement]
|
labels: [triage, site-enhancement]
|
||||||
body:
|
body:
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
@@ -47,3 +47,26 @@ body:
|
|||||||
placeholder: WRITE DESCRIPTION HERE
|
placeholder: WRITE DESCRIPTION HERE
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: log
|
||||||
|
attributes:
|
||||||
|
label: Verbose log
|
||||||
|
description: |
|
||||||
|
Provide the complete verbose output of yt-dlp that demonstrates the need for the enhancement.
|
||||||
|
Add the `-Uv` flag to your command line you run yt-dlp with (`yt-dlp -Uv <your command line>`), copy the WHOLE output and insert it below.
|
||||||
|
It should look similar to this:
|
||||||
|
placeholder: |
|
||||||
|
[debug] Command-line config: ['-Uv', '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 %(version)s (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 (%(version)s)
|
||||||
|
<more lines>
|
||||||
|
render: shell
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
name: Bug report
|
name: Bug report
|
||||||
description: Report a bug unrelated to any particular site or extractor
|
description: Report a bug unrelated to any particular site or extractor
|
||||||
labels: [triage,bug]
|
labels: [triage, bug]
|
||||||
body:
|
body:
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: checklist
|
id: checklist
|
||||||
@@ -37,8 +37,8 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Verbose log
|
label: Verbose log
|
||||||
description: |
|
description: |
|
||||||
Provide the complete verbose output of yt-dlp that clearly demonstrates the problem.
|
Provide the complete verbose output of yt-dlp **that clearly demonstrates the problem**.
|
||||||
Add the `-Uv` flag to your command line you run yt-dlp with (`yt-dlp -Uv <your command line>`), copy the WHOLE output and insert it below.
|
Add the `-Uv` flag to **your** command line you run yt-dlp with (`yt-dlp -Uv <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: |
|
||||||
[debug] Command-line config: ['-Uv', 'http://www.youtube.com/watch?v=BaW_jenozKc']
|
[debug] Command-line config: ['-Uv', 'http://www.youtube.com/watch?v=BaW_jenozKc']
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Feature request request
|
name: Feature request
|
||||||
description: Request a new functionality unrelated to any particular site or extractor
|
description: Request a new functionality unrelated to any particular site or extractor
|
||||||
labels: [triage, enhancement]
|
labels: [triage, enhancement]
|
||||||
body:
|
body:
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ body:
|
|||||||
description: |
|
description: |
|
||||||
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
|
Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp:
|
||||||
options:
|
options:
|
||||||
- label: I'm asking a question and not reporting a bug/feature request
|
- label: I'm asking a question and **not** reporting a bug/feature request
|
||||||
required: true
|
required: true
|
||||||
- label: I've looked through the [README](https://github.com/yt-dlp/yt-dlp#readme)
|
- label: I've looked through the [README](https://github.com/yt-dlp/yt-dlp#readme)
|
||||||
required: true
|
required: true
|
||||||
@@ -24,7 +24,29 @@ body:
|
|||||||
description: |
|
description: |
|
||||||
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
|
||||||
placeholder: WRITE QUESTION HERE
|
placeholder: WRITE QUESTION HERE
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: log
|
||||||
|
attributes:
|
||||||
|
label: Verbose log
|
||||||
|
description: |
|
||||||
|
If your question involes a yt-dlp command, provide the complete verbose output of that command.
|
||||||
|
Add the `-Uv` flag to **your** command line you run yt-dlp with (`yt-dlp -Uv <your command line>`), copy the WHOLE output and insert it below.
|
||||||
|
It should look similar to this:
|
||||||
|
placeholder: |
|
||||||
|
[debug] Command-line config: ['-Uv', '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
|
||||||
|
|||||||
+33
-22
@@ -1,14 +1,11 @@
|
|||||||
name: Build
|
name: Build
|
||||||
|
on: workflow_dispatch
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- release
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build_unix:
|
build_unix:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
|
version_suffix: ${{ steps.version_suffix.outputs.version_suffix }}
|
||||||
ytdlp_version: ${{ steps.bump_version.outputs.ytdlp_version }}
|
ytdlp_version: ${{ steps.bump_version.outputs.ytdlp_version }}
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
sha256_bin: ${{ steps.sha256_bin.outputs.sha256_bin }}
|
sha256_bin: ${{ steps.sha256_bin.outputs.sha256_bin }}
|
||||||
@@ -26,23 +23,32 @@ jobs:
|
|||||||
python-version: '3.8'
|
python-version: '3.8'
|
||||||
- name: Install packages
|
- name: Install packages
|
||||||
run: sudo apt-get -y install zip pandoc man
|
run: sudo apt-get -y install zip pandoc man
|
||||||
|
- name: Set version suffix
|
||||||
|
id: version_suffix
|
||||||
|
env:
|
||||||
|
PUSH_VERSION_COMMIT: ${{ secrets.PUSH_VERSION_COMMIT }}
|
||||||
|
if: "env.PUSH_VERSION_COMMIT == ''"
|
||||||
|
run: echo ::set-output name=version_suffix::$(date -u +"%H%M%S")
|
||||||
- name: Bump version
|
- name: Bump version
|
||||||
id: bump_version
|
id: bump_version
|
||||||
run: |
|
run: |
|
||||||
python devscripts/update-version.py
|
python devscripts/update-version.py ${{ steps.version_suffix.outputs.version_suffix }}
|
||||||
make issuetemplates
|
make issuetemplates
|
||||||
- name: Print version
|
- name: Push to release
|
||||||
run: echo "${{ steps.bump_version.outputs.ytdlp_version }}"
|
id: push_release
|
||||||
- name: Update master
|
|
||||||
id: push_update
|
|
||||||
run: |
|
run: |
|
||||||
git config --global user.email "${{ github.event.pusher.email }}"
|
git config --global user.name github-actions
|
||||||
git config --global user.name "${{ github.event.pusher.name }}"
|
git config --global user.email github-actions@example.com
|
||||||
git add -u
|
git add -u
|
||||||
git commit -m "[version] update" -m ":ci skip all"
|
git commit -m "[version] update" -m "Created by: ${{ github.event.sender.login }}" -m ":ci skip all"
|
||||||
git pull --rebase origin ${{ github.event.repository.master_branch }}
|
git push origin --force ${{ github.event.ref }}:release
|
||||||
git push origin ${{ github.event.ref }}:${{ github.event.repository.master_branch }}
|
|
||||||
echo ::set-output name=head_sha::$(git rev-parse HEAD)
|
echo ::set-output name=head_sha::$(git rev-parse HEAD)
|
||||||
|
- name: Update master
|
||||||
|
id: push_master
|
||||||
|
env:
|
||||||
|
PUSH_VERSION_COMMIT: ${{ secrets.PUSH_VERSION_COMMIT }}
|
||||||
|
if: "env.PUSH_VERSION_COMMIT != ''"
|
||||||
|
run: git push origin ${{ github.event.ref }}
|
||||||
- name: Get Changelog
|
- name: Get Changelog
|
||||||
id: get_changelog
|
id: get_changelog
|
||||||
run: |
|
run: |
|
||||||
@@ -113,14 +119,14 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
tag_name: ${{ steps.bump_version.outputs.ytdlp_version }}
|
tag_name: ${{ steps.bump_version.outputs.ytdlp_version }}
|
||||||
release_name: yt-dlp ${{ steps.bump_version.outputs.ytdlp_version }}
|
release_name: yt-dlp ${{ steps.bump_version.outputs.ytdlp_version }}
|
||||||
commitish: ${{ steps.push_update.outputs.head_sha }}
|
commitish: ${{ steps.push_release.outputs.head_sha }}
|
||||||
body: |
|
body: |
|
||||||
### Changelog:
|
#### [A description of the various files]((https://github.com/yt-dlp/yt-dlp#release-files)) are in the README
|
||||||
${{ env.changelog }}
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### See [this](https://github.com/yt-dlp/yt-dlp#release-files) for a description of the release files
|
### Changelog:
|
||||||
|
${{ env.changelog }}
|
||||||
draft: false
|
draft: false
|
||||||
prerelease: false
|
prerelease: false
|
||||||
- name: Upload yt-dlp Unix binary
|
- name: Upload yt-dlp Unix binary
|
||||||
@@ -155,10 +161,11 @@ 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 mutagen pycryptodomex websockets
|
/usr/bin/python3 -m pip install -U --user pip Pyinstaller==4.5.1 mutagen pycryptodomex websockets
|
||||||
- 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
|
||||||
@@ -232,7 +239,9 @@ jobs:
|
|||||||
pip install "https://yt-dlp.github.io/Pyinstaller-Builds/x86_64/pyinstaller-4.5.1-py3-none-any.whl" mutagen pycryptodomex websockets
|
pip install "https://yt-dlp.github.io/Pyinstaller-Builds/x86_64/pyinstaller-4.5.1-py3-none-any.whl" mutagen pycryptodomex websockets
|
||||||
- name: Bump version
|
- name: Bump version
|
||||||
id: bump_version
|
id: bump_version
|
||||||
run: python devscripts/update-version.py
|
env:
|
||||||
|
version_suffix: ${{ needs.build_unix.outputs.version_suffix }}
|
||||||
|
run: python devscripts/update-version.py ${{ env.version_suffix }}
|
||||||
- name: Build lazy extractors
|
- name: Build lazy extractors
|
||||||
id: lazy_extractors
|
id: lazy_extractors
|
||||||
run: python devscripts/make_lazy_extractors.py
|
run: python devscripts/make_lazy_extractors.py
|
||||||
@@ -319,7 +328,9 @@ jobs:
|
|||||||
pip install "https://yt-dlp.github.io/Pyinstaller-Builds/i686/pyinstaller-4.5.1-py3-none-any.whl" mutagen pycryptodomex websockets
|
pip install "https://yt-dlp.github.io/Pyinstaller-Builds/i686/pyinstaller-4.5.1-py3-none-any.whl" mutagen pycryptodomex websockets
|
||||||
- name: Bump version
|
- name: Bump version
|
||||||
id: bump_version
|
id: bump_version
|
||||||
run: python devscripts/update-version.py
|
env:
|
||||||
|
version_suffix: ${{ needs.build_unix.outputs.version_suffix }}
|
||||||
|
run: python devscripts/update-version.py ${{ env.version_suffix }}
|
||||||
- name: Build lazy extractors
|
- name: Build lazy extractors
|
||||||
id: lazy_extractors
|
id: lazy_extractors
|
||||||
run: python devscripts/make_lazy_extractors.py
|
run: python devscripts/make_lazy_extractors.py
|
||||||
|
|||||||
+35
-29
@@ -1,46 +1,53 @@
|
|||||||
# Config
|
# Config
|
||||||
*.conf
|
*.conf
|
||||||
*.spec
|
|
||||||
cookies
|
cookies
|
||||||
*cookies.txt
|
*cookies.txt
|
||||||
.netrc
|
.netrc
|
||||||
|
|
||||||
# Downloaded
|
# Downloaded
|
||||||
*.srt
|
*.annotations.xml
|
||||||
*.ttml
|
*.aria2
|
||||||
*.sbv
|
*.description
|
||||||
*.vtt
|
|
||||||
*.flv
|
|
||||||
*.mp4
|
|
||||||
*.m4a
|
|
||||||
*.m4v
|
|
||||||
*.mp3
|
|
||||||
*.3gp
|
|
||||||
*.webm
|
|
||||||
*.wav
|
|
||||||
*.ape
|
|
||||||
*.mkv
|
|
||||||
*.flac
|
|
||||||
*.avi
|
|
||||||
*.swf
|
|
||||||
*.part
|
|
||||||
*.part-*
|
|
||||||
*.ytdl
|
|
||||||
*.dump
|
*.dump
|
||||||
*.frag
|
*.frag
|
||||||
|
*.frag.aria2
|
||||||
*.frag.urls
|
*.frag.urls
|
||||||
*.aria2
|
|
||||||
*.swp
|
|
||||||
*.ogg
|
|
||||||
*.opus
|
|
||||||
*.info.json
|
*.info.json
|
||||||
*.live_chat.json
|
*.live_chat.json
|
||||||
*.jpg
|
*.part*
|
||||||
|
*.unknown_video
|
||||||
|
*.ytdl
|
||||||
|
.cache/
|
||||||
|
|
||||||
|
*.3gp
|
||||||
|
*.ape
|
||||||
|
*.avi
|
||||||
|
*.desktop
|
||||||
|
*.flac
|
||||||
|
*.flv
|
||||||
*.jpeg
|
*.jpeg
|
||||||
|
*.jpg
|
||||||
|
*.m4a
|
||||||
|
*.m4v
|
||||||
|
*.mhtml
|
||||||
|
*.mkv
|
||||||
|
*.mov
|
||||||
|
*.mp3
|
||||||
|
*.mp4
|
||||||
|
*.ogg
|
||||||
|
*.opus
|
||||||
*.png
|
*.png
|
||||||
|
*.sbv
|
||||||
|
*.srt
|
||||||
|
*.swf
|
||||||
|
*.swp
|
||||||
|
*.ttml
|
||||||
|
*.url
|
||||||
|
*.vtt
|
||||||
|
*.wav
|
||||||
|
*.webloc
|
||||||
|
*.webm
|
||||||
*.webp
|
*.webp
|
||||||
*.annotations.xml
|
|
||||||
*.description
|
|
||||||
|
|
||||||
# Allow config/media files in testdata
|
# Allow config/media files in testdata
|
||||||
!test/**
|
!test/**
|
||||||
@@ -79,7 +86,6 @@ README.txt
|
|||||||
*.1
|
*.1
|
||||||
*.bash-completion
|
*.bash-completion
|
||||||
*.fish
|
*.fish
|
||||||
*.exe
|
|
||||||
*.tar.gz
|
*.tar.gz
|
||||||
*.zsh
|
*.zsh
|
||||||
*.spec
|
*.spec
|
||||||
|
|||||||
+14
-6
@@ -10,6 +10,7 @@
|
|||||||
- [Does the issue involve one problem, and one problem only?](#does-the-issue-involve-one-problem-and-one-problem-only)
|
- [Does the issue involve one problem, and one problem only?](#does-the-issue-involve-one-problem-and-one-problem-only)
|
||||||
- [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)
|
||||||
- [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)
|
||||||
@@ -105,7 +106,7 @@ Only post features that you (or an incapacitated friend you can personally talk
|
|||||||
|
|
||||||
### Is your question about yt-dlp?
|
### Is your question about yt-dlp?
|
||||||
|
|
||||||
Some bug reports are completely unrelated to yt-dlp and relate to a different, or even the reporter's own, application. Please make sure that you are actually using yt-dlp. If you are using a UI for yt-dlp, report the bug to the maintainer of the actual application providing the UI. On the other hand, if your UI for yt-dlp fails in some way you believe is related to yt-dlp, by all means, go ahead and report the bug.
|
Some bug reports are completely unrelated to yt-dlp and relate to a different, or even the reporter's own, application. Please make sure that you are actually using yt-dlp. If you are using a UI for yt-dlp, report the bug to the maintainer of the actual application providing the UI. In general, if you are unable to provide the verbose log, you should not be opening the issue here.
|
||||||
|
|
||||||
If the issue is with `youtube-dl` (the upstream fork of yt-dlp) and not with yt-dlp, the issue should be raised in the youtube-dl project.
|
If the issue is with `youtube-dl` (the upstream fork of yt-dlp) and not with yt-dlp, the issue should be raised in the youtube-dl project.
|
||||||
|
|
||||||
@@ -117,7 +118,7 @@ By sharing an account with anyone, you agree to bear all risks associated with i
|
|||||||
|
|
||||||
While these steps won't necessarily ensure that no misuse of the account takes place, these are still some good practices to follow.
|
While these steps won't necessarily ensure that no misuse of the account takes place, these are still some good practices to follow.
|
||||||
|
|
||||||
- Look for people with `Member` or `Contributor` tag on their messages.
|
- Look for people with `Member` (maintainers of the project) or `Contributor` (people who have previously contributed code) tag on their messages.
|
||||||
- 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.
|
||||||
|
|
||||||
@@ -148,7 +149,7 @@ If you want to create a build of yt-dlp yourself, you can follow the instruction
|
|||||||
|
|
||||||
Before you start writing code for implementing a new feature, open an issue explaining your feature request and atleast one use case. This allows the maintainers to decide whether such a feature is desired for the project in the first place, and will provide an avenue to discuss some implementation details. If you open a pull request for a new feature without discussing with us first, do not be surprised when we ask for large changes to the code, or even reject it outright.
|
Before you start writing code for implementing a new feature, open an issue explaining your feature request and atleast one use case. This allows the maintainers to decide whether such a feature is desired for the project in the first place, and will provide an avenue to discuss some implementation details. If you open a pull request for a new feature without discussing with us first, do not be surprised when we ask for large changes to the code, or even reject it outright.
|
||||||
|
|
||||||
The same applies for overarching changes to the architecture, documentation or code style
|
The same applies for changes to the documentation, code style, or overarching changes to the architecture
|
||||||
|
|
||||||
|
|
||||||
## Adding support for a new site
|
## Adding support for a new site
|
||||||
@@ -209,13 +210,13 @@ 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`. 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 purticular 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):
|
||||||
|
|
||||||
$ flake8 yt_dlp/extractor/yourextractor.py
|
$ flake8 yt_dlp/extractor/yourextractor.py
|
||||||
|
|
||||||
1. Make sure your code works under all [Python](https://www.python.org/) versions supported by yt-dlp, namely CPython and PyPy for Python 3.6 and above. Backward compatability is not required for even older versions of Python.
|
1. Make sure your code works under all [Python](https://www.python.org/) versions supported by yt-dlp, namely CPython and PyPy for Python 3.6 and above. Backward compatibility is not required for even older versions of Python.
|
||||||
1. When the tests pass, [add](https://git-scm.com/docs/git-add) the new files, [commit](https://git-scm.com/docs/git-commit) them and [push](https://git-scm.com/docs/git-push) the result, like this:
|
1. When the tests pass, [add](https://git-scm.com/docs/git-add) the new files, [commit](https://git-scm.com/docs/git-commit) them and [push](https://git-scm.com/docs/git-push) the result, like this:
|
||||||
|
|
||||||
$ git add yt_dlp/extractor/extractors.py
|
$ git add yt_dlp/extractor/extractors.py
|
||||||
@@ -227,6 +228,13 @@ After you have ensured this site is distributing its content legally, you can fo
|
|||||||
|
|
||||||
In any case, thank you very much for your contributions!
|
In any case, thank you very much for your contributions!
|
||||||
|
|
||||||
|
**Tip:** To test extractors that require login information, create a file `test/local_parameters.json` and add `"usenetrc": true` or your username and password in it:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"username": "your user name",
|
||||||
|
"password": "your password"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## yt-dlp coding conventions
|
## yt-dlp coding conventions
|
||||||
|
|
||||||
@@ -243,7 +251,7 @@ 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 compatability 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, 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.
|
||||||
|
|
||||||
[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.
|
||||||
|
|
||||||
|
|||||||
@@ -129,3 +129,51 @@ Bojidarist
|
|||||||
nixklai
|
nixklai
|
||||||
smplayer-dev
|
smplayer-dev
|
||||||
Zirro
|
Zirro
|
||||||
|
CrypticSignal
|
||||||
|
flashdagger
|
||||||
|
fractalf
|
||||||
|
frafra
|
||||||
|
kaz-us
|
||||||
|
ozburo
|
||||||
|
rhendric
|
||||||
|
sdomi
|
||||||
|
selfisekai
|
||||||
|
stanoarn
|
||||||
|
0xA7404A/Aurora
|
||||||
|
4a1e2y5
|
||||||
|
aarubui
|
||||||
|
chio0hai
|
||||||
|
cntrl-s
|
||||||
|
Deer-Spangle
|
||||||
|
DEvmIb
|
||||||
|
Grabien
|
||||||
|
j54vc1bk
|
||||||
|
mpeter50
|
||||||
|
mrpapersonic
|
||||||
|
pabs3
|
||||||
|
staubichsauger
|
||||||
|
xenova
|
||||||
|
Yakabuff
|
||||||
|
zulaport
|
||||||
|
ehoogeveen-medweb
|
||||||
|
PilzAdam
|
||||||
|
zmousm
|
||||||
|
iw0nderhow
|
||||||
|
unit193
|
||||||
|
TwoThousandHedgehogs
|
||||||
|
Jertzukka
|
||||||
|
cypheron
|
||||||
|
Hyeeji
|
||||||
|
bwildenhain
|
||||||
|
C0D3D3V
|
||||||
|
kebianizao
|
||||||
|
Lapin0t
|
||||||
|
abdullah-if
|
||||||
|
DavidSkrundz
|
||||||
|
mkubecek
|
||||||
|
raleeper
|
||||||
|
YuenSzeHong
|
||||||
|
Sematre
|
||||||
|
jaller94
|
||||||
|
r5d
|
||||||
|
julien-hadleyjack
|
||||||
|
|||||||
+400
-8
@@ -5,15 +5,305 @@
|
|||||||
|
|
||||||
* Run `make doc`
|
* Run `make doc`
|
||||||
* Update Changelog.md and CONTRIBUTORS
|
* Update Changelog.md and CONTRIBUTORS
|
||||||
* Change "Merged with ytdl" version in Readme.md if needed
|
* Change "Based on ytdl" version in Readme.md if needed
|
||||||
* Add new/fixed extractors in "new features" section of Readme.md
|
* Commit as `Release <version>` and push to master
|
||||||
* Commit as `Release <version>`
|
* Dispatch the workflow https://github.com/yt-dlp/yt-dlp/actions/workflows/build.yml on master
|
||||||
* Push to origin/release using `git push origin master:release`
|
|
||||||
build task will now run
|
|
||||||
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|
||||||
|
### 2021.12.25
|
||||||
|
|
||||||
|
* [dash,youtube] **Download live from start to end** by [nao20010128nao](https://github.com/nao20010128nao), [pukkandan](https://github.com/pukkandan)
|
||||||
|
* Add option `--live-from-start` to enable downloading live videos from start
|
||||||
|
* Add key `is_from_start` in formats to identify formats (of live videos) that downloads from start
|
||||||
|
* [dash] Create protocol `http_dash_segments_generator` that allows a function to be passed instead of fragments
|
||||||
|
* [fragment] Allow multiple live dash formats to download simultaneously
|
||||||
|
* [youtube] Implement fragment re-fetching for the live dash formats
|
||||||
|
* [youtube] Re-extract dash manifest every 5 hours (manifest expires in 6hrs)
|
||||||
|
* [postprocessor/ffmpeg] Add `FFmpegFixupDuplicateMoovPP` to fixup duplicated moov atoms
|
||||||
|
* Known issues:
|
||||||
|
* Ctrl+C doesn't work on Windows when downloading multiple formats
|
||||||
|
* If video becomes private, download hangs
|
||||||
|
* [SponsorBlock] Add `Filler` and `Highlight` categories by [nihil-admirari](https://github.com/nihil-admirari), [pukkandan](https://github.com/pukkandan)
|
||||||
|
* Change `--sponsorblock-cut all` to `--sponsorblock-cut default` if you do not want filler sections to be removed
|
||||||
|
* Add field `webpage_url_domain`
|
||||||
|
* Add interactive format selection with `-f -`
|
||||||
|
* Add option `--file-access-retries` by [ehoogeveen-medweb](https://github.com/ehoogeveen-medweb)
|
||||||
|
* [outtmpl] Add alternate forms `S`, `D` and improve `id` detection
|
||||||
|
* [outtmpl] Add operator `&` for replacement text by [PilzAdam](https://github.com/PilzAdam)
|
||||||
|
* [EmbedSubtitle] Disable duration check temporarily
|
||||||
|
* [extractor] Add `_search_nuxt_data` by [nao20010128nao](https://github.com/nao20010128nao)
|
||||||
|
* [extractor] Ignore errors in comment extraction when `-i` is given
|
||||||
|
* [extractor] Standardize `_live_title`
|
||||||
|
* [FormatSort] Prevent incorrect deprecation warning
|
||||||
|
* [generic] Extract m3u8 formats from JSON-LD
|
||||||
|
* [postprocessor/ffmpeg] Always add `faststart`
|
||||||
|
* [utils] Fix parsing `YYYYMMDD` dates in Nov/Dec by [wlritchi](https://github.com/wlritchi)
|
||||||
|
* [utils] Improve `parse_count`
|
||||||
|
* [utils] Update `std_headers` by [kikuyan](https://github.com/kikuyan), [fstirlitz](https://github.com/fstirlitz)
|
||||||
|
* [lazy_extractors] Fix for search IEs
|
||||||
|
* [extractor] Support default implicit graph in JSON-LD by [zmousm](https://github.com/zmousm)
|
||||||
|
* Allow `--no-write-thumbnail` to override `--write-all-thumbnail`
|
||||||
|
* Fix `--throttled-rate`
|
||||||
|
* Fix control characters being printed to `--console-title`
|
||||||
|
* Fix PostProcessor hooks not registered for some PPs
|
||||||
|
* Pre-process when using `--flat-playlist`
|
||||||
|
* Remove known invalid thumbnails from `info_dict`
|
||||||
|
* Add warning when using `-f best`
|
||||||
|
* Use `parse_duration` for `--wait-for-video` and some minor fix
|
||||||
|
* [test/download] Add more fields
|
||||||
|
* [test/download] Ignore field `webpage_url_domain` by [std-move](https://github.com/std-move)
|
||||||
|
* [compat] Suppress errors in enabling VT mode
|
||||||
|
* [docs] Improve manpage format by [iw0nderhow](https://github.com/iw0nderhow), [pukkandan](https://github.com/pukkandan)
|
||||||
|
* [docs,cleanup] Minor fixes and cleanup
|
||||||
|
* [cleanup] Fix some typos by [unit193](https://github.com/unit193)
|
||||||
|
* [ABC:iview] Add show extractor by [pabs3](https://github.com/pabs3)
|
||||||
|
* [dropout] Add extractor by [TwoThousandHedgehogs](https://github.com/TwoThousandHedgehogs), [pukkandan](https://github.com/pukkandan)
|
||||||
|
* [GameJolt] Add extractors by [MinePlayersPE](https://github.com/MinePlayersPE)
|
||||||
|
* [gofile] Add extractor by [Jertzukka](https://github.com/Jertzukka), [Ashish0804](https://github.com/Ashish0804)
|
||||||
|
* [hse] Add extractors by [cypheron](https://github.com/cypheron), [pukkandan](https://github.com/pukkandan)
|
||||||
|
* [NateTV] Add NateIE and NateProgramIE by [Ashish0804](https://github.com/Ashish0804), [Hyeeji](https://github.com/Hyeeji)
|
||||||
|
* [OpenCast] Add extractors by [bwildenhain](https://github.com/bwildenhain), [C0D3D3V](https://github.com/C0D3D3V)
|
||||||
|
* [rtve] Add `RTVEAudioIE` by [kebianizao](https://github.com/kebianizao)
|
||||||
|
* [Rutube] Add RutubeChannelIE by [Ashish0804](https://github.com/Ashish0804)
|
||||||
|
* [skeb] Add extractor by [nao20010128nao](https://github.com/nao20010128nao)
|
||||||
|
* [soundcloud] Add related tracks extractor by [Lapin0t](https://github.com/Lapin0t)
|
||||||
|
* [toggo] Add extractor by [nyuszika7h](https://github.com/nyuszika7h)
|
||||||
|
* [TrueID] Add extractor by [MinePlayersPE](https://github.com/MinePlayersPE)
|
||||||
|
* [audiomack] Update album and song VALID_URL by [abdullah-if](https://github.com/abdullah-if), [dirkf](https://github.com/dirkf)
|
||||||
|
* [CBC Gem] Extract 1080p formats by [DavidSkrundz](https://github.com/DavidSkrundz)
|
||||||
|
* [ceskatelevize] Fetch iframe from nextJS data by [mkubecek](https://github.com/mkubecek)
|
||||||
|
* [crackle] Look for non-DRM formats by [raleeper](https://github.com/raleeper)
|
||||||
|
* [dplay] Temporary fix for `discoveryplus.com/it`
|
||||||
|
* [DiscoveryPlusShowBaseIE] yield actual video id by [Ashish0804](https://github.com/Ashish0804)
|
||||||
|
* [Facebook] Handle redirect URLs
|
||||||
|
* [fujitv] Extract 1080p from `tv_android` m3u8 by [YuenSzeHong](https://github.com/YuenSzeHong)
|
||||||
|
* [gronkh] Support new URL pattern by [Sematre](https://github.com/Sematre)
|
||||||
|
* [instagram] Expand valid URL by [u-spec-png](https://github.com/u-spec-png)
|
||||||
|
* [Instagram] Try bypassing login wall with embed page by [MinePlayersPE](https://github.com/MinePlayersPE)
|
||||||
|
* [Jamendo] Fix use of `_VALID_URL_RE` by [jaller94](https://github.com/jaller94)
|
||||||
|
* [LBRY] Support livestreams by [Ashish0804](https://github.com/Ashish0804), [pukkandan](https://github.com/pukkandan)
|
||||||
|
* [NJPWWorld] Extract formats from m3u8 by [aarubui](https://github.com/aarubui)
|
||||||
|
* [NovaEmbed] update player regex by [std-move](https://github.com/std-move)
|
||||||
|
* [npr] Make SMIL extraction non-fatal by [r5d](https://github.com/r5d)
|
||||||
|
* [ntvcojp] Extract NUXT data by [nao20010128nao](https://github.com/nao20010128nao)
|
||||||
|
* [ok.ru] add mobile fallback by [nao20010128nao](https://github.com/nao20010128nao)
|
||||||
|
* [olympics] Add uploader and cleanup by [u-spec-png](https://github.com/u-spec-png)
|
||||||
|
* [ondemandkorea] Update `jw_config` regex by [julien-hadleyjack](https://github.com/julien-hadleyjack)
|
||||||
|
* [PlutoTV] Expand `_VALID_URL`
|
||||||
|
* [RaiNews] Fix extractor by [nixxo](https://github.com/nixxo)
|
||||||
|
* [RCTIPlusSeries] Lazy extraction and video type selection by [MinePlayersPE](https://github.com/MinePlayersPE)
|
||||||
|
* [redtube] Handle formats delivered inside a JSON by [dirkf](https://github.com/dirkf), [nixxo](https://github.com/nixxo)
|
||||||
|
* [SonyLiv] Add OTP login support by [Ashish0804](https://github.com/Ashish0804)
|
||||||
|
* [Steam] Fix extractor by [u-spec-png](https://github.com/u-spec-png)
|
||||||
|
* [TikTok] Pass cookies to mobile API by [MinePlayersPE](https://github.com/MinePlayersPE)
|
||||||
|
* [trovo] Fix inheritance of `TrovoChannelBaseIE`
|
||||||
|
* [TVer] Extract better thumbnails by [YuenSzeHong](https://github.com/YuenSzeHong)
|
||||||
|
* [vimeo] Extract chapters
|
||||||
|
* [web.archive:youtube] Improve metadata extraction by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
* [youtube:comments] Add more options for limiting number of comments extracted by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
* [youtube:tab] Extract more metadata from feeds/channels/playlists by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
* [youtube:tab] Extract video thumbnails from playlist by [coletdjnz](https://github.com/coletdjnz), [pukkandan](https://github.com/pukkandan)
|
||||||
|
* [youtube:tab] Ignore query when redirecting channel to playlist and cleanup of related code Closes #2046
|
||||||
|
* [youtube] Fix `ytsearchdate`
|
||||||
|
* [zdf] Support videos with different ptmd location by [iw0nderhow](https://github.com/iw0nderhow)
|
||||||
|
* [zee5] Support /episodes in URL
|
||||||
|
|
||||||
|
|
||||||
|
### 2021.12.01
|
||||||
|
|
||||||
|
* **Add option `--wait-for-video` to wait for scheduled streams**
|
||||||
|
* Add option `--break-per-input` to apply --break-on... to each input URL
|
||||||
|
* Add option `--embed-info-json` to embed info.json in mkv
|
||||||
|
* Add compat-option `embed-metadata`
|
||||||
|
* Allow using a custom format selector through API
|
||||||
|
* [AES] Add ECB mode by [nao20010128nao](https://github.com/nao20010128nao)
|
||||||
|
* [build] Fix MacOS Build
|
||||||
|
* [build] Save Git HEAD at release alongside version info
|
||||||
|
* [build] Use `workflow_dispatch` for release
|
||||||
|
* [downloader/ffmpeg] Fix for direct videos inside mpd manifests
|
||||||
|
* [downloader] Add colors to download progress
|
||||||
|
* [EmbedSubtitles] Slightly relax duration check and related cleanup
|
||||||
|
* [ExtractAudio] Fix conversion to `wav` and `vorbis`
|
||||||
|
* [ExtractAudio] Support `alac`
|
||||||
|
* [extractor] Extract `average_rating` from JSON-LD
|
||||||
|
* [FixupM3u8] Fixup MPEG-TS in MP4 container
|
||||||
|
* [generic] Support mpd manifests without extension by [shirt](https://github.com/shirt-dev)
|
||||||
|
* [hls] Better FairPlay DRM detection by [nyuszika7h](https://github.com/nyuszika7h)
|
||||||
|
* [jsinterp] Fix splice to handle float (for youtube js player f1ca6900)
|
||||||
|
* [utils] Allow alignment in `render_table` and add tests
|
||||||
|
* [utils] Fix `PagedList`
|
||||||
|
* [utils] Fix error when copying `LazyList`
|
||||||
|
* Clarify video/audio-only formats in -F
|
||||||
|
* Ensure directory exists when checking formats
|
||||||
|
* Ensure path for link files exists by [Zirro](https://github.com/Zirro)
|
||||||
|
* Ensure same config file is not loaded multiple times
|
||||||
|
* Fix `postprocessor_hooks`
|
||||||
|
* Fix `--break-on-archive` when pre-checking
|
||||||
|
* Fix `--check-formats` for `mhtml`
|
||||||
|
* Fix `--load-info-json` of playlists with failed entries
|
||||||
|
* Fix `--trim-filename` when filename has `.`
|
||||||
|
* Fix bug in parsing `--add-header`
|
||||||
|
* Fix error in `report_unplayable_conflict` by [shirt](https://github.com/shirt-dev)
|
||||||
|
* Fix writing playlist infojson with `--no-clean-infojson`
|
||||||
|
* Validate --get-bypass-country
|
||||||
|
* [blogger] Add extractor by [pabs3](https://github.com/pabs3)
|
||||||
|
* [breitbart] Add extractor by [Grabien](https://github.com/Grabien)
|
||||||
|
* [CableAV] Add extractor by [j54vc1bk](https://github.com/j54vc1bk)
|
||||||
|
* [CanalAlpha] Add extractor by [Ashish0804](https://github.com/Ashish0804)
|
||||||
|
* [CozyTV] Add extractor by [Ashish0804](https://github.com/Ashish0804)
|
||||||
|
* [CPTwentyFour] Add extractor by [Ashish0804](https://github.com/Ashish0804)
|
||||||
|
* [DiscoveryPlus] Add `DiscoveryPlusItalyShowIE` by [Ashish0804](https://github.com/Ashish0804)
|
||||||
|
* [ESPNCricInfo] Add extractor by [Ashish0804](https://github.com/Ashish0804)
|
||||||
|
* [LinkedIn] Add extractor by [u-spec-png](https://github.com/u-spec-png)
|
||||||
|
* [mixch] Add extractor by [nao20010128nao](https://github.com/nao20010128nao)
|
||||||
|
* [nebula] Add `NebulaCollectionIE` and rewrite extractor by [hheimbuerger](https://github.com/hheimbuerger)
|
||||||
|
* [OneFootball] Add extractor by [Ashish0804](https://github.com/Ashish0804)
|
||||||
|
* [peer.tv] Add extractor by [u-spec-png](https://github.com/u-spec-png)
|
||||||
|
* [radiozet] Add extractor by [0xA7404A](https://github.com/0xA7404A) (Aurora)
|
||||||
|
* [redgifs] Add extractor by [chio0hai](https://github.com/chio0hai)
|
||||||
|
* [RedGifs] Add Search and User extractors by [Deer-Spangle](https://github.com/Deer-Spangle)
|
||||||
|
* [rtrfm] Add extractor by [pabs3](https://github.com/pabs3)
|
||||||
|
* [Streamff] Add extractor by [cntrl-s](https://github.com/cntrl-s)
|
||||||
|
* [Stripchat] Add extractor by [zulaport](https://github.com/zulaport)
|
||||||
|
* [Aljazeera] Fix extractor by [u-spec-png](https://github.com/u-spec-png)
|
||||||
|
* [AmazonStoreIE] Fix regex to not match vdp urls by [Ashish0804](https://github.com/Ashish0804)
|
||||||
|
* [ARDBetaMediathek] Handle new URLs
|
||||||
|
* [bbc] Get all available formats by [nyuszika7h](https://github.com/nyuszika7h)
|
||||||
|
* [Bilibili] Fix title extraction by [u-spec-png](https://github.com/u-spec-png)
|
||||||
|
* [CBC Gem] Fix for shows that don't have all seasons by [makeworld-the-better-one](https://github.com/makeworld-the-better-one)
|
||||||
|
* [curiositystream] Add more metadata
|
||||||
|
* [CuriosityStream] Fix series
|
||||||
|
* [DiscoveryPlus] Rewrite extractors by [Ashish0804](https://github.com/Ashish0804), [pukkandan](https://github.com/pukkandan)
|
||||||
|
* [HotStar] Set language field from tags by [Ashish0804](https://github.com/Ashish0804)
|
||||||
|
* [instagram, cleanup] Refactor extractors
|
||||||
|
* [Instagram] Display more login errors by [MinePlayersPE](https://github.com/MinePlayersPE)
|
||||||
|
* [itv] Fix extractor by [staubichsauger](https://github.com/staubichsauger), [pukkandan](https://github.com/pukkandan)
|
||||||
|
* [mediaklikk] Expand valid URL
|
||||||
|
* [MTV] Improve mgid extraction by [Sipherdrakon](https://github.com/Sipherdrakon), [kikuyan](https://github.com/kikuyan)
|
||||||
|
* [nexx] Better error message for unsupported format
|
||||||
|
* [NovaEmbed] Fix extractor by [pukkandan](https://github.com/pukkandan), [std-move](https://github.com/std-move)
|
||||||
|
* [PatreonUser] Do not capture RSS URLs
|
||||||
|
* [Reddit] Add support for 1080p videos by [xenova](https://github.com/xenova)
|
||||||
|
* [RoosterTeethSeries] Fix for multiple pages by [MinePlayersPE](https://github.com/MinePlayersPE)
|
||||||
|
* [sbs] Fix for movies and livestreams
|
||||||
|
* [Senate.gov] Add SenateGovIE and fix SenateISVPIE by [Grabien](https://github.com/Grabien), [pukkandan](https://github.com/pukkandan)
|
||||||
|
* [soundcloud:search] Fix pagination
|
||||||
|
* [tiktok:user] Set `webpage_url` correctly
|
||||||
|
* [Tokentube] Fix description by [u-spec-png](https://github.com/u-spec-png)
|
||||||
|
* [trovo] Fix extractor by [nyuszika7h](https://github.com/nyuszika7h)
|
||||||
|
* [tv2] Expand valid URL
|
||||||
|
* [Tvplayhome] Fix extractor by [pukkandan](https://github.com/pukkandan), [18928172992817182](https://github.com/18928172992817182)
|
||||||
|
* [Twitch:vod] Add chapters by [mpeter50](https://github.com/mpeter50)
|
||||||
|
* [twitch:vod] Extract live status by [DEvmIb](https://github.com/DEvmIb)
|
||||||
|
* [VidLii] Add 720p support by [mrpapersonic](https://github.com/mrpapersonic)
|
||||||
|
* [vimeo] Add fallback for config URL
|
||||||
|
* [vimeo] Sort http formats higher
|
||||||
|
* [WDR] Expand valid URL
|
||||||
|
* [willow] Add extractor by [aarubui](https://github.com/aarubui)
|
||||||
|
* [xvideos] Detect embed URLs by [4a1e2y5](https://github.com/4a1e2y5)
|
||||||
|
* [xvideos] Fix extractor by [Yakabuff](https://github.com/Yakabuff)
|
||||||
|
* [youtube, cleanup] Reorganize Tab and Search extractor inheritances
|
||||||
|
* [youtube:search_url] Add playlist/channel support
|
||||||
|
* [youtube] Add `default` player client by [coletdjnz](https://github.com/coletdjnz)
|
||||||
|
* [youtube] Add storyboard formats
|
||||||
|
* [youtube] Decrypt n-sig for URLs with `ratebypass`
|
||||||
|
* [youtube] Minor improvement to format sorting
|
||||||
|
* [cleanup] Add deprecation warnings
|
||||||
|
* [cleanup] Refactor `JSInterpreter._seperate`
|
||||||
|
* [Cleanup] Remove some unnecessary groups in regexes by [Ashish0804](https://github.com/Ashish0804)
|
||||||
|
* [cleanup] Misc cleanup
|
||||||
|
|
||||||
|
|
||||||
|
### 2021.11.10.1
|
||||||
|
|
||||||
|
* Temporarily disable MacOS Build
|
||||||
|
|
||||||
|
### 2021.11.10
|
||||||
|
|
||||||
|
* [youtube] **Fix throttling by decrypting n-sig**
|
||||||
|
* Merging extractors from [haruhi-dl](https://git.sakamoto.pl/laudom/haruhi-dl) by [selfisekai](https://github.com/selfisekai)
|
||||||
|
* [extractor] Add `_search_nextjs_data`
|
||||||
|
* [tvp] Fix extractors
|
||||||
|
* [tvp] Add TVPStreamIE
|
||||||
|
* [wppilot] Add extractors
|
||||||
|
* [polskieradio] Add extractors
|
||||||
|
* [radiokapital] Add extractors
|
||||||
|
* [polsatgo] Add extractor by [selfisekai](https://github.com/selfisekai), [sdomi](https://github.com/sdomi)
|
||||||
|
* Separate `--check-all-formats` from `--check-formats`
|
||||||
|
* Approximate filesize from bitrate
|
||||||
|
* Don't create console in `windows_enable_vt_mode`
|
||||||
|
* Fix bug in `--load-infojson` of playlists
|
||||||
|
* [minicurses] Add colors to `-F` and standardize color-printing code
|
||||||
|
* [outtmpl] Add type `link` for internet shortcut files
|
||||||
|
* [outtmpl] Add alternate forms for `q` and `j`
|
||||||
|
* [outtmpl] Do not traverse `None`
|
||||||
|
* [fragment] Fix progress display in fragmented downloads
|
||||||
|
* [downloader/ffmpeg] Fix vtt download with ffmpeg
|
||||||
|
* [ffmpeg] Detect presence of setts and libavformat version
|
||||||
|
* [ExtractAudio] Rescale `--audio-quality` correctly by [CrypticSignal](https://github.com/CrypticSignal), [pukkandan](https://github.com/pukkandan)
|
||||||
|
* [ExtractAudio] Use `libfdk_aac` if available by [CrypticSignal](https://github.com/CrypticSignal)
|
||||||
|
* [FormatSort] `eac3` is better than `ac3`
|
||||||
|
* [FormatSort] Fix some fields' defaults
|
||||||
|
* [generic] Detect more json_ld
|
||||||
|
* [generic] parse jwplayer with only the json URL
|
||||||
|
* [extractor] Add keyword automatically to SearchIE descriptions
|
||||||
|
* [extractor] Fix some errors being converted to `ExtractorError`
|
||||||
|
* [utils] Add `join_nonempty`
|
||||||
|
* [utils] Add `jwt_decode_hs256` by [Ashish0804](https://github.com/Ashish0804)
|
||||||
|
* [utils] Create `DownloadCancelled` exception
|
||||||
|
* [utils] Parse `vp09` as vp9
|
||||||
|
* [utils] Sanitize URL when determining protocol
|
||||||
|
* [test/download] Fallback test to `bv`
|
||||||
|
* [docs] Minor documentation improvements
|
||||||
|
* [cleanup] Improvements to error and debug messages
|
||||||
|
* [cleanup] Minor fixes and cleanup
|
||||||
|
* [3speak] Add extractors by [Ashish0804](https://github.com/Ashish0804)
|
||||||
|
* [AmazonStore] Add extractor by [Ashish0804](https://github.com/Ashish0804)
|
||||||
|
* [Gab] Add extractor by [u-spec-png](https://github.com/u-spec-png)
|
||||||
|
* [mediaset] Add playlist support by [nixxo](https://github.com/nixxo)
|
||||||
|
* [MLSScoccer] Add extractor by [Ashish0804](https://github.com/Ashish0804)
|
||||||
|
* [N1] Add support for nova.rs by [u-spec-png](https://github.com/u-spec-png)
|
||||||
|
* [PlanetMarathi] Add extractor by [Ashish0804](https://github.com/Ashish0804)
|
||||||
|
* [RaiplayRadio] Add extractors by [frafra](https://github.com/frafra)
|
||||||
|
* [roosterteeth] Add series extractor
|
||||||
|
* [sky] Add `SkyNewsStoryIE` by [ajj8](https://github.com/ajj8)
|
||||||
|
* [youtube] Fix sorting for some videos
|
||||||
|
* [youtube] Populate `thumbnail` with the best "known" thumbnail
|
||||||
|
* [youtube] Refactor itag processing
|
||||||
|
* [youtube] Remove unnecessary no-playlist warning
|
||||||
|
* [youtube:tab] Add Invidious list for playlists/channels by [rhendric](https://github.com/rhendric)
|
||||||
|
* [Bilibili:comments] Fix infinite loop by [u-spec-png](https://github.com/u-spec-png)
|
||||||
|
* [ceskatelevize] Fix extractor by [flashdagger](https://github.com/flashdagger)
|
||||||
|
* [Coub] Fix media format identification by [wlritchi](https://github.com/wlritchi)
|
||||||
|
* [crunchyroll] Add extractor-args `language` and `hardsub`
|
||||||
|
* [DiscoveryPlus] Allow language codes in URL
|
||||||
|
* [imdb] Fix thumbnail by [ozburo](https://github.com/ozburo)
|
||||||
|
* [instagram] Add IOS URL support by [u-spec-png](https://github.com/u-spec-png)
|
||||||
|
* [instagram] Improve login code by [u-spec-png](https://github.com/u-spec-png)
|
||||||
|
* [Instagram] Improve metadata extraction by [u-spec-png](https://github.com/u-spec-png)
|
||||||
|
* [iPrima] Fix extractor by [stanoarn](https://github.com/stanoarn)
|
||||||
|
* [itv] Add support for ITV News by [ajj8](https://github.com/ajj8)
|
||||||
|
* [la7] Fix extractor by [nixxo](https://github.com/nixxo)
|
||||||
|
* [linkedin] Don't login multiple times
|
||||||
|
* [mtv] Fix some videos by [Sipherdrakon](https://github.com/Sipherdrakon)
|
||||||
|
* [Newgrounds] Fix description by [u-spec-png](https://github.com/u-spec-png)
|
||||||
|
* [Nrk] Minor fixes by [fractalf](https://github.com/fractalf)
|
||||||
|
* [Olympics] Fix extractor by [u-spec-png](https://github.com/u-spec-png)
|
||||||
|
* [piksel] Fix sorting
|
||||||
|
* [twitter] Do not sort by codec
|
||||||
|
* [viewlift] Add cookie-based login and series support by [Ashish0804](https://github.com/Ashish0804), [pukkandan](https://github.com/pukkandan)
|
||||||
|
* [vimeo] Detect source extension and misc cleanup by [flashdagger](https://github.com/flashdagger)
|
||||||
|
* [vimeo] Fix ondemand videos and direct URLs with hash
|
||||||
|
* [vk] Fix login and add subtitles by [kaz-us](https://github.com/kaz-us)
|
||||||
|
* [VLive] Add upload_date and thumbnail by [Ashish0804](https://github.com/Ashish0804)
|
||||||
|
* [VRT] Fix login by [pgaig](https://github.com/pgaig)
|
||||||
|
* [Vupload] Fix extractor by [u-spec-png](https://github.com/u-spec-png)
|
||||||
|
* [wakanim] Add support for MPD manifests by [nyuszika7h](https://github.com/nyuszika7h)
|
||||||
|
* [wakanim] Detect geo-restriction by [nyuszika7h](https://github.com/nyuszika7h)
|
||||||
|
* [ZenYandex] Fix extractor by [u-spec-png](https://github.com/u-spec-png)
|
||||||
|
|
||||||
|
|
||||||
### 2021.10.22
|
### 2021.10.22
|
||||||
|
|
||||||
* [build] Improvements
|
* [build] Improvements
|
||||||
@@ -61,7 +351,7 @@
|
|||||||
* [AdobePass] Fix RCN MSO by [jfogelman](https://github.com/jfogelman)
|
* [AdobePass] Fix RCN MSO by [jfogelman](https://github.com/jfogelman)
|
||||||
* [CBC] Fix Gem livestream by [makeworld-the-better-one](https://github.com/makeworld-the-better-one)
|
* [CBC] Fix Gem livestream by [makeworld-the-better-one](https://github.com/makeworld-the-better-one)
|
||||||
* [CBC] Support CBC Gem member content by [makeworld-the-better-one](https://github.com/makeworld-the-better-one)
|
* [CBC] Support CBC Gem member content by [makeworld-the-better-one](https://github.com/makeworld-the-better-one)
|
||||||
* [crunchyroll] Add season to flat-playlist Closes #1319
|
* [crunchyroll] Add season to flat-playlist
|
||||||
* [crunchyroll] Add support for `beta.crunchyroll` URLs and fix series URLs with language code
|
* [crunchyroll] Add support for `beta.crunchyroll` URLs and fix series URLs with language code
|
||||||
* [EUScreen] Add Extractor by [Ashish0804](https://github.com/Ashish0804)
|
* [EUScreen] Add Extractor by [Ashish0804](https://github.com/Ashish0804)
|
||||||
* [Gronkh] Add extractor by [Ashish0804](https://github.com/Ashish0804)
|
* [Gronkh] Add extractor by [Ashish0804](https://github.com/Ashish0804)
|
||||||
@@ -1283,7 +1573,7 @@
|
|||||||
* Cleaned up the fork for public use
|
* Cleaned up the fork for public use
|
||||||
|
|
||||||
|
|
||||||
**PS**: All uncredited changes above this point are authored by [pukkandan](https://github.com/pukkandan)
|
**Note**: All uncredited changes above this point are authored by [pukkandan](https://github.com/pukkandan)
|
||||||
|
|
||||||
### Unreleased changes in [blackjack4494/yt-dlc](https://github.com/blackjack4494/yt-dlc)
|
### Unreleased changes in [blackjack4494/yt-dlc](https://github.com/blackjack4494/yt-dlc)
|
||||||
* Updated to youtube-dl release 2020.11.26 by [pukkandan](https://github.com/pukkandan)
|
* Updated to youtube-dl release 2020.11.26 by [pukkandan](https://github.com/pukkandan)
|
||||||
@@ -1308,8 +1598,110 @@
|
|||||||
* [spreaker] fix SpreakerShowIE test URL by [pukkandan](https://github.com/pukkandan)
|
* [spreaker] fix SpreakerShowIE test URL by [pukkandan](https://github.com/pukkandan)
|
||||||
* [Vlive] Fix playlist handling when downloading a channel by [kyuyeunk](https://github.com/kyuyeunk)
|
* [Vlive] Fix playlist handling when downloading a channel by [kyuyeunk](https://github.com/kyuyeunk)
|
||||||
* [tmz] Fix extractor by [diegorodriguezv](https://github.com/diegorodriguezv)
|
* [tmz] Fix extractor by [diegorodriguezv](https://github.com/diegorodriguezv)
|
||||||
|
* [ITV] BTCC URL update by [WolfganP](https://github.com/WolfganP)
|
||||||
* [generic] Detect embedded bitchute videos by [pukkandan](https://github.com/pukkandan)
|
* [generic] Detect embedded bitchute videos by [pukkandan](https://github.com/pukkandan)
|
||||||
* [generic] Extract embedded youtube and twitter videos by [diegorodriguezv](https://github.com/diegorodriguezv)
|
* [generic] Extract embedded youtube and twitter videos by [diegorodriguezv](https://github.com/diegorodriguezv)
|
||||||
* [ffmpeg] Ensure all streams are copied by [pukkandan](https://github.com/pukkandan)
|
* [ffmpeg] Ensure all streams are copied by [pukkandan](https://github.com/pukkandan)
|
||||||
* [embedthumbnail] Fix for os.rename error by [pukkandan](https://github.com/pukkandan)
|
* [embedthumbnail] Fix for os.rename error by [pukkandan](https://github.com/pukkandan)
|
||||||
* make_win.bat: don't use UPX to pack vcruntime140.dll by [jbruchon](https://github.com/jbruchon)
|
* make_win.bat: don't use UPX to pack vcruntime140.dll by [jbruchon](https://github.com/jbruchon)
|
||||||
|
|
||||||
|
|
||||||
|
### Changelog of [blackjack4494/yt-dlc](https://github.com/blackjack4494/yt-dlc) till release 2020.11.11-3
|
||||||
|
|
||||||
|
**Note**: This was constructed from the merge commit messages and may not be entirely accurate
|
||||||
|
|
||||||
|
* [bandcamp] fix failing test. remove subclass hack by [insaneracist](https://github.com/insaneracist)
|
||||||
|
* [bandcamp] restore album downloads by [insaneracist](https://github.com/insaneracist)
|
||||||
|
* [francetv] fix extractor by [Surkal](https://github.com/Surkal)
|
||||||
|
* [gdcvault] fix extractor by [blackjack4494](https://github.com/blackjack4494)
|
||||||
|
* [hotstar] Move to API v1 by [theincognito-inc](https://github.com/theincognito-inc)
|
||||||
|
* [hrfernsehen] add extractor by [blocktrron](https://github.com/blocktrron)
|
||||||
|
* [kakao] new apis by [blackjack4494](https://github.com/blackjack4494)
|
||||||
|
* [la7] fix missing protocol by [nixxo](https://github.com/nixxo)
|
||||||
|
* [mailru] removed escaped braces, use urljoin, added tests by [nixxo](https://github.com/nixxo)
|
||||||
|
* [MTV/Nick] universal mgid extractor + fix nick.de feed by [blackjack4494](https://github.com/blackjack4494)
|
||||||
|
* [mtv] Fix a missing match_id by [nixxo](https://github.com/nixxo)
|
||||||
|
* [Mtv] updated extractor logic & more by [blackjack4494](https://github.com/blackjack4494)
|
||||||
|
* [ndr] support Daserste ndr by [blackjack4494](https://github.com/blackjack4494)
|
||||||
|
* [Netzkino] Only use video id to find metadata by [TobiX](https://github.com/TobiX)
|
||||||
|
* [newgrounds] fix: video download by [insaneracist](https://github.com/insaneracist)
|
||||||
|
* [nitter] Add new extractor by [B0pol](https://github.com/B0pol)
|
||||||
|
* [soundcloud] Resolve audio/x-wav by [tfvlrue](https://github.com/tfvlrue)
|
||||||
|
* [soundcloud] sets pattern and tests by [blackjack4494](https://github.com/blackjack4494)
|
||||||
|
* [SouthparkDE/MTV] another mgid extraction (mtv_base) feed url updated by [blackjack4494](https://github.com/blackjack4494)
|
||||||
|
* [StoryFire] Add new extractor by [sgstair](https://github.com/sgstair)
|
||||||
|
* [twitch] by [geauxlo](https://github.com/geauxlo)
|
||||||
|
* [videa] Adapt to updates by [adrianheine](https://github.com/adrianheine)
|
||||||
|
* [Viki] subtitles, formats by [blackjack4494](https://github.com/blackjack4494)
|
||||||
|
* [vlive] fix extractor for revamped website by [exwm](https://github.com/exwm)
|
||||||
|
* [xtube] fix extractor by [insaneracist](https://github.com/insaneracist)
|
||||||
|
* [youtube] Convert subs when download is skipped by [blackjack4494](https://github.com/blackjack4494)
|
||||||
|
* [youtube] Fix age gate detection by [random-nick](https://github.com/random-nick)
|
||||||
|
* [youtube] fix yt-only playback when age restricted/gated - requires cookies by [blackjack4494](https://github.com/blackjack4494)
|
||||||
|
* [youtube] fix: extract artist metadata from ytInitialData by [insaneracist](https://github.com/insaneracist)
|
||||||
|
* [youtube] fix: extract mix playlist ids from ytInitialData by [insaneracist](https://github.com/insaneracist)
|
||||||
|
* [youtube] fix: mix playlist title by [insaneracist](https://github.com/insaneracist)
|
||||||
|
* [youtube] fix: Youtube Music playlists by [insaneracist](https://github.com/insaneracist)
|
||||||
|
* [Youtube] Fixed problem with new youtube player by [peet1993](https://github.com/peet1993)
|
||||||
|
* [zoom] Fix url parsing for url's containing /share/ and dots by [Romern](https://github.com/Romern)
|
||||||
|
* [zoom] new extractor by [insaneracist](https://github.com/insaneracist)
|
||||||
|
* abc by [adrianheine](https://github.com/adrianheine)
|
||||||
|
* Added Comcast_SSO fix by [merval](https://github.com/merval)
|
||||||
|
* Added DRM logic to brightcove by [merval](https://github.com/merval)
|
||||||
|
* Added regex for ABC.com site. by [kucksdorfs](https://github.com/kucksdorfs)
|
||||||
|
* alura by [hugohaa](https://github.com/hugohaa)
|
||||||
|
* Arbitrary merges by [fstirlitz](https://github.com/fstirlitz)
|
||||||
|
* ard.py_add_playlist_support by [martin54](https://github.com/martin54)
|
||||||
|
* Bugfix/youtube/chapters fix extractor by [gschizas](https://github.com/gschizas)
|
||||||
|
* bugfix_youtube_like_extraction by [RedpointsBots](https://github.com/RedpointsBots)
|
||||||
|
* Create build workflow by [blackjack4494](https://github.com/blackjack4494)
|
||||||
|
* deezer by [LucBerge](https://github.com/LucBerge)
|
||||||
|
* Detect embedded bitchute videos by [pukkandan](https://github.com/pukkandan)
|
||||||
|
* Don't install tests by [l29ah](https://github.com/l29ah)
|
||||||
|
* Don't try to embed/convert json subtitles generated by [youtube](https://github.com/youtube) livechat by [pukkandan](https://github.com/pukkandan)
|
||||||
|
* Doodstream by [sxvghd](https://github.com/sxvghd)
|
||||||
|
* duboku by [lkho](https://github.com/lkho)
|
||||||
|
* elonet by [tpikonen](https://github.com/tpikonen)
|
||||||
|
* ext/remuxe-video by [Zocker1999NET](https://github.com/Zocker1999NET)
|
||||||
|
* fall-back to the old way to fetch subtitles, if needed by [RobinD42](https://github.com/RobinD42)
|
||||||
|
* feature_subscriber_count by [RedpointsBots](https://github.com/RedpointsBots)
|
||||||
|
* Fix external downloader when there is no http_header by [pukkandan](https://github.com/pukkandan)
|
||||||
|
* Fix issue triggered by [tubeup](https://github.com/tubeup) by [nsapa](https://github.com/nsapa)
|
||||||
|
* Fix YoutubePlaylistsIE by [ZenulAbidin](https://github.com/ZenulAbidin)
|
||||||
|
* fix-mitele' by [DjMoren](https://github.com/DjMoren)
|
||||||
|
* fix/google-drive-cookie-issue by [legraphista](https://github.com/legraphista)
|
||||||
|
* fix_tiktok by [mervel-mervel](https://github.com/mervel-mervel)
|
||||||
|
* Fixed problem with JS player URL by [peet1993](https://github.com/peet1993)
|
||||||
|
* fixYTSearch by [xarantolus](https://github.com/xarantolus)
|
||||||
|
* FliegendeWurst-3sat-zdf-merger-bugfix-feature
|
||||||
|
* gilou-bandcamp_update
|
||||||
|
* implement ThisVid extractor by [rigstot](https://github.com/rigstot)
|
||||||
|
* JensTimmerman-patch-1 by [JensTimmerman](https://github.com/JensTimmerman)
|
||||||
|
* Keep download archive in memory for better performance by [jbruchon](https://github.com/jbruchon)
|
||||||
|
* la7-fix by [iamleot](https://github.com/iamleot)
|
||||||
|
* magenta by [adrianheine](https://github.com/adrianheine)
|
||||||
|
* Merge 26564 from [adrianheine](https://github.com/adrianheine)
|
||||||
|
* Merge code from [ddland](https://github.com/ddland)
|
||||||
|
* Merge code from [nixxo](https://github.com/nixxo)
|
||||||
|
* Merge code from [ssaqua](https://github.com/ssaqua)
|
||||||
|
* Merge code from [zubearc](https://github.com/zubearc)
|
||||||
|
* mkvthumbnail by [MrDoritos](https://github.com/MrDoritos)
|
||||||
|
* myvideo_ge by [fonkap](https://github.com/fonkap)
|
||||||
|
* naver by [SeonjaeHyeon](https://github.com/SeonjaeHyeon)
|
||||||
|
* ondemandkorea by [julien-hadleyjack](https://github.com/julien-hadleyjack)
|
||||||
|
* rai-update by [iamleot](https://github.com/iamleot)
|
||||||
|
* RFC: youtube: Polymer UI and JSON endpoints for playlists by [wlritchi](https://github.com/wlritchi)
|
||||||
|
* rutv by [adrianheine](https://github.com/adrianheine)
|
||||||
|
* Sc extractor web auth by [blackjack4494](https://github.com/blackjack4494)
|
||||||
|
* Switch from binary search tree to Python sets by [jbruchon](https://github.com/jbruchon)
|
||||||
|
* tiktok by [skyme5](https://github.com/skyme5)
|
||||||
|
* tvnow by [TinyToweringTree](https://github.com/TinyToweringTree)
|
||||||
|
* twitch-fix by [lel-amri](https://github.com/lel-amri)
|
||||||
|
* Twitter shortener by [blackjack4494](https://github.com/blackjack4494)
|
||||||
|
* Update README.md by [JensTimmerman](https://github.com/JensTimmerman)
|
||||||
|
* Update to reflect website changes. by [amigatomte](https://github.com/amigatomte)
|
||||||
|
* use webarchive to fix a dead link in README by [B0pol](https://github.com/B0pol)
|
||||||
|
* Viki the second by [blackjack4494](https://github.com/blackjack4494)
|
||||||
|
* wdr-subtitles by [mrtnmtth](https://github.com/mrtnmtth)
|
||||||
|
* Webpfix by [alexmerkel](https://github.com/alexmerkel)
|
||||||
|
* Youtube live chat by [siikamiika](https://github.com/siikamiika)
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ You can also find lists of all [contributors of yt-dlp](CONTRIBUTORS) and [autho
|
|||||||
[](https://github.com/sponsors/coletdjnz)
|
[](https://github.com/sponsors/coletdjnz)
|
||||||
|
|
||||||
* 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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,11 +13,13 @@ 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 *.3gp *.annotations.xml *.ape *.avi *.description *.dump *.flac *.flv *.frag *.frag.aria2 *.frag.urls \
|
rm -rf test/testdata/player-*.js tmp/ *.annotations.xml *.aria2 *.description *.dump *.frag \
|
||||||
*.info.json *.jpeg *.jpg *.live_chat.json *.m4a *.m4v *.mkv *.mp3 *.mp4 *.ogg *.opus *.part* *.png *.sbv *.srt \
|
*.frag.aria2 *.frag.urls *.info.json *.live_chat.json *.part* *.unknown_video *.ytdl \
|
||||||
*.swf *.swp *.ttml *.vtt *.wav *.webm *.webp *.ytdl test/testdata/player-*.js
|
*.3gp *.ape *.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
|
||||||
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/ yt_dlp/extractor/lazy_extractors.py *.spec CONTRIBUTING.md.tmp yt-dlp yt-dlp.exe yt_dlp.egg-info/ AUTHORS .mailmap
|
rm -rf yt-dlp.1.temp.md yt-dlp.1 README.txt MANIFEST build/ dist/ .coverage cover/ yt-dlp.tar.gz completions/ \
|
||||||
|
yt_dlp/extractor/lazy_extractors.py *.spec CONTRIBUTING.md.tmp yt-dlp yt-dlp.exe yt_dlp.egg-info/ AUTHORS .mailmap
|
||||||
clean-cache:
|
clean-cache:
|
||||||
find . -name "*.pyc" -o -name "*.class" -delete
|
find . -name "*.pyc" -o -name "*.class" -delete
|
||||||
|
|
||||||
@@ -31,7 +33,6 @@ DESTDIR ?= .
|
|||||||
BINDIR ?= $(PREFIX)/bin
|
BINDIR ?= $(PREFIX)/bin
|
||||||
MANDIR ?= $(PREFIX)/man
|
MANDIR ?= $(PREFIX)/man
|
||||||
SHAREDIR ?= $(PREFIX)/share
|
SHAREDIR ?= $(PREFIX)/share
|
||||||
# make_supportedsites.py doesnot work correctly in python2
|
|
||||||
PYTHON ?= /usr/bin/env python3
|
PYTHON ?= /usr/bin/env python3
|
||||||
|
|
||||||
# set SYSCONFDIR to /etc if PREFIX=/usr or PREFIX=/usr/local
|
# set SYSCONFDIR to /etc if PREFIX=/usr or PREFIX=/usr/local
|
||||||
|
|||||||
@@ -39,12 +39,6 @@ class {name}({bases}):
|
|||||||
_module = '{module}'
|
_module = '{module}'
|
||||||
'''
|
'''
|
||||||
|
|
||||||
make_valid_template = '''
|
|
||||||
@classmethod
|
|
||||||
def _make_valid_url(cls):
|
|
||||||
return {valid_url!r}
|
|
||||||
'''
|
|
||||||
|
|
||||||
|
|
||||||
def get_base_name(base):
|
def get_base_name(base):
|
||||||
if base is InfoExtractor:
|
if base is InfoExtractor:
|
||||||
@@ -61,15 +55,14 @@ def build_lazy_ie(ie, name):
|
|||||||
bases=', '.join(map(get_base_name, ie.__bases__)),
|
bases=', '.join(map(get_base_name, ie.__bases__)),
|
||||||
module=ie.__module__)
|
module=ie.__module__)
|
||||||
valid_url = getattr(ie, '_VALID_URL', None)
|
valid_url = getattr(ie, '_VALID_URL', None)
|
||||||
|
if not valid_url and hasattr(ie, '_make_valid_url'):
|
||||||
|
valid_url = ie._make_valid_url()
|
||||||
if valid_url:
|
if valid_url:
|
||||||
s += f' _VALID_URL = {valid_url!r}\n'
|
s += f' _VALID_URL = {valid_url!r}\n'
|
||||||
if not ie._WORKING:
|
if not ie._WORKING:
|
||||||
s += ' _WORKING = False\n'
|
s += ' _WORKING = False\n'
|
||||||
if ie.suitable.__func__ is not InfoExtractor.suitable.__func__:
|
if ie.suitable.__func__ is not InfoExtractor.suitable.__func__:
|
||||||
s += f'\n{getsource(ie.suitable)}'
|
s += f'\n{getsource(ie.suitable)}'
|
||||||
if hasattr(ie, '_make_valid_url'):
|
|
||||||
# search extractors
|
|
||||||
s += make_valid_template.format(valid_url=ie._make_valid_url())
|
|
||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ def main():
|
|||||||
continue
|
continue
|
||||||
if ie_desc is not None:
|
if 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)
|
||||||
|
if search_key is not None:
|
||||||
|
ie_md += f'; "{ie.SEARCH_KEY}:" prefix'
|
||||||
if not ie.working():
|
if not ie.working():
|
||||||
ie_md += ' (Currently broken)'
|
ie_md += ' (Currently broken)'
|
||||||
yield ie_md
|
yield ie_md
|
||||||
|
|||||||
@@ -13,12 +13,14 @@ PREFIX = r'''%yt-dlp(1)
|
|||||||
|
|
||||||
# NAME
|
# NAME
|
||||||
|
|
||||||
youtube\-dl \- download videos from youtube.com or other video platforms
|
yt\-dlp \- A youtube-dl fork with additional features and patches
|
||||||
|
|
||||||
# SYNOPSIS
|
# SYNOPSIS
|
||||||
|
|
||||||
**yt-dlp** \[OPTIONS\] URL [URL...]
|
**yt-dlp** \[OPTIONS\] URL [URL...]
|
||||||
|
|
||||||
|
# DESCRIPTION
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
@@ -33,47 +35,63 @@ def main():
|
|||||||
with io.open(README_FILE, encoding='utf-8') as f:
|
with io.open(README_FILE, encoding='utf-8') as f:
|
||||||
readme = f.read()
|
readme = f.read()
|
||||||
|
|
||||||
readme = re.sub(r'(?s)^.*?(?=# DESCRIPTION)', '', readme)
|
readme = filter_excluded_sections(readme)
|
||||||
readme = re.sub(r'\s+yt-dlp \[OPTIONS\] URL \[URL\.\.\.\]', '', readme)
|
readme = move_sections(readme)
|
||||||
readme = PREFIX + readme
|
|
||||||
|
|
||||||
readme = filter_options(readme)
|
readme = filter_options(readme)
|
||||||
|
|
||||||
with io.open(outfile, 'w', encoding='utf-8') as outf:
|
with io.open(outfile, 'w', encoding='utf-8') as outf:
|
||||||
outf.write(readme)
|
outf.write(PREFIX + readme)
|
||||||
|
|
||||||
|
|
||||||
|
def filter_excluded_sections(readme):
|
||||||
|
EXCLUDED_SECTION_BEGIN_STRING = re.escape('<!-- MANPAGE: BEGIN EXCLUDED SECTION -->')
|
||||||
|
EXCLUDED_SECTION_END_STRING = re.escape('<!-- MANPAGE: END EXCLUDED SECTION -->')
|
||||||
|
return re.sub(
|
||||||
|
rf'(?s){EXCLUDED_SECTION_BEGIN_STRING}.+?{EXCLUDED_SECTION_END_STRING}\n',
|
||||||
|
'', readme)
|
||||||
|
|
||||||
|
|
||||||
|
def move_sections(readme):
|
||||||
|
MOVE_TAG_TEMPLATE = '<!-- MANPAGE: MOVE "%s" SECTION HERE -->'
|
||||||
|
sections = re.findall(r'(?m)^%s$' % (
|
||||||
|
re.escape(MOVE_TAG_TEMPLATE).replace(r'\%', '%') % '(.+)'), readme)
|
||||||
|
|
||||||
|
for section_name in sections:
|
||||||
|
move_tag = MOVE_TAG_TEMPLATE % section_name
|
||||||
|
if readme.count(move_tag) > 1:
|
||||||
|
raise Exception(f'There is more than one occurrence of "{move_tag}". This is unexpected')
|
||||||
|
|
||||||
|
sections = re.findall(rf'(?sm)(^# {re.escape(section_name)}.+?)(?=^# )', readme)
|
||||||
|
if len(sections) < 1:
|
||||||
|
raise Exception(f'The section {section_name} does not exist')
|
||||||
|
elif len(sections) > 1:
|
||||||
|
raise Exception(f'There are multiple occurrences of section {section_name}, this is unhandled')
|
||||||
|
|
||||||
|
readme = readme.replace(sections[0], '', 1).replace(move_tag, sections[0], 1)
|
||||||
|
return readme
|
||||||
|
|
||||||
|
|
||||||
def filter_options(readme):
|
def filter_options(readme):
|
||||||
ret = ''
|
section = re.search(r'(?sm)^# USAGE AND OPTIONS\n.+?(?=^# )', readme).group(0)
|
||||||
in_options = False
|
options = '# OPTIONS\n'
|
||||||
for line in readme.split('\n'):
|
for line in section.split('\n')[1:]:
|
||||||
if line.startswith('# '):
|
if line.lstrip().startswith('-'):
|
||||||
if line[2:].startswith('OPTIONS'):
|
split = re.split(r'\s{2,}', line.lstrip())
|
||||||
in_options = True
|
# Description string may start with `-` as well. If there is
|
||||||
else:
|
# only one piece then it's a description bit not an option.
|
||||||
in_options = False
|
if len(split) > 1:
|
||||||
|
option, description = split
|
||||||
|
split_option = option.split(' ')
|
||||||
|
|
||||||
if in_options:
|
if not split_option[-1].startswith('-'): # metavar
|
||||||
if line.lstrip().startswith('-'):
|
option = ' '.join(split_option[:-1] + [f'*{split_option[-1]}*'])
|
||||||
split = re.split(r'\s{2,}', line.lstrip())
|
|
||||||
# Description string may start with `-` as well. If there is
|
|
||||||
# only one piece then it's a description bit not an option.
|
|
||||||
if len(split) > 1:
|
|
||||||
option, description = split
|
|
||||||
split_option = option.split(' ')
|
|
||||||
|
|
||||||
if not split_option[-1].startswith('-'): # metavar
|
# Pandoc's definition_lists. See http://pandoc.org/README.html
|
||||||
option = ' '.join(split_option[:-1] + ['*%s*' % split_option[-1]])
|
options += f'\n{option}\n: {description}\n'
|
||||||
|
continue
|
||||||
|
options += line.lstrip() + '\n'
|
||||||
|
|
||||||
# Pandoc's definition_lists. See http://pandoc.org/README.html
|
return readme.replace(section, options, 1)
|
||||||
# for more information.
|
|
||||||
ret += '\n%s\n: %s\n' % (option, description)
|
|
||||||
continue
|
|
||||||
ret += line.lstrip() + '\n'
|
|
||||||
else:
|
|
||||||
ret += line + '\n'
|
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@@ -1,33 +1,42 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
# import urllib.request
|
import sys
|
||||||
|
import subprocess
|
||||||
|
|
||||||
# response = urllib.request.urlopen('https://blackjack4494.github.io/youtube-dlc/update/LATEST_VERSION')
|
|
||||||
# old_version = response.read().decode('utf-8')
|
|
||||||
|
|
||||||
exec(compile(open('yt_dlp/version.py').read(), 'yt_dlp/version.py', 'exec'))
|
with open('yt_dlp/version.py', 'rt') as f:
|
||||||
|
exec(compile(f.read(), 'yt_dlp/version.py', 'exec'))
|
||||||
old_version = locals()['__version__']
|
old_version = locals()['__version__']
|
||||||
|
|
||||||
old_version_list = old_version.split(".", 4)
|
old_version_list = old_version.split('.')
|
||||||
|
|
||||||
old_ver = '.'.join(old_version_list[:3])
|
old_ver = '.'.join(old_version_list[:3])
|
||||||
old_rev = old_version_list[3] if len(old_version_list) > 3 else ''
|
old_rev = old_version_list[3] if len(old_version_list) > 3 else ''
|
||||||
|
|
||||||
ver = datetime.utcnow().strftime("%Y.%m.%d")
|
ver = datetime.utcnow().strftime("%Y.%m.%d")
|
||||||
rev = str(int(old_rev or 0) + 1) if old_ver == ver else ''
|
|
||||||
|
rev = (sys.argv[1:] or [''])[0] # Use first argument, if present as revision number
|
||||||
|
if not rev:
|
||||||
|
rev = str(int(old_rev or 0) + 1) if old_ver == ver else ''
|
||||||
|
|
||||||
VERSION = '.'.join((ver, rev)) if rev else ver
|
VERSION = '.'.join((ver, rev)) if rev else ver
|
||||||
# VERSION_LIST = [(int(v) for v in ver.split(".") + [rev or 0])]
|
|
||||||
|
try:
|
||||||
|
sp = subprocess.Popen(['git', 'rev-parse', '--short', 'HEAD'], stdout=subprocess.PIPE)
|
||||||
|
GIT_HEAD = sp.communicate()[0].decode().strip() or None
|
||||||
|
except Exception:
|
||||||
|
GIT_HEAD = None
|
||||||
|
|
||||||
|
VERSION_FILE = f'''\
|
||||||
|
# Autogenerated by devscripts/update-version.py
|
||||||
|
|
||||||
|
__version__ = {VERSION!r}
|
||||||
|
|
||||||
|
RELEASE_GIT_HEAD = {GIT_HEAD!r}
|
||||||
|
'''
|
||||||
|
|
||||||
|
with open('yt_dlp/version.py', 'wt') as f:
|
||||||
|
f.write(VERSION_FILE)
|
||||||
|
|
||||||
print('::set-output name=ytdlp_version::' + VERSION)
|
print('::set-output name=ytdlp_version::' + VERSION)
|
||||||
|
print(f'\nVersion = {VERSION}, Git HEAD = {GIT_HEAD}')
|
||||||
file_version_py = open('yt_dlp/version.py', 'rt')
|
|
||||||
data = file_version_py.read()
|
|
||||||
data = data.replace(old_version, VERSION)
|
|
||||||
file_version_py.close()
|
|
||||||
|
|
||||||
file_version_py = open('yt_dlp/version.py', 'wt')
|
|
||||||
file_version_py.write(data)
|
|
||||||
file_version_py.close()
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
orphan: true
|
||||||
|
---
|
||||||
|
```{include} ../Contributing.md
|
||||||
|
```
|
||||||
@@ -40,7 +40,7 @@ def main():
|
|||||||
'--icon=devscripts/logo.ico',
|
'--icon=devscripts/logo.ico',
|
||||||
'--upx-exclude=vcruntime140.dll',
|
'--upx-exclude=vcruntime140.dll',
|
||||||
'--noconfirm',
|
'--noconfirm',
|
||||||
*dependancy_options(),
|
*dependency_options(),
|
||||||
*opts,
|
*opts,
|
||||||
'yt_dlp/__main__.py',
|
'yt_dlp/__main__.py',
|
||||||
]
|
]
|
||||||
@@ -73,11 +73,11 @@ def version_to_list(version):
|
|||||||
return list(map(int, version_list)) + [0] * (4 - len(version_list))
|
return list(map(int, version_list)) + [0] * (4 - len(version_list))
|
||||||
|
|
||||||
|
|
||||||
def dependancy_options():
|
def dependency_options():
|
||||||
dependancies = [pycryptodome_module(), 'mutagen'] + collect_submodules('websockets')
|
dependencies = [pycryptodome_module(), 'mutagen'] + 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 dependancies)
|
yield from (f'--hidden-import={module}' for module in dependencies)
|
||||||
yield from (f'--exclude-module={module}' for module in excluded_modules)
|
yield from (f'--exclude-module={module}' for module in excluded_modules)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from distutils.spawn import spawn
|
|||||||
exec(compile(open('yt_dlp/version.py').read(), 'yt_dlp/version.py', 'exec'))
|
exec(compile(open('yt_dlp/version.py').read(), 'yt_dlp/version.py', 'exec'))
|
||||||
|
|
||||||
|
|
||||||
DESCRIPTION = 'Command-line program to download videos from YouTube.com and many other other video platforms.'
|
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>',
|
||||||
|
|||||||
+92
-25
@@ -21,6 +21,7 @@
|
|||||||
- **9now.com.au**
|
- **9now.com.au**
|
||||||
- **abc.net.au**
|
- **abc.net.au**
|
||||||
- **abc.net.au:iview**
|
- **abc.net.au:iview**
|
||||||
|
- **abc.net.au:iview:showseries**
|
||||||
- **abcnews**
|
- **abcnews**
|
||||||
- **abcnews:video**
|
- **abcnews:video**
|
||||||
- **abcotvs**: ABC Owned Television Stations
|
- **abcotvs**: ABC Owned Television Stations
|
||||||
@@ -48,6 +49,7 @@
|
|||||||
- **Alura**
|
- **Alura**
|
||||||
- **AluraCourse**
|
- **AluraCourse**
|
||||||
- **Amara**
|
- **Amara**
|
||||||
|
- **AmazonStore**
|
||||||
- **AMCNetworks**
|
- **AMCNetworks**
|
||||||
- **AmericasTestKitchen**
|
- **AmericasTestKitchen**
|
||||||
- **AmericasTestKitchenSeason**
|
- **AmericasTestKitchenSeason**
|
||||||
@@ -127,7 +129,7 @@
|
|||||||
- **BilibiliAudioAlbum**
|
- **BilibiliAudioAlbum**
|
||||||
- **BilibiliChannel**
|
- **BilibiliChannel**
|
||||||
- **BiliBiliPlayer**
|
- **BiliBiliPlayer**
|
||||||
- **BiliBiliSearch**: Bilibili video search, "bilisearch" keyword
|
- **BiliBiliSearch**: Bilibili video search; "bilisearch:" prefix
|
||||||
- **BiliIntl**
|
- **BiliIntl**
|
||||||
- **BiliIntlSeries**
|
- **BiliIntlSeries**
|
||||||
- **BioBioChileTV**
|
- **BioBioChileTV**
|
||||||
@@ -140,6 +142,7 @@
|
|||||||
- **BlackboardCollaborate**
|
- **BlackboardCollaborate**
|
||||||
- **BleacherReport**
|
- **BleacherReport**
|
||||||
- **BleacherReportCMS**
|
- **BleacherReportCMS**
|
||||||
|
- **blogger.com**
|
||||||
- **Bloomberg**
|
- **Bloomberg**
|
||||||
- **BokeCC**
|
- **BokeCC**
|
||||||
- **BongaCams**
|
- **BongaCams**
|
||||||
@@ -149,6 +152,7 @@
|
|||||||
- **BR**: Bayerischer Rundfunk
|
- **BR**: Bayerischer Rundfunk
|
||||||
- **BravoTV**
|
- **BravoTV**
|
||||||
- **Break**
|
- **Break**
|
||||||
|
- **BreitBart**
|
||||||
- **brightcove:legacy**
|
- **brightcove:legacy**
|
||||||
- **brightcove:new**
|
- **brightcove:new**
|
||||||
- **BRMediathek**: Bayerischer Rundfunk Mediathek
|
- **BRMediathek**: Bayerischer Rundfunk Mediathek
|
||||||
@@ -157,11 +161,13 @@
|
|||||||
- **BusinessInsider**
|
- **BusinessInsider**
|
||||||
- **BuzzFeed**
|
- **BuzzFeed**
|
||||||
- **BYUtv**
|
- **BYUtv**
|
||||||
|
- **CableAV**
|
||||||
- **CAM4**
|
- **CAM4**
|
||||||
- **Camdemy**
|
- **Camdemy**
|
||||||
- **CamdemyFolder**
|
- **CamdemyFolder**
|
||||||
- **CamModels**
|
- **CamModels**
|
||||||
- **CamWithHer**
|
- **CamWithHer**
|
||||||
|
- **CanalAlpha**
|
||||||
- **canalc2.tv**
|
- **canalc2.tv**
|
||||||
- **Canalplus**: mycanal.fr and piwiplus.fr
|
- **Canalplus**: mycanal.fr and piwiplus.fr
|
||||||
- **Canvas**
|
- **Canvas**
|
||||||
@@ -184,7 +190,6 @@
|
|||||||
- **CCTV**: 央视网
|
- **CCTV**: 央视网
|
||||||
- **CDA**
|
- **CDA**
|
||||||
- **CeskaTelevize**
|
- **CeskaTelevize**
|
||||||
- **CeskaTelevizePorady**
|
|
||||||
- **CGTN**
|
- **CGTN**
|
||||||
- **channel9**: Channel 9
|
- **channel9**: Channel 9
|
||||||
- **CharlieRose**
|
- **CharlieRose**
|
||||||
@@ -222,6 +227,8 @@
|
|||||||
- **CONtv**
|
- **CONtv**
|
||||||
- **Corus**
|
- **Corus**
|
||||||
- **Coub**
|
- **Coub**
|
||||||
|
- **CozyTV**
|
||||||
|
- **cp24**
|
||||||
- **Cracked**
|
- **Cracked**
|
||||||
- **Crackle**
|
- **Crackle**
|
||||||
- **CrooksAndLiars**
|
- **CrooksAndLiars**
|
||||||
@@ -236,7 +243,8 @@
|
|||||||
- **cu.ntv.co.jp**: Nippon Television Network
|
- **cu.ntv.co.jp**: Nippon Television Network
|
||||||
- **CultureUnplugged**
|
- **CultureUnplugged**
|
||||||
- **curiositystream**
|
- **curiositystream**
|
||||||
- **curiositystream:collection**
|
- **curiositystream:collections**
|
||||||
|
- **curiositystream:series**
|
||||||
- **CWTV**
|
- **CWTV**
|
||||||
- **DagelijkseKost**: dagelijksekost.een.be
|
- **DagelijkseKost**: dagelijksekost.een.be
|
||||||
- **DailyMail**
|
- **DailyMail**
|
||||||
@@ -266,6 +274,8 @@
|
|||||||
- **DiscoveryPlus**
|
- **DiscoveryPlus**
|
||||||
- **DiscoveryPlusIndia**
|
- **DiscoveryPlusIndia**
|
||||||
- **DiscoveryPlusIndiaShow**
|
- **DiscoveryPlusIndiaShow**
|
||||||
|
- **DiscoveryPlusItaly**
|
||||||
|
- **DiscoveryPlusItalyShow**
|
||||||
- **DiscoveryVR**
|
- **DiscoveryVR**
|
||||||
- **Disney**
|
- **Disney**
|
||||||
- **DIYNetwork**
|
- **DIYNetwork**
|
||||||
@@ -279,6 +289,8 @@
|
|||||||
- **DPlay**
|
- **DPlay**
|
||||||
- **DRBonanza**
|
- **DRBonanza**
|
||||||
- **Dropbox**
|
- **Dropbox**
|
||||||
|
- **Dropout**
|
||||||
|
- **DropoutSeason**
|
||||||
- **DrTuber**
|
- **DrTuber**
|
||||||
- **drtv**
|
- **drtv**
|
||||||
- **drtv:live**
|
- **drtv:live**
|
||||||
@@ -315,6 +327,7 @@
|
|||||||
- **Escapist**
|
- **Escapist**
|
||||||
- **ESPN**
|
- **ESPN**
|
||||||
- **ESPNArticle**
|
- **ESPNArticle**
|
||||||
|
- **ESPNCricInfo**
|
||||||
- **EsriVideo**
|
- **EsriVideo**
|
||||||
- **Europa**
|
- **Europa**
|
||||||
- **EUScreen**
|
- **EUScreen**
|
||||||
@@ -366,9 +379,16 @@
|
|||||||
- **Funk**
|
- **Funk**
|
||||||
- **Fusion**
|
- **Fusion**
|
||||||
- **Fux**
|
- **Fux**
|
||||||
|
- **Gab**
|
||||||
- **GabTV**
|
- **GabTV**
|
||||||
- **Gaia**
|
- **Gaia**
|
||||||
- **GameInformer**
|
- **GameInformer**
|
||||||
|
- **GameJolt**
|
||||||
|
- **GameJoltCommunity**
|
||||||
|
- **GameJoltGame**
|
||||||
|
- **GameJoltGameSoundtrack**
|
||||||
|
- **GameJoltSearch**
|
||||||
|
- **GameJoltUser**
|
||||||
- **GameSpot**
|
- **GameSpot**
|
||||||
- **GameStar**
|
- **GameStar**
|
||||||
- **Gaskrank**
|
- **Gaskrank**
|
||||||
@@ -389,6 +409,7 @@
|
|||||||
- **GloboArticle**
|
- **GloboArticle**
|
||||||
- **Go**
|
- **Go**
|
||||||
- **GodTube**
|
- **GodTube**
|
||||||
|
- **Gofile**
|
||||||
- **Golem**
|
- **Golem**
|
||||||
- **google:podcasts**
|
- **google:podcasts**
|
||||||
- **google:podcasts:feed**
|
- **google:podcasts:feed**
|
||||||
@@ -426,6 +447,8 @@
|
|||||||
- **hrfernsehen**
|
- **hrfernsehen**
|
||||||
- **HRTi**
|
- **HRTi**
|
||||||
- **HRTiPlaylist**
|
- **HRTiPlaylist**
|
||||||
|
- **HSEProduct**
|
||||||
|
- **HSEShow**
|
||||||
- **Huajiao**: 花椒直播
|
- **Huajiao**: 花椒直播
|
||||||
- **HuffPost**: Huffington Post
|
- **HuffPost**: Huffington Post
|
||||||
- **Hungama**
|
- **Hungama**
|
||||||
@@ -447,11 +470,13 @@
|
|||||||
- **IndavideoEmbed**
|
- **IndavideoEmbed**
|
||||||
- **InfoQ**
|
- **InfoQ**
|
||||||
- **Instagram**
|
- **Instagram**
|
||||||
- **instagram:tag**: Instagram hashtag search
|
- **instagram:tag**: Instagram hashtag search URLs
|
||||||
- **instagram:user**: Instagram user profile
|
- **instagram:user**: Instagram user profile
|
||||||
|
- **InstagramIOS**: IOS instagram:// URL
|
||||||
- **Internazionale**
|
- **Internazionale**
|
||||||
- **InternetVideoArchive**
|
- **InternetVideoArchive**
|
||||||
- **IPrima**
|
- **IPrima**
|
||||||
|
- **IPrimaCNN**
|
||||||
- **iqiyi**: 爱奇艺
|
- **iqiyi**: 爱奇艺
|
||||||
- **Ir90Tv**
|
- **Ir90Tv**
|
||||||
- **ITTF**
|
- **ITTF**
|
||||||
@@ -521,6 +546,7 @@
|
|||||||
- **LineLive**
|
- **LineLive**
|
||||||
- **LineLiveChannel**
|
- **LineLiveChannel**
|
||||||
- **LineTV**
|
- **LineTV**
|
||||||
|
- **LinkedIn**
|
||||||
- **linkedin:learning**
|
- **linkedin:learning**
|
||||||
- **linkedin:learning:course**
|
- **linkedin:learning:course**
|
||||||
- **LinuxAcademy**
|
- **LinuxAcademy**
|
||||||
@@ -560,6 +586,7 @@
|
|||||||
- **MediaKlikk**
|
- **MediaKlikk**
|
||||||
- **Medialaan**
|
- **Medialaan**
|
||||||
- **Mediaset**
|
- **Mediaset**
|
||||||
|
- **MediasetShow**
|
||||||
- **Mediasite**
|
- **Mediasite**
|
||||||
- **MediasiteCatalog**
|
- **MediasiteCatalog**
|
||||||
- **MediasiteNamedCatalog**
|
- **MediasiteNamedCatalog**
|
||||||
@@ -587,11 +614,13 @@
|
|||||||
- **mirrativ**
|
- **mirrativ**
|
||||||
- **mirrativ:user**
|
- **mirrativ:user**
|
||||||
- **MiTele**: mitele.es
|
- **MiTele**: mitele.es
|
||||||
|
- **mixch**
|
||||||
- **mixcloud**
|
- **mixcloud**
|
||||||
- **mixcloud:playlist**
|
- **mixcloud:playlist**
|
||||||
- **mixcloud:user**
|
- **mixcloud:user**
|
||||||
- **MLB**
|
- **MLB**
|
||||||
- **MLBVideo**
|
- **MLBVideo**
|
||||||
|
- **MLSSoccer**
|
||||||
- **Mnet**
|
- **Mnet**
|
||||||
- **MNetTV**
|
- **MNetTV**
|
||||||
- **MoeVideo**: LetitBit video services: moevideo.net, playreplay.net and videochart.net
|
- **MoeVideo**: LetitBit video services: moevideo.net, playreplay.net and videochart.net
|
||||||
@@ -636,6 +665,8 @@
|
|||||||
- **n-tv.de**
|
- **n-tv.de**
|
||||||
- **N1Info:article**
|
- **N1Info:article**
|
||||||
- **N1InfoAsset**
|
- **N1InfoAsset**
|
||||||
|
- **Nate**
|
||||||
|
- **NateProgram**
|
||||||
- **natgeo:video**
|
- **natgeo:video**
|
||||||
- **NationalGeographicTV**
|
- **NationalGeographicTV**
|
||||||
- **Naver**
|
- **Naver**
|
||||||
@@ -658,6 +689,7 @@
|
|||||||
- **ndr:embed:base**
|
- **ndr:embed:base**
|
||||||
- **NDTV**
|
- **NDTV**
|
||||||
- **Nebula**
|
- **Nebula**
|
||||||
|
- **nebula:collection**
|
||||||
- **NerdCubedFeed**
|
- **NerdCubedFeed**
|
||||||
- **netease:album**: 网易云音乐 - 专辑
|
- **netease:album**: 网易云音乐 - 专辑
|
||||||
- **netease:djradio**: 网易云音乐 - 电台
|
- **netease:djradio**: 网易云音乐 - 电台
|
||||||
@@ -691,8 +723,8 @@
|
|||||||
- **niconico**: ニコニコ動画
|
- **niconico**: ニコニコ動画
|
||||||
- **NiconicoPlaylist**
|
- **NiconicoPlaylist**
|
||||||
- **NiconicoUser**
|
- **NiconicoUser**
|
||||||
- **nicovideo:search**: Nico video searches
|
- **nicovideo:search**: Nico video search; "nicosearch:" prefix
|
||||||
- **nicovideo:search:date**: Nico video searches, newest first
|
- **nicovideo:search:date**: Nico video search, newest first; "nicosearchdate:" prefix
|
||||||
- **nicovideo:search_url**: Nico video search URLs
|
- **nicovideo:search_url**: Nico video search URLs
|
||||||
- **Nintendo**
|
- **Nintendo**
|
||||||
- **Nitter**
|
- **Nitter**
|
||||||
@@ -741,6 +773,7 @@
|
|||||||
- **OlympicsReplay**
|
- **OlympicsReplay**
|
||||||
- **on24**: ON24
|
- **on24**: ON24
|
||||||
- **OnDemandKorea**
|
- **OnDemandKorea**
|
||||||
|
- **OneFootball**
|
||||||
- **onet.pl**
|
- **onet.pl**
|
||||||
- **onet.tv**
|
- **onet.tv**
|
||||||
- **onet.tv:channel**
|
- **onet.tv:channel**
|
||||||
@@ -748,6 +781,8 @@
|
|||||||
- **OnionStudios**
|
- **OnionStudios**
|
||||||
- **Ooyala**
|
- **Ooyala**
|
||||||
- **OoyalaExternal**
|
- **OoyalaExternal**
|
||||||
|
- **Opencast**
|
||||||
|
- **OpencastPlaylist**
|
||||||
- **openrec**
|
- **openrec**
|
||||||
- **openrec:capture**
|
- **openrec:capture**
|
||||||
- **OraTV**
|
- **OraTV**
|
||||||
@@ -783,6 +818,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**
|
||||||
|
- **peer.tv**
|
||||||
- **PeerTube**
|
- **PeerTube**
|
||||||
- **PeerTube:Playlist**
|
- **PeerTube:Playlist**
|
||||||
- **peloton**
|
- **peloton**
|
||||||
@@ -801,6 +837,7 @@
|
|||||||
- **Pinterest**
|
- **Pinterest**
|
||||||
- **PinterestCollection**
|
- **PinterestCollection**
|
||||||
- **Pladform**
|
- **Pladform**
|
||||||
|
- **PlanetMarathi**
|
||||||
- **Platzi**
|
- **Platzi**
|
||||||
- **PlatziCourse**
|
- **PlatziCourse**
|
||||||
- **play.fm**
|
- **play.fm**
|
||||||
@@ -817,7 +854,12 @@
|
|||||||
- **podomatic**
|
- **podomatic**
|
||||||
- **Pokemon**
|
- **Pokemon**
|
||||||
- **PokemonWatch**
|
- **PokemonWatch**
|
||||||
|
- **PolsatGo**
|
||||||
- **PolskieRadio**
|
- **PolskieRadio**
|
||||||
|
- **polskieradio:kierowcow**
|
||||||
|
- **polskieradio:player**
|
||||||
|
- **polskieradio:podcast**
|
||||||
|
- **polskieradio:podcast:list**
|
||||||
- **PolskieRadioCategory**
|
- **PolskieRadioCategory**
|
||||||
- **Popcorntimes**
|
- **Popcorntimes**
|
||||||
- **PopcornTV**
|
- **PopcornTV**
|
||||||
@@ -860,6 +902,9 @@
|
|||||||
- **radiocanada:audiovideo**
|
- **radiocanada:audiovideo**
|
||||||
- **radiofrance**
|
- **radiofrance**
|
||||||
- **RadioJavan**
|
- **RadioJavan**
|
||||||
|
- **radiokapital**
|
||||||
|
- **radiokapital:show**
|
||||||
|
- **RadioZetPodcast**
|
||||||
- **radlive**
|
- **radlive**
|
||||||
- **radlive:channel**
|
- **radlive:channel**
|
||||||
- **radlive:season**
|
- **radlive:season**
|
||||||
@@ -867,6 +912,8 @@
|
|||||||
- **RaiPlay**
|
- **RaiPlay**
|
||||||
- **RaiPlayLive**
|
- **RaiPlayLive**
|
||||||
- **RaiPlayPlaylist**
|
- **RaiPlayPlaylist**
|
||||||
|
- **RaiPlayRadio**
|
||||||
|
- **RaiPlayRadioPlaylist**
|
||||||
- **RayWenderlich**
|
- **RayWenderlich**
|
||||||
- **RayWenderlichCourse**
|
- **RayWenderlichCourse**
|
||||||
- **RBMARadio**
|
- **RBMARadio**
|
||||||
@@ -882,7 +929,9 @@
|
|||||||
- **RedBullTV**
|
- **RedBullTV**
|
||||||
- **RedBullTVRrnContent**
|
- **RedBullTVRrnContent**
|
||||||
- **Reddit**
|
- **Reddit**
|
||||||
- **RedditR**
|
- **RedGifs**
|
||||||
|
- **RedGifsSearch**: Redgifs search
|
||||||
|
- **RedGifsUser**: Redgifs user
|
||||||
- **RedTube**
|
- **RedTube**
|
||||||
- **RegioTV**
|
- **RegioTV**
|
||||||
- **RENTV**
|
- **RENTV**
|
||||||
@@ -894,6 +943,7 @@
|
|||||||
- **RMCDecouverte**
|
- **RMCDecouverte**
|
||||||
- **RockstarGames**
|
- **RockstarGames**
|
||||||
- **RoosterTeeth**
|
- **RoosterTeeth**
|
||||||
|
- **RoosterTeethSeries**
|
||||||
- **RottenTomatoes**
|
- **RottenTomatoes**
|
||||||
- **Roxwel**
|
- **Roxwel**
|
||||||
- **Rozhlas**
|
- **Rozhlas**
|
||||||
@@ -905,8 +955,10 @@
|
|||||||
- **rtl2:you**
|
- **rtl2:you**
|
||||||
- **rtl2:you:series**
|
- **rtl2:you:series**
|
||||||
- **RTP**
|
- **RTP**
|
||||||
|
- **RTRFM**
|
||||||
- **RTS**: RTS.ch
|
- **RTS**: RTS.ch
|
||||||
- **rtve.es:alacarta**: RTVE a la carta
|
- **rtve.es:alacarta**: RTVE a la carta
|
||||||
|
- **rtve.es:audio**: RTVE audio
|
||||||
- **rtve.es:infantil**: RTVE infantil
|
- **rtve.es:infantil**: RTVE infantil
|
||||||
- **rtve.es:live**: RTVE.es live streams
|
- **rtve.es:live**: RTVE.es live streams
|
||||||
- **rtve.es:television**
|
- **rtve.es:television**
|
||||||
@@ -916,11 +968,12 @@
|
|||||||
- **RumbleChannel**
|
- **RumbleChannel**
|
||||||
- **RumbleEmbed**
|
- **RumbleEmbed**
|
||||||
- **rutube**: Rutube videos
|
- **rutube**: Rutube videos
|
||||||
- **rutube:channel**: Rutube channels
|
- **rutube:channel**: Rutube channel
|
||||||
- **rutube:embed**: Rutube embedded videos
|
- **rutube:embed**: Rutube embedded videos
|
||||||
- **rutube:movie**: Rutube movies
|
- **rutube:movie**: Rutube movies
|
||||||
- **rutube:person**: Rutube person videos
|
- **rutube:person**: Rutube person videos
|
||||||
- **rutube:playlist**: Rutube playlists
|
- **rutube:playlist**: Rutube playlists
|
||||||
|
- **rutube:tags**: Rutube tags
|
||||||
- **RUTV**: RUTV.RU
|
- **RUTV**: RUTV.RU
|
||||||
- **Ruutu**
|
- **Ruutu**
|
||||||
- **Ruv**
|
- **Ruv**
|
||||||
@@ -936,7 +989,7 @@
|
|||||||
- **SBS**: sbs.com.au
|
- **SBS**: sbs.com.au
|
||||||
- **schooltv**
|
- **schooltv**
|
||||||
- **ScienceChannel**
|
- **ScienceChannel**
|
||||||
- **screen.yahoo:search**: Yahoo screen search
|
- **screen.yahoo:search**: Yahoo screen search; "yvsearch:" prefix
|
||||||
- **Screencast**
|
- **Screencast**
|
||||||
- **ScreencastOMatic**
|
- **ScreencastOMatic**
|
||||||
- **ScrippsNetworks**
|
- **ScrippsNetworks**
|
||||||
@@ -944,6 +997,7 @@
|
|||||||
- **SCTE**
|
- **SCTE**
|
||||||
- **SCTECourse**
|
- **SCTECourse**
|
||||||
- **Seeker**
|
- **Seeker**
|
||||||
|
- **SenateGov**
|
||||||
- **SenateISVP**
|
- **SenateISVP**
|
||||||
- **SendtoNews**
|
- **SendtoNews**
|
||||||
- **Servus**
|
- **Servus**
|
||||||
@@ -959,8 +1013,10 @@
|
|||||||
- **simplecast:episode**
|
- **simplecast:episode**
|
||||||
- **simplecast:podcast**
|
- **simplecast:podcast**
|
||||||
- **Sina**
|
- **Sina**
|
||||||
|
- **Skeb**
|
||||||
- **sky.it**
|
- **sky.it**
|
||||||
- **sky:news**
|
- **sky:news**
|
||||||
|
- **sky:news:story**
|
||||||
- **sky:sports**
|
- **sky:sports**
|
||||||
- **sky:sports:news**
|
- **sky:sports:news**
|
||||||
- **skyacademy.it**
|
- **skyacademy.it**
|
||||||
@@ -977,7 +1033,8 @@
|
|||||||
- **SonyLIVSeries**
|
- **SonyLIVSeries**
|
||||||
- **soundcloud**
|
- **soundcloud**
|
||||||
- **soundcloud:playlist**
|
- **soundcloud:playlist**
|
||||||
- **soundcloud:search**: Soundcloud search, "scsearch" keyword
|
- **soundcloud:related**
|
||||||
|
- **soundcloud:search**: Soundcloud search; "scsearch:" prefix
|
||||||
- **soundcloud:set**
|
- **soundcloud:set**
|
||||||
- **soundcloud:trackstation**
|
- **soundcloud:trackstation**
|
||||||
- **soundcloud:user**
|
- **soundcloud:user**
|
||||||
@@ -1021,8 +1078,10 @@
|
|||||||
- **Streamanity**
|
- **Streamanity**
|
||||||
- **streamcloud.eu**
|
- **streamcloud.eu**
|
||||||
- **StreamCZ**
|
- **StreamCZ**
|
||||||
|
- **StreamFF**
|
||||||
- **StreetVoice**
|
- **StreetVoice**
|
||||||
- **StretchInternet**
|
- **StretchInternet**
|
||||||
|
- **Stripchat**
|
||||||
- **stv:player**
|
- **stv:player**
|
||||||
- **SunPorno**
|
- **SunPorno**
|
||||||
- **sverigesradio:episode**
|
- **sverigesradio:episode**
|
||||||
@@ -1079,6 +1138,8 @@
|
|||||||
- **ThisAmericanLife**
|
- **ThisAmericanLife**
|
||||||
- **ThisAV**
|
- **ThisAV**
|
||||||
- **ThisOldHouse**
|
- **ThisOldHouse**
|
||||||
|
- **ThreeSpeak**
|
||||||
|
- **ThreeSpeakUser**
|
||||||
- **TikTok**
|
- **TikTok**
|
||||||
- **tiktok:user**
|
- **tiktok:user**
|
||||||
- **tinypic**: tinypic.com videos
|
- **tinypic**: tinypic.com videos
|
||||||
@@ -1086,6 +1147,7 @@
|
|||||||
- **TNAFlix**
|
- **TNAFlix**
|
||||||
- **TNAFlixNetworkEmbed**
|
- **TNAFlixNetworkEmbed**
|
||||||
- **toggle**
|
- **toggle**
|
||||||
|
- **toggo**
|
||||||
- **Tokentube**
|
- **Tokentube**
|
||||||
- **Tokentube:channel**
|
- **Tokentube:channel**
|
||||||
- **ToonGoggles**
|
- **ToonGoggles**
|
||||||
@@ -1095,9 +1157,10 @@
|
|||||||
- **TrailerAddict** (Currently broken)
|
- **TrailerAddict** (Currently broken)
|
||||||
- **Trilulilu**
|
- **Trilulilu**
|
||||||
- **Trovo**
|
- **Trovo**
|
||||||
- **TrovoChannelClip**: All Clips of a trovo.live channel, "trovoclip" keyword
|
- **TrovoChannelClip**: All Clips of a trovo.live channel; "trovoclip:" prefix
|
||||||
- **TrovoChannelVod**: All VODs of a trovo.live channel, "trovovod" keyword
|
- **TrovoChannelVod**: All VODs of a trovo.live channel; "trovovod:" prefix
|
||||||
- **TrovoVod**
|
- **TrovoVod**
|
||||||
|
- **TrueID**
|
||||||
- **TruNews**
|
- **TruNews**
|
||||||
- **TruTV**
|
- **TruTV**
|
||||||
- **Tube8**
|
- **Tube8**
|
||||||
@@ -1142,6 +1205,7 @@
|
|||||||
- **tvp**: Telewizja Polska
|
- **tvp**: Telewizja Polska
|
||||||
- **tvp:embed**: Telewizja Polska
|
- **tvp:embed**: Telewizja Polska
|
||||||
- **tvp:series**
|
- **tvp:series**
|
||||||
|
- **tvp:stream**
|
||||||
- **TVPlayer**
|
- **TVPlayer**
|
||||||
- **TVPlayHome**
|
- **TVPlayHome**
|
||||||
- **Tweakers**
|
- **Tweakers**
|
||||||
@@ -1201,7 +1265,7 @@
|
|||||||
- **Viddler**
|
- **Viddler**
|
||||||
- **Videa**
|
- **Videa**
|
||||||
- **video.arnes.si**: Arnes Video
|
- **video.arnes.si**: Arnes Video
|
||||||
- **video.google:search**: Google Video search (Currently broken)
|
- **video.google:search**: Google Video search; "gvsearch:" prefix (Currently broken)
|
||||||
- **video.sky.it**
|
- **video.sky.it**
|
||||||
- **video.sky.it:live**
|
- **video.sky.it:live**
|
||||||
- **VideoDetective**
|
- **VideoDetective**
|
||||||
@@ -1291,11 +1355,14 @@
|
|||||||
- **WeiboMobile**
|
- **WeiboMobile**
|
||||||
- **WeiqiTV**: WQTV
|
- **WeiqiTV**: WQTV
|
||||||
- **whowatch**
|
- **whowatch**
|
||||||
|
- **Willow**
|
||||||
- **WimTV**
|
- **WimTV**
|
||||||
- **Wistia**
|
- **Wistia**
|
||||||
- **WistiaPlaylist**
|
- **WistiaPlaylist**
|
||||||
- **wnl**: npo.nl, ntr.nl, omroepwnl.nl, zapp.nl and npo3.nl
|
- **wnl**: npo.nl, ntr.nl, omroepwnl.nl, zapp.nl and npo3.nl
|
||||||
- **WorldStarHipHop**
|
- **WorldStarHipHop**
|
||||||
|
- **wppilot**
|
||||||
|
- **wppilot:channels**
|
||||||
- **WSJ**: Wall Street Journal
|
- **WSJ**: Wall Street Journal
|
||||||
- **WSJArticle**
|
- **WSJArticle**
|
||||||
- **WWE**
|
- **WWE**
|
||||||
@@ -1343,19 +1410,19 @@
|
|||||||
- **YouPorn**
|
- **YouPorn**
|
||||||
- **YourPorn**
|
- **YourPorn**
|
||||||
- **YourUpload**
|
- **YourUpload**
|
||||||
- **youtube**: YouTube.com
|
- **youtube**: YouTube
|
||||||
- **youtube:favorites**: YouTube.com liked videos, ":ytfav" for short (requires authentication)
|
- **youtube:favorites**: YouTube liked videos; ":ytfav" keyword (requires cookies)
|
||||||
- **youtube:history**: Youtube watch history, ":ythis" for short (requires authentication)
|
- **youtube:history**: Youtube watch history; ":ythis" keyword (requires cookies)
|
||||||
- **youtube:playlist**: YouTube.com playlists
|
- **youtube:playlist**: YouTube playlists
|
||||||
- **youtube:recommended**: YouTube.com recommended videos, ":ytrec" for short (requires authentication)
|
- **youtube:recommended**: YouTube recommended videos; ":ytrec" keyword
|
||||||
- **youtube:search**: YouTube.com searches, "ytsearch" keyword
|
- **youtube:search**: YouTube search; "ytsearch:" prefix
|
||||||
- **youtube:search:date**: YouTube.com searches, newest videos first, "ytsearchdate" keyword
|
- **youtube:search:date**: YouTube search, newest videos first; "ytsearchdate:" prefix
|
||||||
- **youtube:search_url**: YouTube.com search URLs
|
- **youtube:search_url**: YouTube search URLs with sorting and filter support
|
||||||
- **youtube:subscriptions**: YouTube.com subscriptions feed, ":ytsubs" for short (requires authentication)
|
- **youtube:subscriptions**: YouTube subscriptions feed; ":ytsubs" keyword (requires cookies)
|
||||||
- **youtube:tab**: YouTube.com tab
|
- **youtube:tab**: YouTube Tabs
|
||||||
- **youtube:watchlater**: Youtube watch later list, ":ytwatchlater" for short (requires authentication)
|
- **youtube:watchlater**: Youtube watch later list; ":ytwatchlater" keyword (requires cookies)
|
||||||
- **YoutubeYtBe**: youtu.be
|
- **YoutubeYtBe**: youtu.be
|
||||||
- **YoutubeYtUser**: YouTube.com user videos, URL or "ytuser" keyword
|
- **YoutubeYtUser**: YouTube user videos; "ytuser:" prefix
|
||||||
- **Zapiks**
|
- **Zapiks**
|
||||||
- **Zattoo**
|
- **Zattoo**
|
||||||
- **ZattooLive**
|
- **ZattooLive**
|
||||||
|
|||||||
+47
-4
@@ -194,6 +194,51 @@ def expect_dict(self, got_dict, expected_dict):
|
|||||||
expect_value(self, got, expected, info_field)
|
expect_value(self, got, expected, info_field)
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_got_info_dict(got_dict):
|
||||||
|
IGNORED_FIELDS = (
|
||||||
|
# Format keys
|
||||||
|
'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
|
||||||
|
'formats', 'thumbnails', 'subtitles', 'automatic_captions', 'comments', 'entries',
|
||||||
|
|
||||||
|
# Auto-generated
|
||||||
|
'autonumber', 'playlist', 'format_index', 'video_ext', 'audio_ext', 'duration_string', 'epoch',
|
||||||
|
'fulltitle', 'extractor', 'extractor_key', 'filepath', 'infojson_filename', 'original_url',
|
||||||
|
|
||||||
|
# Only live_status needs to be checked
|
||||||
|
'is_live', 'was_live',
|
||||||
|
)
|
||||||
|
|
||||||
|
IGNORED_PREFIXES = ('', 'playlist', 'requested', 'webpage')
|
||||||
|
|
||||||
|
def sanitize(key, value):
|
||||||
|
if isinstance(value, str) and len(value) > 100:
|
||||||
|
return f'md5:{md5(value)}'
|
||||||
|
elif isinstance(value, list) and len(value) > 10:
|
||||||
|
return f'count:{len(value)}'
|
||||||
|
return value
|
||||||
|
|
||||||
|
test_info_dict = {
|
||||||
|
key: sanitize(key, value) for key, value in got_dict.items()
|
||||||
|
if value is not None and key not in IGNORED_FIELDS and not any(
|
||||||
|
key.startswith(f'{prefix}_') for prefix in IGNORED_PREFIXES)
|
||||||
|
}
|
||||||
|
|
||||||
|
# display_id may be generated from id
|
||||||
|
if test_info_dict.get('display_id') == test_info_dict['id']:
|
||||||
|
test_info_dict.pop('display_id')
|
||||||
|
|
||||||
|
return test_info_dict
|
||||||
|
|
||||||
|
|
||||||
def expect_info_dict(self, got_dict, expected_dict):
|
def expect_info_dict(self, got_dict, expected_dict):
|
||||||
expect_dict(self, got_dict, expected_dict)
|
expect_dict(self, got_dict, expected_dict)
|
||||||
# Check for the presence of mandatory fields
|
# Check for the presence of mandatory fields
|
||||||
@@ -207,10 +252,8 @@ def expect_info_dict(self, got_dict, expected_dict):
|
|||||||
for key in ['webpage_url', 'extractor', 'extractor_key']:
|
for key in ['webpage_url', 'extractor', 'extractor_key']:
|
||||||
self.assertTrue(got_dict.get(key), 'Missing field: %s' % key)
|
self.assertTrue(got_dict.get(key), 'Missing field: %s' % key)
|
||||||
|
|
||||||
# Are checkable fields missing from the test case definition?
|
test_info_dict = sanitize_got_info_dict(got_dict)
|
||||||
test_info_dict = dict((key, value if not isinstance(value, compat_str) or len(value) < 250 else 'md5:' + md5(value))
|
|
||||||
for key, value in got_dict.items()
|
|
||||||
if value and key in ('id', 'title', 'description', 'uploader', 'upload_date', 'timestamp', 'uploader_id', 'location', 'age_limit'))
|
|
||||||
missing_keys = set(test_info_dict.keys()) - set(expected_dict.keys())
|
missing_keys = set(test_info_dict.keys()) - set(expected_dict.keys())
|
||||||
if missing_keys:
|
if missing_keys:
|
||||||
def _repr(v):
|
def _repr(v):
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"forcetitle": false,
|
"forcetitle": false,
|
||||||
"forceurl": false,
|
"forceurl": false,
|
||||||
"force_write_download_archive": false,
|
"force_write_download_archive": false,
|
||||||
"format": "best",
|
"format": "b/bv",
|
||||||
"ignoreerrors": false,
|
"ignoreerrors": false,
|
||||||
"listformats": null,
|
"listformats": null,
|
||||||
"logtostderr": false,
|
"logtostderr": false,
|
||||||
|
|||||||
+84
-15
@@ -99,10 +99,10 @@ class TestInfoExtractor(unittest.TestCase):
|
|||||||
self.assertRaises(RegexNotFoundError, ie._html_search_meta, ('z', 'x'), html, None, fatal=True)
|
self.assertRaises(RegexNotFoundError, ie._html_search_meta, ('z', 'x'), html, None, fatal=True)
|
||||||
|
|
||||||
def test_search_json_ld_realworld(self):
|
def test_search_json_ld_realworld(self):
|
||||||
# https://github.com/ytdl-org/youtube-dl/issues/23306
|
_TESTS = [
|
||||||
expect_dict(
|
# https://github.com/ytdl-org/youtube-dl/issues/23306
|
||||||
self,
|
(
|
||||||
self.ie._search_json_ld(r'''<script type="application/ld+json">
|
r'''<script type="application/ld+json">
|
||||||
{
|
{
|
||||||
"@context": "http://schema.org/",
|
"@context": "http://schema.org/",
|
||||||
"@type": "VideoObject",
|
"@type": "VideoObject",
|
||||||
@@ -135,17 +135,86 @@ class TestInfoExtractor(unittest.TestCase):
|
|||||||
"name": "Kleio Valentien",
|
"name": "Kleio Valentien",
|
||||||
"url": "https://www.eporner.com/pornstar/kleio-valentien/"
|
"url": "https://www.eporner.com/pornstar/kleio-valentien/"
|
||||||
}]}
|
}]}
|
||||||
</script>''', None),
|
</script>''',
|
||||||
{
|
{
|
||||||
'title': '1 On 1 With Kleio',
|
'title': '1 On 1 With Kleio',
|
||||||
'description': 'Kleio Valentien',
|
'description': 'Kleio Valentien',
|
||||||
'url': 'https://gvideo.eporner.com/xN49A1cT3eB/xN49A1cT3eB.mp4',
|
'url': 'https://gvideo.eporner.com/xN49A1cT3eB/xN49A1cT3eB.mp4',
|
||||||
'timestamp': 1449347075,
|
'timestamp': 1449347075,
|
||||||
'duration': 743.0,
|
'duration': 743.0,
|
||||||
'view_count': 1120958,
|
'view_count': 1120958,
|
||||||
'width': 1920,
|
'width': 1920,
|
||||||
'height': 1080,
|
'height': 1080,
|
||||||
})
|
},
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
r'''<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@graph": [
|
||||||
|
{
|
||||||
|
"@type": "NewsArticle",
|
||||||
|
"mainEntityOfPage": {
|
||||||
|
"@type": "WebPage",
|
||||||
|
"@id": "https://www.ant1news.gr/Society/article/620286/symmoria-anilikon-dikigoros-thymaton-ithelan-na-toys-apoteleiosoyn"
|
||||||
|
},
|
||||||
|
"headline": "Συμμορία ανηλίκων – δικηγόρος θυμάτων: ήθελαν να τους αποτελειώσουν",
|
||||||
|
"name": "Συμμορία ανηλίκων – δικηγόρος θυμάτων: ήθελαν να τους αποτελειώσουν",
|
||||||
|
"description": "Τα παιδιά δέχθηκαν την επίθεση επειδή αρνήθηκαν να γίνουν μέλη της συμμορίας, ανέφερε ο Γ. Ζαχαρόπουλος.",
|
||||||
|
"image": {
|
||||||
|
"@type": "ImageObject",
|
||||||
|
"url": "https://ant1media.azureedge.net/imgHandler/1100/a635c968-be71-447c-bf9c-80d843ece21e.jpg",
|
||||||
|
"width": 1100,
|
||||||
|
"height": 756 },
|
||||||
|
"datePublished": "2021-11-10T08:50:00+03:00",
|
||||||
|
"dateModified": "2021-11-10T08:52:53+03:00",
|
||||||
|
"author": {
|
||||||
|
"@type": "Person",
|
||||||
|
"@id": "https://www.ant1news.gr/",
|
||||||
|
"name": "Ant1news",
|
||||||
|
"image": "https://www.ant1news.gr/images/logo-e5d7e4b3e714c88e8d2eca96130142f6.png",
|
||||||
|
"url": "https://www.ant1news.gr/"
|
||||||
|
},
|
||||||
|
"publisher": {
|
||||||
|
"@type": "Organization",
|
||||||
|
"@id": "https://www.ant1news.gr#publisher",
|
||||||
|
"name": "Ant1news",
|
||||||
|
"url": "https://www.ant1news.gr",
|
||||||
|
"logo": {
|
||||||
|
"@type": "ImageObject",
|
||||||
|
"url": "https://www.ant1news.gr/images/logo-e5d7e4b3e714c88e8d2eca96130142f6.png",
|
||||||
|
"width": 400,
|
||||||
|
"height": 400 },
|
||||||
|
"sameAs": [
|
||||||
|
"https://www.facebook.com/Ant1news.gr",
|
||||||
|
"https://twitter.com/antennanews",
|
||||||
|
"https://www.youtube.com/channel/UC0smvAbfczoN75dP0Hw4Pzw",
|
||||||
|
"https://www.instagram.com/ant1news/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"keywords": "μαχαίρωμα,συμμορία ανηλίκων,ΕΙΔΗΣΕΙΣ,ΕΙΔΗΣΕΙΣ ΣΗΜΕΡΑ,ΝΕΑ,Κοινωνία - Ant1news",
|
||||||
|
|
||||||
|
|
||||||
|
"articleSection": "Κοινωνία"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</script>''',
|
||||||
|
{
|
||||||
|
'timestamp': 1636523400,
|
||||||
|
'title': 'md5:91fe569e952e4d146485740ae927662b',
|
||||||
|
},
|
||||||
|
{'expected_type': 'NewsArticle'},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
for html, expected_dict, search_json_ld_kwargs in _TESTS:
|
||||||
|
expect_dict(
|
||||||
|
self,
|
||||||
|
self.ie._search_json_ld(html, None, **search_json_ld_kwargs),
|
||||||
|
expected_dict
|
||||||
|
)
|
||||||
|
|
||||||
def test_download_json(self):
|
def test_download_json(self):
|
||||||
uri = encode_data_uri(b'{"foo": "blah"}', 'application/json')
|
uri = encode_data_uri(b'{"foo": "blah"}', 'application/json')
|
||||||
|
|||||||
+23
-7
@@ -137,7 +137,7 @@ class TestFormatSelection(unittest.TestCase):
|
|||||||
test('webm/mp4', '47')
|
test('webm/mp4', '47')
|
||||||
test('3gp/40/mp4', '35')
|
test('3gp/40/mp4', '35')
|
||||||
test('example-with-dashes', 'example-with-dashes')
|
test('example-with-dashes', 'example-with-dashes')
|
||||||
test('all', '35', 'example-with-dashes', '45', '47', '2') # Order doesn't actually matter for this
|
test('all', '2', '47', '45', 'example-with-dashes', '35')
|
||||||
test('mergeall', '2+47+45+example-with-dashes+35', multi=True)
|
test('mergeall', '2+47+45+example-with-dashes+35', multi=True)
|
||||||
|
|
||||||
def test_format_selection_audio(self):
|
def test_format_selection_audio(self):
|
||||||
@@ -520,7 +520,7 @@ class TestFormatSelection(unittest.TestCase):
|
|||||||
ydl = YDL({'format': 'all[width>=400][width<=600]'})
|
ydl = YDL({'format': 'all[width>=400][width<=600]'})
|
||||||
ydl.process_ie_result(info_dict)
|
ydl.process_ie_result(info_dict)
|
||||||
downloaded_ids = [info['format_id'] for info in ydl.downloaded_info_dicts]
|
downloaded_ids = [info['format_id'] for info in ydl.downloaded_info_dicts]
|
||||||
self.assertEqual(downloaded_ids, ['B', 'C', 'D'])
|
self.assertEqual(downloaded_ids, ['D', 'C', 'B'])
|
||||||
|
|
||||||
ydl = YDL({'format': 'best[height<40]'})
|
ydl = YDL({'format': 'best[height<40]'})
|
||||||
try:
|
try:
|
||||||
@@ -656,7 +656,7 @@ class TestYoutubeDL(unittest.TestCase):
|
|||||||
'playlist_autonumber': 2,
|
'playlist_autonumber': 2,
|
||||||
'_last_playlist_index': 100,
|
'_last_playlist_index': 100,
|
||||||
'n_entries': 10,
|
'n_entries': 10,
|
||||||
'formats': [{'id': 'id1'}, {'id': 'id2'}, {'id': 'id3'}]
|
'formats': [{'id': 'id 1'}, {'id': 'id 2'}, {'id': 'id 3'}]
|
||||||
}
|
}
|
||||||
|
|
||||||
def test_prepare_outtmpl_and_filename(self):
|
def test_prepare_outtmpl_and_filename(self):
|
||||||
@@ -717,6 +717,7 @@ class TestYoutubeDL(unittest.TestCase):
|
|||||||
test('%(id)s', '.abcd', info={'id': '.abcd'})
|
test('%(id)s', '.abcd', info={'id': '.abcd'})
|
||||||
test('%(id)s', 'ab__cd', info={'id': 'ab__cd'})
|
test('%(id)s', 'ab__cd', info={'id': 'ab__cd'})
|
||||||
test('%(id)s', ('ab:cd', 'ab -cd'), info={'id': 'ab:cd'})
|
test('%(id)s', ('ab:cd', 'ab -cd'), info={'id': 'ab:cd'})
|
||||||
|
test('%(id.0)s', '-', info={'id': '--'})
|
||||||
|
|
||||||
# Invalid templates
|
# Invalid templates
|
||||||
self.assertTrue(isinstance(YoutubeDL.validate_outtmpl('%(title)'), ValueError))
|
self.assertTrue(isinstance(YoutubeDL.validate_outtmpl('%(title)'), ValueError))
|
||||||
@@ -737,6 +738,7 @@ class TestYoutubeDL(unittest.TestCase):
|
|||||||
test(NA_TEST_OUTTMPL, 'NA-NA-def-1234.mp4')
|
test(NA_TEST_OUTTMPL, 'NA-NA-def-1234.mp4')
|
||||||
test(NA_TEST_OUTTMPL, 'none-none-def-1234.mp4', outtmpl_na_placeholder='none')
|
test(NA_TEST_OUTTMPL, 'none-none-def-1234.mp4', outtmpl_na_placeholder='none')
|
||||||
test(NA_TEST_OUTTMPL, '--def-1234.mp4', outtmpl_na_placeholder='')
|
test(NA_TEST_OUTTMPL, '--def-1234.mp4', outtmpl_na_placeholder='')
|
||||||
|
test('%(non_existent.0)s', 'NA')
|
||||||
|
|
||||||
# String formatting
|
# String formatting
|
||||||
FMT_TEST_OUTTMPL = '%%(height)%s.%%(ext)s'
|
FMT_TEST_OUTTMPL = '%%(height)%s.%%(ext)s'
|
||||||
@@ -762,23 +764,32 @@ class TestYoutubeDL(unittest.TestCase):
|
|||||||
test('a%(width|)d', 'a', outtmpl_na_placeholder='none')
|
test('a%(width|)d', 'a', outtmpl_na_placeholder='none')
|
||||||
|
|
||||||
FORMATS = self.outtmpl_info['formats']
|
FORMATS = self.outtmpl_info['formats']
|
||||||
sanitize = lambda x: x.replace(':', ' -').replace('"', "'")
|
sanitize = lambda x: x.replace(':', ' -').replace('"', "'").replace('\n', ' ')
|
||||||
|
|
||||||
# Custom type casting
|
# Custom type casting
|
||||||
test('%(formats.:.id)l', 'id1, id2, id3')
|
test('%(formats.:.id)l', 'id 1, id 2, id 3')
|
||||||
test('%(formats.:.id)#l', ('id1\nid2\nid3', 'id1 id2 id3'))
|
test('%(formats.:.id)#l', ('id 1\nid 2\nid 3', 'id 1 id 2 id 3'))
|
||||||
test('%(ext)l', 'mp4')
|
test('%(ext)l', 'mp4')
|
||||||
test('%(formats.:.id) 15l', ' id1, id2, id3')
|
test('%(formats.:.id) 18l', ' id 1, id 2, id 3')
|
||||||
test('%(formats)j', (json.dumps(FORMATS), sanitize(json.dumps(FORMATS))))
|
test('%(formats)j', (json.dumps(FORMATS), sanitize(json.dumps(FORMATS))))
|
||||||
|
test('%(formats)#j', (json.dumps(FORMATS, indent=4), sanitize(json.dumps(FORMATS, indent=4))))
|
||||||
test('%(title5).3B', 'á')
|
test('%(title5).3B', 'á')
|
||||||
test('%(title5)U', 'áéí 𝐀')
|
test('%(title5)U', 'áéí 𝐀')
|
||||||
test('%(title5)#U', 'a\u0301e\u0301i\u0301 𝐀')
|
test('%(title5)#U', 'a\u0301e\u0301i\u0301 𝐀')
|
||||||
test('%(title5)+U', 'áéí A')
|
test('%(title5)+U', 'áéí A')
|
||||||
test('%(title5)+#U', 'a\u0301e\u0301i\u0301 A')
|
test('%(title5)+#U', 'a\u0301e\u0301i\u0301 A')
|
||||||
|
test('%(height)D', '1K')
|
||||||
|
test('%(height)5.2D', ' 1.08K')
|
||||||
|
test('%(title4)#S', 'foo_bar_test')
|
||||||
|
test('%(title4).10S', ('foo \'bar\' ', 'foo \'bar\'' + ('#' if compat_os_name == 'nt' else ' ')))
|
||||||
if compat_os_name == 'nt':
|
if compat_os_name == 'nt':
|
||||||
test('%(title4)q', ('"foo \\"bar\\" test"', "'foo _'bar_' test'"))
|
test('%(title4)q', ('"foo \\"bar\\" test"', "'foo _'bar_' test'"))
|
||||||
|
test('%(formats.:.id)#q', ('"id 1" "id 2" "id 3"', "'id 1' 'id 2' 'id 3'"))
|
||||||
|
test('%(formats.0.id)#q', ('"id 1"', "'id 1'"))
|
||||||
else:
|
else:
|
||||||
test('%(title4)q', ('\'foo "bar" test\'', "'foo 'bar' test'"))
|
test('%(title4)q', ('\'foo "bar" test\'', "'foo 'bar' test'"))
|
||||||
|
test('%(formats.:.id)#q', "'id 1' 'id 2' 'id 3'")
|
||||||
|
test('%(formats.0.id)#q', "'id 1'")
|
||||||
|
|
||||||
# Internal formatting
|
# Internal formatting
|
||||||
test('%(timestamp-1000>%H-%M-%S)s', '11-43-20')
|
test('%(timestamp-1000>%H-%M-%S)s', '11-43-20')
|
||||||
@@ -802,6 +813,11 @@ class TestYoutubeDL(unittest.TestCase):
|
|||||||
test('%(width-100,height+width|def)s', 'def')
|
test('%(width-100,height+width|def)s', 'def')
|
||||||
test('%(timestamp-x>%H\\,%M\\,%S,timestamp>%H\\,%M\\,%S)s', '12,00,00')
|
test('%(timestamp-x>%H\\,%M\\,%S,timestamp>%H\\,%M\\,%S)s', '12,00,00')
|
||||||
|
|
||||||
|
# Replacement
|
||||||
|
test('%(id&foo)s.bar', 'foo.bar')
|
||||||
|
test('%(title&foo)s.bar', 'NA.bar')
|
||||||
|
test('%(title&foo|baz)s.bar', 'baz.bar')
|
||||||
|
|
||||||
# Laziness
|
# Laziness
|
||||||
def gen():
|
def gen():
|
||||||
yield from range(5)
|
yield from range(5)
|
||||||
|
|||||||
+17
-1
@@ -10,6 +10,8 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|||||||
from yt_dlp.aes import (
|
from yt_dlp.aes import (
|
||||||
aes_decrypt,
|
aes_decrypt,
|
||||||
aes_encrypt,
|
aes_encrypt,
|
||||||
|
aes_ecb_encrypt,
|
||||||
|
aes_ecb_decrypt,
|
||||||
aes_cbc_decrypt,
|
aes_cbc_decrypt,
|
||||||
aes_cbc_decrypt_bytes,
|
aes_cbc_decrypt_bytes,
|
||||||
aes_cbc_encrypt,
|
aes_cbc_encrypt,
|
||||||
@@ -17,7 +19,8 @@ from yt_dlp.aes import (
|
|||||||
aes_ctr_encrypt,
|
aes_ctr_encrypt,
|
||||||
aes_gcm_decrypt_and_verify,
|
aes_gcm_decrypt_and_verify,
|
||||||
aes_gcm_decrypt_and_verify_bytes,
|
aes_gcm_decrypt_and_verify_bytes,
|
||||||
aes_decrypt_text
|
aes_decrypt_text,
|
||||||
|
BLOCK_SIZE_BYTES,
|
||||||
)
|
)
|
||||||
from yt_dlp.compat import compat_pycrypto_AES
|
from yt_dlp.compat import compat_pycrypto_AES
|
||||||
from yt_dlp.utils import bytes_to_intlist, intlist_to_bytes
|
from yt_dlp.utils import bytes_to_intlist, intlist_to_bytes
|
||||||
@@ -94,6 +97,19 @@ class TestAES(unittest.TestCase):
|
|||||||
decrypted = (aes_decrypt_text(encrypted, password, 32))
|
decrypted = (aes_decrypt_text(encrypted, password, 32))
|
||||||
self.assertEqual(decrypted, self.secret_msg)
|
self.assertEqual(decrypted, self.secret_msg)
|
||||||
|
|
||||||
|
def test_ecb_encrypt(self):
|
||||||
|
data = bytes_to_intlist(self.secret_msg)
|
||||||
|
data += [0x08] * (BLOCK_SIZE_BYTES - len(data) % BLOCK_SIZE_BYTES)
|
||||||
|
encrypted = intlist_to_bytes(aes_ecb_encrypt(data, self.key, self.iv))
|
||||||
|
self.assertEqual(
|
||||||
|
encrypted,
|
||||||
|
b'\xaa\x86]\x81\x97>\x02\x92\x9d\x1bR[[L/u\xd3&\xd1(h\xde{\x81\x94\xba\x02\xae\xbd\xa6\xd0:')
|
||||||
|
|
||||||
|
def test_ecb_decrypt(self):
|
||||||
|
data = bytes_to_intlist(b'\xaa\x86]\x81\x97>\x02\x92\x9d\x1bR[[L/u\xd3&\xd1(h\xde{\x81\x94\xba\x02\xae\xbd\xa6\xd0:')
|
||||||
|
decrypted = intlist_to_bytes(aes_ecb_decrypt(data, self.key, self.iv))
|
||||||
|
self.assertEqual(decrypted.rstrip(b'\x08'), self.secret_msg)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ class TestAllURLsMatching(unittest.TestCase):
|
|||||||
assertTab('https://www.youtube.com/AsapSCIENCE')
|
assertTab('https://www.youtube.com/AsapSCIENCE')
|
||||||
assertTab('https://www.youtube.com/embedded')
|
assertTab('https://www.youtube.com/embedded')
|
||||||
assertTab('https://www.youtube.com/playlist?list=UUBABnxM4Ar9ten8Mdjj1j0Q')
|
assertTab('https://www.youtube.com/playlist?list=UUBABnxM4Ar9ten8Mdjj1j0Q')
|
||||||
assertTab('https://www.youtube.com/course?list=ECUl4u3cNGP61MdtwGTqZA0MreSaDybji8')
|
|
||||||
assertTab('https://www.youtube.com/playlist?list=PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC')
|
assertTab('https://www.youtube.com/playlist?list=PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC')
|
||||||
assertTab('https://www.youtube.com/watch?v=AV6J6_AeFEQ&playnext=1&list=PL4023E734DA416012') # 668
|
assertTab('https://www.youtube.com/watch?v=AV6J6_AeFEQ&playnext=1&list=PL4023E734DA416012') # 668
|
||||||
self.assertFalse('youtube:playlist' in self.matching_ies('PLtS2H6bU1M'))
|
self.assertFalse('youtube:playlist' in self.matching_ies('PLtS2H6bU1M'))
|
||||||
|
|||||||
@@ -112,6 +112,71 @@ class TestJSInterpreter(unittest.TestCase):
|
|||||||
''')
|
''')
|
||||||
self.assertEqual(jsi.call_function('z'), 5)
|
self.assertEqual(jsi.call_function('z'), 5)
|
||||||
|
|
||||||
|
def test_for_loop(self):
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x() { a=0; for (i=0; i-10; i++) {a++} a }
|
||||||
|
''')
|
||||||
|
self.assertEqual(jsi.call_function('x'), 10)
|
||||||
|
|
||||||
|
def test_switch(self):
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x(f) { switch(f){
|
||||||
|
case 1:f+=1;
|
||||||
|
case 2:f+=2;
|
||||||
|
case 3:f+=3;break;
|
||||||
|
case 4:f+=4;
|
||||||
|
default:f=0;
|
||||||
|
} return f }
|
||||||
|
''')
|
||||||
|
self.assertEqual(jsi.call_function('x', 1), 7)
|
||||||
|
self.assertEqual(jsi.call_function('x', 3), 6)
|
||||||
|
self.assertEqual(jsi.call_function('x', 5), 0)
|
||||||
|
|
||||||
|
def test_switch_default(self):
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x(f) { switch(f){
|
||||||
|
case 2: f+=2;
|
||||||
|
default: f-=1;
|
||||||
|
case 5:
|
||||||
|
case 6: f+=6;
|
||||||
|
case 0: break;
|
||||||
|
case 1: f+=1;
|
||||||
|
} return f }
|
||||||
|
''')
|
||||||
|
self.assertEqual(jsi.call_function('x', 1), 2)
|
||||||
|
self.assertEqual(jsi.call_function('x', 5), 11)
|
||||||
|
self.assertEqual(jsi.call_function('x', 9), 14)
|
||||||
|
|
||||||
|
def test_try(self):
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x() { try{return 10} catch(e){return 5} }
|
||||||
|
''')
|
||||||
|
self.assertEqual(jsi.call_function('x'), 10)
|
||||||
|
|
||||||
|
def test_for_loop_continue(self):
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x() { a=0; for (i=0; i-10; i++) { continue; a++ } a }
|
||||||
|
''')
|
||||||
|
self.assertEqual(jsi.call_function('x'), 0)
|
||||||
|
|
||||||
|
def test_for_loop_break(self):
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x() { a=0; for (i=0; i-10; i++) { break; a++ } a }
|
||||||
|
''')
|
||||||
|
self.assertEqual(jsi.call_function('x'), 0)
|
||||||
|
|
||||||
|
def test_literal_list(self):
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x() { [1, 2, "asdf", [5, 6, 7]][3] }
|
||||||
|
''')
|
||||||
|
self.assertEqual(jsi.call_function('x'), [5, 6, 7])
|
||||||
|
|
||||||
|
def test_comma(self):
|
||||||
|
jsi = JSInterpreter('''
|
||||||
|
function x() { a=5; a -= 1, a+=3; return a }
|
||||||
|
''')
|
||||||
|
self.assertEqual(jsi.call_function('x'), 7)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -124,11 +124,11 @@ class TestModifyChaptersPP(unittest.TestCase):
|
|||||||
chapters = self._chapters([70], ['c']) + [
|
chapters = self._chapters([70], ['c']) + [
|
||||||
self._sponsor_chapter(10, 20, 'sponsor'),
|
self._sponsor_chapter(10, 20, 'sponsor'),
|
||||||
self._sponsor_chapter(30, 40, 'preview'),
|
self._sponsor_chapter(30, 40, 'preview'),
|
||||||
self._sponsor_chapter(50, 60, 'sponsor')]
|
self._sponsor_chapter(50, 60, 'filler')]
|
||||||
expected = self._chapters(
|
expected = self._chapters(
|
||||||
[10, 20, 30, 40, 50, 60, 70],
|
[10, 20, 30, 40, 50, 60, 70],
|
||||||
['c', '[SponsorBlock]: Sponsor', 'c', '[SponsorBlock]: Preview/Recap',
|
['c', '[SponsorBlock]: Sponsor', 'c', '[SponsorBlock]: Preview/Recap',
|
||||||
'c', '[SponsorBlock]: Sponsor', 'c'])
|
'c', '[SponsorBlock]: Filler Tangent', 'c'])
|
||||||
self._remove_marked_arrange_sponsors_test_impl(chapters, expected, [])
|
self._remove_marked_arrange_sponsors_test_impl(chapters, expected, [])
|
||||||
|
|
||||||
def test_remove_marked_arrange_sponsors_UniqueNamesForOverlappingSponsors(self):
|
def test_remove_marked_arrange_sponsors_UniqueNamesForOverlappingSponsors(self):
|
||||||
|
|||||||
+51
-7
@@ -1156,9 +1156,16 @@ class TestUtil(unittest.TestCase):
|
|||||||
self.assertEqual(parse_count('1000'), 1000)
|
self.assertEqual(parse_count('1000'), 1000)
|
||||||
self.assertEqual(parse_count('1.000'), 1000)
|
self.assertEqual(parse_count('1.000'), 1000)
|
||||||
self.assertEqual(parse_count('1.1k'), 1100)
|
self.assertEqual(parse_count('1.1k'), 1100)
|
||||||
|
self.assertEqual(parse_count('1.1 k'), 1100)
|
||||||
|
self.assertEqual(parse_count('1,1 k'), 1100)
|
||||||
self.assertEqual(parse_count('1.1kk'), 1100000)
|
self.assertEqual(parse_count('1.1kk'), 1100000)
|
||||||
self.assertEqual(parse_count('1.1kk '), 1100000)
|
self.assertEqual(parse_count('1.1kk '), 1100000)
|
||||||
|
self.assertEqual(parse_count('1,1kk'), 1100000)
|
||||||
|
self.assertEqual(parse_count('100 views'), 100)
|
||||||
|
self.assertEqual(parse_count('1,100 views'), 1100)
|
||||||
self.assertEqual(parse_count('1.1kk views'), 1100000)
|
self.assertEqual(parse_count('1.1kk views'), 1100000)
|
||||||
|
self.assertEqual(parse_count('10M views'), 10000000)
|
||||||
|
self.assertEqual(parse_count('has 10M views'), 10000000)
|
||||||
|
|
||||||
def test_parse_resolution(self):
|
def test_parse_resolution(self):
|
||||||
self.assertEqual(parse_resolution(None), {})
|
self.assertEqual(parse_resolution(None), {})
|
||||||
@@ -1222,12 +1229,49 @@ ffmpeg version 2.4.4 Copyright (c) 2000-2014 the FFmpeg ...'''), '2.4.4')
|
|||||||
def test_render_table(self):
|
def test_render_table(self):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
render_table(
|
render_table(
|
||||||
['a', 'bcd'],
|
['a', 'empty', 'bcd'],
|
||||||
[[123, 4], [9999, 51]]),
|
[[123, '', 4], [9999, '', 51]]),
|
||||||
|
'a empty bcd\n'
|
||||||
|
'123 4\n'
|
||||||
|
'9999 51')
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
render_table(
|
||||||
|
['a', 'empty', 'bcd'],
|
||||||
|
[[123, '', 4], [9999, '', 51]],
|
||||||
|
hide_empty=True),
|
||||||
'a bcd\n'
|
'a bcd\n'
|
||||||
'123 4\n'
|
'123 4\n'
|
||||||
'9999 51')
|
'9999 51')
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
render_table(
|
||||||
|
['\ta', 'bcd'],
|
||||||
|
[['1\t23', 4], ['\t9999', 51]]),
|
||||||
|
' a bcd\n'
|
||||||
|
'1 23 4\n'
|
||||||
|
'9999 51')
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
render_table(
|
||||||
|
['a', 'bcd'],
|
||||||
|
[[123, 4], [9999, 51]],
|
||||||
|
delim='-'),
|
||||||
|
'a bcd\n'
|
||||||
|
'--------\n'
|
||||||
|
'123 4\n'
|
||||||
|
'9999 51')
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
render_table(
|
||||||
|
['a', 'bcd'],
|
||||||
|
[[123, 4], [9999, 51]],
|
||||||
|
delim='-', extra_gap=2),
|
||||||
|
'a bcd\n'
|
||||||
|
'----------\n'
|
||||||
|
'123 4\n'
|
||||||
|
'9999 51')
|
||||||
|
|
||||||
def test_match_str(self):
|
def test_match_str(self):
|
||||||
# Unary
|
# Unary
|
||||||
self.assertFalse(match_str('xy', {'x': 1200}))
|
self.assertFalse(match_str('xy', {'x': 1200}))
|
||||||
@@ -1620,9 +1664,9 @@ Line 1
|
|||||||
self.assertEqual(repr(LazyList(it)), repr(it))
|
self.assertEqual(repr(LazyList(it)), repr(it))
|
||||||
self.assertEqual(str(LazyList(it)), str(it))
|
self.assertEqual(str(LazyList(it)), str(it))
|
||||||
|
|
||||||
self.assertEqual(list(LazyList(it).reverse()), it[::-1])
|
self.assertEqual(list(LazyList(it, reverse=True)), it[::-1])
|
||||||
self.assertEqual(list(LazyList(it).reverse()[1:3:7]), it[::-1][1:3:7])
|
self.assertEqual(list(reversed(LazyList(it))[::-1]), it)
|
||||||
self.assertEqual(list(LazyList(it).reverse()[::-1]), it)
|
self.assertEqual(list(reversed(LazyList(it))[1:3:7]), it[::-1][1:3:7])
|
||||||
|
|
||||||
def test_LazyList_laziness(self):
|
def test_LazyList_laziness(self):
|
||||||
|
|
||||||
@@ -1635,13 +1679,13 @@ Line 1
|
|||||||
test(ll, 5, 5, range(6))
|
test(ll, 5, 5, range(6))
|
||||||
test(ll, -3, 7, range(10))
|
test(ll, -3, 7, range(10))
|
||||||
|
|
||||||
ll = LazyList(range(10)).reverse()
|
ll = LazyList(range(10), reverse=True)
|
||||||
test(ll, -1, 0, range(1))
|
test(ll, -1, 0, range(1))
|
||||||
test(ll, 3, 6, range(10))
|
test(ll, 3, 6, range(10))
|
||||||
|
|
||||||
ll = LazyList(itertools.count())
|
ll = LazyList(itertools.count())
|
||||||
test(ll, 10, 10, range(11))
|
test(ll, 10, 10, range(11))
|
||||||
ll.reverse()
|
ll = reversed(ll)
|
||||||
test(ll, -15, 14, range(15))
|
test(ll, -15, 14, range(15))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+12
-10
@@ -26,29 +26,31 @@ class TestYoutubeLists(unittest.TestCase):
|
|||||||
def test_youtube_playlist_noplaylist(self):
|
def test_youtube_playlist_noplaylist(self):
|
||||||
dl = FakeYDL()
|
dl = FakeYDL()
|
||||||
dl.params['noplaylist'] = True
|
dl.params['noplaylist'] = True
|
||||||
ie = YoutubePlaylistIE(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=FXxLjLQi3Fg&list=PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re')
|
||||||
self.assertEqual(result['_type'], 'url')
|
self.assertEqual(result['_type'], 'url')
|
||||||
self.assertEqual(YoutubeIE().extract_id(result['url']), 'FXxLjLQi3Fg')
|
self.assertEqual(YoutubeIE.extract_id(result['url']), 'FXxLjLQi3Fg')
|
||||||
|
|
||||||
def test_youtube_course(self):
|
def test_youtube_course(self):
|
||||||
|
print('Skipping: Course URLs no longer exists')
|
||||||
|
return
|
||||||
dl = FakeYDL()
|
dl = FakeYDL()
|
||||||
ie = YoutubePlaylistIE(dl)
|
ie = YoutubePlaylistIE(dl)
|
||||||
# TODO find a > 100 (paginating?) videos course
|
# TODO find a > 100 (paginating?) videos course
|
||||||
result = ie.extract('https://www.youtube.com/course?list=ECUl4u3cNGP61MdtwGTqZA0MreSaDybji8')
|
result = ie.extract('https://www.youtube.com/course?list=ECUl4u3cNGP61MdtwGTqZA0MreSaDybji8')
|
||||||
entries = list(result['entries'])
|
entries = list(result['entries'])
|
||||||
self.assertEqual(YoutubeIE().extract_id(entries[0]['url']), 'j9WZyLZCBzs')
|
self.assertEqual(YoutubeIE.extract_id(entries[0]['url']), 'j9WZyLZCBzs')
|
||||||
self.assertEqual(len(entries), 25)
|
self.assertEqual(len(entries), 25)
|
||||||
self.assertEqual(YoutubeIE().extract_id(entries[-1]['url']), 'rYefUsYuEp0')
|
self.assertEqual(YoutubeIE.extract_id(entries[-1]['url']), 'rYefUsYuEp0')
|
||||||
|
|
||||||
def test_youtube_mix(self):
|
def test_youtube_mix(self):
|
||||||
dl = FakeYDL()
|
dl = FakeYDL()
|
||||||
ie = YoutubePlaylistIE(dl)
|
ie = YoutubeTabIE(dl)
|
||||||
result = ie.extract('https://www.youtube.com/watch?v=W01L70IGBgE&index=2&list=RDOQpdSVF_k_w')
|
result = ie.extract('https://www.youtube.com/watch?v=tyITL_exICo&list=RDCLAK5uy_kLWIr9gv1XLlPbaDS965-Db4TrBoUTxQ8')
|
||||||
entries = result['entries']
|
entries = list(result['entries'])
|
||||||
self.assertTrue(len(entries) >= 50)
|
self.assertTrue(len(entries) >= 50)
|
||||||
original_video = entries[0]
|
original_video = entries[0]
|
||||||
self.assertEqual(original_video['id'], 'OQpdSVF_k_w')
|
self.assertEqual(original_video['id'], 'tyITL_exICo')
|
||||||
|
|
||||||
def test_youtube_toptracks(self):
|
def test_youtube_toptracks(self):
|
||||||
print('Skipping: The playlist page gives error 500')
|
print('Skipping: The playlist page gives error 500')
|
||||||
@@ -68,10 +70,10 @@ class TestYoutubeLists(unittest.TestCase):
|
|||||||
entries = list(result['entries'])
|
entries = list(result['entries'])
|
||||||
self.assertTrue(len(entries) == 1)
|
self.assertTrue(len(entries) == 1)
|
||||||
video = entries[0]
|
video = entries[0]
|
||||||
self.assertEqual(video['_type'], 'url_transparent')
|
self.assertEqual(video['_type'], 'url')
|
||||||
self.assertEqual(video['ie_key'], 'Youtube')
|
self.assertEqual(video['ie_key'], 'Youtube')
|
||||||
self.assertEqual(video['id'], 'BaW_jenozKc')
|
self.assertEqual(video['id'], 'BaW_jenozKc')
|
||||||
self.assertEqual(video['url'], 'BaW_jenozKc')
|
self.assertEqual(video['url'], 'https://www.youtube.com/watch?v=BaW_jenozKc')
|
||||||
self.assertEqual(video['title'], 'youtube-dl test video "\'/\\ä↭𝕐')
|
self.assertEqual(video['title'], 'youtube-dl test video "\'/\\ä↭𝕐')
|
||||||
self.assertEqual(video['duration'], 10)
|
self.assertEqual(video['duration'], 10)
|
||||||
self.assertEqual(video['uploader'], 'Philipp Hagemeister')
|
self.assertEqual(video['uploader'], 'Philipp Hagemeister')
|
||||||
|
|||||||
@@ -14,9 +14,10 @@ import string
|
|||||||
|
|
||||||
from test.helper import FakeYDL, is_download_test
|
from test.helper import FakeYDL, is_download_test
|
||||||
from yt_dlp.extractor import YoutubeIE
|
from yt_dlp.extractor import YoutubeIE
|
||||||
|
from yt_dlp.jsinterp import JSInterpreter
|
||||||
from yt_dlp.compat import compat_str, compat_urlretrieve
|
from yt_dlp.compat import compat_str, compat_urlretrieve
|
||||||
|
|
||||||
_TESTS = [
|
_SIG_TESTS = [
|
||||||
(
|
(
|
||||||
'https://s.ytimg.com/yts/jsbin/html5player-vflHOr_nV.js',
|
'https://s.ytimg.com/yts/jsbin/html5player-vflHOr_nV.js',
|
||||||
86,
|
86,
|
||||||
@@ -64,6 +65,29 @@ _TESTS = [
|
|||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
_NSIG_TESTS = [
|
||||||
|
(
|
||||||
|
'https://www.youtube.com/s/player/9216d1f7/player_ias.vflset/en_US/base.js',
|
||||||
|
'SLp9F5bwjAdhE9F-', 'gWnb9IK2DJ8Q1w',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'https://www.youtube.com/s/player/f8cb7a3b/player_ias.vflset/en_US/base.js',
|
||||||
|
'oBo2h5euWy6osrUt', 'ivXHpm7qJjJN',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'https://www.youtube.com/s/player/2dfe380c/player_ias.vflset/en_US/base.js',
|
||||||
|
'oBo2h5euWy6osrUt', '3DIBbn3qdQ',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'https://www.youtube.com/s/player/f1ca6900/player_ias.vflset/en_US/base.js',
|
||||||
|
'cu3wyu6LQn2hse', 'jvxetvmlI9AN9Q',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'https://www.youtube.com/s/player/8040e515/player_ias.vflset/en_US/base.js',
|
||||||
|
'wvOFaY-yjgDuIEg5', 'HkfBFDHmgw4rsw',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@is_download_test
|
@is_download_test
|
||||||
class TestPlayerInfo(unittest.TestCase):
|
class TestPlayerInfo(unittest.TestCase):
|
||||||
@@ -97,35 +121,49 @@ class TestSignature(unittest.TestCase):
|
|||||||
os.mkdir(self.TESTDATA_DIR)
|
os.mkdir(self.TESTDATA_DIR)
|
||||||
|
|
||||||
|
|
||||||
def make_tfunc(url, sig_input, expected_sig):
|
def t_factory(name, sig_func, url_pattern):
|
||||||
m = re.match(r'.*-([a-zA-Z0-9_-]+)(?:/watch_as3|/html5player)?\.[a-z]+$', url)
|
def make_tfunc(url, sig_input, expected_sig):
|
||||||
assert m, '%r should follow URL format' % url
|
m = url_pattern.match(url)
|
||||||
test_id = m.group(1)
|
assert m, '%r should follow URL format' % url
|
||||||
|
test_id = m.group('id')
|
||||||
|
|
||||||
def test_func(self):
|
def test_func(self):
|
||||||
basename = 'player-%s.js' % test_id
|
basename = f'player-{name}-{test_id}.js'
|
||||||
fn = os.path.join(self.TESTDATA_DIR, basename)
|
fn = os.path.join(self.TESTDATA_DIR, basename)
|
||||||
|
|
||||||
if not os.path.exists(fn):
|
if not os.path.exists(fn):
|
||||||
compat_urlretrieve(url, fn)
|
compat_urlretrieve(url, fn)
|
||||||
|
with io.open(fn, encoding='utf-8') as testf:
|
||||||
|
jscode = testf.read()
|
||||||
|
self.assertEqual(sig_func(jscode, sig_input), expected_sig)
|
||||||
|
|
||||||
ydl = FakeYDL()
|
test_func.__name__ = f'test_{name}_js_{test_id}'
|
||||||
ie = YoutubeIE(ydl)
|
setattr(TestSignature, test_func.__name__, test_func)
|
||||||
with io.open(fn, encoding='utf-8') as testf:
|
return make_tfunc
|
||||||
jscode = testf.read()
|
|
||||||
func = ie._parse_sig_js(jscode)
|
|
||||||
src_sig = (
|
|
||||||
compat_str(string.printable[:sig_input])
|
|
||||||
if isinstance(sig_input, int) else sig_input)
|
|
||||||
got_sig = func(src_sig)
|
|
||||||
self.assertEqual(got_sig, expected_sig)
|
|
||||||
|
|
||||||
test_func.__name__ = str('test_signature_js_' + test_id)
|
|
||||||
setattr(TestSignature, test_func.__name__, test_func)
|
|
||||||
|
|
||||||
|
|
||||||
for test_spec in _TESTS:
|
def signature(jscode, sig_input):
|
||||||
make_tfunc(*test_spec)
|
func = YoutubeIE(FakeYDL())._parse_sig_js(jscode)
|
||||||
|
src_sig = (
|
||||||
|
compat_str(string.printable[:sig_input])
|
||||||
|
if isinstance(sig_input, int) else sig_input)
|
||||||
|
return func(src_sig)
|
||||||
|
|
||||||
|
|
||||||
|
def n_sig(jscode, sig_input):
|
||||||
|
funcname = YoutubeIE(FakeYDL())._extract_n_function_name(jscode)
|
||||||
|
return JSInterpreter(jscode).call_function(funcname, sig_input)
|
||||||
|
|
||||||
|
|
||||||
|
make_sig_test = t_factory(
|
||||||
|
'signature', signature, re.compile(r'.*-(?P<id>[a-zA-Z0-9_-]+)(?:/watch_as3|/html5player)?\.[a-z]+$'))
|
||||||
|
for test_spec in _SIG_TESTS:
|
||||||
|
make_sig_test(*test_spec)
|
||||||
|
|
||||||
|
make_nsig_test = t_factory(
|
||||||
|
'nsig', n_sig, re.compile(r'.+/player/(?P<id>[a-zA-Z0-9_-]+)/.+.js$'))
|
||||||
|
for test_spec in _NSIG_TESTS:
|
||||||
|
make_nsig_test(*test_spec)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
+550
-331
File diff suppressed because it is too large
Load Diff
+99
-58
@@ -18,6 +18,7 @@ from .options import (
|
|||||||
)
|
)
|
||||||
from .compat import (
|
from .compat import (
|
||||||
compat_getpass,
|
compat_getpass,
|
||||||
|
compat_os_name,
|
||||||
compat_shlex_quote,
|
compat_shlex_quote,
|
||||||
workaround_optparse_bug9161,
|
workaround_optparse_bug9161,
|
||||||
)
|
)
|
||||||
@@ -25,16 +26,17 @@ from .cookies import SUPPORTED_BROWSERS
|
|||||||
from .utils import (
|
from .utils import (
|
||||||
DateRange,
|
DateRange,
|
||||||
decodeOption,
|
decodeOption,
|
||||||
|
DownloadCancelled,
|
||||||
DownloadError,
|
DownloadError,
|
||||||
error_to_compat_str,
|
error_to_compat_str,
|
||||||
ExistingVideoReached,
|
|
||||||
expand_path,
|
expand_path,
|
||||||
|
GeoUtils,
|
||||||
|
float_or_none,
|
||||||
|
int_or_none,
|
||||||
match_filter_func,
|
match_filter_func,
|
||||||
MaxDownloadsReached,
|
|
||||||
parse_duration,
|
parse_duration,
|
||||||
preferredencoding,
|
preferredencoding,
|
||||||
read_batch_urls,
|
read_batch_urls,
|
||||||
RejectedVideoReached,
|
|
||||||
render_table,
|
render_table,
|
||||||
SameFileError,
|
SameFileError,
|
||||||
setproctitle,
|
setproctitle,
|
||||||
@@ -71,7 +73,7 @@ def _real_main(argv=None):
|
|||||||
setproctitle('yt-dlp')
|
setproctitle('yt-dlp')
|
||||||
|
|
||||||
parser, opts, args = parseOpts(argv)
|
parser, opts, args = parseOpts(argv)
|
||||||
warnings = []
|
warnings, deprecation_warnings = [], []
|
||||||
|
|
||||||
# Set user agent
|
# Set user agent
|
||||||
if opts.user_agent is not None:
|
if opts.user_agent is not None:
|
||||||
@@ -94,6 +96,8 @@ def _real_main(argv=None):
|
|||||||
if opts.batchfile is not None:
|
if opts.batchfile is not None:
|
||||||
try:
|
try:
|
||||||
if opts.batchfile == '-':
|
if opts.batchfile == '-':
|
||||||
|
write_string('Reading URLs from stdin - EOF (%s) to end:\n' % (
|
||||||
|
'Ctrl+Z' if compat_os_name == 'nt' else 'Ctrl+D'))
|
||||||
batchfd = sys.stdin
|
batchfd = sys.stdin
|
||||||
else:
|
else:
|
||||||
batchfd = io.open(
|
batchfd = io.open(
|
||||||
@@ -122,10 +126,10 @@ def _real_main(argv=None):
|
|||||||
desc = getattr(ie, 'IE_DESC', ie.IE_NAME)
|
desc = getattr(ie, 'IE_DESC', ie.IE_NAME)
|
||||||
if desc is False:
|
if desc is False:
|
||||||
continue
|
continue
|
||||||
if hasattr(ie, 'SEARCH_KEY'):
|
if getattr(ie, 'SEARCH_KEY', None) is not None:
|
||||||
_SEARCHES = ('cute kittens', 'slithering pythons', 'falling cat', 'angry poodle', 'purple fish', 'running tortoise', 'sleeping bunny', 'burping cow')
|
_SEARCHES = ('cute kittens', 'slithering pythons', 'falling cat', 'angry poodle', 'purple fish', 'running tortoise', 'sleeping bunny', 'burping cow')
|
||||||
_COUNTS = ('', '5', '10', 'all')
|
_COUNTS = ('', '5', '10', 'all')
|
||||||
desc += ' (Example: "%s%s:%s" )' % (ie.SEARCH_KEY, random.choice(_COUNTS), random.choice(_SEARCHES))
|
desc += f'; "{ie.SEARCH_KEY}:" prefix (Example: "{ie.SEARCH_KEY}{random.choice(_COUNTS)}:{random.choice(_SEARCHES)}")'
|
||||||
write_string(desc + '\n', out=sys.stdout)
|
write_string(desc + '\n', out=sys.stdout)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
if opts.ap_list_mso:
|
if opts.ap_list_mso:
|
||||||
@@ -134,6 +138,11 @@ def _real_main(argv=None):
|
|||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
# Conflicting, missing and erroneous options
|
# Conflicting, missing and erroneous options
|
||||||
|
if opts.format == 'best':
|
||||||
|
warnings.append('.\n '.join((
|
||||||
|
'"-f best" selects the best pre-merged format which is often not the best option',
|
||||||
|
'To let yt-dlp download and merge the best available formats, simply do not pass any format selection',
|
||||||
|
'If you know what you are doing and want only the best pre-merged format, use "-f b" instead to suppress this warning')))
|
||||||
if opts.usenetrc and (opts.username is not None or opts.password is not None):
|
if opts.usenetrc and (opts.username is not None or opts.password is not None):
|
||||||
parser.error('using .netrc conflicts with giving username/password')
|
parser.error('using .netrc conflicts with giving username/password')
|
||||||
if opts.password is not None and opts.username is None:
|
if opts.password is not None and opts.username is None:
|
||||||
@@ -193,7 +202,14 @@ def _real_main(argv=None):
|
|||||||
if opts.overwrites: # --yes-overwrites implies --no-continue
|
if opts.overwrites: # --yes-overwrites implies --no-continue
|
||||||
opts.continue_dl = False
|
opts.continue_dl = False
|
||||||
if opts.concurrent_fragment_downloads <= 0:
|
if opts.concurrent_fragment_downloads <= 0:
|
||||||
raise ValueError('Concurrent fragments must be positive')
|
parser.error('Concurrent fragments must be positive')
|
||||||
|
if opts.wait_for_video is not None:
|
||||||
|
min_wait, max_wait, *_ = map(parse_duration, opts.wait_for_video.split('-', 1) + [None])
|
||||||
|
if min_wait is None or (max_wait is None and '-' in opts.wait_for_video):
|
||||||
|
parser.error('Invalid time range to wait')
|
||||||
|
elif max_wait is not None and max_wait < min_wait:
|
||||||
|
parser.error('Minimum time range to wait must not be longer than the maximum')
|
||||||
|
opts.wait_for_video = (min_wait, max_wait)
|
||||||
|
|
||||||
def parse_retries(retries, name=''):
|
def parse_retries(retries, name=''):
|
||||||
if retries in ('inf', 'infinite'):
|
if retries in ('inf', 'infinite'):
|
||||||
@@ -206,6 +222,8 @@ def _real_main(argv=None):
|
|||||||
return parsed_retries
|
return parsed_retries
|
||||||
if opts.retries is not None:
|
if opts.retries is not None:
|
||||||
opts.retries = parse_retries(opts.retries)
|
opts.retries = parse_retries(opts.retries)
|
||||||
|
if opts.file_access_retries is not None:
|
||||||
|
opts.file_access_retries = parse_retries(opts.file_access_retries, 'file access ')
|
||||||
if opts.fragment_retries is not None:
|
if opts.fragment_retries is not None:
|
||||||
opts.fragment_retries = parse_retries(opts.fragment_retries, 'fragment ')
|
opts.fragment_retries = parse_retries(opts.fragment_retries, 'fragment ')
|
||||||
if opts.extractor_retries is not None:
|
if opts.extractor_retries is not None:
|
||||||
@@ -221,15 +239,17 @@ def _real_main(argv=None):
|
|||||||
parser.error('invalid http chunk size specified')
|
parser.error('invalid http chunk size specified')
|
||||||
opts.http_chunk_size = numeric_chunksize
|
opts.http_chunk_size = numeric_chunksize
|
||||||
if opts.playliststart <= 0:
|
if opts.playliststart <= 0:
|
||||||
raise ValueError('Playlist start must be positive')
|
raise parser.error('Playlist start must be positive')
|
||||||
if opts.playlistend not in (-1, None) and opts.playlistend < opts.playliststart:
|
if opts.playlistend not in (-1, None) and opts.playlistend < opts.playliststart:
|
||||||
raise ValueError('Playlist end must be greater than playlist start')
|
raise parser.error('Playlist end must be greater than playlist start')
|
||||||
if opts.extractaudio:
|
if opts.extractaudio:
|
||||||
|
opts.audioformat = opts.audioformat.lower()
|
||||||
if opts.audioformat not in ['best'] + list(FFmpegExtractAudioPP.SUPPORTED_EXTS):
|
if opts.audioformat not in ['best'] + list(FFmpegExtractAudioPP.SUPPORTED_EXTS):
|
||||||
parser.error('invalid audio format specified')
|
parser.error('invalid audio format specified')
|
||||||
if opts.audioquality:
|
if opts.audioquality:
|
||||||
opts.audioquality = opts.audioquality.strip('k').strip('K')
|
opts.audioquality = opts.audioquality.strip('k').strip('K')
|
||||||
if not opts.audioquality.isdigit():
|
audioquality = int_or_none(float_or_none(opts.audioquality)) # int_or_none prevents inf, nan
|
||||||
|
if audioquality is None or audioquality < 0:
|
||||||
parser.error('invalid audio quality specified')
|
parser.error('invalid audio quality specified')
|
||||||
if opts.recodevideo is not None:
|
if opts.recodevideo is not None:
|
||||||
opts.recodevideo = opts.recodevideo.replace(' ', '')
|
opts.recodevideo = opts.recodevideo.replace(' ', '')
|
||||||
@@ -245,12 +265,17 @@ def _real_main(argv=None):
|
|||||||
if opts.convertthumbnails is not None:
|
if opts.convertthumbnails is not None:
|
||||||
if opts.convertthumbnails not in FFmpegThumbnailsConvertorPP.SUPPORTED_EXTS:
|
if opts.convertthumbnails not in FFmpegThumbnailsConvertorPP.SUPPORTED_EXTS:
|
||||||
parser.error('invalid thumbnail format specified')
|
parser.error('invalid thumbnail format specified')
|
||||||
|
|
||||||
if opts.cookiesfrombrowser is not None:
|
if opts.cookiesfrombrowser is not None:
|
||||||
opts.cookiesfrombrowser = [
|
opts.cookiesfrombrowser = [
|
||||||
part.strip() or None for part in opts.cookiesfrombrowser.split(':', 1)]
|
part.strip() or None for part in opts.cookiesfrombrowser.split(':', 1)]
|
||||||
if opts.cookiesfrombrowser[0].lower() not in SUPPORTED_BROWSERS:
|
if opts.cookiesfrombrowser[0].lower() not in SUPPORTED_BROWSERS:
|
||||||
parser.error('unsupported browser specified for cookies')
|
parser.error('unsupported browser specified for cookies')
|
||||||
|
geo_bypass_code = opts.geo_bypass_ip_block or opts.geo_bypass_country
|
||||||
|
if geo_bypass_code is not None:
|
||||||
|
try:
|
||||||
|
GeoUtils.random_ipv4(geo_bypass_code)
|
||||||
|
except Exception:
|
||||||
|
parser.error('unsupported geo-bypass country or ip-block')
|
||||||
|
|
||||||
if opts.date is not None:
|
if opts.date is not None:
|
||||||
date = DateRange.day(opts.date)
|
date = DateRange.day(opts.date)
|
||||||
@@ -286,6 +311,11 @@ def _real_main(argv=None):
|
|||||||
set_default_compat('abort-on-error', 'ignoreerrors', 'only_download')
|
set_default_compat('abort-on-error', 'ignoreerrors', 'only_download')
|
||||||
set_default_compat('no-playlist-metafiles', 'allow_playlist_files')
|
set_default_compat('no-playlist-metafiles', 'allow_playlist_files')
|
||||||
set_default_compat('no-clean-infojson', 'clean_infojson')
|
set_default_compat('no-clean-infojson', 'clean_infojson')
|
||||||
|
if 'no-attach-info-json' in compat_opts:
|
||||||
|
if opts.embed_infojson:
|
||||||
|
_unused_compat_opt('no-attach-info-json')
|
||||||
|
else:
|
||||||
|
opts.embed_infojson = False
|
||||||
if 'format-sort' in compat_opts:
|
if 'format-sort' in compat_opts:
|
||||||
opts.format_sort.extend(InfoExtractor.FormatSort.ytdl_default)
|
opts.format_sort.extend(InfoExtractor.FormatSort.ytdl_default)
|
||||||
_video_multistreams_set = set_default_compat('multistreams', 'allow_multiple_video_streams', False, remove_compat=False)
|
_video_multistreams_set = set_default_compat('multistreams', 'allow_multiple_video_streams', False, remove_compat=False)
|
||||||
@@ -369,8 +399,6 @@ def _real_main(argv=None):
|
|||||||
opts.sponsorblock_remove = set()
|
opts.sponsorblock_remove = set()
|
||||||
sponsorblock_query = opts.sponsorblock_mark | opts.sponsorblock_remove
|
sponsorblock_query = opts.sponsorblock_mark | opts.sponsorblock_remove
|
||||||
|
|
||||||
if (opts.addmetadata or opts.sponsorblock_mark) and opts.addchapters is None:
|
|
||||||
opts.addchapters = True
|
|
||||||
opts.remove_chapters = opts.remove_chapters or []
|
opts.remove_chapters = opts.remove_chapters or []
|
||||||
|
|
||||||
if (opts.remove_chapters or sponsorblock_query) and opts.sponskrub is not False:
|
if (opts.remove_chapters or sponsorblock_query) and opts.sponskrub is not False:
|
||||||
@@ -391,40 +419,32 @@ def _real_main(argv=None):
|
|||||||
opts.remuxvideo = False
|
opts.remuxvideo = False
|
||||||
|
|
||||||
if opts.allow_unplayable_formats:
|
if opts.allow_unplayable_formats:
|
||||||
if opts.extractaudio:
|
def report_unplayable_conflict(opt_name, arg, default=False, allowed=None):
|
||||||
report_conflict('--allow-unplayable-formats', '--extract-audio')
|
val = getattr(opts, opt_name)
|
||||||
opts.extractaudio = False
|
if (not allowed and val) or (allowed and not allowed(val)):
|
||||||
if opts.remuxvideo:
|
report_conflict('--allow-unplayable-formats', arg)
|
||||||
report_conflict('--allow-unplayable-formats', '--remux-video')
|
setattr(opts, opt_name, default)
|
||||||
opts.remuxvideo = False
|
|
||||||
if opts.recodevideo:
|
report_unplayable_conflict('extractaudio', '--extract-audio')
|
||||||
report_conflict('--allow-unplayable-formats', '--recode-video')
|
report_unplayable_conflict('remuxvideo', '--remux-video')
|
||||||
opts.recodevideo = False
|
report_unplayable_conflict('recodevideo', '--recode-video')
|
||||||
if opts.addmetadata:
|
report_unplayable_conflict('addmetadata', '--embed-metadata')
|
||||||
report_conflict('--allow-unplayable-formats', '--add-metadata')
|
report_unplayable_conflict('addchapters', '--embed-chapters')
|
||||||
opts.addmetadata = False
|
report_unplayable_conflict('embed_infojson', '--embed-info-json')
|
||||||
if opts.embedsubtitles:
|
opts.embed_infojson = False
|
||||||
report_conflict('--allow-unplayable-formats', '--embed-subs')
|
report_unplayable_conflict('embedsubtitles', '--embed-subs')
|
||||||
opts.embedsubtitles = False
|
report_unplayable_conflict('embedthumbnail', '--embed-thumbnail')
|
||||||
if opts.embedthumbnail:
|
report_unplayable_conflict('xattrs', '--xattrs')
|
||||||
report_conflict('--allow-unplayable-formats', '--embed-thumbnail')
|
report_unplayable_conflict('fixup', '--fixup', default='never', allowed=lambda x: x in (None, 'never', 'ignore'))
|
||||||
opts.embedthumbnail = False
|
|
||||||
if opts.xattrs:
|
|
||||||
report_conflict('--allow-unplayable-formats', '--xattrs')
|
|
||||||
opts.xattrs = False
|
|
||||||
if opts.fixup and opts.fixup.lower() not in ('never', 'ignore'):
|
|
||||||
report_conflict('--allow-unplayable-formats', '--fixup')
|
|
||||||
opts.fixup = 'never'
|
opts.fixup = 'never'
|
||||||
if opts.remove_chapters:
|
report_unplayable_conflict('remove_chapters', '--remove-chapters', default=[])
|
||||||
report_conflict('--allow-unplayable-formats', '--remove-chapters')
|
report_unplayable_conflict('sponsorblock_remove', '--sponsorblock-remove', default=set())
|
||||||
opts.remove_chapters = []
|
report_unplayable_conflict('sponskrub', '--sponskrub', default=set())
|
||||||
if opts.sponsorblock_remove:
|
|
||||||
report_conflict('--allow-unplayable-formats', '--sponsorblock-remove')
|
|
||||||
opts.sponsorblock_remove = set()
|
|
||||||
if opts.sponskrub:
|
|
||||||
report_conflict('--allow-unplayable-formats', '--sponskrub')
|
|
||||||
opts.sponskrub = False
|
opts.sponskrub = False
|
||||||
|
|
||||||
|
if (opts.addmetadata or opts.sponsorblock_mark) and opts.addchapters is None:
|
||||||
|
opts.addchapters = True
|
||||||
|
|
||||||
# PostProcessors
|
# PostProcessors
|
||||||
postprocessors = list(opts.add_postprocessors)
|
postprocessors = list(opts.add_postprocessors)
|
||||||
if sponsorblock_query:
|
if sponsorblock_query:
|
||||||
@@ -502,7 +522,7 @@ def _real_main(argv=None):
|
|||||||
if len(dur) == 2 and all(t is not None for t in dur):
|
if len(dur) == 2 and all(t is not None for t in dur):
|
||||||
remove_ranges.append(tuple(dur))
|
remove_ranges.append(tuple(dur))
|
||||||
continue
|
continue
|
||||||
parser.error(f'invalid --remove-chapters time range {regex!r}. Must be of the form ?start-end')
|
parser.error(f'invalid --remove-chapters time range {regex!r}. Must be of the form *start-end')
|
||||||
try:
|
try:
|
||||||
remove_chapters_patterns.append(re.compile(regex))
|
remove_chapters_patterns.append(re.compile(regex))
|
||||||
except re.error as err:
|
except re.error as err:
|
||||||
@@ -522,13 +542,16 @@ def _real_main(argv=None):
|
|||||||
# By default ffmpeg preserves metadata applicable for both
|
# By default ffmpeg preserves metadata applicable for both
|
||||||
# source and target containers. From this point the container won't change,
|
# source and target containers. From this point the container won't change,
|
||||||
# so metadata can be added here.
|
# so metadata can be added here.
|
||||||
if opts.addmetadata or opts.addchapters:
|
if opts.addmetadata or opts.addchapters or opts.embed_infojson:
|
||||||
|
if opts.embed_infojson is None:
|
||||||
|
opts.embed_infojson = 'if_exists'
|
||||||
postprocessors.append({
|
postprocessors.append({
|
||||||
'key': 'FFmpegMetadata',
|
'key': 'FFmpegMetadata',
|
||||||
'add_chapters': opts.addchapters,
|
'add_chapters': opts.addchapters,
|
||||||
'add_metadata': opts.addmetadata,
|
'add_metadata': opts.addmetadata,
|
||||||
|
'add_infojson': opts.embed_infojson,
|
||||||
})
|
})
|
||||||
# Note: Deprecated
|
# Deprecated
|
||||||
# This should be above EmbedThumbnail since sponskrub removes the thumbnail attachment
|
# This should be above EmbedThumbnail since sponskrub removes the thumbnail attachment
|
||||||
# but must be below EmbedSubtitle and FFmpegMetadata
|
# but must be below EmbedSubtitle and FFmpegMetadata
|
||||||
# See https://github.com/yt-dlp/yt-dlp/issues/204 , https://github.com/faissaloo/SponSkrub/issues/29
|
# See https://github.com/yt-dlp/yt-dlp/issues/204 , https://github.com/faissaloo/SponSkrub/issues/29
|
||||||
@@ -541,15 +564,15 @@ def _real_main(argv=None):
|
|||||||
'cut': opts.sponskrub_cut,
|
'cut': opts.sponskrub_cut,
|
||||||
'force': opts.sponskrub_force,
|
'force': opts.sponskrub_force,
|
||||||
'ignoreerror': opts.sponskrub is None,
|
'ignoreerror': opts.sponskrub is None,
|
||||||
|
'_from_cli': True,
|
||||||
})
|
})
|
||||||
if opts.embedthumbnail:
|
if opts.embedthumbnail:
|
||||||
already_have_thumbnail = opts.writethumbnail or opts.write_all_thumbnails
|
|
||||||
postprocessors.append({
|
postprocessors.append({
|
||||||
'key': 'EmbedThumbnail',
|
'key': 'EmbedThumbnail',
|
||||||
# already_have_thumbnail = True prevents the file from being deleted after embedding
|
# already_have_thumbnail = True prevents the file from being deleted after embedding
|
||||||
'already_have_thumbnail': already_have_thumbnail
|
'already_have_thumbnail': opts.writethumbnail
|
||||||
})
|
})
|
||||||
if not already_have_thumbnail:
|
if not opts.writethumbnail:
|
||||||
opts.writethumbnail = True
|
opts.writethumbnail = True
|
||||||
opts.outtmpl['pl_thumbnail'] = ''
|
opts.outtmpl['pl_thumbnail'] = ''
|
||||||
if opts.split_chapters:
|
if opts.split_chapters:
|
||||||
@@ -580,6 +603,19 @@ def _real_main(argv=None):
|
|||||||
opts.postprocessor_args.setdefault('sponskrub', [])
|
opts.postprocessor_args.setdefault('sponskrub', [])
|
||||||
opts.postprocessor_args['default'] = opts.postprocessor_args['default-compat']
|
opts.postprocessor_args['default'] = opts.postprocessor_args['default-compat']
|
||||||
|
|
||||||
|
def report_deprecation(val, old, new=None):
|
||||||
|
if not val:
|
||||||
|
return
|
||||||
|
deprecation_warnings.append(
|
||||||
|
f'{old} is deprecated and may be removed in a future version. Use {new} instead' if new
|
||||||
|
else f'{old} is deprecated and may not work as expected')
|
||||||
|
|
||||||
|
report_deprecation(opts.sponskrub, '--sponskrub', '--sponsorblock-mark or --sponsorblock-remove')
|
||||||
|
report_deprecation(not opts.prefer_ffmpeg, '--prefer-avconv', 'ffmpeg')
|
||||||
|
report_deprecation(opts.include_ads, '--include-ads')
|
||||||
|
# report_deprecation(opts.call_home, '--call-home') # We may re-implement this in future
|
||||||
|
# report_deprecation(opts.writeannotations, '--write-annotations') # It's just that no website has it
|
||||||
|
|
||||||
final_ext = (
|
final_ext = (
|
||||||
opts.recodevideo if opts.recodevideo in FFmpegVideoConvertorPP.SUPPORTED_EXTS
|
opts.recodevideo if opts.recodevideo in FFmpegVideoConvertorPP.SUPPORTED_EXTS
|
||||||
else opts.remuxvideo if opts.remuxvideo in FFmpegVideoRemuxerPP.SUPPORTED_EXTS
|
else opts.remuxvideo if opts.remuxvideo in FFmpegVideoRemuxerPP.SUPPORTED_EXTS
|
||||||
@@ -639,6 +675,7 @@ def _real_main(argv=None):
|
|||||||
'throttledratelimit': opts.throttledratelimit,
|
'throttledratelimit': opts.throttledratelimit,
|
||||||
'overwrites': opts.overwrites,
|
'overwrites': opts.overwrites,
|
||||||
'retries': opts.retries,
|
'retries': opts.retries,
|
||||||
|
'file_access_retries': opts.file_access_retries,
|
||||||
'fragment_retries': opts.fragment_retries,
|
'fragment_retries': opts.fragment_retries,
|
||||||
'extractor_retries': opts.extractor_retries,
|
'extractor_retries': opts.extractor_retries,
|
||||||
'skip_unavailable_fragments': opts.skip_unavailable_fragments,
|
'skip_unavailable_fragments': opts.skip_unavailable_fragments,
|
||||||
@@ -666,8 +703,8 @@ def _real_main(argv=None):
|
|||||||
'allow_playlist_files': opts.allow_playlist_files,
|
'allow_playlist_files': opts.allow_playlist_files,
|
||||||
'clean_infojson': opts.clean_infojson,
|
'clean_infojson': opts.clean_infojson,
|
||||||
'getcomments': opts.getcomments,
|
'getcomments': opts.getcomments,
|
||||||
'writethumbnail': opts.writethumbnail,
|
'writethumbnail': opts.writethumbnail is True,
|
||||||
'write_all_thumbnails': opts.write_all_thumbnails,
|
'write_all_thumbnails': opts.writethumbnail == 'all',
|
||||||
'writelink': opts.writelink,
|
'writelink': opts.writelink,
|
||||||
'writeurllink': opts.writeurllink,
|
'writeurllink': opts.writeurllink,
|
||||||
'writewebloclink': opts.writewebloclink,
|
'writewebloclink': opts.writewebloclink,
|
||||||
@@ -699,6 +736,7 @@ def _real_main(argv=None):
|
|||||||
'download_archive': download_archive_fn,
|
'download_archive': download_archive_fn,
|
||||||
'break_on_existing': opts.break_on_existing,
|
'break_on_existing': opts.break_on_existing,
|
||||||
'break_on_reject': opts.break_on_reject,
|
'break_on_reject': opts.break_on_reject,
|
||||||
|
'break_per_url': opts.break_per_url,
|
||||||
'skip_playlist_after_errors': opts.skip_playlist_after_errors,
|
'skip_playlist_after_errors': opts.skip_playlist_after_errors,
|
||||||
'cookiefile': opts.cookiefile,
|
'cookiefile': opts.cookiefile,
|
||||||
'cookiesfrombrowser': opts.cookiesfrombrowser,
|
'cookiesfrombrowser': opts.cookiesfrombrowser,
|
||||||
@@ -717,6 +755,8 @@ def _real_main(argv=None):
|
|||||||
'youtube_include_hls_manifest': opts.youtube_include_hls_manifest,
|
'youtube_include_hls_manifest': opts.youtube_include_hls_manifest,
|
||||||
'encoding': opts.encoding,
|
'encoding': opts.encoding,
|
||||||
'extract_flat': opts.extract_flat,
|
'extract_flat': opts.extract_flat,
|
||||||
|
'live_from_start': opts.live_from_start,
|
||||||
|
'wait_for_video': opts.wait_for_video,
|
||||||
'mark_watched': opts.mark_watched,
|
'mark_watched': opts.mark_watched,
|
||||||
'merge_output_format': opts.merge_output_format,
|
'merge_output_format': opts.merge_output_format,
|
||||||
'final_ext': final_ext,
|
'final_ext': final_ext,
|
||||||
@@ -746,11 +786,12 @@ def _real_main(argv=None):
|
|||||||
'geo_bypass_country': opts.geo_bypass_country,
|
'geo_bypass_country': opts.geo_bypass_country,
|
||||||
'geo_bypass_ip_block': opts.geo_bypass_ip_block,
|
'geo_bypass_ip_block': opts.geo_bypass_ip_block,
|
||||||
'_warnings': warnings,
|
'_warnings': warnings,
|
||||||
|
'_deprecation_warnings': deprecation_warnings,
|
||||||
'compat_opts': compat_opts,
|
'compat_opts': compat_opts,
|
||||||
}
|
}
|
||||||
|
|
||||||
with YoutubeDL(ydl_opts) as ydl:
|
with YoutubeDL(ydl_opts) as ydl:
|
||||||
actual_use = len(all_urls) or opts.load_info_filename
|
actual_use = all_urls or opts.load_info_filename
|
||||||
|
|
||||||
# Remove cache dir
|
# Remove cache dir
|
||||||
if opts.rm_cachedir:
|
if opts.rm_cachedir:
|
||||||
@@ -779,7 +820,7 @@ def _real_main(argv=None):
|
|||||||
retcode = ydl.download_with_info_file(expand_path(opts.load_info_filename))
|
retcode = ydl.download_with_info_file(expand_path(opts.load_info_filename))
|
||||||
else:
|
else:
|
||||||
retcode = ydl.download(all_urls)
|
retcode = ydl.download(all_urls)
|
||||||
except (MaxDownloadsReached, ExistingVideoReached, RejectedVideoReached):
|
except DownloadCancelled:
|
||||||
ydl.to_screen('Aborting remaining downloads')
|
ydl.to_screen('Aborting remaining downloads')
|
||||||
retcode = 101
|
retcode = 101
|
||||||
|
|
||||||
@@ -791,15 +832,15 @@ def main(argv=None):
|
|||||||
_real_main(argv)
|
_real_main(argv)
|
||||||
except DownloadError:
|
except DownloadError:
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
except SameFileError:
|
except SameFileError as e:
|
||||||
sys.exit('ERROR: fixed output name but more than one file to download')
|
sys.exit(f'ERROR: {e}')
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
sys.exit('\nERROR: Interrupted by user')
|
sys.exit('\nERROR: Interrupted by user')
|
||||||
except BrokenPipeError:
|
except BrokenPipeError as e:
|
||||||
# https://docs.python.org/3/library/signal.html#note-on-sigpipe
|
# https://docs.python.org/3/library/signal.html#note-on-sigpipe
|
||||||
devnull = os.open(os.devnull, os.O_WRONLY)
|
devnull = os.open(os.devnull, os.O_WRONLY)
|
||||||
os.dup2(devnull, sys.stdout.fileno())
|
os.dup2(devnull, sys.stdout.fileno())
|
||||||
sys.exit(r'\nERROR: {err}')
|
sys.exit(f'\nERROR: {e}')
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['main', 'YoutubeDL', 'gen_extractors', 'list_extractors']
|
__all__ = ['main', 'YoutubeDL', 'gen_extractors', 'list_extractors']
|
||||||
|
|||||||
@@ -28,6 +28,48 @@ else:
|
|||||||
BLOCK_SIZE_BYTES = 16
|
BLOCK_SIZE_BYTES = 16
|
||||||
|
|
||||||
|
|
||||||
|
def aes_ecb_encrypt(data, key, iv=None):
|
||||||
|
"""
|
||||||
|
Encrypt with aes in ECB mode
|
||||||
|
|
||||||
|
@param {int[]} data cleartext
|
||||||
|
@param {int[]} key 16/24/32-Byte cipher key
|
||||||
|
@param {int[]} iv Unused for this mode
|
||||||
|
@returns {int[]} encrypted data
|
||||||
|
"""
|
||||||
|
expanded_key = key_expansion(key)
|
||||||
|
block_count = int(ceil(float(len(data)) / BLOCK_SIZE_BYTES))
|
||||||
|
|
||||||
|
encrypted_data = []
|
||||||
|
for i in range(block_count):
|
||||||
|
block = data[i * BLOCK_SIZE_BYTES: (i + 1) * BLOCK_SIZE_BYTES]
|
||||||
|
encrypted_data += aes_encrypt(block, expanded_key)
|
||||||
|
encrypted_data = encrypted_data[:len(data)]
|
||||||
|
|
||||||
|
return encrypted_data
|
||||||
|
|
||||||
|
|
||||||
|
def aes_ecb_decrypt(data, key, iv=None):
|
||||||
|
"""
|
||||||
|
Decrypt with aes in ECB mode
|
||||||
|
|
||||||
|
@param {int[]} data cleartext
|
||||||
|
@param {int[]} key 16/24/32-Byte cipher key
|
||||||
|
@param {int[]} iv Unused for this mode
|
||||||
|
@returns {int[]} decrypted data
|
||||||
|
"""
|
||||||
|
expanded_key = key_expansion(key)
|
||||||
|
block_count = int(ceil(float(len(data)) / BLOCK_SIZE_BYTES))
|
||||||
|
|
||||||
|
encrypted_data = []
|
||||||
|
for i in range(block_count):
|
||||||
|
block = data[i * BLOCK_SIZE_BYTES: (i + 1) * BLOCK_SIZE_BYTES]
|
||||||
|
encrypted_data += aes_decrypt(block, expanded_key)
|
||||||
|
encrypted_data = encrypted_data[:len(data)]
|
||||||
|
|
||||||
|
return encrypted_data
|
||||||
|
|
||||||
|
|
||||||
def aes_ctr_decrypt(data, key, iv):
|
def aes_ctr_decrypt(data, key, iv):
|
||||||
"""
|
"""
|
||||||
Decrypt with aes in counter mode
|
Decrypt with aes in counter mode
|
||||||
|
|||||||
+13
-1
@@ -19,6 +19,7 @@ import shlex
|
|||||||
import shutil
|
import shutil
|
||||||
import socket
|
import socket
|
||||||
import struct
|
import struct
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tokenize
|
import tokenize
|
||||||
import urllib
|
import urllib
|
||||||
@@ -159,10 +160,20 @@ except ImportError:
|
|||||||
compat_pycrypto_AES = None
|
compat_pycrypto_AES = None
|
||||||
|
|
||||||
|
|
||||||
|
WINDOWS_VT_MODE = False if compat_os_name == 'nt' else None
|
||||||
|
|
||||||
|
|
||||||
def windows_enable_vt_mode(): # TODO: Do this the proper way https://bugs.python.org/issue30075
|
def windows_enable_vt_mode(): # TODO: Do this the proper way https://bugs.python.org/issue30075
|
||||||
if compat_os_name != 'nt':
|
if compat_os_name != 'nt':
|
||||||
return
|
return
|
||||||
os.system('')
|
global WINDOWS_VT_MODE
|
||||||
|
startupinfo = subprocess.STARTUPINFO()
|
||||||
|
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||||
|
try:
|
||||||
|
subprocess.Popen('', shell=True, startupinfo=startupinfo)
|
||||||
|
WINDOWS_VT_MODE = True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
# Deprecated
|
# Deprecated
|
||||||
@@ -223,6 +234,7 @@ compat_xml_parse_error = etree.ParseError
|
|||||||
# Set public objects
|
# Set public objects
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
'WINDOWS_VT_MODE',
|
||||||
'compat_HTMLParseError',
|
'compat_HTMLParseError',
|
||||||
'compat_HTMLParser',
|
'compat_HTMLParser',
|
||||||
'compat_HTTPError',
|
'compat_HTTPError',
|
||||||
|
|||||||
+2
-2
@@ -117,7 +117,7 @@ def _extract_firefox_cookies(profile, logger):
|
|||||||
raise FileNotFoundError('could not find firefox cookies database in {}'.format(search_root))
|
raise FileNotFoundError('could not find firefox cookies database in {}'.format(search_root))
|
||||||
logger.debug('Extracting cookies from: "{}"'.format(cookie_database_path))
|
logger.debug('Extracting cookies from: "{}"'.format(cookie_database_path))
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory(prefix='youtube_dl') as tmpdir:
|
with tempfile.TemporaryDirectory(prefix='yt_dlp') as tmpdir:
|
||||||
cursor = None
|
cursor = None
|
||||||
try:
|
try:
|
||||||
cursor = _open_database_copy(cookie_database_path, tmpdir)
|
cursor = _open_database_copy(cookie_database_path, tmpdir)
|
||||||
@@ -236,7 +236,7 @@ def _extract_chrome_cookies(browser_name, profile, logger):
|
|||||||
|
|
||||||
decryptor = get_cookie_decryptor(config['browser_dir'], config['keyring_name'], logger)
|
decryptor = get_cookie_decryptor(config['browser_dir'], config['keyring_name'], logger)
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory(prefix='youtube_dl') as tmpdir:
|
with tempfile.TemporaryDirectory(prefix='yt_dlp') as tmpdir:
|
||||||
cursor = None
|
cursor = None
|
||||||
try:
|
try:
|
||||||
cursor = _open_database_copy(cookie_database_path, tmpdir)
|
cursor = _open_database_copy(cookie_database_path, tmpdir)
|
||||||
|
|||||||
@@ -12,10 +12,15 @@ def get_suitable_downloader(info_dict, params={}, default=NO_DEFAULT, protocol=N
|
|||||||
info_copy = info_dict.copy()
|
info_copy = info_dict.copy()
|
||||||
info_copy['to_stdout'] = to_stdout
|
info_copy['to_stdout'] = to_stdout
|
||||||
|
|
||||||
downloaders = [_get_suitable_downloader(info_copy, proto, params, default)
|
protocols = (protocol or info_copy['protocol']).split('+')
|
||||||
for proto in (protocol or info_copy['protocol']).split('+')]
|
downloaders = [_get_suitable_downloader(info_copy, proto, params, default) for proto in protocols]
|
||||||
|
|
||||||
if set(downloaders) == {FFmpegFD} and FFmpegFD.can_merge_formats(info_copy, params):
|
if set(downloaders) == {FFmpegFD} and FFmpegFD.can_merge_formats(info_copy, params):
|
||||||
return FFmpegFD
|
return FFmpegFD
|
||||||
|
elif (set(downloaders) == {DashSegmentsFD}
|
||||||
|
and not (to_stdout and len(protocols) > 1)
|
||||||
|
and set(protocols) == {'http_dash_segments_generator'}):
|
||||||
|
return DashSegmentsFD
|
||||||
elif len(downloaders) == 1:
|
elif len(downloaders) == 1:
|
||||||
return downloaders[0]
|
return downloaders[0]
|
||||||
return None
|
return None
|
||||||
@@ -41,6 +46,7 @@ from .external import (
|
|||||||
|
|
||||||
PROTOCOL_MAP = {
|
PROTOCOL_MAP = {
|
||||||
'rtmp': RtmpFD,
|
'rtmp': RtmpFD,
|
||||||
|
'rtmpe': RtmpFD,
|
||||||
'rtmp_ffmpeg': FFmpegFD,
|
'rtmp_ffmpeg': FFmpegFD,
|
||||||
'm3u8_native': HlsFD,
|
'm3u8_native': HlsFD,
|
||||||
'm3u8': FFmpegFD,
|
'm3u8': FFmpegFD,
|
||||||
@@ -48,6 +54,7 @@ PROTOCOL_MAP = {
|
|||||||
'rtsp': RtspFD,
|
'rtsp': RtspFD,
|
||||||
'f4m': F4mFD,
|
'f4m': F4mFD,
|
||||||
'http_dash_segments': DashSegmentsFD,
|
'http_dash_segments': DashSegmentsFD,
|
||||||
|
'http_dash_segments_generator': DashSegmentsFD,
|
||||||
'ism': IsmFD,
|
'ism': IsmFD,
|
||||||
'mhtml': MhtmlFD,
|
'mhtml': MhtmlFD,
|
||||||
'niconico_dmc': NiconicoDmcFD,
|
'niconico_dmc': NiconicoDmcFD,
|
||||||
@@ -62,6 +69,7 @@ def shorten_protocol_name(proto, simplify=False):
|
|||||||
'm3u8_native': 'm3u8_n',
|
'm3u8_native': 'm3u8_n',
|
||||||
'rtmp_ffmpeg': 'rtmp_f',
|
'rtmp_ffmpeg': 'rtmp_f',
|
||||||
'http_dash_segments': 'dash',
|
'http_dash_segments': 'dash',
|
||||||
|
'http_dash_segments_generator': 'dash_g',
|
||||||
'niconico_dmc': 'dmc',
|
'niconico_dmc': 'dmc',
|
||||||
'websocket_frag': 'WSfrag',
|
'websocket_frag': 'WSfrag',
|
||||||
}
|
}
|
||||||
@@ -70,6 +78,7 @@ def shorten_protocol_name(proto, simplify=False):
|
|||||||
'https': 'http',
|
'https': 'http',
|
||||||
'ftps': 'ftp',
|
'ftps': 'ftp',
|
||||||
'm3u8_native': 'm3u8',
|
'm3u8_native': 'm3u8',
|
||||||
|
'http_dash_segments_generator': 'dash',
|
||||||
'rtmp_ffmpeg': 'rtmp',
|
'rtmp_ffmpeg': 'rtmp',
|
||||||
'm3u8_frag_urls': 'm3u8',
|
'm3u8_frag_urls': 'm3u8',
|
||||||
'dash_frag_urls': 'dash',
|
'dash_frag_urls': 'dash',
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ import os
|
|||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
import random
|
import random
|
||||||
|
import errno
|
||||||
|
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
decodeArgument,
|
decodeArgument,
|
||||||
encodeFilename,
|
encodeFilename,
|
||||||
error_to_compat_str,
|
error_to_compat_str,
|
||||||
format_bytes,
|
format_bytes,
|
||||||
|
sanitize_open,
|
||||||
shell_quote,
|
shell_quote,
|
||||||
timeconvert,
|
timeconvert,
|
||||||
timetuple_from_msec,
|
timetuple_from_msec,
|
||||||
@@ -39,6 +41,7 @@ class FileDownloader(object):
|
|||||||
ratelimit: Download speed limit, in bytes/sec.
|
ratelimit: Download speed limit, in bytes/sec.
|
||||||
throttledratelimit: Assume the download is being throttled below this speed (bytes/sec)
|
throttledratelimit: Assume the download is being throttled below this speed (bytes/sec)
|
||||||
retries: Number of times to retry for HTTP error 5xx
|
retries: Number of times to retry for HTTP error 5xx
|
||||||
|
file_access_retries: Number of times to retry on file access error
|
||||||
buffersize: Size of download buffer in bytes.
|
buffersize: Size of download buffer in bytes.
|
||||||
noresizebuffer: Do not automatically resize the download buffer.
|
noresizebuffer: Do not automatically resize the download buffer.
|
||||||
continuedl: Try to continue downloads if possible.
|
continuedl: Try to continue downloads if possible.
|
||||||
@@ -93,6 +96,8 @@ class FileDownloader(object):
|
|||||||
def format_percent(percent):
|
def format_percent(percent):
|
||||||
if percent is None:
|
if percent is None:
|
||||||
return '---.-%'
|
return '---.-%'
|
||||||
|
elif percent == 100:
|
||||||
|
return '100%'
|
||||||
return '%6s' % ('%3.1f%%' % percent)
|
return '%6s' % ('%3.1f%%' % percent)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -205,6 +210,21 @@ 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):
|
||||||
|
file_access_retries = self.params.get('file_access_retries', 10)
|
||||||
|
retry = 0
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
return sanitize_open(filename, open_mode)
|
||||||
|
except (IOError, OSError) as err:
|
||||||
|
retry = retry + 1
|
||||||
|
if retry > file_access_retries or err.errno not in (errno.EACCES,):
|
||||||
|
raise
|
||||||
|
self.to_screen(
|
||||||
|
'[download] Got file access error. Retrying (attempt %d of %s) ...'
|
||||||
|
% (retry, self.format_retries(file_access_retries)))
|
||||||
|
time.sleep(0.01)
|
||||||
|
|
||||||
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
|
||||||
@@ -247,11 +267,29 @@ class FileDownloader(object):
|
|||||||
self._multiline = BreaklineStatusPrinter(self.ydl._screen_file, lines)
|
self._multiline = BreaklineStatusPrinter(self.ydl._screen_file, lines)
|
||||||
else:
|
else:
|
||||||
self._multiline = MultilinePrinter(self.ydl._screen_file, lines, not self.params.get('quiet'))
|
self._multiline = MultilinePrinter(self.ydl._screen_file, lines, not self.params.get('quiet'))
|
||||||
|
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):
|
||||||
self._multiline.end()
|
self._multiline.end()
|
||||||
|
|
||||||
def _report_progress_status(self, s):
|
_progress_styles = {
|
||||||
|
'downloaded_bytes': 'light blue',
|
||||||
|
'percent': 'light blue',
|
||||||
|
'eta': 'yellow',
|
||||||
|
'speed': 'green',
|
||||||
|
'elapsed': 'bold white',
|
||||||
|
'total_bytes': '',
|
||||||
|
'total_bytes_estimate': '',
|
||||||
|
}
|
||||||
|
|
||||||
|
def _report_progress_status(self, s, default_template):
|
||||||
|
for name, style in self._progress_styles.items():
|
||||||
|
name = f'_{name}_str'
|
||||||
|
if name not in s:
|
||||||
|
continue
|
||||||
|
s[name] = self._format_progress(s[name], style)
|
||||||
|
s['_default_template'] = default_template % s
|
||||||
|
|
||||||
progress_dict = s.copy()
|
progress_dict = s.copy()
|
||||||
progress_dict.pop('info_dict')
|
progress_dict.pop('info_dict')
|
||||||
progress_dict = {'info': s['info_dict'], 'progress': progress_dict}
|
progress_dict = {'info': s['info_dict'], 'progress': progress_dict}
|
||||||
@@ -264,6 +302,10 @@ class FileDownloader(object):
|
|||||||
progress_template.get('download-title') or 'yt-dlp %(progress._default_template)s',
|
progress_template.get('download-title') or 'yt-dlp %(progress._default_template)s',
|
||||||
progress_dict))
|
progress_dict))
|
||||||
|
|
||||||
|
def _format_progress(self, *args, **kwargs):
|
||||||
|
return self.ydl._format_text(
|
||||||
|
self._multiline.stream, self._multiline.allow_colors, *args, **kwargs)
|
||||||
|
|
||||||
def report_progress(self, s):
|
def report_progress(self, s):
|
||||||
if s['status'] == 'finished':
|
if s['status'] == 'finished':
|
||||||
if self.params.get('noprogress'):
|
if self.params.get('noprogress'):
|
||||||
@@ -276,8 +318,7 @@ class FileDownloader(object):
|
|||||||
s['_elapsed_str'] = self.format_seconds(s['elapsed'])
|
s['_elapsed_str'] = self.format_seconds(s['elapsed'])
|
||||||
msg_template += ' in %(_elapsed_str)s'
|
msg_template += ' in %(_elapsed_str)s'
|
||||||
s['_percent_str'] = self.format_percent(100)
|
s['_percent_str'] = self.format_percent(100)
|
||||||
s['_default_template'] = msg_template % s
|
self._report_progress_status(s, msg_template)
|
||||||
self._report_progress_status(s)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if s['status'] != 'downloading':
|
if s['status'] != 'downloading':
|
||||||
@@ -286,7 +327,7 @@ class FileDownloader(object):
|
|||||||
if s.get('eta') is not None:
|
if s.get('eta') is not None:
|
||||||
s['_eta_str'] = self.format_eta(s['eta'])
|
s['_eta_str'] = self.format_eta(s['eta'])
|
||||||
else:
|
else:
|
||||||
s['_eta_str'] = 'Unknown ETA'
|
s['_eta_str'] = 'Unknown'
|
||||||
|
|
||||||
if s.get('total_bytes') and s.get('downloaded_bytes') is not None:
|
if s.get('total_bytes') and s.get('downloaded_bytes') is not None:
|
||||||
s['_percent_str'] = self.format_percent(100 * s['downloaded_bytes'] / s['total_bytes'])
|
s['_percent_str'] = self.format_percent(100 * s['downloaded_bytes'] / s['total_bytes'])
|
||||||
@@ -318,9 +359,12 @@ class FileDownloader(object):
|
|||||||
else:
|
else:
|
||||||
msg_template = '%(_downloaded_bytes_str)s at %(_speed_str)s'
|
msg_template = '%(_downloaded_bytes_str)s at %(_speed_str)s'
|
||||||
else:
|
else:
|
||||||
msg_template = '%(_percent_str)s % at %(_speed_str)s ETA %(_eta_str)s'
|
msg_template = '%(_percent_str)s at %(_speed_str)s ETA %(_eta_str)s'
|
||||||
s['_default_template'] = msg_template % s
|
if s.get('fragment_index') and s.get('fragment_count'):
|
||||||
self._report_progress_status(s)
|
msg_template += ' (frag %(fragment_index)s/%(fragment_count)s)'
|
||||||
|
elif s.get('fragment_index'):
|
||||||
|
msg_template += ' (frag %(fragment_index)s)'
|
||||||
|
self._report_progress_status(s, msg_template)
|
||||||
|
|
||||||
def report_resuming_byte(self, resume_len):
|
def report_resuming_byte(self, resume_len):
|
||||||
"""Report attempt to resume at given byte."""
|
"""Report attempt to resume at given byte."""
|
||||||
@@ -371,6 +415,7 @@ class FileDownloader(object):
|
|||||||
'status': 'finished',
|
'status': 'finished',
|
||||||
'total_bytes': os.path.getsize(encodeFilename(filename)),
|
'total_bytes': os.path.getsize(encodeFilename(filename)),
|
||||||
}, info_dict)
|
}, info_dict)
|
||||||
|
self._finish_multiline_status()
|
||||||
return True, False
|
return True, False
|
||||||
|
|
||||||
if subtitle is False:
|
if subtitle is False:
|
||||||
|
|||||||
+43
-25
@@ -1,4 +1,5 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
import time
|
||||||
|
|
||||||
from ..downloader import get_suitable_downloader
|
from ..downloader import get_suitable_downloader
|
||||||
from .fragment import FragmentFD
|
from .fragment import FragmentFD
|
||||||
@@ -15,27 +16,53 @@ class DashSegmentsFD(FragmentFD):
|
|||||||
FD_NAME = 'dashsegments'
|
FD_NAME = 'dashsegments'
|
||||||
|
|
||||||
def real_download(self, filename, info_dict):
|
def real_download(self, filename, info_dict):
|
||||||
if info_dict.get('is_live'):
|
if info_dict.get('is_live') and set(info_dict['protocol'].split('+')) != {'http_dash_segments_generator'}:
|
||||||
self.report_error('Live DASH videos are not supported')
|
self.report_error('Live DASH videos are not supported')
|
||||||
|
|
||||||
fragment_base_url = info_dict.get('fragment_base_url')
|
real_start = time.time()
|
||||||
fragments = info_dict['fragments'][:1] if self.params.get(
|
|
||||||
'test', False) else info_dict['fragments']
|
|
||||||
|
|
||||||
real_downloader = get_suitable_downloader(
|
real_downloader = get_suitable_downloader(
|
||||||
info_dict, self.params, None, protocol='dash_frag_urls', to_stdout=(filename == '-'))
|
info_dict, self.params, None, protocol='dash_frag_urls', to_stdout=(filename == '-'))
|
||||||
|
|
||||||
ctx = {
|
requested_formats = [{**info_dict, **fmt} for fmt in info_dict.get('requested_formats', [])]
|
||||||
'filename': filename,
|
args = []
|
||||||
'total_frags': len(fragments),
|
for fmt in requested_formats or [info_dict]:
|
||||||
}
|
try:
|
||||||
|
fragment_count = 1 if self.params.get('test') else len(fmt['fragments'])
|
||||||
|
except TypeError:
|
||||||
|
fragment_count = None
|
||||||
|
ctx = {
|
||||||
|
'filename': fmt.get('filepath') or filename,
|
||||||
|
'live': 'is_from_start' if fmt.get('is_from_start') else fmt.get('is_live'),
|
||||||
|
'total_frags': fragment_count,
|
||||||
|
}
|
||||||
|
|
||||||
if real_downloader:
|
if real_downloader:
|
||||||
self._prepare_external_frag_download(ctx)
|
self._prepare_external_frag_download(ctx)
|
||||||
else:
|
else:
|
||||||
self._prepare_and_start_frag_download(ctx, info_dict)
|
self._prepare_and_start_frag_download(ctx, fmt)
|
||||||
|
ctx['start'] = real_start
|
||||||
|
|
||||||
|
fragments_to_download = self._get_fragments(fmt, ctx)
|
||||||
|
|
||||||
|
if real_downloader:
|
||||||
|
self.to_screen(
|
||||||
|
'[%s] Fragment downloads will be delegated to %s' % (self.FD_NAME, real_downloader.get_basename()))
|
||||||
|
info_dict['fragments'] = list(fragments_to_download)
|
||||||
|
fd = real_downloader(self.ydl, self.params)
|
||||||
|
return fd.real_download(filename, info_dict)
|
||||||
|
|
||||||
|
args.append([ctx, fragments_to_download, fmt])
|
||||||
|
|
||||||
|
return self.download_and_append_fragments_multiple(*args)
|
||||||
|
|
||||||
|
def _resolve_fragments(self, fragments, ctx):
|
||||||
|
fragments = fragments(ctx) if callable(fragments) else fragments
|
||||||
|
return [next(iter(fragments))] if self.params.get('test') else fragments
|
||||||
|
|
||||||
|
def _get_fragments(self, fmt, ctx):
|
||||||
|
fragment_base_url = fmt.get('fragment_base_url')
|
||||||
|
fragments = self._resolve_fragments(fmt['fragments'], ctx)
|
||||||
|
|
||||||
fragments_to_download = []
|
|
||||||
frag_index = 0
|
frag_index = 0
|
||||||
for i, fragment in enumerate(fragments):
|
for i, fragment in enumerate(fragments):
|
||||||
frag_index += 1
|
frag_index += 1
|
||||||
@@ -46,17 +73,8 @@ class DashSegmentsFD(FragmentFD):
|
|||||||
assert fragment_base_url
|
assert fragment_base_url
|
||||||
fragment_url = urljoin(fragment_base_url, fragment['path'])
|
fragment_url = urljoin(fragment_base_url, fragment['path'])
|
||||||
|
|
||||||
fragments_to_download.append({
|
yield {
|
||||||
'frag_index': frag_index,
|
'frag_index': frag_index,
|
||||||
'index': i,
|
'index': i,
|
||||||
'url': fragment_url,
|
'url': fragment_url,
|
||||||
})
|
}
|
||||||
|
|
||||||
if real_downloader:
|
|
||||||
self.to_screen(
|
|
||||||
'[%s] Fragment downloads will be delegated to %s' % (self.FD_NAME, real_downloader.get_basename()))
|
|
||||||
info_dict['fragments'] = fragments_to_download
|
|
||||||
fd = real_downloader(self.ydl, self.params)
|
|
||||||
return fd.real_download(filename, info_dict)
|
|
||||||
|
|
||||||
return self.download_and_append_fragments(ctx, fragments_to_download, info_dict)
|
|
||||||
|
|||||||
@@ -21,9 +21,7 @@ from ..utils import (
|
|||||||
encodeArgument,
|
encodeArgument,
|
||||||
handle_youtubedl_headers,
|
handle_youtubedl_headers,
|
||||||
check_executable,
|
check_executable,
|
||||||
is_outdated_version,
|
|
||||||
Popen,
|
Popen,
|
||||||
sanitize_open,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -145,11 +143,11 @@ class ExternalFD(FragmentFD):
|
|||||||
return -1
|
return -1
|
||||||
|
|
||||||
decrypt_fragment = self.decrypter(info_dict)
|
decrypt_fragment = self.decrypter(info_dict)
|
||||||
dest, _ = sanitize_open(tmpfilename, 'wb')
|
dest, _ = self.sanitize_open(tmpfilename, 'wb')
|
||||||
for frag_index, fragment in enumerate(info_dict['fragments']):
|
for frag_index, fragment in enumerate(info_dict['fragments']):
|
||||||
fragment_filename = '%s-Frag%d' % (tmpfilename, frag_index)
|
fragment_filename = '%s-Frag%d' % (tmpfilename, frag_index)
|
||||||
try:
|
try:
|
||||||
src, _ = sanitize_open(fragment_filename, 'rb')
|
src, _ = self.sanitize_open(fragment_filename, 'rb')
|
||||||
except IOError as err:
|
except IOError as err:
|
||||||
if skip_unavailable_fragments and frag_index > 1:
|
if skip_unavailable_fragments and frag_index > 1:
|
||||||
self.report_skip_fragment(frag_index, err)
|
self.report_skip_fragment(frag_index, err)
|
||||||
@@ -291,7 +289,7 @@ class Aria2cFD(ExternalFD):
|
|||||||
for frag_index, fragment in enumerate(info_dict['fragments']):
|
for frag_index, fragment in enumerate(info_dict['fragments']):
|
||||||
fragment_filename = '%s-Frag%d' % (os.path.basename(tmpfilename), frag_index)
|
fragment_filename = '%s-Frag%d' % (os.path.basename(tmpfilename), frag_index)
|
||||||
url_list.append('%s\n\tout=%s' % (fragment['url'], fragment_filename))
|
url_list.append('%s\n\tout=%s' % (fragment['url'], fragment_filename))
|
||||||
stream, _ = sanitize_open(url_list_file, 'wb')
|
stream, _ = self.sanitize_open(url_list_file, 'wb')
|
||||||
stream.write('\n'.join(url_list).encode('utf-8'))
|
stream.write('\n'.join(url_list).encode('utf-8'))
|
||||||
stream.close()
|
stream.close()
|
||||||
cmd += ['-i', url_list_file]
|
cmd += ['-i', url_list_file]
|
||||||
@@ -444,8 +442,7 @@ class FFmpegFD(ExternalFD):
|
|||||||
if info_dict.get('requested_formats') or protocol == 'http_dash_segments':
|
if info_dict.get('requested_formats') or protocol == 'http_dash_segments':
|
||||||
for (i, fmt) in enumerate(info_dict.get('requested_formats') or [info_dict]):
|
for (i, fmt) in enumerate(info_dict.get('requested_formats') or [info_dict]):
|
||||||
stream_number = fmt.get('manifest_stream_number', 0)
|
stream_number = fmt.get('manifest_stream_number', 0)
|
||||||
a_or_v = 'a' if fmt.get('acodec') != 'none' else 'v'
|
args.extend(['-map', f'{i}:{stream_number}'])
|
||||||
args.extend(['-map', f'{i}:{a_or_v}:{stream_number}'])
|
|
||||||
|
|
||||||
if self.params.get('test', False):
|
if self.params.get('test', False):
|
||||||
args += ['-fs', compat_str(self._TEST_FILE_SIZE)]
|
args += ['-fs', compat_str(self._TEST_FILE_SIZE)]
|
||||||
@@ -459,7 +456,7 @@ class FFmpegFD(ExternalFD):
|
|||||||
args += ['-f', 'mpegts']
|
args += ['-f', 'mpegts']
|
||||||
else:
|
else:
|
||||||
args += ['-f', 'mp4']
|
args += ['-f', 'mp4']
|
||||||
if (ffpp.basename == 'ffmpeg' and is_outdated_version(ffpp._versions['ffmpeg'], '3.2', False)) and (not info_dict.get('acodec') or info_dict['acodec'].split('.')[0] in ('aac', 'mp4a')):
|
if (ffpp.basename == 'ffmpeg' and ffpp._features.get('needs_adtstoasc')) and (not info_dict.get('acodec') or info_dict['acodec'].split('.')[0] in ('aac', 'mp4a')):
|
||||||
args += ['-bsf:a', 'aac_adtstoasc']
|
args += ['-bsf:a', 'aac_adtstoasc']
|
||||||
elif protocol == 'rtmp':
|
elif protocol == 'rtmp':
|
||||||
args += ['-f', 'flv']
|
args += ['-f', 'flv']
|
||||||
|
|||||||
@@ -366,7 +366,7 @@ class F4mFD(FragmentFD):
|
|||||||
ctx = {
|
ctx = {
|
||||||
'filename': filename,
|
'filename': filename,
|
||||||
'total_frags': total_frags,
|
'total_frags': total_frags,
|
||||||
'live': live,
|
'live': bool(live),
|
||||||
}
|
}
|
||||||
|
|
||||||
self._prepare_frag_download(ctx)
|
self._prepare_frag_download(ctx)
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
from __future__ import division, unicode_literals
|
from __future__ import division, unicode_literals
|
||||||
|
|
||||||
|
import http.client
|
||||||
|
import json
|
||||||
|
import math
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import json
|
|
||||||
from math import ceil
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
@@ -15,6 +16,7 @@ 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
|
||||||
from ..compat import (
|
from ..compat import (
|
||||||
|
compat_os_name,
|
||||||
compat_urllib_error,
|
compat_urllib_error,
|
||||||
compat_struct_pack,
|
compat_struct_pack,
|
||||||
)
|
)
|
||||||
@@ -22,7 +24,6 @@ from ..utils import (
|
|||||||
DownloadError,
|
DownloadError,
|
||||||
error_to_compat_str,
|
error_to_compat_str,
|
||||||
encodeFilename,
|
encodeFilename,
|
||||||
sanitize_open,
|
|
||||||
sanitized_Request,
|
sanitized_Request,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -31,6 +32,10 @@ class HttpQuietDownloader(HttpFD):
|
|||||||
def to_screen(self, *args, **kargs):
|
def to_screen(self, *args, **kargs):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def report_retry(self, err, count, retries):
|
||||||
|
super().to_screen(
|
||||||
|
f'[download] Got server HTTP error: {err}. Retrying (attempt {count} of {self.format_retries(retries)}) ...')
|
||||||
|
|
||||||
|
|
||||||
class FragmentFD(FileDownloader):
|
class FragmentFD(FileDownloader):
|
||||||
"""
|
"""
|
||||||
@@ -44,6 +49,7 @@ class FragmentFD(FileDownloader):
|
|||||||
Skip unavailable fragments (DASH and hlsnative only)
|
Skip unavailable fragments (DASH and hlsnative only)
|
||||||
keep_fragments: Keep downloaded fragments on disk after downloading is
|
keep_fragments: Keep downloaded fragments on disk after downloading is
|
||||||
finished
|
finished
|
||||||
|
concurrent_fragment_downloads: The number of threads to use for native hls and dash downloads
|
||||||
_no_ytdl_file: Don't use .ytdl file
|
_no_ytdl_file: Don't use .ytdl file
|
||||||
|
|
||||||
For each incomplete fragment download yt-dlp keeps on disk a special
|
For each incomplete fragment download yt-dlp keeps on disk a special
|
||||||
@@ -85,11 +91,11 @@ class FragmentFD(FileDownloader):
|
|||||||
self._start_frag_download(ctx, info_dict)
|
self._start_frag_download(ctx, info_dict)
|
||||||
|
|
||||||
def __do_ytdl_file(self, ctx):
|
def __do_ytdl_file(self, ctx):
|
||||||
return not ctx['live'] and not ctx['tmpfilename'] == '-' and not self.params.get('_no_ytdl_file')
|
return ctx['live'] is not True and ctx['tmpfilename'] != '-' and not self.params.get('_no_ytdl_file')
|
||||||
|
|
||||||
def _read_ytdl_file(self, ctx):
|
def _read_ytdl_file(self, ctx):
|
||||||
assert 'ytdl_corrupt' not in ctx
|
assert 'ytdl_corrupt' not in ctx
|
||||||
stream, _ = sanitize_open(self.ytdl_filename(ctx['filename']), 'r')
|
stream, _ = self.sanitize_open(self.ytdl_filename(ctx['filename']), 'r')
|
||||||
try:
|
try:
|
||||||
ytdl_data = json.loads(stream.read())
|
ytdl_data = json.loads(stream.read())
|
||||||
ctx['fragment_index'] = ytdl_data['downloader']['current_fragment']['index']
|
ctx['fragment_index'] = ytdl_data['downloader']['current_fragment']['index']
|
||||||
@@ -101,7 +107,7 @@ class FragmentFD(FileDownloader):
|
|||||||
stream.close()
|
stream.close()
|
||||||
|
|
||||||
def _write_ytdl_file(self, ctx):
|
def _write_ytdl_file(self, ctx):
|
||||||
frag_index_stream, _ = sanitize_open(self.ytdl_filename(ctx['filename']), 'w')
|
frag_index_stream, _ = self.sanitize_open(self.ytdl_filename(ctx['filename']), 'w')
|
||||||
try:
|
try:
|
||||||
downloader = {
|
downloader = {
|
||||||
'current_fragment': {
|
'current_fragment': {
|
||||||
@@ -133,7 +139,7 @@ class FragmentFD(FileDownloader):
|
|||||||
return True, self._read_fragment(ctx)
|
return True, self._read_fragment(ctx)
|
||||||
|
|
||||||
def _read_fragment(self, ctx):
|
def _read_fragment(self, ctx):
|
||||||
down, frag_sanitized = sanitize_open(ctx['fragment_filename_sanitized'], 'rb')
|
down, frag_sanitized = self.sanitize_open(ctx['fragment_filename_sanitized'], 'rb')
|
||||||
ctx['fragment_filename_sanitized'] = frag_sanitized
|
ctx['fragment_filename_sanitized'] = frag_sanitized
|
||||||
frag_content = down.read()
|
frag_content = down.read()
|
||||||
down.close()
|
down.close()
|
||||||
@@ -167,7 +173,7 @@ class FragmentFD(FileDownloader):
|
|||||||
self.ydl,
|
self.ydl,
|
||||||
{
|
{
|
||||||
'continuedl': True,
|
'continuedl': True,
|
||||||
'quiet': True,
|
'quiet': self.params.get('quiet'),
|
||||||
'noprogress': True,
|
'noprogress': True,
|
||||||
'ratelimit': self.params.get('ratelimit'),
|
'ratelimit': self.params.get('ratelimit'),
|
||||||
'retries': self.params.get('retries', 0),
|
'retries': self.params.get('retries', 0),
|
||||||
@@ -209,7 +215,7 @@ class FragmentFD(FileDownloader):
|
|||||||
self._write_ytdl_file(ctx)
|
self._write_ytdl_file(ctx)
|
||||||
assert ctx['fragment_index'] == 0
|
assert ctx['fragment_index'] == 0
|
||||||
|
|
||||||
dest_stream, tmpfilename = sanitize_open(tmpfilename, open_mode)
|
dest_stream, tmpfilename = self.sanitize_open(tmpfilename, open_mode)
|
||||||
|
|
||||||
ctx.update({
|
ctx.update({
|
||||||
'dl': dl,
|
'dl': dl,
|
||||||
@@ -237,6 +243,7 @@ class FragmentFD(FileDownloader):
|
|||||||
start = time.time()
|
start = time.time()
|
||||||
ctx.update({
|
ctx.update({
|
||||||
'started': start,
|
'started': start,
|
||||||
|
'fragment_started': start,
|
||||||
# Amount of fragment's bytes downloaded by the time of the previous
|
# Amount of fragment's bytes downloaded by the time of the previous
|
||||||
# frag progress hook invocation
|
# frag progress hook invocation
|
||||||
'prev_frag_downloaded_bytes': 0,
|
'prev_frag_downloaded_bytes': 0,
|
||||||
@@ -267,6 +274,9 @@ class FragmentFD(FileDownloader):
|
|||||||
ctx['fragment_index'] = state['fragment_index']
|
ctx['fragment_index'] = state['fragment_index']
|
||||||
state['downloaded_bytes'] += frag_total_bytes - ctx['prev_frag_downloaded_bytes']
|
state['downloaded_bytes'] += frag_total_bytes - ctx['prev_frag_downloaded_bytes']
|
||||||
ctx['complete_frags_downloaded_bytes'] = state['downloaded_bytes']
|
ctx['complete_frags_downloaded_bytes'] = state['downloaded_bytes']
|
||||||
|
ctx['speed'] = state['speed'] = self.calc_speed(
|
||||||
|
ctx['fragment_started'], time_now, frag_total_bytes)
|
||||||
|
ctx['fragment_started'] = time.time()
|
||||||
ctx['prev_frag_downloaded_bytes'] = 0
|
ctx['prev_frag_downloaded_bytes'] = 0
|
||||||
else:
|
else:
|
||||||
frag_downloaded_bytes = s['downloaded_bytes']
|
frag_downloaded_bytes = s['downloaded_bytes']
|
||||||
@@ -275,8 +285,8 @@ class FragmentFD(FileDownloader):
|
|||||||
state['eta'] = self.calc_eta(
|
state['eta'] = self.calc_eta(
|
||||||
start, time_now, estimated_size - resume_len,
|
start, time_now, estimated_size - resume_len,
|
||||||
state['downloaded_bytes'] - resume_len)
|
state['downloaded_bytes'] - resume_len)
|
||||||
state['speed'] = s.get('speed') or ctx.get('speed')
|
ctx['speed'] = state['speed'] = self.calc_speed(
|
||||||
ctx['speed'] = state['speed']
|
ctx['fragment_started'], time_now, frag_downloaded_bytes)
|
||||||
ctx['prev_frag_downloaded_bytes'] = frag_downloaded_bytes
|
ctx['prev_frag_downloaded_bytes'] = frag_downloaded_bytes
|
||||||
self._hook_progress(state, info_dict)
|
self._hook_progress(state, info_dict)
|
||||||
|
|
||||||
@@ -366,17 +376,20 @@ class FragmentFD(FileDownloader):
|
|||||||
@params (ctx1, fragments1, info_dict1), (ctx2, fragments2, info_dict2), ...
|
@params (ctx1, fragments1, info_dict1), (ctx2, fragments2, info_dict2), ...
|
||||||
all args must be either tuple or list
|
all args must be either tuple or list
|
||||||
'''
|
'''
|
||||||
|
interrupt_trigger = [True]
|
||||||
max_progress = len(args)
|
max_progress = len(args)
|
||||||
if max_progress == 1:
|
if max_progress == 1:
|
||||||
return self.download_and_append_fragments(*args[0], pack_func=pack_func, finish_func=finish_func)
|
return self.download_and_append_fragments(*args[0], pack_func=pack_func, finish_func=finish_func)
|
||||||
max_workers = self.params.get('concurrent_fragment_downloads', max_progress)
|
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)
|
||||||
|
|
||||||
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
|
||||||
ctx['progress_idx'] = idx
|
ctx['progress_idx'] = idx
|
||||||
return self.download_and_append_fragments(ctx, fragments, info_dict, pack_func=pack_func, finish_func=finish_func, tpe=tpe)
|
return self.download_and_append_fragments(
|
||||||
|
ctx, fragments, info_dict, pack_func=pack_func, finish_func=finish_func,
|
||||||
|
tpe=tpe, interrupt_trigger=interrupt_trigger)
|
||||||
|
|
||||||
class FTPE(concurrent.futures.ThreadPoolExecutor):
|
class FTPE(concurrent.futures.ThreadPoolExecutor):
|
||||||
# has to stop this or it's going to wait on the worker thread itself
|
# has to stop this or it's going to wait on the worker thread itself
|
||||||
@@ -384,8 +397,11 @@ class FragmentFD(FileDownloader):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
spins = []
|
spins = []
|
||||||
|
if compat_os_name == 'nt':
|
||||||
|
self.report_warning('Ctrl+C does not work on Windows when used with parallel threads. '
|
||||||
|
'This is a known issue and patches are welcome')
|
||||||
for idx, (ctx, fragments, info_dict) in enumerate(args):
|
for idx, (ctx, fragments, info_dict) in enumerate(args):
|
||||||
tpe = FTPE(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, fragments, info_dict, tpe)
|
||||||
spins.append((tpe, job))
|
spins.append((tpe, job))
|
||||||
|
|
||||||
@@ -393,18 +409,32 @@ class FragmentFD(FileDownloader):
|
|||||||
for tpe, job in spins:
|
for tpe, job in spins:
|
||||||
try:
|
try:
|
||||||
result = result and job.result()
|
result = result and job.result()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
interrupt_trigger[0] = False
|
||||||
finally:
|
finally:
|
||||||
tpe.shutdown(wait=True)
|
tpe.shutdown(wait=True)
|
||||||
|
if not interrupt_trigger[0]:
|
||||||
|
raise KeyboardInterrupt()
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def download_and_append_fragments(self, ctx, fragments, info_dict, *, pack_func=None, finish_func=None, tpe=None):
|
def download_and_append_fragments(
|
||||||
|
self, ctx, fragments, info_dict, *, pack_func=None, finish_func=None,
|
||||||
|
tpe=None, interrupt_trigger=None):
|
||||||
|
if not interrupt_trigger:
|
||||||
|
interrupt_trigger = (True, )
|
||||||
|
|
||||||
fragment_retries = self.params.get('fragment_retries', 0)
|
fragment_retries = self.params.get('fragment_retries', 0)
|
||||||
is_fatal = (lambda idx: idx == 0) if self.params.get('skip_unavailable_fragments', True) else (lambda _: True)
|
is_fatal = (
|
||||||
|
((lambda _: False) if info_dict.get('is_live') else (lambda idx: idx == 0))
|
||||||
|
if self.params.get('skip_unavailable_fragments', True) else (lambda _: True))
|
||||||
|
|
||||||
if not pack_func:
|
if not pack_func:
|
||||||
pack_func = lambda frag_content, _: frag_content
|
pack_func = lambda frag_content, _: frag_content
|
||||||
|
|
||||||
def download_fragment(fragment, ctx):
|
def download_fragment(fragment, ctx):
|
||||||
frag_index = ctx['fragment_index'] = fragment['frag_index']
|
frag_index = ctx['fragment_index'] = fragment['frag_index']
|
||||||
|
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:
|
||||||
@@ -419,7 +449,7 @@ class FragmentFD(FileDownloader):
|
|||||||
if not success:
|
if not success:
|
||||||
return False, frag_index
|
return False, frag_index
|
||||||
break
|
break
|
||||||
except compat_urllib_error.HTTPError 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.
|
||||||
# See https://github.com/ytdl-org/youtube-dl/issues/10165,
|
# See https://github.com/ytdl-org/youtube-dl/issues/10165,
|
||||||
@@ -457,7 +487,8 @@ class FragmentFD(FileDownloader):
|
|||||||
|
|
||||||
decrypt_fragment = self.decrypter(info_dict)
|
decrypt_fragment = self.decrypter(info_dict)
|
||||||
|
|
||||||
max_workers = self.params.get('concurrent_fragment_downloads', 1)
|
max_workers = math.ceil(
|
||||||
|
self.params.get('concurrent_fragment_downloads', 1) / ctx.get('max_progress', 1))
|
||||||
if can_threaded_download and max_workers > 1:
|
if can_threaded_download and max_workers > 1:
|
||||||
|
|
||||||
def _download_fragment(fragment):
|
def _download_fragment(fragment):
|
||||||
@@ -468,6 +499,8 @@ class FragmentFD(FileDownloader):
|
|||||||
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_content, 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, frag_content), frag_index, ctx)
|
||||||
@@ -475,6 +508,8 @@ class FragmentFD(FileDownloader):
|
|||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
for fragment in fragments:
|
for fragment in fragments:
|
||||||
|
if not interrupt_trigger[0]:
|
||||||
|
break
|
||||||
frag_content, frag_index = download_fragment(fragment, ctx)
|
frag_content, frag_index = download_fragment(fragment, ctx)
|
||||||
result = append_fragment(decrypt_fragment(fragment, frag_content), frag_index, ctx)
|
result = append_fragment(decrypt_fragment(fragment, frag_content), frag_index, ctx)
|
||||||
if not result:
|
if not result:
|
||||||
|
|||||||
@@ -77,6 +77,15 @@ class HlsFD(FragmentFD):
|
|||||||
message = ('The stream has AES-128 encryption and neither ffmpeg nor pycryptodomex are available; '
|
message = ('The stream has AES-128 encryption and neither ffmpeg nor pycryptodomex are available; '
|
||||||
'Decryption will be performed natively, but will be extremely slow')
|
'Decryption will be performed natively, but will be extremely slow')
|
||||||
if not can_download:
|
if not can_download:
|
||||||
|
has_drm = re.search('|'.join([
|
||||||
|
r'#EXT-X-FAXS-CM:', # Adobe Flash Access
|
||||||
|
r'#EXT-X-(?:SESSION-)?KEY:.*?URI="skd://', # Apple FairPlay
|
||||||
|
]), s)
|
||||||
|
if has_drm and not self.params.get('allow_unplayable_formats'):
|
||||||
|
self.report_error(
|
||||||
|
'This video is DRM protected; Try selecting another format with --format or '
|
||||||
|
'add --check-formats to automatically fallback to the next best format')
|
||||||
|
return False
|
||||||
message = message or 'Unsupported features have been detected'
|
message = message or 'Unsupported features have been detected'
|
||||||
fd = FFmpegFD(self.ydl, self.params)
|
fd = FFmpegFD(self.ydl, self.params)
|
||||||
self.report_warning(f'{message}; extraction will be delegated to {fd.get_basename()}')
|
self.report_warning(f'{message}; extraction will be delegated to {fd.get_basename()}')
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ from ..utils import (
|
|||||||
ContentTooShortError,
|
ContentTooShortError,
|
||||||
encodeFilename,
|
encodeFilename,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
sanitize_open,
|
|
||||||
sanitized_Request,
|
sanitized_Request,
|
||||||
ThrottledDownload,
|
ThrottledDownload,
|
||||||
write_xattr,
|
write_xattr,
|
||||||
@@ -263,7 +262,7 @@ class HttpFD(FileDownloader):
|
|||||||
# Open destination file just in time
|
# Open destination file just in time
|
||||||
if ctx.stream is None:
|
if ctx.stream is None:
|
||||||
try:
|
try:
|
||||||
ctx.stream, ctx.tmpfilename = sanitize_open(
|
ctx.stream, ctx.tmpfilename = self.sanitize_open(
|
||||||
ctx.tmpfilename, ctx.open_mode)
|
ctx.tmpfilename, ctx.open_mode)
|
||||||
assert ctx.stream is not None
|
assert ctx.stream is not None
|
||||||
ctx.filename = self.undo_temp_name(ctx.tmpfilename)
|
ctx.filename = self.undo_temp_name(ctx.tmpfilename)
|
||||||
|
|||||||
@@ -114,8 +114,8 @@ body > figure > img {
|
|||||||
fragment_base_url = info_dict.get('fragment_base_url')
|
fragment_base_url = info_dict.get('fragment_base_url')
|
||||||
fragments = info_dict['fragments'][:1] if self.params.get(
|
fragments = info_dict['fragments'][:1] if self.params.get(
|
||||||
'test', False) else info_dict['fragments']
|
'test', False) else info_dict['fragments']
|
||||||
title = info_dict['title']
|
title = info_dict.get('title', info_dict['format_id'])
|
||||||
origin = info_dict['webpage_url']
|
origin = info_dict.get('webpage_url', info_dict['url'])
|
||||||
|
|
||||||
ctx = {
|
ctx = {
|
||||||
'filename': filename,
|
'filename': filename,
|
||||||
|
|||||||
+64
-2
@@ -8,6 +8,7 @@ import time
|
|||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..compat import compat_str
|
from ..compat import compat_str
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
|
dict_get,
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
js_to_json,
|
js_to_json,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
@@ -233,8 +234,6 @@ class ABCIViewIE(InfoExtractor):
|
|||||||
}]
|
}]
|
||||||
|
|
||||||
is_live = video_params.get('livestream') == '1'
|
is_live = video_params.get('livestream') == '1'
|
||||||
if is_live:
|
|
||||||
title = self._live_title(title)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
@@ -255,3 +254,66 @@ class ABCIViewIE(InfoExtractor):
|
|||||||
'subtitles': subtitles,
|
'subtitles': subtitles,
|
||||||
'is_live': is_live,
|
'is_live': is_live,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ABCIViewShowSeriesIE(InfoExtractor):
|
||||||
|
IE_NAME = 'abc.net.au:iview:showseries'
|
||||||
|
_VALID_URL = r'https?://iview\.abc\.net\.au/show/(?P<id>[^/]+)(?:/series/\d+)?$'
|
||||||
|
_GEO_COUNTRIES = ['AU']
|
||||||
|
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://iview.abc.net.au/show/upper-middle-bogan',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '124870-1',
|
||||||
|
'title': 'Series 1',
|
||||||
|
'description': 'md5:93119346c24a7c322d446d8eece430ff',
|
||||||
|
'series': 'Upper Middle Bogan',
|
||||||
|
'season': 'Series 1',
|
||||||
|
'thumbnail': r're:^https?://cdn\.iview\.abc\.net\.au/thumbs/.*\.jpg$'
|
||||||
|
},
|
||||||
|
'playlist_count': 8,
|
||||||
|
}, {
|
||||||
|
'url': 'https://iview.abc.net.au/show/upper-middle-bogan',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'CO1108V001S00',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Series 1 Ep 1 I\'m A Swan',
|
||||||
|
'description': 'md5:7b676758c1de11a30b79b4d301e8da93',
|
||||||
|
'series': 'Upper Middle Bogan',
|
||||||
|
'uploader_id': 'abc1',
|
||||||
|
'upload_date': '20210630',
|
||||||
|
'timestamp': 1625036400,
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'noplaylist': True,
|
||||||
|
'skip_download': 'm3u8',
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
show_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, show_id)
|
||||||
|
webpage_data = self._search_regex(
|
||||||
|
r'window\.__INITIAL_STATE__\s*=\s*[\'"](.+?)[\'"]\s*;',
|
||||||
|
webpage, 'initial state')
|
||||||
|
video_data = self._parse_json(
|
||||||
|
unescapeHTML(webpage_data).encode('utf-8').decode('unicode_escape'), show_id)
|
||||||
|
video_data = video_data['route']['pageData']['_embedded']
|
||||||
|
|
||||||
|
if self.get_param('noplaylist') and 'highlightVideo' in video_data:
|
||||||
|
self.to_screen('Downloading just the highlight video because of --no-playlist')
|
||||||
|
return self.url_result(video_data['highlightVideo']['shareUrl'], 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']
|
||||||
|
return {
|
||||||
|
'_type': 'playlist',
|
||||||
|
'entries': [self.url_result(episode['shareUrl'])
|
||||||
|
for episode in series['_embedded']['videoEpisodes']],
|
||||||
|
'id': series.get('id'),
|
||||||
|
'title': dict_get(series, ('title', 'displaySubtitle')),
|
||||||
|
'description': series.get('description'),
|
||||||
|
'series': dict_get(series, ('showTitle', 'displayTitle')),
|
||||||
|
'season': dict_get(series, ('title', 'displaySubtitle')),
|
||||||
|
'thumbnail': series.get('thumbnail'),
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class AdobeConnectIE(InfoExtractor):
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'title': self._live_title(title) if is_live else title,
|
'title': title,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
'is_live': is_live,
|
'is_live': is_live,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from ..utils import (
|
|||||||
float_or_none,
|
float_or_none,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
ISO639Utils,
|
ISO639Utils,
|
||||||
|
join_nonempty,
|
||||||
OnDemandPagedList,
|
OnDemandPagedList,
|
||||||
parse_duration,
|
parse_duration,
|
||||||
str_or_none,
|
str_or_none,
|
||||||
@@ -263,7 +264,7 @@ class AdobeTVVideoIE(AdobeTVBaseIE):
|
|||||||
continue
|
continue
|
||||||
formats.append({
|
formats.append({
|
||||||
'filesize': int_or_none(source.get('kilobytes') or None, invscale=1000),
|
'filesize': int_or_none(source.get('kilobytes') or None, invscale=1000),
|
||||||
'format_id': '-'.join(filter(None, [source.get('format'), source.get('label')])),
|
'format_id': join_nonempty(source.get('format'), source.get('label')),
|
||||||
'height': int_or_none(source.get('height') or None),
|
'height': int_or_none(source.get('height') or None),
|
||||||
'tbr': int_or_none(source.get('bitrate') or None),
|
'tbr': int_or_none(source.get('bitrate') or None),
|
||||||
'width': int_or_none(source.get('width') or None),
|
'width': int_or_none(source.get('width') or None),
|
||||||
|
|||||||
@@ -1,55 +1,86 @@
|
|||||||
|
# coding: utf-8
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
try_get,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AlJazeeraIE(InfoExtractor):
|
class AlJazeeraIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?aljazeera\.com/(?P<type>program/[^/]+|(?:feature|video)s)/\d{4}/\d{1,2}/\d{1,2}/(?P<id>[^/?&#]+)'
|
_VALID_URL = r'https?://(?P<base>\w+\.aljazeera\.\w+)/(?P<type>programs?/[^/]+|(?:feature|video|new)s)?/\d{4}/\d{1,2}/\d{1,2}/(?P<id>[^/?&#]+)'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.aljazeera.com/program/episode/2014/9/19/deliverance',
|
'url': 'https://balkans.aljazeera.net/videos/2021/11/6/pojedini-domovi-u-sarajevu-jos-pod-vodom-mjestanima-se-dostavlja-hrana',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '3792260579001',
|
'id': '6280641530001',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'The Slum - Episode 1: Deliverance',
|
'title': 'Pojedini domovi u Sarajevu još pod vodom, mještanima se dostavlja hrana',
|
||||||
'description': 'As a birth attendant advocating for family planning, Remy is on the frontline of Tondo\'s battle with overcrowding.',
|
'timestamp': 1636219149,
|
||||||
'uploader_id': '665003303001',
|
'description': 'U sarajevskim naseljima Rajlovac i Reljevo stambeni objekti, ali i industrijska postrojenja i dalje su pod vodom.',
|
||||||
'timestamp': 1411116829,
|
'upload_date': '20211106',
|
||||||
'upload_date': '20140919',
|
}
|
||||||
|
}, {
|
||||||
|
'url': 'https://balkans.aljazeera.net/videos/2021/11/6/djokovic-usao-u-finale-mastersa-u-parizu',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '6280654936001',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Đoković ušao u finale Mastersa u Parizu',
|
||||||
|
'timestamp': 1636221686,
|
||||||
|
'description': 'Novak Đoković je u polufinalu Mastersa u Parizu nakon preokreta pobijedio Poljaka Huberta Hurkacza.',
|
||||||
|
'upload_date': '20211106',
|
||||||
},
|
},
|
||||||
'add_ie': ['BrightcoveNew'],
|
|
||||||
'skip': 'Not accessible from Travis CI server',
|
|
||||||
}, {
|
|
||||||
'url': 'https://www.aljazeera.com/videos/2017/5/11/sierra-leone-709-carat-diamond-to-be-auctioned-off',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'https://www.aljazeera.com/features/2017/8/21/transforming-pakistans-buses-into-art',
|
|
||||||
'only_matching': True,
|
|
||||||
}]
|
}]
|
||||||
BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/%s/%s_default/index.html?videoId=%s'
|
BRIGHTCOVE_URL_RE = r'https?://players.brightcove.net/(?P<account>\d+)/(?P<player_id>[a-zA-Z0-9]+)_(?P<embed>[^/]+)/index.html\?videoId=(?P<id>\d+)'
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
post_type, name = self._match_valid_url(url).groups()
|
base, post_type, id = self._match_valid_url(url).groups()
|
||||||
|
wp = {
|
||||||
|
'balkans.aljazeera.net': 'ajb',
|
||||||
|
'chinese.aljazeera.net': 'chinese',
|
||||||
|
'mubasher.aljazeera.net': 'ajm',
|
||||||
|
}.get(base) or 'aje'
|
||||||
post_type = {
|
post_type = {
|
||||||
'features': 'post',
|
'features': 'post',
|
||||||
'program': 'episode',
|
'program': 'episode',
|
||||||
|
'programs': 'episode',
|
||||||
'videos': 'video',
|
'videos': 'video',
|
||||||
|
'news': 'news',
|
||||||
}[post_type.split('/')[0]]
|
}[post_type.split('/')[0]]
|
||||||
video = self._download_json(
|
video = self._download_json(
|
||||||
'https://www.aljazeera.com/graphql', name, query={
|
f'https://{base}/graphql', id, query={
|
||||||
|
'wp-site': wp,
|
||||||
'operationName': 'ArchipelagoSingleArticleQuery',
|
'operationName': 'ArchipelagoSingleArticleQuery',
|
||||||
'variables': json.dumps({
|
'variables': json.dumps({
|
||||||
'name': name,
|
'name': id,
|
||||||
'postType': post_type,
|
'postType': post_type,
|
||||||
}),
|
}),
|
||||||
}, headers={
|
}, headers={
|
||||||
'wp-site': 'aje',
|
'wp-site': wp,
|
||||||
})['data']['article']['video']
|
})
|
||||||
video_id = video['id']
|
video = try_get(video, lambda x: x['data']['article']['video']) or {}
|
||||||
account_id = video.get('accountId') or '665003303001'
|
video_id = video.get('id')
|
||||||
player_id = video.get('playerId') or 'BkeSH5BDb'
|
account = video.get('accountId') or '911432371001'
|
||||||
return self.url_result(
|
player_id = video.get('playerId') or 'csvTfAlKW'
|
||||||
self.BRIGHTCOVE_URL_TEMPLATE % (account_id, player_id, video_id),
|
embed = 'default'
|
||||||
'BrightcoveNew', video_id)
|
|
||||||
|
if video_id is None:
|
||||||
|
webpage = self._download_webpage(url, id)
|
||||||
|
|
||||||
|
account, player_id, embed, video_id = self._search_regex(self.BRIGHTCOVE_URL_RE, webpage, 'video id',
|
||||||
|
group=(1, 2, 3, 4), default=(None, None, None, None))
|
||||||
|
|
||||||
|
if video_id is None:
|
||||||
|
return {
|
||||||
|
'_type': 'url_transparent',
|
||||||
|
'url': url,
|
||||||
|
'ie_key': 'Generic'
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'_type': 'url_transparent',
|
||||||
|
'url': f'https://players.brightcove.net/{account}/{player_id}_{embed}/index.html?videoId={video_id}',
|
||||||
|
'ie_key': 'BrightcoveNew'
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import int_or_none
|
||||||
|
|
||||||
|
|
||||||
|
class AmazonStoreIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?amazon\.(?:[a-z]{2,3})(?:\.[a-z]{2})?/(?:[^/]+/)?(?:dp|gp/product)/(?P<id>[^/&#$?]+)'
|
||||||
|
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.amazon.co.uk/dp/B098XNCHLD/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'B098XNCHLD',
|
||||||
|
'title': 'md5:5f3194dbf75a8dcfc83079bd63a2abed',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 1,
|
||||||
|
'playlist': [{
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'A1F83G8C2ARO7P',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'mcdodo usb c cable 100W 5a',
|
||||||
|
'thumbnail': r're:^https?://.*\.jpg$',
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.amazon.in/Sony-WH-1000XM4-Cancelling-Headphones-Bluetooth/dp/B0863TXGM3',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'B0863TXGM3',
|
||||||
|
'title': 'md5:b0bde4881d3cfd40d63af19f7898b8ff',
|
||||||
|
},
|
||||||
|
'playlist_mincount': 4,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.amazon.com/dp/B0845NXCXF/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'B0845NXCXF',
|
||||||
|
'title': 'md5:2145cd4e3c7782f1ee73649a3cff1171',
|
||||||
|
},
|
||||||
|
'playlist-mincount': 1,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, id)
|
||||||
|
data_json = self._parse_json(self._html_search_regex(r'var\s?obj\s?=\s?jQuery\.parseJSON\(\'(.*)\'\)', webpage, 'data'), id)
|
||||||
|
entries = [{
|
||||||
|
'id': video['marketPlaceID'],
|
||||||
|
'url': video['url'],
|
||||||
|
'title': video.get('title'),
|
||||||
|
'thumbnail': video.get('thumbUrl') or video.get('thumb'),
|
||||||
|
'duration': video.get('durationSeconds'),
|
||||||
|
'height': int_or_none(video.get('videoHeight')),
|
||||||
|
'width': int_or_none(video.get('videoWidth')),
|
||||||
|
} for video in (data_json.get('videos') or []) if video.get('isVideo') and video.get('url')]
|
||||||
|
return self.playlist_result(entries, playlist_id=id, playlist_title=data_json['title'])
|
||||||
@@ -8,6 +8,7 @@ from ..utils import (
|
|||||||
determine_ext,
|
determine_ext,
|
||||||
extract_attributes,
|
extract_attributes,
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
|
join_nonempty,
|
||||||
url_or_none,
|
url_or_none,
|
||||||
urlencode_postdata,
|
urlencode_postdata,
|
||||||
urljoin,
|
urljoin,
|
||||||
@@ -140,15 +141,8 @@ class AnimeOnDemandIE(InfoExtractor):
|
|||||||
kind = self._search_regex(
|
kind = self._search_regex(
|
||||||
r'videomaterialurl/\d+/([^/]+)/',
|
r'videomaterialurl/\d+/([^/]+)/',
|
||||||
playlist_url, 'media kind', default=None)
|
playlist_url, 'media kind', default=None)
|
||||||
format_id_list = []
|
format_id = join_nonempty(lang, kind) if lang or kind else str(num)
|
||||||
if lang:
|
format_note = join_nonempty(kind, lang_note, delim=', ')
|
||||||
format_id_list.append(lang)
|
|
||||||
if kind:
|
|
||||||
format_id_list.append(kind)
|
|
||||||
if not format_id_list and num is not None:
|
|
||||||
format_id_list.append(compat_str(num))
|
|
||||||
format_id = '-'.join(format_id_list)
|
|
||||||
format_note = ', '.join(filter(None, (kind, lang_note)))
|
|
||||||
item_id_list = []
|
item_id_list = []
|
||||||
if format_id:
|
if format_id:
|
||||||
item_id_list.append(format_id)
|
item_id_list.append(format_id)
|
||||||
@@ -195,12 +189,10 @@ class AnimeOnDemandIE(InfoExtractor):
|
|||||||
if not file_:
|
if not file_:
|
||||||
continue
|
continue
|
||||||
ext = determine_ext(file_)
|
ext = determine_ext(file_)
|
||||||
format_id_list = [lang, kind]
|
format_id = join_nonempty(
|
||||||
if ext == 'm3u8':
|
lang, kind,
|
||||||
format_id_list.append('hls')
|
'hls' if ext == 'm3u8' else None,
|
||||||
elif source.get('type') == 'video/dash' or ext == 'mpd':
|
'dash' if source.get('type') == 'video/dash' or ext == 'mpd' else None)
|
||||||
format_id_list.append('dash')
|
|
||||||
format_id = '-'.join(filter(None, format_id_list))
|
|
||||||
if ext == 'm3u8':
|
if ext == 'm3u8':
|
||||||
file_formats = self._extract_m3u8_formats(
|
file_formats = self._extract_m3u8_formats(
|
||||||
file_, video_id, 'mp4',
|
file_, video_id, 'mp4',
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from ..utils import (
|
|||||||
determine_ext,
|
determine_ext,
|
||||||
intlist_to_bytes,
|
intlist_to_bytes,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
|
join_nonempty,
|
||||||
strip_jsonp,
|
strip_jsonp,
|
||||||
unescapeHTML,
|
unescapeHTML,
|
||||||
unsmuggle_url,
|
unsmuggle_url,
|
||||||
@@ -303,13 +304,13 @@ class AnvatoIE(InfoExtractor):
|
|||||||
tbr = int_or_none(published_url.get('kbps'))
|
tbr = int_or_none(published_url.get('kbps'))
|
||||||
a_format = {
|
a_format = {
|
||||||
'url': video_url,
|
'url': video_url,
|
||||||
'format_id': ('-'.join(filter(None, ['http', published_url.get('cdn_name')]))).lower(),
|
'format_id': join_nonempty('http', published_url.get('cdn_name')).lower(),
|
||||||
'tbr': tbr if tbr != 0 else None,
|
'tbr': tbr or None,
|
||||||
}
|
}
|
||||||
|
|
||||||
if media_format == 'm3u8' and tbr is not None:
|
if media_format == 'm3u8' and tbr is not None:
|
||||||
a_format.update({
|
a_format.update({
|
||||||
'format_id': '-'.join(filter(None, ['hls', compat_str(tbr)])),
|
'format_id': join_nonempty('hls', tbr),
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
})
|
})
|
||||||
elif media_format == 'm3u8-variant' or ext == 'm3u8':
|
elif media_format == 'm3u8-variant' or ext == 'm3u8':
|
||||||
|
|||||||
+360
-107
@@ -3,33 +3,36 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from .youtube import YoutubeIE
|
from .youtube import YoutubeIE, YoutubeBaseInfoExtractor
|
||||||
from ..compat import (
|
from ..compat import (
|
||||||
compat_urllib_parse_unquote,
|
compat_urllib_parse_unquote,
|
||||||
compat_urllib_parse_unquote_plus,
|
compat_urllib_parse_unquote_plus,
|
||||||
compat_HTTPError
|
compat_HTTPError
|
||||||
)
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
|
bug_reports_message,
|
||||||
clean_html,
|
clean_html,
|
||||||
determine_ext,
|
|
||||||
dict_get,
|
dict_get,
|
||||||
extract_attributes,
|
extract_attributes,
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
|
get_element_by_id,
|
||||||
HEADRequest,
|
HEADRequest,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
KNOWN_EXTENSIONS,
|
KNOWN_EXTENSIONS,
|
||||||
merge_dicts,
|
merge_dicts,
|
||||||
mimetype2ext,
|
mimetype2ext,
|
||||||
|
orderedSet,
|
||||||
parse_duration,
|
parse_duration,
|
||||||
parse_qs,
|
parse_qs,
|
||||||
RegexNotFoundError,
|
|
||||||
str_to_int,
|
str_to_int,
|
||||||
str_or_none,
|
str_or_none,
|
||||||
|
traverse_obj,
|
||||||
try_get,
|
try_get,
|
||||||
unified_strdate,
|
unified_strdate,
|
||||||
unified_timestamp,
|
unified_timestamp,
|
||||||
|
urlhandle_detect_ext,
|
||||||
|
url_or_none
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -262,12 +265,12 @@ class YoutubeWebArchiveIE(InfoExtractor):
|
|||||||
_VALID_URL = r"""(?x)^
|
_VALID_URL = r"""(?x)^
|
||||||
(?:https?://)?web\.archive\.org/
|
(?:https?://)?web\.archive\.org/
|
||||||
(?:web/)?
|
(?:web/)?
|
||||||
(?:[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])//)?
|
(?:https?(?::|%3[Aa])//)?
|
||||||
(?:
|
(?:
|
||||||
(?:\w+\.)?youtube\.com/watch(?:\?|%3[fF])(?:[^\#]+(?:&|%26))?v(?:=|%3[dD]) # Youtube URL
|
(?:\w+\.)?youtube\.com(?::(?:80|443))?/watch(?:\.php)?(?:\?|%3[fF])(?:[^\#]+(?:&|%26))?v(?:=|%3[dD]) # Youtube URL
|
||||||
|(wayback-fakeurl\.archive\.org/yt/) # Or the internal fake url
|
|(?:wayback-fakeurl\.archive\.org/yt/) # Or the internal fake url
|
||||||
)
|
)
|
||||||
(?P<id>[0-9A-Za-z_-]{11})(?:%26|\#|&|$)
|
(?P<id>[0-9A-Za-z_-]{11})(?:%26|\#|&|$)
|
||||||
"""
|
"""
|
||||||
@@ -278,141 +281,391 @@ class YoutubeWebArchiveIE(InfoExtractor):
|
|||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'aYAGB11YrSs',
|
'id': 'aYAGB11YrSs',
|
||||||
'ext': 'webm',
|
'ext': 'webm',
|
||||||
'title': 'Team Fortress 2 - Sandviches!'
|
'title': 'Team Fortress 2 - Sandviches!',
|
||||||
|
'description': 'md5:4984c0f9a07f349fc5d8e82ab7af4eaf',
|
||||||
|
'upload_date': '20110926',
|
||||||
|
'uploader': 'Zeurel',
|
||||||
|
'channel_id': 'UCukCyHaD-bK3in_pKpfH9Eg',
|
||||||
|
'duration': 32,
|
||||||
|
'uploader_id': 'Zeurel',
|
||||||
|
'uploader_url': 'http://www.youtube.com/user/Zeurel'
|
||||||
}
|
}
|
||||||
},
|
}, {
|
||||||
{
|
|
||||||
# Internal link
|
# Internal link
|
||||||
'url': 'https://web.archive.org/web/2oe/http://wayback-fakeurl.archive.org/yt/97t7Xj_iBv0',
|
'url': 'https://web.archive.org/web/2oe/http://wayback-fakeurl.archive.org/yt/97t7Xj_iBv0',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '97t7Xj_iBv0',
|
'id': '97t7Xj_iBv0',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'How Flexible Machines Could Save The World'
|
'title': 'Why Machines That Bend Are Better',
|
||||||
|
'description': 'md5:00404df2c632d16a674ff8df1ecfbb6c',
|
||||||
|
'upload_date': '20190312',
|
||||||
|
'uploader': 'Veritasium',
|
||||||
|
'channel_id': 'UCHnyfMqiRRG1u-2MsSQLbXA',
|
||||||
|
'duration': 771,
|
||||||
|
'uploader_id': '1veritasium',
|
||||||
|
'uploader_url': 'http://www.youtube.com/user/1veritasium'
|
||||||
}
|
}
|
||||||
},
|
}, {
|
||||||
{
|
# Video from 2012, webm format itag 45. Newest capture is deleted video, with an invalid description.
|
||||||
# Video from 2012, webm format itag 45.
|
# Should use the date in the link. Title ends with '- Youtube'. Capture has description in eow-description
|
||||||
'url': 'https://web.archive.org/web/20120712231619/http://www.youtube.com/watch?v=AkhihxRKcrs&gl=US&hl=en',
|
'url': 'https://web.archive.org/web/20120712231619/http://www.youtube.com/watch?v=AkhihxRKcrs&gl=US&hl=en',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'AkhihxRKcrs',
|
'id': 'AkhihxRKcrs',
|
||||||
'ext': 'webm',
|
'ext': 'webm',
|
||||||
'title': 'Limited Run: Mondo\'s Modern Classic 1 of 3 (SDCC 2012)'
|
'title': 'Limited Run: Mondo\'s Modern Classic 1 of 3 (SDCC 2012)',
|
||||||
|
'upload_date': '20120712',
|
||||||
|
'duration': 398,
|
||||||
|
'description': 'md5:ff4de6a7980cb65d951c2f6966a4f2f3',
|
||||||
|
'uploader_id': 'machinima',
|
||||||
|
'uploader_url': 'http://www.youtube.com/user/machinima'
|
||||||
}
|
}
|
||||||
},
|
}, {
|
||||||
{
|
# FLV video. Video file URL does not provide itag information
|
||||||
# Old flash-only video. Webpage title starts with "YouTube - ".
|
|
||||||
'url': 'https://web.archive.org/web/20081211103536/http://www.youtube.com/watch?v=jNQXAC9IVRw',
|
'url': 'https://web.archive.org/web/20081211103536/http://www.youtube.com/watch?v=jNQXAC9IVRw',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'jNQXAC9IVRw',
|
'id': 'jNQXAC9IVRw',
|
||||||
'ext': 'unknown_video',
|
'ext': 'flv',
|
||||||
'title': 'Me at the zoo'
|
'title': 'Me at the zoo',
|
||||||
|
'upload_date': '20050423',
|
||||||
|
'channel_id': 'UC4QobU6STFB0P71PMvOGN5A',
|
||||||
|
'duration': 19,
|
||||||
|
'description': 'md5:10436b12e07ac43ff8df65287a56efb4',
|
||||||
|
'uploader_id': 'jawed',
|
||||||
|
'uploader_url': 'http://www.youtube.com/user/jawed'
|
||||||
}
|
}
|
||||||
},
|
}, {
|
||||||
{
|
|
||||||
# Flash video with .flv extension (itag 34). Title has prefix "YouTube -"
|
|
||||||
# Title has some weird unicode characters too.
|
|
||||||
'url': 'https://web.archive.org/web/20110712231407/http://www.youtube.com/watch?v=lTx3G6h2xyA',
|
'url': 'https://web.archive.org/web/20110712231407/http://www.youtube.com/watch?v=lTx3G6h2xyA',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'lTx3G6h2xyA',
|
'id': 'lTx3G6h2xyA',
|
||||||
'ext': 'flv',
|
'ext': 'flv',
|
||||||
'title': 'Madeon - Pop Culture (live mashup)'
|
'title': 'Madeon - Pop Culture (live mashup)',
|
||||||
|
'upload_date': '20110711',
|
||||||
|
'uploader': 'Madeon',
|
||||||
|
'channel_id': 'UCqMDNf3Pn5L7pcNkuSEeO3w',
|
||||||
|
'duration': 204,
|
||||||
|
'description': 'md5:f7535343b6eda34a314eff8b85444680',
|
||||||
|
'uploader_id': 'itsmadeon',
|
||||||
|
'uploader_url': 'http://www.youtube.com/user/itsmadeon'
|
||||||
}
|
}
|
||||||
},
|
}, {
|
||||||
{ # Some versions of Youtube have have "YouTube" as page title in html (and later rewritten by js).
|
# First capture is of dead video, second is the oldest from CDX response.
|
||||||
|
'url': 'https://web.archive.org/https://www.youtube.com/watch?v=1JYutPM8O6E',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '1JYutPM8O6E',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Fake Teen Doctor Strikes AGAIN! - Weekly Weird News',
|
||||||
|
'upload_date': '20160218',
|
||||||
|
'channel_id': 'UCdIaNUarhzLSXGoItz7BHVA',
|
||||||
|
'duration': 1236,
|
||||||
|
'description': 'md5:21032bae736421e89c2edf36d1936947',
|
||||||
|
'uploader_id': 'MachinimaETC',
|
||||||
|
'uploader_url': 'http://www.youtube.com/user/MachinimaETC'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
# First capture of dead video, capture date in link links to dead capture.
|
||||||
|
'url': 'https://web.archive.org/web/20180803221945/https://www.youtube.com/watch?v=6FPhZJGvf4E',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '6FPhZJGvf4E',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'WTF: Video Games Still Launch BROKEN?! - T.U.G.S.',
|
||||||
|
'upload_date': '20160219',
|
||||||
|
'channel_id': 'UCdIaNUarhzLSXGoItz7BHVA',
|
||||||
|
'duration': 798,
|
||||||
|
'description': 'md5:a1dbf12d9a3bd7cb4c5e33b27d77ffe7',
|
||||||
|
'uploader_id': 'MachinimaETC',
|
||||||
|
'uploader_url': 'http://www.youtube.com/user/MachinimaETC'
|
||||||
|
},
|
||||||
|
'expected_warnings': [
|
||||||
|
r'unable to download capture webpage \(it may not be archived\)'
|
||||||
|
]
|
||||||
|
}, { # Very old YouTube page, has - YouTube in title.
|
||||||
|
'url': 'http://web.archive.org/web/20070302011044/http://youtube.com/watch?v=-06-KB9XTzg',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '-06-KB9XTzg',
|
||||||
|
'ext': 'flv',
|
||||||
|
'title': 'New Coin Hack!! 100% Safe!!'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
'url': 'web.archive.org/https://www.youtube.com/watch?v=dWW7qP423y8',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'dWW7qP423y8',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'It\'s Bootleg AirPods Time.',
|
||||||
|
'upload_date': '20211021',
|
||||||
|
'channel_id': 'UC7Jwj9fkrf1adN4fMmTkpug',
|
||||||
|
'channel_url': 'http://www.youtube.com/channel/UC7Jwj9fkrf1adN4fMmTkpug',
|
||||||
|
'duration': 810,
|
||||||
|
'description': 'md5:7b567f898d8237b256f36c1a07d6d7bc',
|
||||||
|
'uploader': 'DankPods',
|
||||||
|
'uploader_id': 'UC7Jwj9fkrf1adN4fMmTkpug',
|
||||||
|
'uploader_url': 'http://www.youtube.com/channel/UC7Jwj9fkrf1adN4fMmTkpug'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
# player response contains '};' See: https://github.com/ytdl-org/youtube-dl/issues/27093
|
||||||
|
'url': 'https://web.archive.org/web/20200827003909if_/http://www.youtube.com/watch?v=6Dh-RL__uN4',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '6Dh-RL__uN4',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'bitch lasagna',
|
||||||
|
'upload_date': '20181005',
|
||||||
|
'channel_id': 'UC-lHJZR3Gqxm24_Vd_AJ5Yw',
|
||||||
|
'channel_url': 'http://www.youtube.com/channel/UC-lHJZR3Gqxm24_Vd_AJ5Yw',
|
||||||
|
'duration': 135,
|
||||||
|
'description': 'md5:2dbe4051feeff2dab5f41f82bb6d11d0',
|
||||||
|
'uploader': 'PewDiePie',
|
||||||
|
'uploader_id': 'PewDiePie',
|
||||||
|
'uploader_url': 'http://www.youtube.com/user/PewDiePie'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
'url': 'https://web.archive.org/web/http://www.youtube.com/watch?v=kH-G_aIBlFw',
|
'url': 'https://web.archive.org/web/http://www.youtube.com/watch?v=kH-G_aIBlFw',
|
||||||
'info_dict': {
|
'only_matching': True
|
||||||
'id': 'kH-G_aIBlFw',
|
}, {
|
||||||
'ext': 'mp4',
|
'url': 'https://web.archive.org/web/20050214000000_if/http://www.youtube.com/watch?v=0altSZ96U4M',
|
||||||
'title': 'kH-G_aIBlFw'
|
'only_matching': True
|
||||||
},
|
}, {
|
||||||
'expected_warnings': [
|
|
||||||
'unable to extract title',
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
# First capture is a 302 redirect intermediary page.
|
|
||||||
'url': 'https://web.archive.org/web/20050214000000/http://www.youtube.com/watch?v=0altSZ96U4M',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '0altSZ96U4M',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': '0altSZ96U4M'
|
|
||||||
},
|
|
||||||
'expected_warnings': [
|
|
||||||
'unable to extract title',
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
# Video not archived, only capture is unavailable video page
|
# Video not archived, only capture is unavailable video page
|
||||||
'url': 'https://web.archive.org/web/20210530071008/https://www.youtube.com/watch?v=lHJTf93HL1s&spfreload=10',
|
'url': 'https://web.archive.org/web/20210530071008/https://www.youtube.com/watch?v=lHJTf93HL1s&spfreload=10',
|
||||||
'only_matching': True,
|
'only_matching': True
|
||||||
},
|
}, { # Encoded url
|
||||||
{ # Encoded url
|
|
||||||
'url': 'https://web.archive.org/web/20120712231619/http%3A//www.youtube.com/watch%3Fgl%3DUS%26v%3DAkhihxRKcrs%26hl%3Den',
|
'url': 'https://web.archive.org/web/20120712231619/http%3A//www.youtube.com/watch%3Fgl%3DUS%26v%3DAkhihxRKcrs%26hl%3Den',
|
||||||
'only_matching': True,
|
'only_matching': True
|
||||||
},
|
}, {
|
||||||
{
|
|
||||||
'url': 'https://web.archive.org/web/20120712231619/http%3A//www.youtube.com/watch%3Fv%3DAkhihxRKcrs%26gl%3DUS%26hl%3Den',
|
'url': 'https://web.archive.org/web/20120712231619/http%3A//www.youtube.com/watch%3Fv%3DAkhihxRKcrs%26gl%3DUS%26hl%3Den',
|
||||||
'only_matching': True,
|
'only_matching': True
|
||||||
|
}, {
|
||||||
|
'url': 'https://web.archive.org/web/20060527081937/http://www.youtube.com:80/watch.php?v=ELTFsLT73fA&search=soccer',
|
||||||
|
'only_matching': True
|
||||||
|
}, {
|
||||||
|
'url': 'https://web.archive.org/http://www.youtube.com:80/watch?v=-05VVye-ffg',
|
||||||
|
'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_PLAYER_RESPONSE_RE = r'(?:(?:(?:window\s*\[\s*["\']ytInitialPlayerResponse["\']\s*\]|ytInitialPlayerResponse)\s*=[(\s]*({.+?})[)\s]*;)|%s)' % YoutubeBaseInfoExtractor._YT_INITIAL_PLAYER_RESPONSE_RE
|
||||||
|
_YT_INITIAL_BOUNDARY_RE = r'(?:(?:var\s+meta|</script|\n)|%s)' % YoutubeBaseInfoExtractor._YT_INITIAL_BOUNDARY_RE
|
||||||
|
|
||||||
|
_YT_DEFAULT_THUMB_SERVERS = ['i.ytimg.com'] # thumbnails most likely archived on these servers
|
||||||
|
_YT_ALL_THUMB_SERVERS = orderedSet(
|
||||||
|
_YT_DEFAULT_THUMB_SERVERS + ['img.youtube.com', *[f'{c}{n or ""}.ytimg.com' for c in ('i', 's') for n in (*range(0, 5), 9)]])
|
||||||
|
|
||||||
|
_WAYBACK_BASE_URL = 'https://web.archive.org/web/%sif_/'
|
||||||
|
_OLDEST_CAPTURE_DATE = 20050214000000
|
||||||
|
_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'):
|
||||||
|
# CDX docs: https://github.com/internetarchive/wayback/blob/master/wayback-cdx-server/README.md
|
||||||
|
query = {
|
||||||
|
'url': url,
|
||||||
|
'output': 'json',
|
||||||
|
'fl': 'original,mimetype,length,timestamp',
|
||||||
|
'limit': 500,
|
||||||
|
'filter': ['statuscode:200'] + (filters or []),
|
||||||
|
'collapse': collapse or [],
|
||||||
|
**(query or {})
|
||||||
|
}
|
||||||
|
res = self._download_json('https://web.archive.org/cdx/search/cdx', item_id, note, query=query)
|
||||||
|
if isinstance(res, list) and len(res) >= 2:
|
||||||
|
# format response to make it easier to use
|
||||||
|
return list(dict(zip(res[0], v)) for v in res[1:])
|
||||||
|
elif not isinstance(res, list) or len(res) != 0:
|
||||||
|
self.report_warning('Error while parsing CDX API response' + bug_reports_message())
|
||||||
|
|
||||||
|
def _extract_yt_initial_variable(self, webpage, regex, video_id, name):
|
||||||
|
return self._parse_json(self._search_regex(
|
||||||
|
(r'%s\s*%s' % (regex, self._YT_INITIAL_BOUNDARY_RE),
|
||||||
|
regex), webpage, name, default='{}'), video_id, fatal=False)
|
||||||
|
|
||||||
|
def _extract_webpage_title(self, webpage):
|
||||||
|
page_title = self._html_search_regex(
|
||||||
|
r'<title>([^<]*)</title>', webpage, 'title', default='')
|
||||||
|
# YouTube video pages appear to always have either 'YouTube -' as prefix or '- YouTube' as suffix.
|
||||||
|
return self._html_search_regex(
|
||||||
|
r'(?:YouTube\s*-\s*(.*)$)|(?:(.*)\s*-\s*YouTube$)',
|
||||||
|
page_title, 'title', default='')
|
||||||
|
|
||||||
|
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))
|
||||||
|
player_response = self._extract_yt_initial_variable(
|
||||||
|
webpage, self._YT_INITIAL_PLAYER_RESPONSE_RE, video_id, 'initial player response') or {}
|
||||||
|
initial_data = self._extract_yt_initial_variable(
|
||||||
|
webpage, self._YT_INITIAL_DATA_RE, video_id, 'initial player response') or {}
|
||||||
|
|
||||||
|
initial_data_video = traverse_obj(
|
||||||
|
initial_data, ('contents', 'twoColumnWatchNextResults', 'results', 'results', 'contents', ..., 'videoPrimaryInfoRenderer'),
|
||||||
|
expected_type=dict, get_all=False, default={})
|
||||||
|
|
||||||
|
video_details = traverse_obj(
|
||||||
|
player_response, 'videoDetails', expected_type=dict, get_all=False, default={})
|
||||||
|
|
||||||
|
microformats = traverse_obj(
|
||||||
|
player_response, ('microformat', 'playerMicroformatRenderer'), expected_type=dict, get_all=False, default={})
|
||||||
|
|
||||||
|
video_title = (
|
||||||
|
video_details.get('title')
|
||||||
|
or YoutubeBaseInfoExtractor._get_text(microformats, 'title')
|
||||||
|
or YoutubeBaseInfoExtractor._get_text(initial_data_video, 'title')
|
||||||
|
or self._extract_webpage_title(webpage)
|
||||||
|
or search_meta(['og:title', 'twitter:title', 'title']))
|
||||||
|
|
||||||
|
channel_id = str_or_none(
|
||||||
|
video_details.get('channelId')
|
||||||
|
or microformats.get('externalChannelId')
|
||||||
|
or search_meta('channelId')
|
||||||
|
or self._search_regex(
|
||||||
|
r'data-channel-external-id=(["\'])(?P<id>(?:(?!\1).)+)\1', # @b45a9e6
|
||||||
|
webpage, 'channel id', default=None, group='id'))
|
||||||
|
channel_url = f'http://www.youtube.com/channel/{channel_id}' if channel_id else None
|
||||||
|
|
||||||
|
duration = int_or_none(
|
||||||
|
video_details.get('lengthSeconds')
|
||||||
|
or microformats.get('lengthSeconds')
|
||||||
|
or parse_duration(search_meta('duration')))
|
||||||
|
description = (
|
||||||
|
video_details.get('shortDescription')
|
||||||
|
or YoutubeBaseInfoExtractor._get_text(microformats, 'description')
|
||||||
|
or clean_html(get_element_by_id('eow-description', webpage)) # @9e6dd23
|
||||||
|
or search_meta(['description', 'og:description', 'twitter:description']))
|
||||||
|
|
||||||
|
uploader = video_details.get('author')
|
||||||
|
|
||||||
|
# Uploader ID and URL
|
||||||
|
uploader_mobj = re.search(
|
||||||
|
r'<link itemprop="url" href="(?P<uploader_url>https?://www\.youtube\.com/(?:user|channel)/(?P<uploader_id>[^"]+))">', # @fd05024
|
||||||
|
webpage)
|
||||||
|
if uploader_mobj is not None:
|
||||||
|
uploader_id, uploader_url = uploader_mobj.group('uploader_id'), uploader_mobj.group('uploader_url')
|
||||||
|
else:
|
||||||
|
# @a6211d2
|
||||||
|
uploader_url = url_or_none(microformats.get('ownerProfileUrl'))
|
||||||
|
uploader_id = self._search_regex(
|
||||||
|
r'(?:user|channel)/([^/]+)', uploader_url or '', 'uploader id', default=None)
|
||||||
|
|
||||||
|
upload_date = unified_strdate(
|
||||||
|
dict_get(microformats, ('uploadDate', 'publishDate'))
|
||||||
|
or search_meta(['uploadDate', 'datePublished'])
|
||||||
|
or self._search_regex(
|
||||||
|
[r'(?s)id="eow-date.*?>(.*?)</span>',
|
||||||
|
r'(?:id="watch-uploader-info".*?>.*?|["\']simpleText["\']\s*:\s*["\'])(?:Published|Uploaded|Streamed live|Started) on (.+?)[<"\']'], # @7998520
|
||||||
|
webpage, 'upload date', default=None))
|
||||||
|
|
||||||
|
return {
|
||||||
|
'title': video_title,
|
||||||
|
'description': description,
|
||||||
|
'upload_date': upload_date,
|
||||||
|
'uploader': uploader,
|
||||||
|
'channel_id': channel_id,
|
||||||
|
'channel_url': channel_url,
|
||||||
|
'duration': duration,
|
||||||
|
'uploader_url': uploader_url,
|
||||||
|
'uploader_id': uploader_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _extract_thumbnails(self, video_id):
|
||||||
|
try_all = 'thumbnails' in self._configuration_arg('check_all')
|
||||||
|
thumbnail_base_urls = ['http://{server}/vi{webp}/{video_id}'.format(
|
||||||
|
webp='_webp' if ext == 'webp' else '', video_id=video_id, server=server)
|
||||||
|
for server in (self._YT_ALL_THUMB_SERVERS if try_all else self._YT_DEFAULT_THUMB_SERVERS) for ext in (('jpg', 'webp') if try_all else ('jpg',))]
|
||||||
|
|
||||||
|
thumbnails = []
|
||||||
|
for url in thumbnail_base_urls:
|
||||||
|
response = self._call_cdx_api(
|
||||||
|
video_id, url, filters=['mimetype:image/(?:webp|jpeg)'],
|
||||||
|
collapse=['urlkey'], query={'matchType': 'prefix'})
|
||||||
|
if not response:
|
||||||
|
continue
|
||||||
|
thumbnails.extend(
|
||||||
|
{
|
||||||
|
'url': (self._WAYBACK_BASE_URL % (int_or_none(thumbnail_dict.get('timestamp')) or self._OLDEST_CAPTURE_DATE)) + thumbnail_dict.get('original'),
|
||||||
|
'filesize': int_or_none(thumbnail_dict.get('length')),
|
||||||
|
'preference': int_or_none(thumbnail_dict.get('length'))
|
||||||
|
} for thumbnail_dict in response)
|
||||||
|
if not try_all:
|
||||||
|
break
|
||||||
|
|
||||||
|
self._remove_duplicate_formats(thumbnails)
|
||||||
|
return thumbnails
|
||||||
|
|
||||||
|
def _get_capture_dates(self, video_id, url_date):
|
||||||
|
capture_dates = []
|
||||||
|
# Note: CDX API will not find watch pages with extra params in the url.
|
||||||
|
response = self._call_cdx_api(
|
||||||
|
video_id, f'https://www.youtube.com/watch?v={video_id}',
|
||||||
|
filters=['mimetype:text/html'], collapse=['timestamp:6', 'digest'], query={'matchType': 'prefix'}) or []
|
||||||
|
all_captures = sorted([int_or_none(r['timestamp']) for r in response if int_or_none(r['timestamp']) is not None])
|
||||||
|
|
||||||
|
# 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
|
||||||
|
modern_captures = list(filter(lambda x: x >= 20200701000000, all_captures))
|
||||||
|
if modern_captures:
|
||||||
|
capture_dates.append(modern_captures[0])
|
||||||
|
capture_dates.append(url_date)
|
||||||
|
if all_captures:
|
||||||
|
capture_dates.append(all_captures[0])
|
||||||
|
|
||||||
|
if 'captures' in self._configuration_arg('check_all'):
|
||||||
|
capture_dates.extend(modern_captures + all_captures)
|
||||||
|
|
||||||
|
# Fallbacks if any of the above fail
|
||||||
|
capture_dates.extend([self._OLDEST_CAPTURE_DATE, self._NEWEST_CAPTURE_DATE])
|
||||||
|
return orderedSet(capture_dates)
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
|
||||||
title = video_id # if we are not able get a title
|
|
||||||
|
|
||||||
def _extract_title(webpage):
|
url_date, video_id = self._match_valid_url(url).groups()
|
||||||
page_title = self._html_search_regex(
|
|
||||||
r'<title>([^<]*)</title>', webpage, 'title', fatal=False) or ''
|
|
||||||
# YouTube video pages appear to always have either 'YouTube -' as suffix or '- YouTube' as prefix.
|
|
||||||
try:
|
|
||||||
page_title = self._html_search_regex(
|
|
||||||
r'(?:YouTube\s*-\s*(.*)$)|(?:(.*)\s*-\s*YouTube$)',
|
|
||||||
page_title, 'title', default='')
|
|
||||||
except RegexNotFoundError:
|
|
||||||
page_title = None
|
|
||||||
|
|
||||||
if not page_title:
|
urlh = None
|
||||||
self.report_warning('unable to extract title', video_id=video_id)
|
|
||||||
return
|
|
||||||
return page_title
|
|
||||||
|
|
||||||
# If the video is no longer available, the oldest capture may be one before it was removed.
|
|
||||||
# Setting the capture date in url to early date seems to redirect to earliest capture.
|
|
||||||
webpage = self._download_webpage(
|
|
||||||
'https://web.archive.org/web/20050214000000/http://www.youtube.com/watch?v=%s' % video_id,
|
|
||||||
video_id=video_id, fatal=False, errnote='unable to download video webpage (probably not archived).')
|
|
||||||
if webpage:
|
|
||||||
title = _extract_title(webpage) or title
|
|
||||||
|
|
||||||
# Use link translator mentioned in https://github.com/ytdl-org/youtube-dl/issues/13655
|
|
||||||
internal_fake_url = 'https://web.archive.org/web/2oe_/http://wayback-fakeurl.archive.org/yt/%s' % video_id
|
|
||||||
try:
|
try:
|
||||||
video_file_webpage = self._request_webpage(
|
urlh = self._request_webpage(
|
||||||
HEADRequest(internal_fake_url), video_id,
|
HEADRequest('https://web.archive.org/web/2oe_/http://wayback-fakeurl.archive.org/yt/%s' % video_id),
|
||||||
note='Fetching video file url', expected_status=True)
|
video_id, note='Fetching archived video file url', expected_status=True)
|
||||||
except ExtractorError as e:
|
except ExtractorError as e:
|
||||||
# HTTP Error 404 is expected if the video is not saved.
|
# HTTP Error 404 is expected if the video is not saved.
|
||||||
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 404:
|
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 404:
|
||||||
raise ExtractorError(
|
self.raise_no_formats(
|
||||||
'HTTP Error %s. Most likely the video is not archived or issue with web.archive.org.' % e.cause.code,
|
'The requested video is not archived, indexed, or there is an issue with web.archive.org',
|
||||||
expected=True)
|
expected=True)
|
||||||
raise
|
else:
|
||||||
video_file_url = compat_urllib_parse_unquote(video_file_webpage.url)
|
raise
|
||||||
video_file_url_qs = parse_qs(video_file_url)
|
|
||||||
|
|
||||||
# Attempt to recover any ext & format info from playback url
|
capture_dates = self._get_capture_dates(video_id, int_or_none(url_date))
|
||||||
format = {'url': video_file_url}
|
self.write_debug('Captures to try: ' + ', '.join(str(i) for i in capture_dates if i is not None))
|
||||||
itag = try_get(video_file_url_qs, lambda x: x['itag'][0])
|
info = {'id': video_id}
|
||||||
if itag and itag in YoutubeIE._formats: # Naughty access but it works
|
for capture in capture_dates:
|
||||||
format.update(YoutubeIE._formats[itag])
|
if not capture:
|
||||||
format.update({'format_id': itag})
|
continue
|
||||||
else:
|
webpage = self._download_webpage(
|
||||||
mime = try_get(video_file_url_qs, lambda x: x['mime'][0])
|
(self._WAYBACK_BASE_URL + 'http://www.youtube.com/watch?v=%s') % (capture, video_id),
|
||||||
ext = mimetype2ext(mime) or determine_ext(video_file_url)
|
video_id=video_id, fatal=False, errnote='unable to download capture webpage (it may not be archived)',
|
||||||
format.update({'ext': ext})
|
note='Downloading capture webpage')
|
||||||
return {
|
current_info = self._extract_metadata(video_id, webpage or '')
|
||||||
'id': video_id,
|
# Try avoid getting deleted video metadata
|
||||||
'title': title,
|
if current_info.get('title'):
|
||||||
'formats': [format],
|
info = merge_dicts(info, current_info)
|
||||||
'duration': str_to_int(try_get(video_file_url_qs, lambda x: x['dur'][0]))
|
if 'captures' not in self._configuration_arg('check_all'):
|
||||||
}
|
break
|
||||||
|
|
||||||
|
info['thumbnails'] = self._extract_thumbnails(video_id)
|
||||||
|
|
||||||
|
if urlh:
|
||||||
|
url = compat_urllib_parse_unquote(urlh.url)
|
||||||
|
video_file_url_qs = parse_qs(url)
|
||||||
|
# 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'))}
|
||||||
|
itag = try_get(video_file_url_qs, lambda x: x['itag'][0])
|
||||||
|
if itag and itag in YoutubeIE._formats:
|
||||||
|
format.update(YoutubeIE._formats[itag])
|
||||||
|
format.update({'format_id': itag})
|
||||||
|
else:
|
||||||
|
mime = try_get(video_file_url_qs, lambda x: x['mime'][0])
|
||||||
|
ext = (mimetype2ext(mime)
|
||||||
|
or urlhandle_detect_ext(urlh)
|
||||||
|
or mimetype2ext(urlh.headers.get('x-archive-guessed-content-type')))
|
||||||
|
format.update({'ext': ext})
|
||||||
|
info['formats'] = [format]
|
||||||
|
if not info.get('duration'):
|
||||||
|
info['duration'] = str_to_int(try_get(video_file_url_qs, lambda x: x['dur'][0]))
|
||||||
|
|
||||||
|
if not info.get('title'):
|
||||||
|
info['title'] = video_id
|
||||||
|
return info
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ class ArcPublishingIE(InfoExtractor):
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
'id': uuid,
|
'id': uuid,
|
||||||
'title': self._live_title(title) if is_live else title,
|
'title': title,
|
||||||
'thumbnail': try_get(video, lambda x: x['promo_image']['url']),
|
'thumbnail': try_get(video, lambda x: x['promo_image']['url']),
|
||||||
'description': try_get(video, lambda x: x['subheadlines']['basic']),
|
'description': try_get(video, lambda x: x['subheadlines']['basic']),
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
|
|||||||
+32
-16
@@ -280,7 +280,7 @@ class ARDMediathekIE(ARDMediathekBaseIE):
|
|||||||
|
|
||||||
info.update({
|
info.update({
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'title': self._live_title(title) if info.get('is_live') else title,
|
'title': title,
|
||||||
'description': description,
|
'description': description,
|
||||||
'thumbnail': thumbnail,
|
'thumbnail': thumbnail,
|
||||||
})
|
})
|
||||||
@@ -388,7 +388,13 @@ class ARDIE(InfoExtractor):
|
|||||||
|
|
||||||
|
|
||||||
class ARDBetaMediathekIE(ARDMediathekBaseIE):
|
class ARDBetaMediathekIE(ARDMediathekBaseIE):
|
||||||
_VALID_URL = r'https://(?:(?:beta|www)\.)?ardmediathek\.de/(?P<client>[^/]+)/(?P<mode>player|live|video|sendung|sammlung)/(?P<display_id>(?:[^/]+/)*)(?P<video_id>[a-zA-Z0-9]+)'
|
_VALID_URL = r'''(?x)https://
|
||||||
|
(?:(?:beta|www)\.)?ardmediathek\.de/
|
||||||
|
(?:(?P<client>[^/]+)/)?
|
||||||
|
(?:player|live|video|(?P<playlist>sendung|sammlung))/
|
||||||
|
(?:(?P<display_id>[^?#]+)/)?
|
||||||
|
(?P<id>(?(playlist)|Y3JpZDovL)[a-zA-Z0-9]+)'''
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.ardmediathek.de/mdr/video/die-robuste-roswita/Y3JpZDovL21kci5kZS9iZWl0cmFnL2Ntcy84MWMxN2MzZC0wMjkxLTRmMzUtODk4ZS0wYzhlOWQxODE2NGI/',
|
'url': 'https://www.ardmediathek.de/mdr/video/die-robuste-roswita/Y3JpZDovL21kci5kZS9iZWl0cmFnL2Ntcy84MWMxN2MzZC0wMjkxLTRmMzUtODk4ZS0wYzhlOWQxODE2NGI/',
|
||||||
'md5': 'a1dc75a39c61601b980648f7c9f9f71d',
|
'md5': 'a1dc75a39c61601b980648f7c9f9f71d',
|
||||||
@@ -403,6 +409,18 @@ class ARDBetaMediathekIE(ARDMediathekBaseIE):
|
|||||||
'upload_date': '20200805',
|
'upload_date': '20200805',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
},
|
},
|
||||||
|
'skip': 'Error',
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.ardmediathek.de/video/tagesschau-oder-tagesschau-20-00-uhr/das-erste/Y3JpZDovL2Rhc2Vyc3RlLmRlL3RhZ2Vzc2NoYXUvZmM4ZDUxMjgtOTE0ZC00Y2MzLTgzNzAtNDZkNGNiZWJkOTll',
|
||||||
|
'md5': 'f1837e563323b8a642a8ddeff0131f51',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '10049223',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'tagesschau, 20:00 Uhr',
|
||||||
|
'timestamp': 1636398000,
|
||||||
|
'description': 'md5:39578c7b96c9fe50afdf5674ad985e6b',
|
||||||
|
'upload_date': '20211108',
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://beta.ardmediathek.de/ard/video/Y3JpZDovL2Rhc2Vyc3RlLmRlL3RhdG9ydC9mYmM4NGM1NC0xNzU4LTRmZGYtYWFhZS0wYzcyZTIxNGEyMDE',
|
'url': 'https://beta.ardmediathek.de/ard/video/Y3JpZDovL2Rhc2Vyc3RlLmRlL3RhdG9ydC9mYmM4NGM1NC0xNzU4LTRmZGYtYWFhZS0wYzcyZTIxNGEyMDE',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
@@ -426,6 +444,12 @@ class ARDBetaMediathekIE(ARDMediathekBaseIE):
|
|||||||
# playlist of type 'sammlung'
|
# playlist of type 'sammlung'
|
||||||
'url': 'https://www.ardmediathek.de/ard/sammlung/team-muenster/5JpTzLSbWUAK8184IOvEir/',
|
'url': 'https://www.ardmediathek.de/ard/sammlung/team-muenster/5JpTzLSbWUAK8184IOvEir/',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.ardmediathek.de/video/coronavirus-update-ndr-info/astrazeneca-kurz-lockdown-und-pims-syndrom-81/ndr/Y3JpZDovL25kci5kZS84NzE0M2FjNi0wMWEwLTQ5ODEtOTE5NS1mOGZhNzdhOTFmOTI/',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.ardmediathek.de/ard/player/Y3JpZDovL3dkci5kZS9CZWl0cmFnLWQ2NDJjYWEzLTMwZWYtNGI4NS1iMTI2LTU1N2UxYTcxOGIzOQ/tatort-duo-koeln-leipzig-ihr-kinderlein-kommet',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _ARD_load_playlist_snipped(self, playlist_id, display_id, client, mode, pageNumber):
|
def _ARD_load_playlist_snipped(self, playlist_id, display_id, client, mode, pageNumber):
|
||||||
@@ -525,20 +549,12 @@ class ARDBetaMediathekIE(ARDMediathekBaseIE):
|
|||||||
return self.playlist_result(entries, playlist_title=display_id)
|
return self.playlist_result(entries, playlist_title=display_id)
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = self._match_valid_url(url)
|
video_id, display_id, playlist_type, client = self._match_valid_url(url).group(
|
||||||
video_id = mobj.group('video_id')
|
'id', 'display_id', 'playlist', 'client')
|
||||||
display_id = mobj.group('display_id')
|
display_id, client = display_id or video_id, client or 'ard'
|
||||||
if display_id:
|
|
||||||
display_id = display_id.rstrip('/')
|
|
||||||
if not display_id:
|
|
||||||
display_id = video_id
|
|
||||||
|
|
||||||
if mobj.group('mode') in ('sendung', 'sammlung'):
|
if playlist_type:
|
||||||
# this is a playlist-URL
|
return self._ARD_extract_playlist(url, video_id, display_id, client, playlist_type)
|
||||||
return self._ARD_extract_playlist(
|
|
||||||
url, video_id, display_id,
|
|
||||||
mobj.group('client'),
|
|
||||||
mobj.group('mode'))
|
|
||||||
|
|
||||||
player_page = self._download_json(
|
player_page = self._download_json(
|
||||||
'https://api.ardmediathek.de/public-gateway',
|
'https://api.ardmediathek.de/public-gateway',
|
||||||
@@ -574,7 +590,7 @@ class ARDBetaMediathekIE(ARDMediathekBaseIE):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}''' % (mobj.group('client'), video_id),
|
}''' % (client, video_id),
|
||||||
}).encode(), headers={
|
}).encode(), headers={
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
})['data']['playerPage']
|
})['data']['playerPage']
|
||||||
|
|||||||
@@ -24,9 +24,6 @@ class AtresPlayerIE(InfoExtractor):
|
|||||||
'description': 'md5:7634cdcb4d50d5381bedf93efb537fbc',
|
'description': 'md5:7634cdcb4d50d5381bedf93efb537fbc',
|
||||||
'duration': 3413,
|
'duration': 3413,
|
||||||
},
|
},
|
||||||
'params': {
|
|
||||||
'format': 'bestvideo',
|
|
||||||
},
|
|
||||||
'skip': 'This video is only available for registered users'
|
'skip': 'This video is only available for registered users'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from ..utils import (
|
|||||||
|
|
||||||
|
|
||||||
class AudiomackIE(InfoExtractor):
|
class AudiomackIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?audiomack\.com/song/(?P<id>[\w/-]+)'
|
_VALID_URL = r'https?://(?:www\.)?audiomack\.com/(?:song/|(?=.+/song/))(?P<id>[\w/-]+)'
|
||||||
IE_NAME = 'audiomack'
|
IE_NAME = 'audiomack'
|
||||||
_TESTS = [
|
_TESTS = [
|
||||||
# hosted on audiomack
|
# hosted on audiomack
|
||||||
@@ -39,15 +39,16 @@ class AudiomackIE(InfoExtractor):
|
|||||||
'title': 'Black Mamba Freestyle [Prod. By Danny Wolf]',
|
'title': 'Black Mamba Freestyle [Prod. By Danny Wolf]',
|
||||||
'uploader': 'ILOVEMAKONNEN',
|
'uploader': 'ILOVEMAKONNEN',
|
||||||
'upload_date': '20160414',
|
'upload_date': '20160414',
|
||||||
}
|
},
|
||||||
|
'skip': 'Song has been removed from the site',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
# URLs end with [uploader name]/[uploader title]
|
# URLs end with [uploader name]/song/[uploader title]
|
||||||
# this title is whatever the user types in, and is rarely
|
# this title is whatever the user types in, and is rarely
|
||||||
# the proper song title. Real metadata is in the api response
|
# the proper song title. Real metadata is in the api response
|
||||||
album_url_tag = self._match_id(url)
|
album_url_tag = self._match_id(url).replace('/song/', '/')
|
||||||
|
|
||||||
# Request the extended version of the api for extra fields like artist and title
|
# Request the extended version of the api for extra fields like artist and title
|
||||||
api_response = self._download_json(
|
api_response = self._download_json(
|
||||||
@@ -73,13 +74,13 @@ class AudiomackIE(InfoExtractor):
|
|||||||
|
|
||||||
|
|
||||||
class AudiomackAlbumIE(InfoExtractor):
|
class AudiomackAlbumIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?audiomack\.com/album/(?P<id>[\w/-]+)'
|
_VALID_URL = r'https?://(?:www\.)?audiomack\.com/(?:album/|(?=.+/album/))(?P<id>[\w/-]+)'
|
||||||
IE_NAME = 'audiomack:album'
|
IE_NAME = 'audiomack:album'
|
||||||
_TESTS = [
|
_TESTS = [
|
||||||
# Standard album playlist
|
# Standard album playlist
|
||||||
{
|
{
|
||||||
'url': 'http://www.audiomack.com/album/flytunezcom/tha-tour-part-2-mixtape',
|
'url': 'http://www.audiomack.com/album/flytunezcom/tha-tour-part-2-mixtape',
|
||||||
'playlist_count': 15,
|
'playlist_count': 11,
|
||||||
'info_dict':
|
'info_dict':
|
||||||
{
|
{
|
||||||
'id': '812251',
|
'id': '812251',
|
||||||
@@ -95,24 +96,27 @@ class AudiomackAlbumIE(InfoExtractor):
|
|||||||
},
|
},
|
||||||
'playlist': [{
|
'playlist': [{
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'title': 'PPP (Pistol P Project) - 9. Heaven or Hell (CHIMACA) ft Zuse (prod by DJ FU)',
|
'title': 'PPP (Pistol P Project) - 8. Real (prod by SYK SENSE )',
|
||||||
'id': '837577',
|
'id': '837576',
|
||||||
|
'ext': 'mp3',
|
||||||
|
'uploader': 'Lil Herb a.k.a. G Herbo',
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
'info_dict': {
|
||||||
|
'title': 'PPP (Pistol P Project) - 10. 4 Minutes Of Hell Part 4 (prod by DY OF 808 MAFIA)',
|
||||||
|
'id': '837580',
|
||||||
'ext': 'mp3',
|
'ext': 'mp3',
|
||||||
'uploader': 'Lil Herb a.k.a. G Herbo',
|
'uploader': 'Lil Herb a.k.a. G Herbo',
|
||||||
}
|
}
|
||||||
}],
|
}],
|
||||||
'params': {
|
|
||||||
'playliststart': 9,
|
|
||||||
'playlistend': 9,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
# URLs end with [uploader name]/[uploader title]
|
# URLs end with [uploader name]/album/[uploader title]
|
||||||
# this title is whatever the user types in, and is rarely
|
# this title is whatever the user types in, and is rarely
|
||||||
# the proper song title. Real metadata is in the api response
|
# the proper song title. Real metadata is in the api response
|
||||||
album_url_tag = self._match_id(url)
|
album_url_tag = self._match_id(url).replace('/album/', '/')
|
||||||
result = {'_type': 'playlist', 'entries': []}
|
result = {'_type': 'playlist', 'entries': []}
|
||||||
# There is no one endpoint for album metadata - instead it is included/repeated in each song's metadata
|
# There is no one endpoint for album metadata - instead it is included/repeated in each song's metadata
|
||||||
# Therefore we don't know how many songs the album has and must infi-loop until failure
|
# Therefore we don't know how many songs the album has and must infi-loop until failure
|
||||||
@@ -134,7 +138,7 @@ class AudiomackAlbumIE(InfoExtractor):
|
|||||||
# Pull out the album metadata and add to result (if it exists)
|
# Pull out the album metadata and add to result (if it exists)
|
||||||
for resultkey, apikey in [('id', 'album_id'), ('title', 'album_title')]:
|
for resultkey, apikey in [('id', 'album_id'), ('title', 'album_title')]:
|
||||||
if apikey in api_response and resultkey not in result:
|
if apikey in api_response and resultkey not in result:
|
||||||
result[resultkey] = api_response[apikey]
|
result[resultkey] = compat_str(api_response[apikey])
|
||||||
song_id = url_basename(api_response['url']).rpartition('.')[0]
|
song_id = url_basename(api_response['url']).rpartition('.')[0]
|
||||||
result['entries'].append({
|
result['entries'].append({
|
||||||
'id': compat_str(api_response.get('id', song_id)),
|
'id': compat_str(api_response.get('id', song_id)),
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class AWAANBaseIE(InfoExtractor):
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'title': self._live_title(title) if is_live else title,
|
'title': title,
|
||||||
'description': video_data.get('description_en') or video_data.get('description_ar'),
|
'description': video_data.get('description_en') or video_data.get('description_ar'),
|
||||||
'thumbnail': 'http://admin.mangomolo.com/analytics/%s' % img if img else None,
|
'thumbnail': 'http://admin.mangomolo.com/analytics/%s' % img if img else None,
|
||||||
'duration': int_or_none(video_data.get('duration')),
|
'duration': int_or_none(video_data.get('duration')),
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ class BandaiChannelIE(BrightcoveNewIE):
|
|||||||
'duration': 1387.733,
|
'duration': 1387.733,
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'format': 'bestvideo',
|
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
}]
|
}]
|
||||||
|
|||||||
+14
-4
@@ -451,9 +451,10 @@ class BBCCoUkIE(InfoExtractor):
|
|||||||
playlist = self._download_json(
|
playlist = self._download_json(
|
||||||
'http://www.bbc.co.uk/programmes/%s/playlist.json' % playlist_id,
|
'http://www.bbc.co.uk/programmes/%s/playlist.json' % playlist_id,
|
||||||
playlist_id, 'Downloading playlist JSON')
|
playlist_id, 'Downloading playlist JSON')
|
||||||
|
formats = []
|
||||||
|
subtitles = {}
|
||||||
|
|
||||||
version = playlist.get('defaultAvailableVersion')
|
for version in playlist.get('allAvailableVersions', []):
|
||||||
if version:
|
|
||||||
smp_config = version['smpConfig']
|
smp_config = version['smpConfig']
|
||||||
title = smp_config['title']
|
title = smp_config['title']
|
||||||
description = smp_config['summary']
|
description = smp_config['summary']
|
||||||
@@ -463,8 +464,17 @@ class BBCCoUkIE(InfoExtractor):
|
|||||||
continue
|
continue
|
||||||
programme_id = item.get('vpid')
|
programme_id = item.get('vpid')
|
||||||
duration = int_or_none(item.get('duration'))
|
duration = int_or_none(item.get('duration'))
|
||||||
formats, subtitles = self._download_media_selector(programme_id)
|
version_formats, version_subtitles = self._download_media_selector(programme_id)
|
||||||
return programme_id, title, description, duration, formats, subtitles
|
types = version['types']
|
||||||
|
for f in version_formats:
|
||||||
|
f['format_note'] = ', '.join(types)
|
||||||
|
if any('AudioDescribed' in x for x in types):
|
||||||
|
f['language_preference'] = -10
|
||||||
|
formats += version_formats
|
||||||
|
for tag, subformats in (version_subtitles or {}).items():
|
||||||
|
subtitles.setdefault(tag, []).extend(subformats)
|
||||||
|
|
||||||
|
return programme_id, title, description, duration, formats, subtitles
|
||||||
except ExtractorError as ee:
|
except ExtractorError as ee:
|
||||||
if not (isinstance(ee.cause, compat_HTTPError) and ee.cause.code == 404):
|
if not (isinstance(ee.cause, compat_HTTPError) and ee.cause.code == 404):
|
||||||
raise
|
raise
|
||||||
|
|||||||
@@ -346,7 +346,8 @@ class BiliBiliIE(InfoExtractor):
|
|||||||
def _extract_anthology_entries(self, bv_id, video_id, webpage):
|
def _extract_anthology_entries(self, bv_id, video_id, webpage):
|
||||||
title = self._html_search_regex(
|
title = self._html_search_regex(
|
||||||
(r'<h1[^>]+\btitle=(["\'])(?P<title>(?:(?!\1).)+)\1',
|
(r'<h1[^>]+\btitle=(["\'])(?P<title>(?:(?!\1).)+)\1',
|
||||||
r'(?s)<h1[^>]*>(?P<title>.+?)</h1>'), webpage, 'title',
|
r'(?s)<h1[^>]*>(?P<title>.+?)</h1>',
|
||||||
|
r'<title>(?P<title>.+?)</title>'), webpage, 'title',
|
||||||
group='title')
|
group='title')
|
||||||
json_data = self._download_json(
|
json_data = 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',
|
||||||
@@ -376,8 +377,10 @@ class BiliBiliIE(InfoExtractor):
|
|||||||
replies = traverse_obj(
|
replies = traverse_obj(
|
||||||
self._download_json(
|
self._download_json(
|
||||||
f'https://api.bilibili.com/x/v2/reply?pn={idx}&oid={video_id}&type=1&jsonp=jsonp&sort=2&_=1567227301685',
|
f'https://api.bilibili.com/x/v2/reply?pn={idx}&oid={video_id}&type=1&jsonp=jsonp&sort=2&_=1567227301685',
|
||||||
video_id, note=f'Extracting comments from page {idx}'),
|
video_id, note=f'Extracting comments from page {idx}', fatal=False),
|
||||||
('data', 'replies')) or []
|
('data', 'replies'))
|
||||||
|
if not replies:
|
||||||
|
return
|
||||||
for children in map(self._get_all_children, replies):
|
for children in map(self._get_all_children, replies):
|
||||||
yield from children
|
yield from children
|
||||||
|
|
||||||
@@ -566,7 +569,7 @@ class BilibiliCategoryIE(InfoExtractor):
|
|||||||
|
|
||||||
|
|
||||||
class BiliBiliSearchIE(SearchInfoExtractor):
|
class BiliBiliSearchIE(SearchInfoExtractor):
|
||||||
IE_DESC = 'Bilibili video search, "bilisearch" keyword'
|
IE_DESC = 'Bilibili video search'
|
||||||
_MAX_RESULTS = 100000
|
_MAX_RESULTS = 100000
|
||||||
_SEARCH_KEY = 'bilisearch'
|
_SEARCH_KEY = 'bilisearch'
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class BitwaveStreamIE(InfoExtractor):
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
'id': username,
|
'id': username,
|
||||||
'title': self._live_title(channel['data']['title']),
|
'title': channel['data']['title'],
|
||||||
'uploader': username,
|
'uploader': username,
|
||||||
'uploader_id': username,
|
'uploader_id': username,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from ..utils import (
|
||||||
|
mimetype2ext,
|
||||||
|
parse_duration,
|
||||||
|
parse_qs,
|
||||||
|
str_or_none,
|
||||||
|
traverse_obj,
|
||||||
|
)
|
||||||
|
from .common import InfoExtractor
|
||||||
|
|
||||||
|
|
||||||
|
class BloggerIE(InfoExtractor):
|
||||||
|
IE_NAME = 'blogger.com'
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?blogger\.com/video\.g\?token=(?P<id>.+)'
|
||||||
|
_VALID_EMBED = r'''<iframe[^>]+src=["']((?:https?:)?//(?:www\.)?blogger\.com/video\.g\?token=[^"']+)["']'''
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.blogger.com/video.g?token=AD6v5dzEe9hfcARr5Hlq1WTkYy6t-fXH3BBahVhGvVHe5szdEUBEloSEDSTA8-b111089KbfWuBvTN7fnbxMtymsHhXAXwVvyzHH4Qch2cfLQdGxKQrrEuFpC1amSl_9GuLWODjPgw',
|
||||||
|
'md5': 'f1bc19b6ea1b0fd1d81e84ca9ec467ac',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'BLOGGER-video-3c740e3a49197e16-796',
|
||||||
|
'title': 'BLOGGER-video-3c740e3a49197e16-796',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'thumbnail': r're:^https?://.*',
|
||||||
|
'duration': 76.068,
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_urls(webpage):
|
||||||
|
return re.findall(BloggerIE._VALID_EMBED, webpage)
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
token_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, token_id)
|
||||||
|
data_json = self._search_regex(r'var\s+VIDEO_CONFIG\s*=\s*(\{.*)', webpage, 'JSON data')
|
||||||
|
data = self._parse_json(data_json.encode('utf-8').decode('unicode_escape'), token_id)
|
||||||
|
streams = data['streams']
|
||||||
|
formats = [{
|
||||||
|
'ext': mimetype2ext(traverse_obj(parse_qs(stream['play_url']), ('mime', 0))),
|
||||||
|
'url': stream['play_url'],
|
||||||
|
'format_id': str_or_none(stream.get('format_id')),
|
||||||
|
} for stream in streams]
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': data.get('iframe_id', token_id),
|
||||||
|
'title': data.get('iframe_id', token_id),
|
||||||
|
'formats': formats,
|
||||||
|
'thumbnail': data.get('thumbnail'),
|
||||||
|
'duration': parse_duration(traverse_obj(parse_qs(streams[0]['play_url']), ('dur', 0))),
|
||||||
|
}
|
||||||
@@ -49,7 +49,7 @@ class BongaCamsIE(InfoExtractor):
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
'id': channel_id,
|
'id': channel_id,
|
||||||
'title': self._live_title(uploader or uploader_id),
|
'title': uploader or uploader_id,
|
||||||
'uploader': uploader,
|
'uploader': uploader,
|
||||||
'uploader_id': uploader_id,
|
'uploader_id': uploader_id,
|
||||||
'like_count': like_count,
|
'like_count': like_count,
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
|
||||||
|
|
||||||
|
class BreitBartIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?:\/\/(?:www\.)breitbart.com/videos/v/(?P<id>[^/]+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.breitbart.com/videos/v/5cOz1yup/?pl=Ij6NDOji',
|
||||||
|
'md5': '0aa6d1d6e183ac5ca09207fe49f17ade',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '5cOz1yup',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Watch \u2013 Clyburn: Statues in Congress Have to Go Because they Are Honoring Slavery',
|
||||||
|
'description': 'md5:bac35eb0256d1cb17f517f54c79404d5',
|
||||||
|
'thumbnail': 'https://cdn.jwplayer.com/thumbs/5cOz1yup-1920.jpg',
|
||||||
|
'age_limit': 0,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.breitbart.com/videos/v/eaiZjVOn/',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
|
formats = self._extract_m3u8_formats(f'https://cdn.jwplayer.com/manifests/{video_id}.m3u8', video_id, ext='mp4')
|
||||||
|
self._sort_formats(formats)
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': self._og_search_title(
|
||||||
|
webpage, default=None) or self._html_search_regex(
|
||||||
|
r'(?s)<title>(.*?)</title>', webpage, 'video title'),
|
||||||
|
'description': self._og_search_description(webpage),
|
||||||
|
'thumbnail': self._og_search_thumbnail(webpage),
|
||||||
|
'age_limit': self._rta_search(webpage),
|
||||||
|
'formats': formats
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ from ..compat import (
|
|||||||
)
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
clean_html,
|
clean_html,
|
||||||
|
dict_get,
|
||||||
extract_attributes,
|
extract_attributes,
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
find_xpath_attr,
|
find_xpath_attr,
|
||||||
@@ -471,32 +472,22 @@ class BrightcoveNewIE(AdobePassIE):
|
|||||||
def _parse_brightcove_metadata(self, json_data, video_id, headers={}):
|
def _parse_brightcove_metadata(self, json_data, video_id, headers={}):
|
||||||
title = json_data['name'].strip()
|
title = json_data['name'].strip()
|
||||||
|
|
||||||
num_drm_sources = 0
|
|
||||||
formats, subtitles = [], {}
|
formats, subtitles = [], {}
|
||||||
sources = json_data.get('sources') or []
|
sources = json_data.get('sources') or []
|
||||||
for source in sources:
|
for source in sources:
|
||||||
container = source.get('container')
|
container = source.get('container')
|
||||||
ext = mimetype2ext(source.get('type'))
|
ext = mimetype2ext(source.get('type'))
|
||||||
src = source.get('src')
|
src = source.get('src')
|
||||||
skip_unplayable = not self.get_param('allow_unplayable_formats')
|
if ext == 'm3u8' or container == 'M2TS':
|
||||||
# https://support.brightcove.com/playback-api-video-fields-reference#key_systems_object
|
|
||||||
if skip_unplayable and (container == 'WVM' or source.get('key_systems')):
|
|
||||||
num_drm_sources += 1
|
|
||||||
continue
|
|
||||||
elif ext == 'ism' and skip_unplayable:
|
|
||||||
continue
|
|
||||||
elif ext == 'm3u8' or container == 'M2TS':
|
|
||||||
if not src:
|
if not src:
|
||||||
continue
|
continue
|
||||||
f, subs = self._extract_m3u8_formats_and_subtitles(
|
fmts, subs = self._extract_m3u8_formats_and_subtitles(
|
||||||
src, video_id, 'mp4', 'm3u8_native', m3u8_id='hls', fatal=False)
|
src, video_id, 'mp4', 'm3u8_native', m3u8_id='hls', fatal=False)
|
||||||
formats.extend(f)
|
|
||||||
subtitles = self._merge_subtitles(subtitles, subs)
|
subtitles = self._merge_subtitles(subtitles, subs)
|
||||||
elif ext == 'mpd':
|
elif ext == 'mpd':
|
||||||
if not src:
|
if not src:
|
||||||
continue
|
continue
|
||||||
f, subs = self._extract_mpd_formats_and_subtitles(src, video_id, 'dash', fatal=False)
|
fmts, subs = self._extract_mpd_formats_and_subtitles(src, video_id, 'dash', fatal=False)
|
||||||
formats.extend(f)
|
|
||||||
subtitles = self._merge_subtitles(subtitles, subs)
|
subtitles = self._merge_subtitles(subtitles, subs)
|
||||||
else:
|
else:
|
||||||
streaming_src = source.get('streaming_src')
|
streaming_src = source.get('streaming_src')
|
||||||
@@ -543,7 +534,13 @@ class BrightcoveNewIE(AdobePassIE):
|
|||||||
'play_path': stream_name,
|
'play_path': stream_name,
|
||||||
'format_id': build_format_id('rtmp'),
|
'format_id': build_format_id('rtmp'),
|
||||||
})
|
})
|
||||||
formats.append(f)
|
fmts = [f]
|
||||||
|
|
||||||
|
# https://support.brightcove.com/playback-api-video-fields-reference#key_systems_object
|
||||||
|
if container == 'WVM' or source.get('key_systems') or ext == 'ism':
|
||||||
|
for f in fmts:
|
||||||
|
f['has_drm'] = True
|
||||||
|
formats.extend(fmts)
|
||||||
|
|
||||||
if not formats:
|
if not formats:
|
||||||
errors = json_data.get('errors')
|
errors = json_data.get('errors')
|
||||||
@@ -551,9 +548,6 @@ class BrightcoveNewIE(AdobePassIE):
|
|||||||
error = errors[0]
|
error = errors[0]
|
||||||
self.raise_no_formats(
|
self.raise_no_formats(
|
||||||
error.get('message') or error.get('error_subcode') or error['error_code'], expected=True)
|
error.get('message') or error.get('error_subcode') or error['error_code'], expected=True)
|
||||||
elif (not self.get_param('allow_unplayable_formats')
|
|
||||||
and sources and num_drm_sources == len(sources)):
|
|
||||||
self.report_drm(video_id)
|
|
||||||
|
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
@@ -577,11 +571,19 @@ class BrightcoveNewIE(AdobePassIE):
|
|||||||
if duration is not None and duration <= 0:
|
if duration is not None and duration <= 0:
|
||||||
is_live = True
|
is_live = True
|
||||||
|
|
||||||
|
common_res = [(160, 90), (320, 180), (480, 720), (640, 360), (768, 432), (1024, 576), (1280, 720), (1366, 768), (1920, 1080)]
|
||||||
|
thumb_base_url = dict_get(json_data, ('poster', 'thumbnail'))
|
||||||
|
thumbnails = [{
|
||||||
|
'url': re.sub(r'\d+x\d+', f'{w}x{h}', thumb_base_url),
|
||||||
|
'width': w,
|
||||||
|
'height': h,
|
||||||
|
} for w, h in common_res] if thumb_base_url else None
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'title': self._live_title(title) if is_live else title,
|
'title': title,
|
||||||
'description': clean_html(json_data.get('description')),
|
'description': clean_html(json_data.get('description')),
|
||||||
'thumbnail': json_data.get('thumbnail') or json_data.get('poster'),
|
'thumbnails': thumbnails,
|
||||||
'duration': duration,
|
'duration': duration,
|
||||||
'timestamp': parse_iso8601(json_data.get('published_at')),
|
'timestamp': parse_iso8601(json_data.get('published_at')),
|
||||||
'uploader_id': json_data.get('account_id'),
|
'uploader_id': json_data.get('account_id'),
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from .common import InfoExtractor
|
||||||
|
|
||||||
|
|
||||||
|
class CableAVIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https://cableav\.tv/(?P<id>[a-zA-Z0-9]+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://cableav.tv/lS4iR9lWjN8/',
|
||||||
|
'md5': '7e3fe5e49d61c4233b7f5b0f69b15e18',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'lS4iR9lWjN8',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': '國產麻豆AV 叮叮映畫 DDF001 情欲小說家 - CableAV',
|
||||||
|
'description': '國產AV 480p, 720p 国产麻豆AV 叮叮映画 DDF001 情欲小说家',
|
||||||
|
'thumbnail': r're:^https?://.*\.jpg$',
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
|
video_url = self._og_search_video_url(webpage, secure=False)
|
||||||
|
|
||||||
|
formats = self._extract_m3u8_formats(video_url, video_id, 'mp4')
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': self._og_search_title(webpage),
|
||||||
|
'description': self._og_search_description(webpage),
|
||||||
|
'thumbnail': self._og_search_thumbnail(webpage),
|
||||||
|
'formats': formats,
|
||||||
|
}
|
||||||
@@ -25,7 +25,7 @@ class CAM4IE(InfoExtractor):
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
'id': channel_id,
|
'id': channel_id,
|
||||||
'title': self._live_title(channel_id),
|
'title': channel_id,
|
||||||
'is_live': True,
|
'is_live': True,
|
||||||
'age_limit': 18,
|
'age_limit': 18,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ class CamModelsIE(InfoExtractor):
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
'id': user_id,
|
'id': user_id,
|
||||||
'title': self._live_title(user_id),
|
'title': user_id,
|
||||||
'is_live': True,
|
'is_live': True,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
'age_limit': 18
|
'age_limit': 18
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
clean_html,
|
||||||
|
dict_get,
|
||||||
|
try_get,
|
||||||
|
unified_strdate,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CanalAlphaIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?canalalpha\.ch/play/[^/]+/[^/]+/(?P<id>\d+)/?.*'
|
||||||
|
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.canalalpha.ch/play/le-journal/episode/24520/jeudi-28-octobre-2021',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '24520',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Jeudi 28 octobre 2021',
|
||||||
|
'description': 'md5:d30c6c3e53f8ad40d405379601973b30',
|
||||||
|
'thumbnail': 'https://static.canalalpha.ch/poster/journal/journal_20211028.jpg',
|
||||||
|
'upload_date': '20211028',
|
||||||
|
'duration': 1125,
|
||||||
|
},
|
||||||
|
'params': {'skip_download': True}
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.canalalpha.ch/play/le-journal/topic/24512/la-poste-fait-de-neuchatel-un-pole-cryptographique',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '24512',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'La Poste fait de Neuchâtel un pôle cryptographique',
|
||||||
|
'description': 'md5:4ba63ae78a0974d1a53d6703b6e1dedf',
|
||||||
|
'thumbnail': 'https://static.canalalpha.ch/poster/news/news_39712.jpg',
|
||||||
|
'upload_date': '20211028',
|
||||||
|
'duration': 138,
|
||||||
|
},
|
||||||
|
'params': {'skip_download': True}
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.canalalpha.ch/play/eureka/episode/24484/ces-innovations-qui-veulent-rendre-lagriculture-plus-durable',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '24484',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Ces innovations qui veulent rendre l’agriculture plus durable',
|
||||||
|
'description': 'md5:3de3f151180684621e85be7c10e4e613',
|
||||||
|
'thumbnail': 'https://static.canalalpha.ch/poster/magazine/magazine_10236.jpg',
|
||||||
|
'upload_date': '20211026',
|
||||||
|
'duration': 360,
|
||||||
|
},
|
||||||
|
'params': {'skip_download': True}
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.canalalpha.ch/play/avec-le-temps/episode/23516/redonner-de-leclat-grace-au-polissage',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '23516',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Redonner de l\'éclat grâce au polissage',
|
||||||
|
'description': 'md5:0d8fbcda1a5a4d6f6daa3165402177e1',
|
||||||
|
'thumbnail': 'https://static.canalalpha.ch/poster/magazine/magazine_9990.png',
|
||||||
|
'upload_date': '20210726',
|
||||||
|
'duration': 360,
|
||||||
|
},
|
||||||
|
'params': {'skip_download': True}
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, id)
|
||||||
|
data_json = self._parse_json(self._search_regex(
|
||||||
|
r'window\.__SERVER_STATE__\s?=\s?({(?:(?!};)[^"]|"([^"]|\\")*")+})\s?;',
|
||||||
|
webpage, 'data_json'), id)['1']['data']['data']
|
||||||
|
manifests = try_get(data_json, lambda x: x['video']['manifests'], expected_type=dict) or {}
|
||||||
|
subtitles = {}
|
||||||
|
formats = [{
|
||||||
|
'url': video['$url'],
|
||||||
|
'ext': 'mp4',
|
||||||
|
'width': try_get(video, lambda x: x['res']['width'], expected_type=int),
|
||||||
|
'height': try_get(video, lambda x: x['res']['height'], expected_type=int),
|
||||||
|
} for video in try_get(data_json, lambda x: x['video']['mp4'], expected_type=list) or [] if video.get('$url')]
|
||||||
|
if manifests.get('hls'):
|
||||||
|
m3u8_frmts, m3u8_subs = self._parse_m3u8_formats_and_subtitles(manifests['hls'], id)
|
||||||
|
formats.extend(m3u8_frmts)
|
||||||
|
subtitles = self._merge_subtitles(subtitles, m3u8_subs)
|
||||||
|
if manifests.get('dash'):
|
||||||
|
dash_frmts, dash_subs = self._parse_mpd_formats_and_subtitles(manifests['dash'], id)
|
||||||
|
formats.extend(dash_frmts)
|
||||||
|
subtitles = self._merge_subtitles(subtitles, dash_subs)
|
||||||
|
self._sort_formats(formats)
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'title': data_json.get('title').strip(),
|
||||||
|
'description': clean_html(dict_get(data_json, ('longDesc', 'shortDesc'))),
|
||||||
|
'thumbnail': data_json.get('poster'),
|
||||||
|
'upload_date': unified_strdate(dict_get(data_json, ('webPublishAt', 'featuredAt', 'diffusionDate'))),
|
||||||
|
'duration': try_get(data_json, lambda x: x['video']['duration'], expected_type=int),
|
||||||
|
'formats': formats,
|
||||||
|
'subtitles': subtitles,
|
||||||
|
}
|
||||||
+25
-21
@@ -1,4 +1,5 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
@@ -41,9 +42,9 @@ class CanvasIE(InfoExtractor):
|
|||||||
_GEO_BYPASS = False
|
_GEO_BYPASS = False
|
||||||
_HLS_ENTRY_PROTOCOLS_MAP = {
|
_HLS_ENTRY_PROTOCOLS_MAP = {
|
||||||
'HLS': 'm3u8_native',
|
'HLS': 'm3u8_native',
|
||||||
'HLS_AES': 'm3u8',
|
'HLS_AES': 'm3u8_native',
|
||||||
}
|
}
|
||||||
_REST_API_BASE = 'https://media-services-public.vrt.be/vualto-video-aggregator-web/rest/external/v1'
|
_REST_API_BASE = 'https://media-services-public.vrt.be/vualto-video-aggregator-web/rest/external/v2'
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = self._match_valid_url(url)
|
mobj = self._match_valid_url(url)
|
||||||
@@ -59,16 +60,21 @@ class CanvasIE(InfoExtractor):
|
|||||||
|
|
||||||
# New API endpoint
|
# New API endpoint
|
||||||
if not data:
|
if not data:
|
||||||
|
vrtnutoken = self._download_json('https://token.vrt.be/refreshtoken',
|
||||||
|
video_id, note='refreshtoken: Retrieve vrtnutoken',
|
||||||
|
errnote='refreshtoken failed')['vrtnutoken']
|
||||||
headers = self.geo_verification_headers()
|
headers = self.geo_verification_headers()
|
||||||
headers.update({'Content-Type': 'application/json'})
|
headers.update({'Content-Type': 'application/json; charset=utf-8'})
|
||||||
token = self._download_json(
|
vrtPlayerToken = self._download_json(
|
||||||
'%s/tokens' % self._REST_API_BASE, video_id,
|
'%s/tokens' % self._REST_API_BASE, video_id,
|
||||||
'Downloading token', data=b'', headers=headers)['vrtPlayerToken']
|
'Downloading token', headers=headers, data=json.dumps({
|
||||||
|
'identityToken': vrtnutoken
|
||||||
|
}).encode('utf-8'))['vrtPlayerToken']
|
||||||
data = self._download_json(
|
data = self._download_json(
|
||||||
'%s/videos/%s' % (self._REST_API_BASE, video_id),
|
'%s/videos/%s' % (self._REST_API_BASE, video_id),
|
||||||
video_id, 'Downloading video JSON', query={
|
video_id, 'Downloading video JSON', query={
|
||||||
'vrtPlayerToken': token,
|
'vrtPlayerToken': vrtPlayerToken,
|
||||||
'client': '%s@PROD' % site_id,
|
'client': 'null',
|
||||||
}, expected_status=400)
|
}, expected_status=400)
|
||||||
if not data.get('title'):
|
if not data.get('title'):
|
||||||
code = data.get('code')
|
code = data.get('code')
|
||||||
@@ -264,7 +270,7 @@ class VrtNUIE(GigyaBaseIE):
|
|||||||
'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_qhEcPa5JGFROVwu5SWKqJ4mVOIkwlFNMSKwzPDAh8QZOtHqu6L4nD5Q7lk0eXOOG'
|
_APIKEY = '3_0Z2HujMtiWq_pkAjgnS2Md2E11a1AwZjYiBETtwNE-EoEHDINgtnvcAOpNgmrVGy'
|
||||||
_CONTEXT_ID = 'R3595707040'
|
_CONTEXT_ID = 'R3595707040'
|
||||||
|
|
||||||
def _real_initialize(self):
|
def _real_initialize(self):
|
||||||
@@ -275,16 +281,13 @@ class VrtNUIE(GigyaBaseIE):
|
|||||||
if username is None:
|
if username is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
auth_info = self._download_json(
|
auth_info = self._gigya_login({
|
||||||
'https://accounts.vrt.be/accounts.login', None,
|
'APIKey': self._APIKEY,
|
||||||
note='Login data', errnote='Could not get Login data',
|
'targetEnv': 'jssdk',
|
||||||
headers={}, data=urlencode_postdata({
|
'loginID': username,
|
||||||
'loginID': username,
|
'password': password,
|
||||||
'password': password,
|
'authMode': 'cookie',
|
||||||
'sessionExpiration': '-2',
|
})
|
||||||
'APIKey': self._APIKEY,
|
|
||||||
'targetEnv': 'jssdk',
|
|
||||||
}))
|
|
||||||
|
|
||||||
if auth_info.get('errorDetails'):
|
if auth_info.get('errorDetails'):
|
||||||
raise ExtractorError('Unable to login: VrtNU said: ' + auth_info.get('errorDetails'), expected=True)
|
raise ExtractorError('Unable to login: VrtNU said: ' + auth_info.get('errorDetails'), expected=True)
|
||||||
@@ -301,14 +304,15 @@ class VrtNUIE(GigyaBaseIE):
|
|||||||
'UID': auth_info['UID'],
|
'UID': auth_info['UID'],
|
||||||
'UIDSignature': auth_info['UIDSignature'],
|
'UIDSignature': auth_info['UIDSignature'],
|
||||||
'signatureTimestamp': auth_info['signatureTimestamp'],
|
'signatureTimestamp': auth_info['signatureTimestamp'],
|
||||||
'client_id': 'vrtnu-site',
|
|
||||||
'_csrf': self._get_cookies('https://login.vrt.be').get('OIDCXSRF').value,
|
'_csrf': self._get_cookies('https://login.vrt.be').get('OIDCXSRF').value,
|
||||||
}
|
}
|
||||||
|
|
||||||
self._request_webpage(
|
self._request_webpage(
|
||||||
'https://login.vrt.be/perform_login',
|
'https://login.vrt.be/perform_login',
|
||||||
None, note='Requesting a token', errnote='Could not get a token',
|
None, note='Performing login', errnote='perform login failed',
|
||||||
headers={}, data=urlencode_postdata(post_data))
|
headers={}, query={
|
||||||
|
'client_id': 'vrtnu-site'
|
||||||
|
}, data=urlencode_postdata(post_data))
|
||||||
|
|
||||||
except ExtractorError as e:
|
except ExtractorError as e:
|
||||||
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 401:
|
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 401:
|
||||||
|
|||||||
+38
-3
@@ -11,11 +11,13 @@ from ..compat import (
|
|||||||
compat_str,
|
compat_str,
|
||||||
)
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
|
int_or_none,
|
||||||
|
join_nonempty,
|
||||||
js_to_json,
|
js_to_json,
|
||||||
smuggle_url,
|
|
||||||
try_get,
|
|
||||||
orderedSet,
|
orderedSet,
|
||||||
|
smuggle_url,
|
||||||
strip_or_none,
|
strip_or_none,
|
||||||
|
try_get,
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -313,6 +315,37 @@ class CBCGemIE(InfoExtractor):
|
|||||||
return
|
return
|
||||||
self._claims_token = self._downloader.cache.load(self._NETRC_MACHINE, 'claims_token')
|
self._claims_token = self._downloader.cache.load(self._NETRC_MACHINE, 'claims_token')
|
||||||
|
|
||||||
|
def _find_secret_formats(self, formats, video_id):
|
||||||
|
""" Find a valid video url and convert it to the secret variant """
|
||||||
|
base_format = next((f for f in formats if f.get('vcodec') != 'none'), None)
|
||||||
|
if not base_format:
|
||||||
|
return
|
||||||
|
|
||||||
|
base_url = re.sub(r'(Manifest\(.*?),filter=[\w-]+(.*?\))', r'\1\2', base_format['url'])
|
||||||
|
url = re.sub(r'(Manifest\(.*?),format=[\w-]+(.*?\))', r'\1\2', base_url)
|
||||||
|
|
||||||
|
secret_xml = self._download_xml(url, video_id, note='Downloading secret XML', fatal=False)
|
||||||
|
if not secret_xml:
|
||||||
|
return
|
||||||
|
|
||||||
|
for child in secret_xml:
|
||||||
|
if child.attrib.get('Type') != 'video':
|
||||||
|
continue
|
||||||
|
for video_quality in child:
|
||||||
|
bitrate = int_or_none(video_quality.attrib.get('Bitrate'))
|
||||||
|
if not bitrate or 'Index' not in video_quality.attrib:
|
||||||
|
continue
|
||||||
|
height = int_or_none(video_quality.attrib.get('MaxHeight'))
|
||||||
|
|
||||||
|
yield {
|
||||||
|
**base_format,
|
||||||
|
'format_id': join_nonempty('sec', height),
|
||||||
|
'url': re.sub(r'(QualityLevels\()\d+(\))', fr'\1{bitrate}\2', base_url),
|
||||||
|
'width': int_or_none(video_quality.attrib.get('MaxWidth')),
|
||||||
|
'tbr': bitrate / 1000.0,
|
||||||
|
'height': height,
|
||||||
|
}
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
video_info = self._download_json('https://services.radio-canada.ca/ott/cbc-api/v2/assets/' + video_id, video_id)
|
video_info = self._download_json('https://services.radio-canada.ca/ott/cbc-api/v2/assets/' + video_id, video_id)
|
||||||
@@ -335,6 +368,7 @@ class CBCGemIE(InfoExtractor):
|
|||||||
|
|
||||||
formats = self._extract_m3u8_formats(m3u8_url, video_id, m3u8_id='hls')
|
formats = self._extract_m3u8_formats(m3u8_url, video_id, m3u8_id='hls')
|
||||||
self._remove_duplicate_formats(formats)
|
self._remove_duplicate_formats(formats)
|
||||||
|
formats.extend(self._find_secret_formats(formats, video_id))
|
||||||
|
|
||||||
for format in formats:
|
for format in formats:
|
||||||
if format.get('vcodec') == 'none':
|
if format.get('vcodec') == 'none':
|
||||||
@@ -390,7 +424,8 @@ class CBCGemPlaylistIE(InfoExtractor):
|
|||||||
show = match.group('show')
|
show = match.group('show')
|
||||||
show_info = self._download_json(self._API_BASE + show, season_id)
|
show_info = self._download_json(self._API_BASE + show, season_id)
|
||||||
season = int(match.group('season'))
|
season = int(match.group('season'))
|
||||||
season_info = try_get(show_info, lambda x: x['seasons'][season - 1])
|
|
||||||
|
season_info = next((s for s in show_info['seasons'] if s.get('season') == season), None)
|
||||||
|
|
||||||
if season_info is None:
|
if season_info is None:
|
||||||
raise ExtractorError(f'Couldn\'t find season {season} of {show}')
|
raise ExtractorError(f'Couldn\'t find season {season} of {show}')
|
||||||
|
|||||||
@@ -12,30 +12,15 @@ from ..utils import (
|
|||||||
ExtractorError,
|
ExtractorError,
|
||||||
float_or_none,
|
float_or_none,
|
||||||
sanitized_Request,
|
sanitized_Request,
|
||||||
unescapeHTML,
|
traverse_obj,
|
||||||
update_url_query,
|
|
||||||
urlencode_postdata,
|
urlencode_postdata,
|
||||||
USER_AGENTS,
|
USER_AGENTS,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CeskaTelevizeIE(InfoExtractor):
|
class CeskaTelevizeIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?ceskatelevize\.cz/ivysilani/(?:[^/?#&]+/)*(?P<id>[^/#?]+)'
|
_VALID_URL = r'https?://(?:www\.)?ceskatelevize\.cz/(?:ivysilani|porady)/(?:[^/?#&]+/)*(?P<id>[^/#?]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.ceskatelevize.cz/ivysilani/ivysilani/10441294653-hyde-park-civilizace/214411058091220',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '61924494877246241',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Hyde Park Civilizace: Život v Grónsku',
|
|
||||||
'description': 'md5:3fec8f6bb497be5cdb0c9e8781076626',
|
|
||||||
'thumbnail': r're:^https?://.*\.jpg',
|
|
||||||
'duration': 3350,
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
# m3u8 download
|
|
||||||
'skip_download': True,
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'url': 'http://www.ceskatelevize.cz/ivysilani/10441294653-hyde-park-civilizace/215411058090502/bonus/20641-bonus-01-en',
|
'url': 'http://www.ceskatelevize.cz/ivysilani/10441294653-hyde-park-civilizace/215411058090502/bonus/20641-bonus-01-en',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '61924494877028507',
|
'id': '61924494877028507',
|
||||||
@@ -66,12 +51,60 @@ class CeskaTelevizeIE(InfoExtractor):
|
|||||||
}, {
|
}, {
|
||||||
'url': 'http://www.ceskatelevize.cz/ivysilani/embed/iFramePlayer.php?hash=d6a3e1370d2e4fa76296b90bad4dfc19673b641e&IDEC=217 562 22150/0004&channelID=1&width=100%25',
|
'url': 'http://www.ceskatelevize.cz/ivysilani/embed/iFramePlayer.php?hash=d6a3e1370d2e4fa76296b90bad4dfc19673b641e&IDEC=217 562 22150/0004&channelID=1&width=100%25',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
# video with 18+ caution trailer
|
||||||
|
'url': 'http://www.ceskatelevize.cz/porady/10520528904-queer/215562210900007-bogotart/',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '215562210900007-bogotart',
|
||||||
|
'title': 'Queer: Bogotart',
|
||||||
|
'description': 'Hlavní město Kolumbie v doprovodu queer umělců. Vroucí svět plný vášně, sebevědomí, ale i násilí a bolesti. Připravil Peter Serge Butko',
|
||||||
|
},
|
||||||
|
'playlist': [{
|
||||||
|
'info_dict': {
|
||||||
|
'id': '61924494877311053',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Queer: Bogotart (Varování 18+)',
|
||||||
|
'duration': 11.9,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'info_dict': {
|
||||||
|
'id': '61924494877068022',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Queer: Bogotart (Queer)',
|
||||||
|
'thumbnail': r're:^https?://.*\.jpg',
|
||||||
|
'duration': 1558.3,
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
'params': {
|
||||||
|
# m3u8 download
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
# iframe embed
|
||||||
|
'url': 'http://www.ceskatelevize.cz/porady/10614999031-neviditelni/21251212048/',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
playlist_id = self._match_id(url)
|
playlist_id = self._match_id(url)
|
||||||
|
parsed_url = compat_urllib_parse_urlparse(url)
|
||||||
webpage = self._download_webpage(url, playlist_id)
|
webpage = self._download_webpage(url, playlist_id)
|
||||||
|
site_name = self._og_search_property('site_name', webpage, fatal=False, default=None)
|
||||||
|
playlist_title = self._og_search_title(webpage, default=None)
|
||||||
|
if site_name and playlist_title:
|
||||||
|
playlist_title = playlist_title.replace(f' — {site_name}', '', 1)
|
||||||
|
playlist_description = self._og_search_description(webpage, default=None)
|
||||||
|
if playlist_description:
|
||||||
|
playlist_description = playlist_description.replace('\xa0', ' ')
|
||||||
|
|
||||||
|
if parsed_url.path.startswith('/porady/'):
|
||||||
|
next_data = self._search_nextjs_data(webpage, playlist_id)
|
||||||
|
idec = traverse_obj(next_data, ('props', 'pageProps', 'data', ('show', 'mediaMeta'), 'idec'), get_all=False)
|
||||||
|
if not idec:
|
||||||
|
raise ExtractorError('Failed to find IDEC id')
|
||||||
|
iframe_hash = self._download_webpage('https://www.ceskatelevize.cz/v-api/iframe-hash/', playlist_id)
|
||||||
|
webpage = self._download_webpage('https://www.ceskatelevize.cz/ivysilani/embed/iFramePlayer.php', playlist_id,
|
||||||
|
query={'hash': iframe_hash, 'origin': 'iVysilani', 'autoStart': 'true', 'IDEC': idec})
|
||||||
|
|
||||||
NOT_AVAILABLE_STRING = 'This content is not available at your territory due to limited copyright.'
|
NOT_AVAILABLE_STRING = 'This content is not available at your territory due to limited copyright.'
|
||||||
if '%s</p>' % NOT_AVAILABLE_STRING in webpage:
|
if '%s</p>' % NOT_AVAILABLE_STRING in webpage:
|
||||||
@@ -100,7 +133,7 @@ class CeskaTelevizeIE(InfoExtractor):
|
|||||||
data = {
|
data = {
|
||||||
'playlist[0][type]': type_,
|
'playlist[0][type]': type_,
|
||||||
'playlist[0][id]': episode_id,
|
'playlist[0][id]': episode_id,
|
||||||
'requestUrl': compat_urllib_parse_urlparse(url).path,
|
'requestUrl': parsed_url.path,
|
||||||
'requestSource': 'iVysilani',
|
'requestSource': 'iVysilani',
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +141,7 @@ class CeskaTelevizeIE(InfoExtractor):
|
|||||||
|
|
||||||
for user_agent in (None, USER_AGENTS['Safari']):
|
for user_agent in (None, USER_AGENTS['Safari']):
|
||||||
req = sanitized_Request(
|
req = sanitized_Request(
|
||||||
'https://www.ceskatelevize.cz/ivysilani/ajax/get-client-playlist',
|
'https://www.ceskatelevize.cz/ivysilani/ajax/get-client-playlist/',
|
||||||
data=urlencode_postdata(data))
|
data=urlencode_postdata(data))
|
||||||
|
|
||||||
req.add_header('Content-type', 'application/x-www-form-urlencoded')
|
req.add_header('Content-type', 'application/x-www-form-urlencoded')
|
||||||
@@ -130,9 +163,6 @@ class CeskaTelevizeIE(InfoExtractor):
|
|||||||
req = sanitized_Request(compat_urllib_parse_unquote(playlist_url))
|
req = sanitized_Request(compat_urllib_parse_unquote(playlist_url))
|
||||||
req.add_header('Referer', url)
|
req.add_header('Referer', url)
|
||||||
|
|
||||||
playlist_title = self._og_search_title(webpage, default=None)
|
|
||||||
playlist_description = self._og_search_description(webpage, default=None)
|
|
||||||
|
|
||||||
playlist = self._download_json(req, playlist_id, fatal=False)
|
playlist = self._download_json(req, playlist_id, fatal=False)
|
||||||
if not playlist:
|
if not playlist:
|
||||||
continue
|
continue
|
||||||
@@ -182,8 +212,6 @@ class CeskaTelevizeIE(InfoExtractor):
|
|||||||
|
|
||||||
if playlist_len == 1:
|
if playlist_len == 1:
|
||||||
final_title = playlist_title or title
|
final_title = playlist_title or title
|
||||||
if is_live:
|
|
||||||
final_title = self._live_title(final_title)
|
|
||||||
else:
|
else:
|
||||||
final_title = '%s (%s)' % (playlist_title, title)
|
final_title = '%s (%s)' % (playlist_title, title)
|
||||||
|
|
||||||
@@ -237,54 +265,3 @@ class CeskaTelevizeIE(InfoExtractor):
|
|||||||
yield line
|
yield line
|
||||||
|
|
||||||
return '\r\n'.join(_fix_subtitle(subtitles))
|
return '\r\n'.join(_fix_subtitle(subtitles))
|
||||||
|
|
||||||
|
|
||||||
class CeskaTelevizePoradyIE(InfoExtractor):
|
|
||||||
_VALID_URL = r'https?://(?:www\.)?ceskatelevize\.cz/porady/(?:[^/?#&]+/)*(?P<id>[^/#?]+)'
|
|
||||||
_TESTS = [{
|
|
||||||
# video with 18+ caution trailer
|
|
||||||
'url': 'http://www.ceskatelevize.cz/porady/10520528904-queer/215562210900007-bogotart/',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '215562210900007-bogotart',
|
|
||||||
'title': 'Queer: Bogotart',
|
|
||||||
'description': 'Alternativní průvodce současným queer světem',
|
|
||||||
},
|
|
||||||
'playlist': [{
|
|
||||||
'info_dict': {
|
|
||||||
'id': '61924494876844842',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Queer: Bogotart (Varování 18+)',
|
|
||||||
'duration': 10.2,
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'info_dict': {
|
|
||||||
'id': '61924494877068022',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Queer: Bogotart (Queer)',
|
|
||||||
'thumbnail': r're:^https?://.*\.jpg',
|
|
||||||
'duration': 1558.3,
|
|
||||||
},
|
|
||||||
}],
|
|
||||||
'params': {
|
|
||||||
# m3u8 download
|
|
||||||
'skip_download': True,
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
# iframe embed
|
|
||||||
'url': 'http://www.ceskatelevize.cz/porady/10614999031-neviditelni/21251212048/',
|
|
||||||
'only_matching': True,
|
|
||||||
}]
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
video_id = self._match_id(url)
|
|
||||||
|
|
||||||
webpage = self._download_webpage(url, video_id)
|
|
||||||
|
|
||||||
data_url = update_url_query(unescapeHTML(self._search_regex(
|
|
||||||
(r'<span[^>]*\bdata-url=(["\'])(?P<url>(?:(?!\1).)+)\1',
|
|
||||||
r'<iframe[^>]+\bsrc=(["\'])(?P<url>(?:https?:)?//(?:www\.)?ceskatelevize\.cz/ivysilani/embed/iFramePlayer\.php.*?)\1'),
|
|
||||||
webpage, 'iframe player url', group='url')), query={
|
|
||||||
'autoStart': 'true',
|
|
||||||
})
|
|
||||||
|
|
||||||
return self.url_result(data_url, ie=CeskaTelevizeIE.ie_key())
|
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ class ChaturbateIE(InfoExtractor):
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'title': self._live_title(video_id),
|
'title': video_id,
|
||||||
'thumbnail': 'https://roomimg.stream.highwebmedia.com/ri/%s.jpg' % video_id,
|
'thumbnail': 'https://roomimg.stream.highwebmedia.com/ri/%s.jpg' % video_id,
|
||||||
'age_limit': self._rta_search(webpage),
|
'age_limit': self._rta_search(webpage),
|
||||||
'is_live': True,
|
'is_live': True,
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class ChingariBaseIE(InfoExtractor):
|
|||||||
|
|
||||||
|
|
||||||
class ChingariIE(ChingariBaseIE):
|
class ChingariIE(ChingariBaseIE):
|
||||||
_VALID_URL = r'(?:https?://)(?:www\.)?chingari\.io/share/post\?id=(?P<id>[^&/#?]+)'
|
_VALID_URL = r'https?://(?:www\.)?chingari\.io/share/post\?id=(?P<id>[^&/#?]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://chingari.io/share/post?id=612f8f4ce1dc57090e8a7beb',
|
'url': 'https://chingari.io/share/post?id=612f8f4ce1dc57090e8a7beb',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@@ -102,7 +102,7 @@ class ChingariIE(ChingariBaseIE):
|
|||||||
|
|
||||||
|
|
||||||
class ChingariUserIE(ChingariBaseIE):
|
class ChingariUserIE(ChingariBaseIE):
|
||||||
_VALID_URL = r'(?:https?://)(?:www\.)?chingari\.io/(?!share/post)(?P<id>[^/?]+)'
|
_VALID_URL = r'https?://(?:www\.)?chingari\.io/(?!share/post)(?P<id>[^/?]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://chingari.io/dada1023',
|
'url': 'https://chingari.io/dada1023',
|
||||||
'playlist_mincount': 3,
|
'playlist_mincount': 3,
|
||||||
|
|||||||
+130
-76
@@ -2,7 +2,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
import datetime
|
import collections
|
||||||
import hashlib
|
import hashlib
|
||||||
import itertools
|
import itertools
|
||||||
import json
|
import json
|
||||||
@@ -54,6 +54,7 @@ from ..utils import (
|
|||||||
GeoRestrictedError,
|
GeoRestrictedError,
|
||||||
GeoUtils,
|
GeoUtils,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
|
join_nonempty,
|
||||||
js_to_json,
|
js_to_json,
|
||||||
JSON_LD_RE,
|
JSON_LD_RE,
|
||||||
mimetype2ext,
|
mimetype2ext,
|
||||||
@@ -74,6 +75,7 @@ from ..utils import (
|
|||||||
strip_or_none,
|
strip_or_none,
|
||||||
traverse_obj,
|
traverse_obj,
|
||||||
unescapeHTML,
|
unescapeHTML,
|
||||||
|
UnsupportedError,
|
||||||
unified_strdate,
|
unified_strdate,
|
||||||
unified_timestamp,
|
unified_timestamp,
|
||||||
update_Request,
|
update_Request,
|
||||||
@@ -161,9 +163,8 @@ class InfoExtractor(object):
|
|||||||
* filesize_approx An estimate for the number of bytes
|
* filesize_approx An estimate for the number of bytes
|
||||||
* player_url SWF Player URL (used for rtmpdump).
|
* player_url SWF Player URL (used for rtmpdump).
|
||||||
* protocol The protocol that will be used for the actual
|
* protocol The protocol that will be used for the actual
|
||||||
download, lower-case.
|
download, lower-case. One of "http", "https" or
|
||||||
"http", "https", "rtsp", "rtmp", "rtmp_ffmpeg", "rtmpe",
|
one of the protocols defined in downloader.PROTOCOL_MAP
|
||||||
"m3u8", "m3u8_native" or "http_dash_segments".
|
|
||||||
* fragment_base_url
|
* fragment_base_url
|
||||||
Base URL for fragments. Each fragment's path
|
Base URL for fragments. Each fragment's path
|
||||||
value (if present) will be relative to
|
value (if present) will be relative to
|
||||||
@@ -179,6 +180,8 @@ class InfoExtractor(object):
|
|||||||
fragment_base_url
|
fragment_base_url
|
||||||
* "duration" (optional, int or float)
|
* "duration" (optional, int or float)
|
||||||
* "filesize" (optional, int)
|
* "filesize" (optional, int)
|
||||||
|
* is_from_start Is a live format that can be downloaded
|
||||||
|
from the start. Boolean
|
||||||
* preference Order number of this format. If this field is
|
* preference Order number of this format. If this field is
|
||||||
present and not None, the formats get sorted
|
present and not None, the formats get sorted
|
||||||
by this field, regardless of all other values.
|
by this field, regardless of all other values.
|
||||||
@@ -340,6 +343,7 @@ class InfoExtractor(object):
|
|||||||
series, programme or podcast:
|
series, programme or podcast:
|
||||||
|
|
||||||
series: Title of the series or programme the video episode belongs to.
|
series: Title of the series or programme the video episode belongs to.
|
||||||
|
series_id: Id of the series or programme the video episode belongs to, as a unicode string.
|
||||||
season: Title of the season the video episode belongs to.
|
season: Title of the season the video episode belongs to.
|
||||||
season_number: Number of the season the video episode belongs to, as an integer.
|
season_number: Number of the season the video episode belongs to, as an integer.
|
||||||
season_id: Id of the season the video episode belongs to, as a unicode string.
|
season_id: Id of the season the video episode belongs to, as a unicode string.
|
||||||
@@ -440,11 +444,11 @@ class InfoExtractor(object):
|
|||||||
_WORKING = True
|
_WORKING = True
|
||||||
|
|
||||||
_LOGIN_HINTS = {
|
_LOGIN_HINTS = {
|
||||||
'any': 'Use --cookies, --username and --password or --netrc to provide account credentials',
|
'any': 'Use --cookies, --username and --password, or --netrc to provide account credentials',
|
||||||
'cookies': (
|
'cookies': (
|
||||||
'Use --cookies-from-browser or --cookies for the authentication. '
|
'Use --cookies-from-browser or --cookies for the authentication. '
|
||||||
'See https://github.com/ytdl-org/youtube-dl#how-do-i-pass-cookies-to-youtube-dl for how to manually pass cookies'),
|
'See https://github.com/ytdl-org/youtube-dl#how-do-i-pass-cookies-to-youtube-dl for how to manually pass cookies'),
|
||||||
'password': 'Use --username and --password or --netrc to provide account credentials',
|
'password': 'Use --username and --password, or --netrc to provide account credentials',
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, downloader=None):
|
def __init__(self, downloader=None):
|
||||||
@@ -462,6 +466,8 @@ class InfoExtractor(object):
|
|||||||
# we have cached the regexp for *this* class, whereas getattr would also
|
# we have cached the regexp for *this* class, whereas getattr would also
|
||||||
# match the superclass
|
# match the superclass
|
||||||
if '_VALID_URL_RE' not in cls.__dict__:
|
if '_VALID_URL_RE' not in cls.__dict__:
|
||||||
|
if '_VALID_URL' not in cls.__dict__:
|
||||||
|
cls._VALID_URL = cls._make_valid_url()
|
||||||
cls._VALID_URL_RE = re.compile(cls._VALID_URL)
|
cls._VALID_URL_RE = re.compile(cls._VALID_URL)
|
||||||
return cls._VALID_URL_RE.match(url)
|
return cls._VALID_URL_RE.match(url)
|
||||||
|
|
||||||
@@ -604,10 +610,19 @@ class InfoExtractor(object):
|
|||||||
if self.__maybe_fake_ip_and_retry(e.countries):
|
if self.__maybe_fake_ip_and_retry(e.countries):
|
||||||
continue
|
continue
|
||||||
raise
|
raise
|
||||||
|
except UnsupportedError:
|
||||||
|
raise
|
||||||
except ExtractorError as e:
|
except ExtractorError as e:
|
||||||
video_id = e.video_id or self.get_temp_id(url)
|
kwargs = {
|
||||||
raise ExtractorError(
|
'video_id': e.video_id or self.get_temp_id(url),
|
||||||
e.msg, video_id=video_id, ie=self.IE_NAME, tb=e.traceback, expected=e.expected, cause=e.cause)
|
'ie': self.IE_NAME,
|
||||||
|
'tb': e.traceback or sys.exc_info()[2],
|
||||||
|
'expected': e.expected,
|
||||||
|
'cause': e.cause
|
||||||
|
}
|
||||||
|
if hasattr(e, 'countries'):
|
||||||
|
kwargs['countries'] = e.countries
|
||||||
|
raise type(e)(e.msg, **kwargs)
|
||||||
except compat_http_client.IncompleteRead as e:
|
except compat_http_client.IncompleteRead as e:
|
||||||
raise ExtractorError('A network error has occurred.', cause=e, expected=True, video_id=self.get_temp_id(url))
|
raise ExtractorError('A network error has occurred.', cause=e, expected=True, video_id=self.get_temp_id(url))
|
||||||
except (KeyError, StopIteration) as e:
|
except (KeyError, StopIteration) as e:
|
||||||
@@ -1066,7 +1081,8 @@ class InfoExtractor(object):
|
|||||||
def raise_login_required(
|
def raise_login_required(
|
||||||
self, msg='This video is only available for registered users',
|
self, msg='This video is only available for registered users',
|
||||||
metadata_available=False, method='any'):
|
metadata_available=False, method='any'):
|
||||||
if metadata_available and self.get_param('ignore_no_formats_error'):
|
if metadata_available and (
|
||||||
|
self.get_param('ignore_no_formats_error') or self.get_param('wait_for_video')):
|
||||||
self.report_warning(msg)
|
self.report_warning(msg)
|
||||||
if method is not None:
|
if method is not None:
|
||||||
msg = '%s. %s' % (msg, self._LOGIN_HINTS[method])
|
msg = '%s. %s' % (msg, self._LOGIN_HINTS[method])
|
||||||
@@ -1075,13 +1091,15 @@ class InfoExtractor(object):
|
|||||||
def raise_geo_restricted(
|
def raise_geo_restricted(
|
||||||
self, msg='This video is not available from your location due to geo restriction',
|
self, msg='This video is not available from your location due to geo restriction',
|
||||||
countries=None, metadata_available=False):
|
countries=None, metadata_available=False):
|
||||||
if metadata_available and self.get_param('ignore_no_formats_error'):
|
if metadata_available and (
|
||||||
|
self.get_param('ignore_no_formats_error') or self.get_param('wait_for_video')):
|
||||||
self.report_warning(msg)
|
self.report_warning(msg)
|
||||||
else:
|
else:
|
||||||
raise GeoRestrictedError(msg, countries=countries)
|
raise GeoRestrictedError(msg, countries=countries)
|
||||||
|
|
||||||
def raise_no_formats(self, msg, expected=False, video_id=None):
|
def raise_no_formats(self, msg, expected=False, video_id=None):
|
||||||
if expected and self.get_param('ignore_no_formats_error'):
|
if expected and (
|
||||||
|
self.get_param('ignore_no_formats_error') or self.get_param('wait_for_video')):
|
||||||
self.report_warning(msg, video_id)
|
self.report_warning(msg, video_id)
|
||||||
elif isinstance(msg, ExtractorError):
|
elif isinstance(msg, ExtractorError):
|
||||||
raise msg
|
raise msg
|
||||||
@@ -1139,7 +1157,7 @@ class InfoExtractor(object):
|
|||||||
if mobj:
|
if mobj:
|
||||||
break
|
break
|
||||||
|
|
||||||
_name = self._downloader._color_text(name, 'blue')
|
_name = self._downloader._format_err(name, self._downloader.Styles.EMPHASIS)
|
||||||
|
|
||||||
if mobj:
|
if mobj:
|
||||||
if group is None:
|
if group is None:
|
||||||
@@ -1434,11 +1452,19 @@ class InfoExtractor(object):
|
|||||||
})
|
})
|
||||||
extract_interaction_statistic(e)
|
extract_interaction_statistic(e)
|
||||||
|
|
||||||
for e in json_ld:
|
def traverse_json_ld(json_ld, at_top_level=True):
|
||||||
if '@context' in e:
|
for e in json_ld:
|
||||||
|
if at_top_level and '@context' not in e:
|
||||||
|
continue
|
||||||
|
if at_top_level and set(e.keys()) == {'@context', '@graph'}:
|
||||||
|
traverse_json_ld(variadic(e['@graph'], allowed_types=(dict,)), at_top_level=False)
|
||||||
|
break
|
||||||
item_type = e.get('@type')
|
item_type = e.get('@type')
|
||||||
if expected_type is not None and expected_type != item_type:
|
if expected_type is not None and expected_type != item_type:
|
||||||
continue
|
continue
|
||||||
|
rating = traverse_obj(e, ('aggregateRating', 'ratingValue'), expected_type=float_or_none)
|
||||||
|
if rating is not None:
|
||||||
|
info['average_rating'] = rating
|
||||||
if item_type in ('TVEpisode', 'Episode'):
|
if item_type in ('TVEpisode', 'Episode'):
|
||||||
episode_name = unescapeHTML(e.get('name'))
|
episode_name = unescapeHTML(e.get('name'))
|
||||||
info.update({
|
info.update({
|
||||||
@@ -1468,7 +1494,7 @@ class InfoExtractor(object):
|
|||||||
info.update({
|
info.update({
|
||||||
'timestamp': parse_iso8601(e.get('datePublished')),
|
'timestamp': parse_iso8601(e.get('datePublished')),
|
||||||
'title': unescapeHTML(e.get('headline')),
|
'title': unescapeHTML(e.get('headline')),
|
||||||
'description': unescapeHTML(e.get('articleBody')),
|
'description': unescapeHTML(e.get('articleBody') or e.get('description')),
|
||||||
})
|
})
|
||||||
elif item_type == 'VideoObject':
|
elif item_type == 'VideoObject':
|
||||||
extract_video_object(e)
|
extract_video_object(e)
|
||||||
@@ -1483,8 +1509,35 @@ class InfoExtractor(object):
|
|||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
|
traverse_json_ld(json_ld)
|
||||||
|
|
||||||
return dict((k, v) for k, v in info.items() if v is not None)
|
return dict((k, v) for k, v in info.items() if v is not None)
|
||||||
|
|
||||||
|
def _search_nextjs_data(self, webpage, video_id, **kw):
|
||||||
|
return self._parse_json(
|
||||||
|
self._search_regex(
|
||||||
|
r'(?s)<script[^>]+id=[\'"]__NEXT_DATA__[\'"][^>]*>([^<]+)</script>',
|
||||||
|
webpage, 'next.js data', **kw),
|
||||||
|
video_id, **kw)
|
||||||
|
|
||||||
|
def _search_nuxt_data(self, webpage, video_id, context_name='__NUXT__'):
|
||||||
|
''' Parses Nuxt.js metadata. This works as long as the function __NUXT__ invokes is a pure function. '''
|
||||||
|
# not all website do this, but it can be changed
|
||||||
|
# https://stackoverflow.com/questions/67463109/how-to-change-or-hide-nuxt-and-nuxt-keyword-in-page-source
|
||||||
|
rectx = re.escape(context_name)
|
||||||
|
js, arg_keys, arg_vals = self._search_regex(
|
||||||
|
(r'<script>window\.%s=\(function\((?P<arg_keys>.*?)\)\{return\s(?P<js>\{.*?\})\}\((?P<arg_vals>.+?)\)\);?</script>' % rectx,
|
||||||
|
r'%s\(.*?\(function\((?P<arg_keys>.*?)\)\{return\s(?P<js>\{.*?\})\}\((?P<arg_vals>.*?)\)' % rectx),
|
||||||
|
webpage, context_name, group=['js', 'arg_keys', 'arg_vals'])
|
||||||
|
|
||||||
|
args = dict(zip(arg_keys.split(','), arg_vals.split(',')))
|
||||||
|
|
||||||
|
for key, val in args.items():
|
||||||
|
if val in ('undefined', 'void 0'):
|
||||||
|
args[key] = 'null'
|
||||||
|
|
||||||
|
return self._parse_json(js_to_json(js, args), video_id)['data'][0]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _hidden_inputs(html):
|
def _hidden_inputs(html):
|
||||||
html = re.sub(r'<!--(?:(?!<!--).)*-->', '', html)
|
html = re.sub(r'<!--(?:(?!<!--).)*-->', '', html)
|
||||||
@@ -1512,20 +1565,20 @@ class InfoExtractor(object):
|
|||||||
|
|
||||||
default = ('hidden', 'aud_or_vid', 'hasvid', 'ie_pref', 'lang', 'quality',
|
default = ('hidden', 'aud_or_vid', 'hasvid', 'ie_pref', 'lang', 'quality',
|
||||||
'res', 'fps', 'hdr:12', 'codec:vp9.2', 'size', 'br', 'asr',
|
'res', 'fps', 'hdr:12', 'codec:vp9.2', 'size', 'br', 'asr',
|
||||||
'proto', 'ext', 'hasaud', 'source', 'format_id') # These must not be aliases
|
'proto', 'ext', 'hasaud', 'source', 'id') # These must not be aliases
|
||||||
ytdl_default = ('hasaud', 'lang', 'quality', 'tbr', 'filesize', 'vbr',
|
ytdl_default = ('hasaud', 'lang', 'quality', 'tbr', 'filesize', 'vbr',
|
||||||
'height', 'width', 'proto', 'vext', 'abr', 'aext',
|
'height', 'width', 'proto', 'vext', 'abr', 'aext',
|
||||||
'fps', 'fs_approx', 'source', 'format_id')
|
'fps', 'fs_approx', 'source', 'id')
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
'vcodec': {'type': 'ordered', 'regex': True,
|
'vcodec': {'type': 'ordered', 'regex': True,
|
||||||
'order': ['av0?1', 'vp0?9.2', 'vp0?9', '[hx]265|he?vc?', '[hx]264|avc', 'vp0?8', 'mp4v|h263', 'theora', '', None, 'none']},
|
'order': ['av0?1', 'vp0?9.2', 'vp0?9', '[hx]265|he?vc?', '[hx]264|avc', 'vp0?8', 'mp4v|h263', 'theora', '', None, 'none']},
|
||||||
'acodec': {'type': 'ordered', 'regex': True,
|
'acodec': {'type': 'ordered', 'regex': True,
|
||||||
'order': ['opus', 'vorbis', 'aac', 'mp?4a?', 'mp3', 'e?a?c-?3', 'dts', '', None, 'none']},
|
'order': ['[af]lac', 'wav|aiff', 'opus', 'vorbis', 'aac', 'mp?4a?', 'mp3', 'e-?a?c-?3', 'ac-?3', 'dts', '', None, 'none']},
|
||||||
'hdr': {'type': 'ordered', 'regex': True, 'field': 'dynamic_range',
|
'hdr': {'type': 'ordered', 'regex': True, 'field': 'dynamic_range',
|
||||||
'order': ['dv', '(hdr)?12', r'(hdr)?10\+', '(hdr)?10', 'hlg', '', 'sdr', None]},
|
'order': ['dv', '(hdr)?12', r'(hdr)?10\+', '(hdr)?10', 'hlg', '', 'sdr', None]},
|
||||||
'proto': {'type': 'ordered', 'regex': True, 'field': 'protocol',
|
'proto': {'type': 'ordered', 'regex': True, 'field': 'protocol',
|
||||||
'order': ['(ht|f)tps', '(ht|f)tp$', 'm3u8.+', '.*dash', 'ws|websocket', '', 'mms|rtsp', 'none', 'f4']},
|
'order': ['(ht|f)tps', '(ht|f)tp$', 'm3u8.*', '.*dash', 'websocket_frag', 'rtmpe?', '', 'mms|rtsp', 'ws|websocket', 'f4']},
|
||||||
'vext': {'type': 'ordered', 'field': 'video_ext',
|
'vext': {'type': 'ordered', 'field': 'video_ext',
|
||||||
'order': ('mp4', 'webm', 'flv', '', 'none'),
|
'order': ('mp4', 'webm', 'flv', '', 'none'),
|
||||||
'order_free': ('webm', 'mp4', 'flv', '', 'none')},
|
'order_free': ('webm', 'mp4', 'flv', '', 'none')},
|
||||||
@@ -1539,8 +1592,8 @@ class InfoExtractor(object):
|
|||||||
'ie_pref': {'priority': True, 'type': 'extractor'},
|
'ie_pref': {'priority': True, 'type': 'extractor'},
|
||||||
'hasvid': {'priority': True, 'field': 'vcodec', 'type': 'boolean', 'not_in_list': ('none',)},
|
'hasvid': {'priority': True, 'field': 'vcodec', 'type': 'boolean', 'not_in_list': ('none',)},
|
||||||
'hasaud': {'field': 'acodec', 'type': 'boolean', 'not_in_list': ('none',)},
|
'hasaud': {'field': 'acodec', 'type': 'boolean', 'not_in_list': ('none',)},
|
||||||
'lang': {'convert': 'ignore', 'field': 'language_preference'},
|
'lang': {'convert': 'float', 'field': 'language_preference', 'default': -1},
|
||||||
'quality': {'convert': 'float_none', 'default': -1},
|
'quality': {'convert': 'float', 'default': -1},
|
||||||
'filesize': {'convert': 'bytes'},
|
'filesize': {'convert': 'bytes'},
|
||||||
'fs_approx': {'convert': 'bytes', 'field': 'filesize_approx'},
|
'fs_approx': {'convert': 'bytes', 'field': 'filesize_approx'},
|
||||||
'id': {'convert': 'string', 'field': 'format_id'},
|
'id': {'convert': 'string', 'field': 'format_id'},
|
||||||
@@ -1551,7 +1604,7 @@ class InfoExtractor(object):
|
|||||||
'vbr': {'convert': 'float_none'},
|
'vbr': {'convert': 'float_none'},
|
||||||
'abr': {'convert': 'float_none'},
|
'abr': {'convert': 'float_none'},
|
||||||
'asr': {'convert': 'float_none'},
|
'asr': {'convert': 'float_none'},
|
||||||
'source': {'convert': 'ignore', 'field': 'source_preference'},
|
'source': {'convert': 'float', 'field': 'source_preference', 'default': -1},
|
||||||
|
|
||||||
'codec': {'type': 'combined', 'field': ('vcodec', 'acodec')},
|
'codec': {'type': 'combined', 'field': ('vcodec', 'acodec')},
|
||||||
'br': {'type': 'combined', 'field': ('tbr', 'vbr', 'abr'), 'same_limit': True},
|
'br': {'type': 'combined', 'field': ('tbr', 'vbr', 'abr'), 'same_limit': True},
|
||||||
@@ -1560,7 +1613,12 @@ class InfoExtractor(object):
|
|||||||
'res': {'type': 'multiple', 'field': ('height', 'width'),
|
'res': {'type': 'multiple', 'field': ('height', 'width'),
|
||||||
'function': lambda it: (lambda l: min(l) if l else 0)(tuple(filter(None, it)))},
|
'function': lambda it: (lambda l: min(l) if l else 0)(tuple(filter(None, it)))},
|
||||||
|
|
||||||
# Most of these exist only for compatibility reasons
|
# For compatibility with youtube-dl
|
||||||
|
'format_id': {'type': 'alias', 'field': 'id'},
|
||||||
|
'preference': {'type': 'alias', 'field': 'ie_pref'},
|
||||||
|
'language_preference': {'type': 'alias', 'field': 'lang'},
|
||||||
|
|
||||||
|
# Deprecated
|
||||||
'dimension': {'type': 'alias', 'field': 'res'},
|
'dimension': {'type': 'alias', 'field': 'res'},
|
||||||
'resolution': {'type': 'alias', 'field': 'res'},
|
'resolution': {'type': 'alias', 'field': 'res'},
|
||||||
'extension': {'type': 'alias', 'field': 'ext'},
|
'extension': {'type': 'alias', 'field': 'ext'},
|
||||||
@@ -1569,7 +1627,6 @@ class InfoExtractor(object):
|
|||||||
'video_bitrate': {'type': 'alias', 'field': 'vbr'},
|
'video_bitrate': {'type': 'alias', 'field': 'vbr'},
|
||||||
'audio_bitrate': {'type': 'alias', 'field': 'abr'},
|
'audio_bitrate': {'type': 'alias', 'field': 'abr'},
|
||||||
'framerate': {'type': 'alias', 'field': 'fps'},
|
'framerate': {'type': 'alias', 'field': 'fps'},
|
||||||
'language_preference': {'type': 'alias', 'field': 'lang'}, # not named as 'language' because such a field exists
|
|
||||||
'protocol': {'type': 'alias', 'field': 'proto'},
|
'protocol': {'type': 'alias', 'field': 'proto'},
|
||||||
'source_preference': {'type': 'alias', 'field': 'source'},
|
'source_preference': {'type': 'alias', 'field': 'source'},
|
||||||
'filesize_approx': {'type': 'alias', 'field': 'fs_approx'},
|
'filesize_approx': {'type': 'alias', 'field': 'fs_approx'},
|
||||||
@@ -1584,15 +1641,23 @@ class InfoExtractor(object):
|
|||||||
'audio': {'type': 'alias', 'field': 'hasaud'},
|
'audio': {'type': 'alias', 'field': 'hasaud'},
|
||||||
'has_audio': {'type': 'alias', 'field': 'hasaud'},
|
'has_audio': {'type': 'alias', 'field': 'hasaud'},
|
||||||
'extractor': {'type': 'alias', 'field': 'ie_pref'},
|
'extractor': {'type': 'alias', 'field': 'ie_pref'},
|
||||||
'preference': {'type': 'alias', 'field': 'ie_pref'},
|
|
||||||
'extractor_preference': {'type': 'alias', 'field': 'ie_pref'},
|
'extractor_preference': {'type': 'alias', 'field': 'ie_pref'},
|
||||||
'format_id': {'type': 'alias', 'field': 'id'},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_order = []
|
def __init__(self, ie, field_preference):
|
||||||
|
self._order = []
|
||||||
|
self.ydl = ie._downloader
|
||||||
|
self.evaluate_params(self.ydl.params, field_preference)
|
||||||
|
if ie.get_param('verbose'):
|
||||||
|
self.print_verbose_info(self.ydl.write_debug)
|
||||||
|
|
||||||
def _get_field_setting(self, field, key):
|
def _get_field_setting(self, field, key):
|
||||||
if field not in self.settings:
|
if field not in self.settings:
|
||||||
|
if key in ('forced', 'priority'):
|
||||||
|
return False
|
||||||
|
self.ydl.deprecation_warning(
|
||||||
|
f'Using arbitrary fields ({field}) for format sorting is deprecated '
|
||||||
|
'and may be removed in a future version')
|
||||||
self.settings[field] = {}
|
self.settings[field] = {}
|
||||||
propObj = self.settings[field]
|
propObj = self.settings[field]
|
||||||
if key not in propObj:
|
if key not in propObj:
|
||||||
@@ -1675,7 +1740,11 @@ class InfoExtractor(object):
|
|||||||
if field is None:
|
if field is None:
|
||||||
continue
|
continue
|
||||||
if self._get_field_setting(field, 'type') == 'alias':
|
if self._get_field_setting(field, 'type') == 'alias':
|
||||||
field = self._get_field_setting(field, 'field')
|
alias, field = field, self._get_field_setting(field, 'field')
|
||||||
|
if alias not in ('format_id', 'preference', 'language_preference'):
|
||||||
|
self.ydl.deprecation_warning(
|
||||||
|
f'Format sorting alias {alias} is deprecated '
|
||||||
|
f'and may be removed in a future version. Please use {field} instead')
|
||||||
reverse = match.group('reverse') is not None
|
reverse = match.group('reverse') is not None
|
||||||
closest = match.group('separator') == '~'
|
closest = match.group('separator') == '~'
|
||||||
limit_text = match.group('limit')
|
limit_text = match.group('limit')
|
||||||
@@ -1779,10 +1848,7 @@ class InfoExtractor(object):
|
|||||||
def _sort_formats(self, formats, field_preference=[]):
|
def _sort_formats(self, formats, field_preference=[]):
|
||||||
if not formats:
|
if not formats:
|
||||||
return
|
return
|
||||||
format_sort = self.FormatSort() # params and to_screen are taken from the downloader
|
format_sort = self.FormatSort(self, field_preference)
|
||||||
format_sort.evaluate_params(self._downloader.params, field_preference)
|
|
||||||
if self.get_param('verbose', False):
|
|
||||||
format_sort.print_verbose_info(self._downloader.write_debug)
|
|
||||||
formats.sort(key=lambda f: format_sort.calculate_preference(f))
|
formats.sort(key=lambda f: format_sort.calculate_preference(f))
|
||||||
|
|
||||||
def _check_formats(self, formats, video_id):
|
def _check_formats(self, formats, video_id):
|
||||||
@@ -1901,7 +1967,7 @@ class InfoExtractor(object):
|
|||||||
tbr = int_or_none(media_el.attrib.get('bitrate'))
|
tbr = int_or_none(media_el.attrib.get('bitrate'))
|
||||||
width = int_or_none(media_el.attrib.get('width'))
|
width = int_or_none(media_el.attrib.get('width'))
|
||||||
height = int_or_none(media_el.attrib.get('height'))
|
height = int_or_none(media_el.attrib.get('height'))
|
||||||
format_id = '-'.join(filter(None, [f4m_id, compat_str(i if tbr is None else tbr)]))
|
format_id = join_nonempty(f4m_id, tbr or i)
|
||||||
# If <bootstrapInfo> is present, the specified f4m is a
|
# If <bootstrapInfo> is present, the specified f4m is a
|
||||||
# stream-level manifest, and only set-level manifests may refer to
|
# stream-level manifest, and only set-level manifests may refer to
|
||||||
# external resources. See section 11.4 and section 4 of F4M spec
|
# external resources. See section 11.4 and section 4 of F4M spec
|
||||||
@@ -1963,7 +2029,7 @@ class InfoExtractor(object):
|
|||||||
|
|
||||||
def _m3u8_meta_format(self, m3u8_url, ext=None, preference=None, quality=None, m3u8_id=None):
|
def _m3u8_meta_format(self, m3u8_url, ext=None, preference=None, quality=None, m3u8_id=None):
|
||||||
return {
|
return {
|
||||||
'format_id': '-'.join(filter(None, [m3u8_id, 'meta'])),
|
'format_id': join_nonempty(m3u8_id, 'meta'),
|
||||||
'url': m3u8_url,
|
'url': m3u8_url,
|
||||||
'ext': ext,
|
'ext': ext,
|
||||||
'protocol': 'm3u8',
|
'protocol': 'm3u8',
|
||||||
@@ -2016,10 +2082,10 @@ class InfoExtractor(object):
|
|||||||
video_id=None):
|
video_id=None):
|
||||||
formats, subtitles = [], {}
|
formats, subtitles = [], {}
|
||||||
|
|
||||||
if '#EXT-X-FAXS-CM:' in m3u8_doc: # Adobe Flash Access
|
has_drm = re.search('|'.join([
|
||||||
return formats, subtitles
|
r'#EXT-X-FAXS-CM:', # Adobe Flash Access
|
||||||
|
r'#EXT-X-(?:SESSION-)?KEY:.*?URI="skd://', # Apple FairPlay
|
||||||
has_drm = re.search(r'#EXT-X-(?:SESSION-)?KEY:.*?URI="skd://', m3u8_doc)
|
]), m3u8_doc)
|
||||||
|
|
||||||
def format_url(url):
|
def format_url(url):
|
||||||
return url if re.match(r'^https?://', url) else compat_urlparse.urljoin(m3u8_url, url)
|
return url if re.match(r'^https?://', url) else compat_urlparse.urljoin(m3u8_url, url)
|
||||||
@@ -2058,7 +2124,7 @@ class InfoExtractor(object):
|
|||||||
|
|
||||||
if '#EXT-X-TARGETDURATION' in m3u8_doc: # media playlist, return as is
|
if '#EXT-X-TARGETDURATION' in m3u8_doc: # media playlist, return as is
|
||||||
formats = [{
|
formats = [{
|
||||||
'format_id': '-'.join(map(str, filter(None, [m3u8_id, idx]))),
|
'format_id': join_nonempty(m3u8_id, idx),
|
||||||
'format_index': idx,
|
'format_index': idx,
|
||||||
'url': m3u8_url,
|
'url': m3u8_url,
|
||||||
'ext': ext,
|
'ext': ext,
|
||||||
@@ -2107,7 +2173,7 @@ class InfoExtractor(object):
|
|||||||
if media_url:
|
if media_url:
|
||||||
manifest_url = format_url(media_url)
|
manifest_url = format_url(media_url)
|
||||||
formats.extend({
|
formats.extend({
|
||||||
'format_id': '-'.join(map(str, filter(None, (m3u8_id, group_id, name, idx)))),
|
'format_id': join_nonempty(m3u8_id, group_id, name, idx),
|
||||||
'format_note': name,
|
'format_note': name,
|
||||||
'format_index': idx,
|
'format_index': idx,
|
||||||
'url': manifest_url,
|
'url': manifest_url,
|
||||||
@@ -2164,9 +2230,9 @@ class InfoExtractor(object):
|
|||||||
# format_id intact.
|
# format_id intact.
|
||||||
if not live:
|
if not live:
|
||||||
stream_name = build_stream_name()
|
stream_name = build_stream_name()
|
||||||
format_id[1] = stream_name if stream_name else '%d' % (tbr if tbr else len(formats))
|
format_id[1] = stream_name or '%d' % (tbr or len(formats))
|
||||||
f = {
|
f = {
|
||||||
'format_id': '-'.join(map(str, filter(None, format_id))),
|
'format_id': join_nonempty(*format_id),
|
||||||
'format_index': idx,
|
'format_index': idx,
|
||||||
'url': manifest_url,
|
'url': manifest_url,
|
||||||
'manifest_url': m3u8_url,
|
'manifest_url': m3u8_url,
|
||||||
@@ -2266,7 +2332,7 @@ class InfoExtractor(object):
|
|||||||
|
|
||||||
if smil is False:
|
if smil is False:
|
||||||
assert not fatal
|
assert not fatal
|
||||||
return []
|
return [], {}
|
||||||
|
|
||||||
namespace = self._parse_smil_namespace(smil)
|
namespace = self._parse_smil_namespace(smil)
|
||||||
|
|
||||||
@@ -2630,7 +2696,7 @@ class InfoExtractor(object):
|
|||||||
|
|
||||||
mpd_duration = parse_duration(mpd_doc.get('mediaPresentationDuration'))
|
mpd_duration = parse_duration(mpd_doc.get('mediaPresentationDuration'))
|
||||||
formats, subtitles = [], {}
|
formats, subtitles = [], {}
|
||||||
stream_numbers = {'audio': 0, 'video': 0}
|
stream_numbers = collections.defaultdict(int)
|
||||||
for period in mpd_doc.findall(_add_ns('Period')):
|
for period in mpd_doc.findall(_add_ns('Period')):
|
||||||
period_duration = parse_duration(period.get('duration')) or mpd_duration
|
period_duration = parse_duration(period.get('duration')) or mpd_duration
|
||||||
period_ms_info = extract_multisegment_info(period, {
|
period_ms_info = extract_multisegment_info(period, {
|
||||||
@@ -2696,10 +2762,8 @@ class InfoExtractor(object):
|
|||||||
'format_note': 'DASH %s' % content_type,
|
'format_note': 'DASH %s' % content_type,
|
||||||
'filesize': filesize,
|
'filesize': filesize,
|
||||||
'container': mimetype2ext(mime_type) + '_dash',
|
'container': mimetype2ext(mime_type) + '_dash',
|
||||||
'manifest_stream_number': stream_numbers[content_type]
|
|
||||||
}
|
}
|
||||||
f.update(parse_codecs(codecs))
|
f.update(parse_codecs(codecs))
|
||||||
stream_numbers[content_type] += 1
|
|
||||||
elif content_type == 'text':
|
elif content_type == 'text':
|
||||||
f = {
|
f = {
|
||||||
'ext': mimetype2ext(mime_type),
|
'ext': mimetype2ext(mime_type),
|
||||||
@@ -2866,7 +2930,9 @@ class InfoExtractor(object):
|
|||||||
else:
|
else:
|
||||||
# Assuming direct URL to unfragmented media.
|
# Assuming direct URL to unfragmented media.
|
||||||
f['url'] = base_url
|
f['url'] = base_url
|
||||||
if content_type in ('video', 'audio') or mime_type == 'image/jpeg':
|
if content_type in ('video', 'audio', 'image/jpeg'):
|
||||||
|
f['manifest_stream_number'] = stream_numbers[f['url']]
|
||||||
|
stream_numbers[f['url']] += 1
|
||||||
formats.append(f)
|
formats.append(f)
|
||||||
elif content_type == 'text':
|
elif content_type == 'text':
|
||||||
subtitles.setdefault(lang or 'und', []).append(f)
|
subtitles.setdefault(lang or 'und', []).append(f)
|
||||||
@@ -2955,13 +3021,6 @@ class InfoExtractor(object):
|
|||||||
})
|
})
|
||||||
fragment_ctx['time'] += fragment_ctx['duration']
|
fragment_ctx['time'] += fragment_ctx['duration']
|
||||||
|
|
||||||
format_id = []
|
|
||||||
if ism_id:
|
|
||||||
format_id.append(ism_id)
|
|
||||||
if stream_name:
|
|
||||||
format_id.append(stream_name)
|
|
||||||
format_id.append(compat_str(tbr))
|
|
||||||
|
|
||||||
if stream_type == 'text':
|
if stream_type == 'text':
|
||||||
subtitles.setdefault(stream_language, []).append({
|
subtitles.setdefault(stream_language, []).append({
|
||||||
'ext': 'ismt',
|
'ext': 'ismt',
|
||||||
@@ -2980,7 +3039,7 @@ class InfoExtractor(object):
|
|||||||
})
|
})
|
||||||
elif stream_type in ('video', 'audio'):
|
elif stream_type in ('video', 'audio'):
|
||||||
formats.append({
|
formats.append({
|
||||||
'format_id': '-'.join(format_id),
|
'format_id': join_nonempty(ism_id, stream_name, tbr),
|
||||||
'url': ism_url,
|
'url': ism_url,
|
||||||
'manifest_url': ism_url,
|
'manifest_url': ism_url,
|
||||||
'ext': 'ismv' if stream_type == 'video' else 'isma',
|
'ext': 'ismv' if stream_type == 'video' else 'isma',
|
||||||
@@ -3404,10 +3463,8 @@ class InfoExtractor(object):
|
|||||||
return formats
|
return formats
|
||||||
|
|
||||||
def _live_title(self, name):
|
def _live_title(self, name):
|
||||||
""" Generate the title for a live video """
|
self._downloader.deprecation_warning('yt_dlp.InfoExtractor._live_title is deprecated and does not work as expected')
|
||||||
now = datetime.datetime.now()
|
return name
|
||||||
now_str = now.strftime('%Y-%m-%d %H:%M')
|
|
||||||
return name + ' ' + now_str
|
|
||||||
|
|
||||||
def _int(self, v, name, fatal=False, **kwargs):
|
def _int(self, v, name, fatal=False, **kwargs):
|
||||||
res = int_or_none(v, **kwargs)
|
res = int_or_none(v, **kwargs)
|
||||||
@@ -3517,14 +3574,18 @@ class InfoExtractor(object):
|
|||||||
|
|
||||||
def extractor():
|
def extractor():
|
||||||
comments = []
|
comments = []
|
||||||
|
interrupted = True
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
comments.append(next(generator))
|
comments.append(next(generator))
|
||||||
except KeyboardInterrupt:
|
|
||||||
interrupted = True
|
|
||||||
self.to_screen('Interrupted by user')
|
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
interrupted = False
|
interrupted = False
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
self.to_screen('Interrupted by user')
|
||||||
|
except Exception as e:
|
||||||
|
if self.get_param('ignoreerrors') is not True:
|
||||||
|
raise
|
||||||
|
self._downloader.report_error(e)
|
||||||
comment_count = len(comments)
|
comment_count = len(comments)
|
||||||
self.to_screen(f'Extracted {comment_count} comments')
|
self.to_screen(f'Extracted {comment_count} comments')
|
||||||
return {
|
return {
|
||||||
@@ -3602,7 +3663,7 @@ class InfoExtractor(object):
|
|||||||
else 'public' if all_known
|
else 'public' if all_known
|
||||||
else None)
|
else None)
|
||||||
|
|
||||||
def _configuration_arg(self, key, default=NO_DEFAULT, casesense=False):
|
def _configuration_arg(self, key, default=NO_DEFAULT, *, ie_key=None, casesense=False):
|
||||||
'''
|
'''
|
||||||
@returns A list of values for the extractor argument given by "key"
|
@returns A list of values for the extractor argument given by "key"
|
||||||
or "default" if no such key is present
|
or "default" if no such key is present
|
||||||
@@ -3610,7 +3671,7 @@ class InfoExtractor(object):
|
|||||||
@param casesense When false, the values are converted to lower case
|
@param casesense When false, the values are converted to lower case
|
||||||
'''
|
'''
|
||||||
val = traverse_obj(
|
val = traverse_obj(
|
||||||
self._downloader.params, ('extractor_args', self.ie_key().lower(), key))
|
self._downloader.params, ('extractor_args', (ie_key or self.ie_key()).lower(), key))
|
||||||
if val is None:
|
if val is None:
|
||||||
return [] if default is NO_DEFAULT else default
|
return [] if default is NO_DEFAULT else default
|
||||||
return list(val) if casesense else [x.lower() for x in val]
|
return list(val) if casesense else [x.lower() for x in val]
|
||||||
@@ -3620,24 +3681,17 @@ class SearchInfoExtractor(InfoExtractor):
|
|||||||
"""
|
"""
|
||||||
Base class for paged search queries extractors.
|
Base class for paged search queries extractors.
|
||||||
They accept URLs in the format _SEARCH_KEY(|all|[0-9]):{query}
|
They accept URLs in the format _SEARCH_KEY(|all|[0-9]):{query}
|
||||||
Instances should define _SEARCH_KEY and _MAX_RESULTS.
|
Instances should define _SEARCH_KEY and optionally _MAX_RESULTS
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_MAX_RESULTS = float('inf')
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _make_valid_url(cls):
|
def _make_valid_url(cls):
|
||||||
return r'%s(?P<prefix>|[1-9][0-9]*|all):(?P<query>[\s\S]+)' % cls._SEARCH_KEY
|
return r'%s(?P<prefix>|[1-9][0-9]*|all):(?P<query>[\s\S]+)' % cls._SEARCH_KEY
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def suitable(cls, url):
|
|
||||||
return re.match(cls._make_valid_url(), url) is not None
|
|
||||||
|
|
||||||
def _real_extract(self, query):
|
def _real_extract(self, query):
|
||||||
mobj = re.match(self._make_valid_url(), query)
|
prefix, query = self._match_valid_url(query).group('prefix', 'query')
|
||||||
if mobj is None:
|
|
||||||
raise ExtractorError('Invalid search query "%s"' % query)
|
|
||||||
|
|
||||||
prefix = mobj.group('prefix')
|
|
||||||
query = mobj.group('query')
|
|
||||||
if prefix == '':
|
if prefix == '':
|
||||||
return self._get_n_results(query, 1)
|
return self._get_n_results(query, 1)
|
||||||
elif prefix == 'all':
|
elif prefix == 'all':
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ class CorusIE(ThePlatformFeedIE):
|
|||||||
'timestamp': 1486392197,
|
'timestamp': 1486392197,
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'format': 'bestvideo',
|
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
'expected_warnings': ['Failed to parse JSON'],
|
'expected_warnings': ['Failed to parse JSON'],
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class CoubIE(InfoExtractor):
|
|||||||
|
|
||||||
file_versions = coub['file_versions']
|
file_versions = coub['file_versions']
|
||||||
|
|
||||||
QUALITIES = ('low', 'med', 'high')
|
QUALITIES = ('low', 'med', 'high', 'higher')
|
||||||
|
|
||||||
MOBILE = 'mobile'
|
MOBILE = 'mobile'
|
||||||
IPHONE = 'iphone'
|
IPHONE = 'iphone'
|
||||||
@@ -86,6 +86,7 @@ class CoubIE(InfoExtractor):
|
|||||||
'format_id': '%s-%s-%s' % (HTML5, kind, quality),
|
'format_id': '%s-%s-%s' % (HTML5, kind, quality),
|
||||||
'filesize': int_or_none(item.get('size')),
|
'filesize': int_or_none(item.get('size')),
|
||||||
'vcodec': 'none' if kind == 'audio' else None,
|
'vcodec': 'none' if kind == 'audio' else None,
|
||||||
|
'acodec': 'none' if kind == 'video' else None,
|
||||||
'quality': quality_key(quality),
|
'quality': quality_key(quality),
|
||||||
'source_preference': preference_key(HTML5),
|
'source_preference': preference_key(HTML5),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import unified_strdate
|
||||||
|
|
||||||
|
|
||||||
|
class CozyTVIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?cozy\.tv/(?P<uploader>[^/]+)/replays/(?P<id>[^/$#&?]+)'
|
||||||
|
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://cozy.tv/beardson/replays/2021-11-19_1',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'beardson-2021-11-19_1',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'pokemon pt2',
|
||||||
|
'uploader': 'beardson',
|
||||||
|
'upload_date': '20211119',
|
||||||
|
'was_live': True,
|
||||||
|
'duration': 7981,
|
||||||
|
},
|
||||||
|
'params': {'skip_download': True}
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
uploader, date = self._match_valid_url(url).groups()
|
||||||
|
id = f'{uploader}-{date}'
|
||||||
|
data_json = self._download_json(f'https://api.cozy.tv/cache/{uploader}/replay/{date}', id)
|
||||||
|
formats, subtitles = self._extract_m3u8_formats_and_subtitles(
|
||||||
|
f'https://cozycdn.foxtrotstream.xyz/replays/{uploader}/{date}/index.m3u8', id, ext='mp4')
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'title': data_json.get('title'),
|
||||||
|
'uploader': data_json.get('user') or uploader,
|
||||||
|
'upload_date': unified_strdate(data_json.get('date')),
|
||||||
|
'was_live': True,
|
||||||
|
'duration': data_json.get('duration'),
|
||||||
|
'formats': formats,
|
||||||
|
'subtitles': subtitles,
|
||||||
|
}
|
||||||
+21
-19
@@ -23,32 +23,35 @@ from ..utils import (
|
|||||||
class CrackleIE(InfoExtractor):
|
class CrackleIE(InfoExtractor):
|
||||||
_VALID_URL = r'(?:crackle:|https?://(?:(?:www|m)\.)?(?:sony)?crackle\.com/(?:playlist/\d+/|(?:[^/]+/)+))(?P<id>\d+)'
|
_VALID_URL = r'(?:crackle:|https?://(?:(?:www|m)\.)?(?:sony)?crackle\.com/(?:playlist/\d+/|(?:[^/]+/)+))(?P<id>\d+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
# geo restricted to CA
|
# Crackle is available in the United States and territories
|
||||||
'url': 'https://www.crackle.com/andromeda/2502343',
|
'url': 'https://www.crackle.com/thanksgiving/2510064',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '2502343',
|
'id': '2510064',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Under The Night',
|
'title': 'Touch Football',
|
||||||
'description': 'md5:d2b8ca816579ae8a7bf28bfff8cefc8a',
|
'description': 'md5:cfbb513cf5de41e8b56d7ab756cff4df',
|
||||||
'duration': 2583,
|
'duration': 1398,
|
||||||
'view_count': int,
|
'view_count': int,
|
||||||
'average_rating': 0,
|
'average_rating': 0,
|
||||||
'age_limit': 14,
|
'age_limit': 17,
|
||||||
'genre': 'Action, Sci-Fi',
|
'genre': 'Comedy',
|
||||||
'creator': 'Allan Kroeker',
|
'creator': 'Daniel Powell',
|
||||||
'artist': 'Keith Hamilton Cobb, Kevin Sorbo, Lisa Ryder, Lexa Doig, Robert Hewitt Wolfe',
|
'artist': 'Chris Elliott, Amy Sedaris',
|
||||||
'release_year': 2000,
|
'release_year': 2016,
|
||||||
'series': 'Andromeda',
|
'series': 'Thanksgiving',
|
||||||
'episode': 'Under The Night',
|
'episode': 'Touch Football',
|
||||||
'season_number': 1,
|
'season_number': 1,
|
||||||
'episode_number': 1,
|
'episode_number': 1,
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
# m3u8 download
|
# m3u8 download
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
}
|
},
|
||||||
|
'expected_warnings': [
|
||||||
|
'Trying with a list of known countries'
|
||||||
|
],
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.sonycrackle.com/andromeda/2502343',
|
'url': 'https://www.sonycrackle.com/thanksgiving/2510064',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
@@ -129,7 +132,6 @@ class CrackleIE(InfoExtractor):
|
|||||||
break
|
break
|
||||||
|
|
||||||
ignore_no_formats = self.get_param('ignore_no_formats_error')
|
ignore_no_formats = self.get_param('ignore_no_formats_error')
|
||||||
allow_unplayable_formats = self.get_param('allow_unplayable_formats')
|
|
||||||
|
|
||||||
if not media or (not media.get('MediaURLs') and not ignore_no_formats):
|
if not media or (not media.get('MediaURLs') and not ignore_no_formats):
|
||||||
raise ExtractorError(
|
raise ExtractorError(
|
||||||
@@ -143,9 +145,9 @@ class CrackleIE(InfoExtractor):
|
|||||||
for e in media.get('MediaURLs') or []:
|
for e in media.get('MediaURLs') or []:
|
||||||
if e.get('UseDRM'):
|
if e.get('UseDRM'):
|
||||||
has_drm = True
|
has_drm = True
|
||||||
if not allow_unplayable_formats:
|
format_url = url_or_none(e.get('DRMPath'))
|
||||||
continue
|
else:
|
||||||
format_url = url_or_none(e.get('Path'))
|
format_url = url_or_none(e.get('Path'))
|
||||||
if not format_url:
|
if not format_url:
|
||||||
continue
|
continue
|
||||||
ext = determine_ext(format_url)
|
ext = determine_ext(format_url)
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ from ..utils import (
|
|||||||
int_or_none,
|
int_or_none,
|
||||||
lowercase_escape,
|
lowercase_escape,
|
||||||
merge_dicts,
|
merge_dicts,
|
||||||
|
qualities,
|
||||||
remove_end,
|
remove_end,
|
||||||
sanitized_Request,
|
sanitized_Request,
|
||||||
try_get,
|
try_get,
|
||||||
@@ -478,19 +479,24 @@ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
|||||||
[r'<a[^>]+href="/publisher/[^"]+"[^>]*>([^<]+)</a>', r'<div>\s*Publisher:\s*<span>\s*(.+?)\s*</span>\s*</div>'],
|
[r'<a[^>]+href="/publisher/[^"]+"[^>]*>([^<]+)</a>', r'<div>\s*Publisher:\s*<span>\s*(.+?)\s*</span>\s*</div>'],
|
||||||
webpage, 'video_uploader', default=False)
|
webpage, 'video_uploader', default=False)
|
||||||
|
|
||||||
|
requested_languages = self._configuration_arg('language')
|
||||||
|
requested_hardsubs = [('' if val == 'none' else val) for val in self._configuration_arg('hardsub')]
|
||||||
|
language_preference = qualities((requested_languages or [language or ''])[::-1])
|
||||||
|
hardsub_preference = qualities((requested_hardsubs or ['', language or ''])[::-1])
|
||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
for stream in media.get('streams', []):
|
for stream in media.get('streams', []):
|
||||||
audio_lang = stream.get('audio_lang')
|
audio_lang = stream.get('audio_lang') or ''
|
||||||
hardsub_lang = stream.get('hardsub_lang')
|
hardsub_lang = stream.get('hardsub_lang') or ''
|
||||||
|
if (requested_languages and audio_lang.lower() not in requested_languages
|
||||||
|
or requested_hardsubs and hardsub_lang.lower() not in requested_hardsubs):
|
||||||
|
continue
|
||||||
vrv_formats = self._extract_vrv_formats(
|
vrv_formats = self._extract_vrv_formats(
|
||||||
stream.get('url'), video_id, stream.get('format'),
|
stream.get('url'), video_id, stream.get('format'),
|
||||||
audio_lang, hardsub_lang)
|
audio_lang, hardsub_lang)
|
||||||
for f in vrv_formats:
|
for f in vrv_formats:
|
||||||
f['language_preference'] = 1 if audio_lang == language else 0
|
f['language_preference'] = language_preference(audio_lang)
|
||||||
f['quality'] = (
|
f['quality'] = hardsub_preference(hardsub_lang)
|
||||||
1 if not hardsub_lang
|
|
||||||
else 0 if hardsub_lang == language
|
|
||||||
else -1)
|
|
||||||
formats.extend(vrv_formats)
|
formats.extend(vrv_formats)
|
||||||
if not formats:
|
if not formats:
|
||||||
available_fmts = []
|
available_fmts = []
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from ..utils import (
|
|||||||
str_to_int,
|
str_to_int,
|
||||||
unescapeHTML,
|
unescapeHTML,
|
||||||
)
|
)
|
||||||
from .senateisvp import SenateISVPIE
|
from .senategov import SenateISVPIE
|
||||||
from .ustream import UstreamIE
|
from .ustream import UstreamIE
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ from ..utils import (
|
|||||||
class CuriosityStreamBaseIE(InfoExtractor):
|
class CuriosityStreamBaseIE(InfoExtractor):
|
||||||
_NETRC_MACHINE = 'curiositystream'
|
_NETRC_MACHINE = 'curiositystream'
|
||||||
_auth_token = None
|
_auth_token = None
|
||||||
_API_BASE_URL = 'https://api.curiositystream.com/v1/'
|
|
||||||
|
|
||||||
def _handle_errors(self, result):
|
def _handle_errors(self, result):
|
||||||
error = result.get('error', {}).get('message')
|
error = result.get('error', {}).get('message')
|
||||||
@@ -39,38 +38,44 @@ class CuriosityStreamBaseIE(InfoExtractor):
|
|||||||
if email is None:
|
if email is None:
|
||||||
return
|
return
|
||||||
result = self._download_json(
|
result = self._download_json(
|
||||||
self._API_BASE_URL + 'login', None, data=urlencode_postdata({
|
'https://api.curiositystream.com/v1/login', None,
|
||||||
|
note='Logging in', data=urlencode_postdata({
|
||||||
'email': email,
|
'email': email,
|
||||||
'password': password,
|
'password': password,
|
||||||
}))
|
}))
|
||||||
self._handle_errors(result)
|
self._handle_errors(result)
|
||||||
self._auth_token = result['message']['auth_token']
|
CuriosityStreamBaseIE._auth_token = result['message']['auth_token']
|
||||||
|
|
||||||
|
|
||||||
class CuriosityStreamIE(CuriosityStreamBaseIE):
|
class CuriosityStreamIE(CuriosityStreamBaseIE):
|
||||||
IE_NAME = 'curiositystream'
|
IE_NAME = 'curiositystream'
|
||||||
_VALID_URL = r'https?://(?:app\.)?curiositystream\.com/video/(?P<id>\d+)'
|
_VALID_URL = r'https?://(?:app\.)?curiositystream\.com/video/(?P<id>\d+)'
|
||||||
_TEST = {
|
_TESTS = [{
|
||||||
'url': 'https://app.curiositystream.com/video/2',
|
'url': 'https://app.curiositystream.com/video/2',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '2',
|
'id': '2',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'How Did You Develop The Internet?',
|
'title': 'How Did You Develop The Internet?',
|
||||||
'description': 'Vint Cerf, Google\'s Chief Internet Evangelist, describes how he and Bob Kahn created the internet.',
|
'description': 'Vint Cerf, Google\'s Chief Internet Evangelist, describes how he and Bob Kahn created the internet.',
|
||||||
|
'channel': 'Curiosity Stream',
|
||||||
|
'categories': ['Technology', 'Interview'],
|
||||||
|
'average_rating': 96.79,
|
||||||
|
'series_id': '2',
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'format': 'bestvideo',
|
|
||||||
# m3u8 download
|
# m3u8 download
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
}
|
}]
|
||||||
|
|
||||||
|
_API_BASE_URL = 'https://api.curiositystream.com/v1/media/'
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
for encoding_format in ('m3u8', 'mpd'):
|
for encoding_format in ('m3u8', 'mpd'):
|
||||||
media = self._call_api('media/' + video_id, video_id, query={
|
media = self._call_api(video_id, video_id, query={
|
||||||
'encodingsNew': 'true',
|
'encodingsNew': 'true',
|
||||||
'encodingsFormat': encoding_format,
|
'encodingsFormat': encoding_format,
|
||||||
})
|
})
|
||||||
@@ -140,12 +145,33 @@ class CuriosityStreamIE(CuriosityStreamBaseIE):
|
|||||||
'duration': int_or_none(media.get('duration')),
|
'duration': int_or_none(media.get('duration')),
|
||||||
'tags': media.get('tags'),
|
'tags': media.get('tags'),
|
||||||
'subtitles': subtitles,
|
'subtitles': subtitles,
|
||||||
|
'channel': media.get('producer'),
|
||||||
|
'categories': [media.get('primary_category'), media.get('type')],
|
||||||
|
'average_rating': media.get('rating_percentage'),
|
||||||
|
'series_id': str(media.get('collection_id') or '') or None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class CuriosityStreamCollectionIE(CuriosityStreamBaseIE):
|
class CuriosityStreamCollectionBaseIE(CuriosityStreamBaseIE):
|
||||||
IE_NAME = 'curiositystream:collection'
|
|
||||||
_VALID_URL = r'https?://(?:app\.)?curiositystream\.com/(?:collections?|series)/(?P<id>\d+)'
|
def _real_extract(self, url):
|
||||||
|
collection_id = self._match_id(url)
|
||||||
|
collection = self._call_api(collection_id, collection_id)
|
||||||
|
entries = []
|
||||||
|
for media in collection.get('media', []):
|
||||||
|
media_id = compat_str(media.get('id'))
|
||||||
|
media_type, ie = ('series', CuriosityStreamSeriesIE) if media.get('is_collection') else ('video', CuriosityStreamIE)
|
||||||
|
entries.append(self.url_result(
|
||||||
|
'https://curiositystream.com/%s/%s' % (media_type, media_id),
|
||||||
|
ie=ie.ie_key(), video_id=media_id))
|
||||||
|
return self.playlist_result(
|
||||||
|
entries, collection_id,
|
||||||
|
collection.get('title'), collection.get('description'))
|
||||||
|
|
||||||
|
|
||||||
|
class CuriosityStreamCollectionsIE(CuriosityStreamCollectionBaseIE):
|
||||||
|
IE_NAME = 'curiositystream:collections'
|
||||||
|
_VALID_URL = r'https?://(?:app\.)?curiositystream\.com/collections/(?P<id>\d+)'
|
||||||
_API_BASE_URL = 'https://api.curiositystream.com/v2/collections/'
|
_API_BASE_URL = 'https://api.curiositystream.com/v2/collections/'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://curiositystream.com/collections/86',
|
'url': 'https://curiositystream.com/collections/86',
|
||||||
@@ -156,7 +182,17 @@ class CuriosityStreamCollectionIE(CuriosityStreamBaseIE):
|
|||||||
},
|
},
|
||||||
'playlist_mincount': 7,
|
'playlist_mincount': 7,
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://app.curiositystream.com/collection/2',
|
'url': 'https://curiositystream.com/collections/36',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
|
||||||
|
class CuriosityStreamSeriesIE(CuriosityStreamCollectionBaseIE):
|
||||||
|
IE_NAME = 'curiositystream:series'
|
||||||
|
_VALID_URL = r'https?://(?:app\.)?curiositystream\.com/(?:series|collection)/(?P<id>\d+)'
|
||||||
|
_API_BASE_URL = 'https://api.curiositystream.com/v2/series/'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://curiositystream.com/series/2',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '2',
|
'id': '2',
|
||||||
'title': 'Curious Minds: The Internet',
|
'title': 'Curious Minds: The Internet',
|
||||||
@@ -164,23 +200,6 @@ class CuriosityStreamCollectionIE(CuriosityStreamBaseIE):
|
|||||||
},
|
},
|
||||||
'playlist_mincount': 16,
|
'playlist_mincount': 16,
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://curiositystream.com/series/2',
|
'url': 'https://curiositystream.com/collection/2',
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'https://curiositystream.com/collections/36',
|
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
collection_id = self._match_id(url)
|
|
||||||
collection = self._call_api(collection_id, collection_id)
|
|
||||||
entries = []
|
|
||||||
for media in collection.get('media', []):
|
|
||||||
media_id = compat_str(media.get('id'))
|
|
||||||
media_type, ie = ('series', CuriosityStreamCollectionIE) if media.get('is_collection') else ('video', CuriosityStreamIE)
|
|
||||||
entries.append(self.url_result(
|
|
||||||
'https://curiositystream.com/%s/%s' % (media_type, media_id),
|
|
||||||
ie=ie.ie_key(), video_id=media_id))
|
|
||||||
return self.playlist_result(
|
|
||||||
entries, collection_id,
|
|
||||||
collection.get('title'), collection.get('description'))
|
|
||||||
|
|||||||
@@ -305,7 +305,7 @@ class DailymotionIE(DailymotionBaseInfoExtractor):
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'title': self._live_title(title) if is_live else title,
|
'title': title,
|
||||||
'description': clean_html(media.get('description')),
|
'description': clean_html(media.get('description')),
|
||||||
'thumbnails': thumbnails,
|
'thumbnails': thumbnails,
|
||||||
'duration': int_or_none(metadata.get('duration')) or None,
|
'duration': int_or_none(metadata.get('duration')) or None,
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
# coding: utf-8
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
|
|
||||||
from .dplay import DPlayIE
|
|
||||||
|
|
||||||
|
|
||||||
class DiscoveryNetworksDeIE(DPlayIE):
|
|
||||||
_VALID_URL = r'https?://(?:www\.)?(?P<domain>(?:tlc|dmax)\.de|dplay\.co\.uk)/(?:programme|show|sendungen)/(?P<programme>[^/]+)/(?:video/)?(?P<alternate_id>[^/]+)'
|
|
||||||
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'https://www.tlc.de/programme/breaking-amish/video/die-welt-da-drauen/DCB331270001100',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '78867',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Die Welt da draußen',
|
|
||||||
'description': 'md5:61033c12b73286e409d99a41742ef608',
|
|
||||||
'timestamp': 1554069600,
|
|
||||||
'upload_date': '20190331',
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
'format': 'bestvideo',
|
|
||||||
'skip_download': True,
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'url': 'https://www.dmax.de/programme/dmax-highlights/video/tuning-star-sidney-hoffmann-exklusiv-bei-dmax/191023082312316',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'https://www.dplay.co.uk/show/ghost-adventures/video/hotel-leger-103620/EHD_280313B',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'https://tlc.de/sendungen/breaking-amish/die-welt-da-drauen/',
|
|
||||||
'only_matching': True,
|
|
||||||
}]
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
domain, programme, alternate_id = self._match_valid_url(url).groups()
|
|
||||||
country = 'GB' if domain == 'dplay.co.uk' else 'DE'
|
|
||||||
realm = 'questuk' if country == 'GB' else domain.replace('.', '')
|
|
||||||
return self._get_disco_api_info(
|
|
||||||
url, '%s/%s' % (programme, alternate_id),
|
|
||||||
'sonic-eu1-prod.disco-api.com', realm, country)
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
# coding: utf-8
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import json
|
|
||||||
|
|
||||||
from ..compat import compat_str
|
|
||||||
from ..utils import try_get
|
|
||||||
from .common import InfoExtractor
|
|
||||||
from .dplay import DPlayIE
|
|
||||||
|
|
||||||
|
|
||||||
class DiscoveryPlusIndiaIE(DPlayIE):
|
|
||||||
_VALID_URL = r'https?://(?:www\.)?discoveryplus\.in/videos?' + DPlayIE._PATH_REGEX
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'https://www.discoveryplus.in/videos/how-do-they-do-it/fugu-and-more?seasonId=8&type=EPISODE',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '27104',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'display_id': 'how-do-they-do-it/fugu-and-more',
|
|
||||||
'title': 'Fugu and More',
|
|
||||||
'description': 'The Japanese catch, prepare and eat the deadliest fish on the planet.',
|
|
||||||
'duration': 1319,
|
|
||||||
'timestamp': 1582309800,
|
|
||||||
'upload_date': '20200221',
|
|
||||||
'series': 'How Do They Do It?',
|
|
||||||
'season_number': 8,
|
|
||||||
'episode_number': 2,
|
|
||||||
'creator': 'Discovery Channel',
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
'format': 'bestvideo',
|
|
||||||
'skip_download': True,
|
|
||||||
},
|
|
||||||
'skip': 'Cookies (not necessarily logged in) are needed'
|
|
||||||
}]
|
|
||||||
|
|
||||||
def _update_disco_api_headers(self, headers, disco_base, display_id, realm):
|
|
||||||
headers['x-disco-params'] = 'realm=%s' % realm
|
|
||||||
headers['x-disco-client'] = 'WEB:UNKNOWN:dplus-india:17.0.0'
|
|
||||||
|
|
||||||
def _download_video_playback_info(self, disco_base, video_id, headers):
|
|
||||||
return self._download_json(
|
|
||||||
disco_base + 'playback/v3/videoPlaybackInfo',
|
|
||||||
video_id, headers=headers, data=json.dumps({
|
|
||||||
'deviceInfo': {
|
|
||||||
'adBlocker': False,
|
|
||||||
},
|
|
||||||
'videoId': video_id,
|
|
||||||
}).encode('utf-8'))['data']['attributes']['streaming']
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
display_id = self._match_id(url)
|
|
||||||
return self._get_disco_api_info(
|
|
||||||
url, display_id, 'ap2-prod-direct.discoveryplus.in', 'dplusindia', 'in')
|
|
||||||
|
|
||||||
|
|
||||||
class DiscoveryPlusIndiaShowIE(InfoExtractor):
|
|
||||||
_VALID_URL = r'https?://(?:www\.)?discoveryplus\.in/show/(?P<show_name>[^/]+)/?(?:[?#]|$)'
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'https://www.discoveryplus.in/show/how-do-they-do-it',
|
|
||||||
'playlist_mincount': 140,
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'how-do-they-do-it',
|
|
||||||
},
|
|
||||||
}]
|
|
||||||
|
|
||||||
def _entries(self, show_name):
|
|
||||||
headers = {
|
|
||||||
'x-disco-client': 'WEB:UNKNOWN:dplus-india:prod',
|
|
||||||
'x-disco-params': 'realm=dplusindia',
|
|
||||||
'referer': 'https://www.discoveryplus.in/',
|
|
||||||
}
|
|
||||||
show_url = 'https://ap2-prod-direct.discoveryplus.in/cms/routes/show/{}?include=default'.format(show_name)
|
|
||||||
show_json = self._download_json(show_url,
|
|
||||||
video_id=show_name,
|
|
||||||
headers=headers)['included'][4]['attributes']['component']
|
|
||||||
show_id = show_json['mandatoryParams'].split('=')[-1]
|
|
||||||
season_url = 'https://ap2-prod-direct.discoveryplus.in/content/videos?sort=episodeNumber&filter[seasonNumber]={}&filter[show.id]={}&page[size]=100&page[number]={}'
|
|
||||||
for season in show_json['filters'][0]['options']:
|
|
||||||
season_id = season['id']
|
|
||||||
total_pages, page_num = 1, 0
|
|
||||||
while page_num < total_pages:
|
|
||||||
season_json = self._download_json(season_url.format(season_id, show_id, compat_str(page_num + 1)),
|
|
||||||
video_id=show_id, headers=headers,
|
|
||||||
note='Downloading JSON metadata%s' % (' page %d' % page_num if page_num else ''))
|
|
||||||
if page_num == 0:
|
|
||||||
total_pages = try_get(season_json, lambda x: x['meta']['totalPages'], int) or 1
|
|
||||||
episodes_json = season_json['data']
|
|
||||||
for episode in episodes_json:
|
|
||||||
video_id = episode['attributes']['path']
|
|
||||||
yield self.url_result(
|
|
||||||
'https://discoveryplus.in/videos/%s' % video_id,
|
|
||||||
ie=DiscoveryPlusIndiaIE.ie_key(), video_id=video_id)
|
|
||||||
page_num += 1
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
show_name = self._match_valid_url(url).group('show_name')
|
|
||||||
return self.playlist_result(self._entries(show_name), playlist_id=show_name)
|
|
||||||
@@ -7,8 +7,8 @@ from .common import InfoExtractor
|
|||||||
from ..utils import (
|
from ..utils import (
|
||||||
int_or_none,
|
int_or_none,
|
||||||
unified_strdate,
|
unified_strdate,
|
||||||
compat_str,
|
|
||||||
determine_ext,
|
determine_ext,
|
||||||
|
join_nonempty,
|
||||||
update_url_query,
|
update_url_query,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -119,18 +119,13 @@ class DisneyIE(InfoExtractor):
|
|||||||
continue
|
continue
|
||||||
formats.append(f)
|
formats.append(f)
|
||||||
continue
|
continue
|
||||||
format_id = []
|
|
||||||
if flavor_format:
|
|
||||||
format_id.append(flavor_format)
|
|
||||||
if tbr:
|
|
||||||
format_id.append(compat_str(tbr))
|
|
||||||
ext = determine_ext(flavor_url)
|
ext = determine_ext(flavor_url)
|
||||||
if flavor_format == 'applehttp' or ext == 'm3u8':
|
if flavor_format == 'applehttp' or ext == 'm3u8':
|
||||||
ext = 'mp4'
|
ext = 'mp4'
|
||||||
width = int_or_none(flavor.get('width'))
|
width = int_or_none(flavor.get('width'))
|
||||||
height = int_or_none(flavor.get('height'))
|
height = int_or_none(flavor.get('height'))
|
||||||
formats.append({
|
formats.append({
|
||||||
'format_id': '-'.join(format_id),
|
'format_id': join_nonempty(flavor_format, tbr),
|
||||||
'url': flavor_url,
|
'url': flavor_url,
|
||||||
'width': width,
|
'width': width,
|
||||||
'height': height,
|
'height': height,
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ class DLiveStreamIE(InfoExtractor):
|
|||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
return {
|
return {
|
||||||
'id': display_name,
|
'id': display_name,
|
||||||
'title': self._live_title(title),
|
'title': title,
|
||||||
'uploader': display_name,
|
'uploader': display_name,
|
||||||
'uploader_id': username,
|
'uploader_id': username,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ class DouyuTVIE(InfoExtractor):
|
|||||||
'aid': 'pcclient'
|
'aid': 'pcclient'
|
||||||
})['data']['live_url']
|
})['data']['live_url']
|
||||||
|
|
||||||
title = self._live_title(unescapeHTML(room['room_name']))
|
title = unescapeHTML(room['room_name'])
|
||||||
description = room.get('show_details')
|
description = room.get('show_details')
|
||||||
thumbnail = room.get('room_src')
|
thumbnail = room.get('room_src')
|
||||||
uploader = room.get('nickname')
|
uploader = room.get('nickname')
|
||||||
|
|||||||
+342
-148
@@ -2,6 +2,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import uuid
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..compat import compat_HTTPError
|
from ..compat import compat_HTTPError
|
||||||
@@ -11,12 +12,172 @@ from ..utils import (
|
|||||||
float_or_none,
|
float_or_none,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
strip_or_none,
|
strip_or_none,
|
||||||
|
try_get,
|
||||||
unified_timestamp,
|
unified_timestamp,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class DPlayIE(InfoExtractor):
|
class DPlayBaseIE(InfoExtractor):
|
||||||
_PATH_REGEX = r'/(?P<id>[^/]+/[^/?#]+)'
|
_PATH_REGEX = r'/(?P<id>[^/]+/[^/?#]+)'
|
||||||
|
_auth_token_cache = {}
|
||||||
|
|
||||||
|
def _get_auth(self, disco_base, display_id, realm, needs_device_id=True):
|
||||||
|
key = (disco_base, realm)
|
||||||
|
st = self._get_cookies(disco_base).get('st')
|
||||||
|
token = (st and st.value) or self._auth_token_cache.get(key)
|
||||||
|
|
||||||
|
if not token:
|
||||||
|
query = {'realm': realm}
|
||||||
|
if needs_device_id:
|
||||||
|
query['deviceId'] = uuid.uuid4().hex
|
||||||
|
token = self._download_json(
|
||||||
|
disco_base + 'token', display_id, 'Downloading token',
|
||||||
|
query=query)['data']['attributes']['token']
|
||||||
|
|
||||||
|
# Save cache only if cookies are not being set
|
||||||
|
if not self._get_cookies(disco_base).get('st'):
|
||||||
|
self._auth_token_cache[key] = token
|
||||||
|
|
||||||
|
return f'Bearer {token}'
|
||||||
|
|
||||||
|
def _process_errors(self, e, geo_countries):
|
||||||
|
info = self._parse_json(e.cause.read().decode('utf-8'), None)
|
||||||
|
error = info['errors'][0]
|
||||||
|
error_code = error.get('code')
|
||||||
|
if error_code == 'access.denied.geoblocked':
|
||||||
|
self.raise_geo_restricted(countries=geo_countries)
|
||||||
|
elif error_code in ('access.denied.missingpackage', 'invalid.token'):
|
||||||
|
raise ExtractorError(
|
||||||
|
'This video is only available for registered users. You may want to use --cookies.', expected=True)
|
||||||
|
raise ExtractorError(info['errors'][0]['detail'], expected=True)
|
||||||
|
|
||||||
|
def _update_disco_api_headers(self, headers, disco_base, display_id, realm):
|
||||||
|
headers['Authorization'] = self._get_auth(disco_base, display_id, realm, False)
|
||||||
|
|
||||||
|
def _download_video_playback_info(self, disco_base, video_id, headers):
|
||||||
|
streaming = self._download_json(
|
||||||
|
disco_base + 'playback/videoPlaybackInfo/' + video_id,
|
||||||
|
video_id, headers=headers)['data']['attributes']['streaming']
|
||||||
|
streaming_list = []
|
||||||
|
for format_id, format_dict in streaming.items():
|
||||||
|
streaming_list.append({
|
||||||
|
'type': format_id,
|
||||||
|
'url': format_dict.get('url'),
|
||||||
|
})
|
||||||
|
return streaming_list
|
||||||
|
|
||||||
|
def _get_disco_api_info(self, url, display_id, disco_host, realm, country, domain=''):
|
||||||
|
geo_countries = [country.upper()]
|
||||||
|
self._initialize_geo_bypass({
|
||||||
|
'countries': geo_countries,
|
||||||
|
})
|
||||||
|
disco_base = 'https://%s/' % disco_host
|
||||||
|
headers = {
|
||||||
|
'Referer': url,
|
||||||
|
}
|
||||||
|
self._update_disco_api_headers(headers, disco_base, display_id, realm)
|
||||||
|
try:
|
||||||
|
video = self._download_json(
|
||||||
|
disco_base + 'content/videos/' + display_id, display_id,
|
||||||
|
headers=headers, query={
|
||||||
|
'fields[channel]': 'name',
|
||||||
|
'fields[image]': 'height,src,width',
|
||||||
|
'fields[show]': 'name',
|
||||||
|
'fields[tag]': 'name',
|
||||||
|
'fields[video]': 'description,episodeNumber,name,publishStart,seasonNumber,videoDuration',
|
||||||
|
'include': 'images,primaryChannel,show,tags'
|
||||||
|
})
|
||||||
|
except ExtractorError as e:
|
||||||
|
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 400:
|
||||||
|
self._process_errors(e, geo_countries)
|
||||||
|
raise
|
||||||
|
video_id = video['data']['id']
|
||||||
|
info = video['data']['attributes']
|
||||||
|
title = info['name'].strip()
|
||||||
|
formats = []
|
||||||
|
subtitles = {}
|
||||||
|
try:
|
||||||
|
streaming = self._download_video_playback_info(
|
||||||
|
disco_base, video_id, headers)
|
||||||
|
except ExtractorError as e:
|
||||||
|
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 403:
|
||||||
|
self._process_errors(e, geo_countries)
|
||||||
|
raise
|
||||||
|
for format_dict in streaming:
|
||||||
|
if not isinstance(format_dict, dict):
|
||||||
|
continue
|
||||||
|
format_url = format_dict.get('url')
|
||||||
|
if not format_url:
|
||||||
|
continue
|
||||||
|
format_id = format_dict.get('type')
|
||||||
|
ext = determine_ext(format_url)
|
||||||
|
if format_id == 'dash' or ext == 'mpd':
|
||||||
|
dash_fmts, dash_subs = self._extract_mpd_formats_and_subtitles(
|
||||||
|
format_url, display_id, mpd_id='dash', fatal=False)
|
||||||
|
formats.extend(dash_fmts)
|
||||||
|
subtitles = self._merge_subtitles(subtitles, dash_subs)
|
||||||
|
elif format_id == 'hls' or ext == 'm3u8':
|
||||||
|
m3u8_fmts, m3u8_subs = self._extract_m3u8_formats_and_subtitles(
|
||||||
|
format_url, display_id, 'mp4',
|
||||||
|
entry_protocol='m3u8_native', m3u8_id='hls',
|
||||||
|
fatal=False)
|
||||||
|
formats.extend(m3u8_fmts)
|
||||||
|
subtitles = self._merge_subtitles(subtitles, m3u8_subs)
|
||||||
|
else:
|
||||||
|
formats.append({
|
||||||
|
'url': format_url,
|
||||||
|
'format_id': format_id,
|
||||||
|
})
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
creator = series = None
|
||||||
|
tags = []
|
||||||
|
thumbnails = []
|
||||||
|
included = video.get('included') or []
|
||||||
|
if isinstance(included, list):
|
||||||
|
for e in included:
|
||||||
|
attributes = e.get('attributes')
|
||||||
|
if not attributes:
|
||||||
|
continue
|
||||||
|
e_type = e.get('type')
|
||||||
|
if e_type == 'channel':
|
||||||
|
creator = attributes.get('name')
|
||||||
|
elif e_type == 'image':
|
||||||
|
src = attributes.get('src')
|
||||||
|
if src:
|
||||||
|
thumbnails.append({
|
||||||
|
'url': src,
|
||||||
|
'width': int_or_none(attributes.get('width')),
|
||||||
|
'height': int_or_none(attributes.get('height')),
|
||||||
|
})
|
||||||
|
if e_type == 'show':
|
||||||
|
series = attributes.get('name')
|
||||||
|
elif e_type == 'tag':
|
||||||
|
name = attributes.get('name')
|
||||||
|
if name:
|
||||||
|
tags.append(name)
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'display_id': display_id,
|
||||||
|
'title': title,
|
||||||
|
'description': strip_or_none(info.get('description')),
|
||||||
|
'duration': float_or_none(info.get('videoDuration'), 1000),
|
||||||
|
'timestamp': unified_timestamp(info.get('publishStart')),
|
||||||
|
'series': series,
|
||||||
|
'season_number': int_or_none(info.get('seasonNumber')),
|
||||||
|
'episode_number': int_or_none(info.get('episodeNumber')),
|
||||||
|
'creator': creator,
|
||||||
|
'tags': tags,
|
||||||
|
'thumbnails': thumbnails,
|
||||||
|
'formats': formats,
|
||||||
|
'subtitles': subtitles,
|
||||||
|
'http_headers': {
|
||||||
|
'referer': domain,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DPlayIE(DPlayBaseIE):
|
||||||
_VALID_URL = r'''(?x)https?://
|
_VALID_URL = r'''(?x)https?://
|
||||||
(?P<domain>
|
(?P<domain>
|
||||||
(?:www\.)?(?P<host>d
|
(?:www\.)?(?P<host>d
|
||||||
@@ -26,7 +187,7 @@ class DPlayIE(InfoExtractor):
|
|||||||
)
|
)
|
||||||
)|
|
)|
|
||||||
(?P<subdomain_country>es|it)\.dplay\.com
|
(?P<subdomain_country>es|it)\.dplay\.com
|
||||||
)/[^/]+''' + _PATH_REGEX
|
)/[^/]+''' + DPlayBaseIE._PATH_REGEX
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
# non geo restricted, via secure api, unsigned download hls URL
|
# non geo restricted, via secure api, unsigned download hls URL
|
||||||
@@ -46,7 +207,6 @@ class DPlayIE(InfoExtractor):
|
|||||||
'episode_number': 1,
|
'episode_number': 1,
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'format': 'bestvideo',
|
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
@@ -67,7 +227,6 @@ class DPlayIE(InfoExtractor):
|
|||||||
'episode_number': 1,
|
'episode_number': 1,
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'format': 'bestvideo',
|
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
@@ -87,7 +246,6 @@ class DPlayIE(InfoExtractor):
|
|||||||
'episode_number': 7,
|
'episode_number': 7,
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'format': 'bestvideo',
|
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
'skip': 'Available for Premium users',
|
'skip': 'Available for Premium users',
|
||||||
@@ -153,138 +311,6 @@ class DPlayIE(InfoExtractor):
|
|||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _process_errors(self, e, geo_countries):
|
|
||||||
info = self._parse_json(e.cause.read().decode('utf-8'), None)
|
|
||||||
error = info['errors'][0]
|
|
||||||
error_code = error.get('code')
|
|
||||||
if error_code == 'access.denied.geoblocked':
|
|
||||||
self.raise_geo_restricted(countries=geo_countries)
|
|
||||||
elif error_code in ('access.denied.missingpackage', 'invalid.token'):
|
|
||||||
raise ExtractorError(
|
|
||||||
'This video is only available for registered users. You may want to use --cookies.', expected=True)
|
|
||||||
raise ExtractorError(info['errors'][0]['detail'], expected=True)
|
|
||||||
|
|
||||||
def _update_disco_api_headers(self, headers, disco_base, display_id, realm):
|
|
||||||
headers['Authorization'] = 'Bearer ' + self._download_json(
|
|
||||||
disco_base + 'token', display_id, 'Downloading token',
|
|
||||||
query={
|
|
||||||
'realm': realm,
|
|
||||||
})['data']['attributes']['token']
|
|
||||||
|
|
||||||
def _download_video_playback_info(self, disco_base, video_id, headers):
|
|
||||||
streaming = self._download_json(
|
|
||||||
disco_base + 'playback/videoPlaybackInfo/' + video_id,
|
|
||||||
video_id, headers=headers)['data']['attributes']['streaming']
|
|
||||||
streaming_list = []
|
|
||||||
for format_id, format_dict in streaming.items():
|
|
||||||
streaming_list.append({
|
|
||||||
'type': format_id,
|
|
||||||
'url': format_dict.get('url'),
|
|
||||||
})
|
|
||||||
return streaming_list
|
|
||||||
|
|
||||||
def _get_disco_api_info(self, url, display_id, disco_host, realm, country):
|
|
||||||
geo_countries = [country.upper()]
|
|
||||||
self._initialize_geo_bypass({
|
|
||||||
'countries': geo_countries,
|
|
||||||
})
|
|
||||||
disco_base = 'https://%s/' % disco_host
|
|
||||||
headers = {
|
|
||||||
'Referer': url,
|
|
||||||
}
|
|
||||||
self._update_disco_api_headers(headers, disco_base, display_id, realm)
|
|
||||||
try:
|
|
||||||
video = self._download_json(
|
|
||||||
disco_base + 'content/videos/' + display_id, display_id,
|
|
||||||
headers=headers, query={
|
|
||||||
'fields[channel]': 'name',
|
|
||||||
'fields[image]': 'height,src,width',
|
|
||||||
'fields[show]': 'name',
|
|
||||||
'fields[tag]': 'name',
|
|
||||||
'fields[video]': 'description,episodeNumber,name,publishStart,seasonNumber,videoDuration',
|
|
||||||
'include': 'images,primaryChannel,show,tags'
|
|
||||||
})
|
|
||||||
except ExtractorError as e:
|
|
||||||
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 400:
|
|
||||||
self._process_errors(e, geo_countries)
|
|
||||||
raise
|
|
||||||
video_id = video['data']['id']
|
|
||||||
info = video['data']['attributes']
|
|
||||||
title = info['name'].strip()
|
|
||||||
formats = []
|
|
||||||
try:
|
|
||||||
streaming = self._download_video_playback_info(
|
|
||||||
disco_base, video_id, headers)
|
|
||||||
except ExtractorError as e:
|
|
||||||
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 403:
|
|
||||||
self._process_errors(e, geo_countries)
|
|
||||||
raise
|
|
||||||
for format_dict in streaming:
|
|
||||||
if not isinstance(format_dict, dict):
|
|
||||||
continue
|
|
||||||
format_url = format_dict.get('url')
|
|
||||||
if not format_url:
|
|
||||||
continue
|
|
||||||
format_id = format_dict.get('type')
|
|
||||||
ext = determine_ext(format_url)
|
|
||||||
if format_id == 'dash' or ext == 'mpd':
|
|
||||||
formats.extend(self._extract_mpd_formats(
|
|
||||||
format_url, display_id, mpd_id='dash', fatal=False))
|
|
||||||
elif format_id == 'hls' or ext == 'm3u8':
|
|
||||||
formats.extend(self._extract_m3u8_formats(
|
|
||||||
format_url, display_id, 'mp4',
|
|
||||||
entry_protocol='m3u8_native', m3u8_id='hls',
|
|
||||||
fatal=False))
|
|
||||||
else:
|
|
||||||
formats.append({
|
|
||||||
'url': format_url,
|
|
||||||
'format_id': format_id,
|
|
||||||
})
|
|
||||||
self._sort_formats(formats)
|
|
||||||
|
|
||||||
creator = series = None
|
|
||||||
tags = []
|
|
||||||
thumbnails = []
|
|
||||||
included = video.get('included') or []
|
|
||||||
if isinstance(included, list):
|
|
||||||
for e in included:
|
|
||||||
attributes = e.get('attributes')
|
|
||||||
if not attributes:
|
|
||||||
continue
|
|
||||||
e_type = e.get('type')
|
|
||||||
if e_type == 'channel':
|
|
||||||
creator = attributes.get('name')
|
|
||||||
elif e_type == 'image':
|
|
||||||
src = attributes.get('src')
|
|
||||||
if src:
|
|
||||||
thumbnails.append({
|
|
||||||
'url': src,
|
|
||||||
'width': int_or_none(attributes.get('width')),
|
|
||||||
'height': int_or_none(attributes.get('height')),
|
|
||||||
})
|
|
||||||
if e_type == 'show':
|
|
||||||
series = attributes.get('name')
|
|
||||||
elif e_type == 'tag':
|
|
||||||
name = attributes.get('name')
|
|
||||||
if name:
|
|
||||||
tags.append(name)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'id': video_id,
|
|
||||||
'display_id': display_id,
|
|
||||||
'title': title,
|
|
||||||
'description': strip_or_none(info.get('description')),
|
|
||||||
'duration': float_or_none(info.get('videoDuration'), 1000),
|
|
||||||
'timestamp': unified_timestamp(info.get('publishStart')),
|
|
||||||
'series': series,
|
|
||||||
'season_number': int_or_none(info.get('seasonNumber')),
|
|
||||||
'episode_number': int_or_none(info.get('episodeNumber')),
|
|
||||||
'creator': creator,
|
|
||||||
'tags': tags,
|
|
||||||
'thumbnails': thumbnails,
|
|
||||||
'formats': formats,
|
|
||||||
}
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = self._match_valid_url(url)
|
mobj = self._match_valid_url(url)
|
||||||
display_id = mobj.group('id')
|
display_id = mobj.group('id')
|
||||||
@@ -292,11 +318,11 @@ class DPlayIE(InfoExtractor):
|
|||||||
country = mobj.group('country') or mobj.group('subdomain_country') or mobj.group('plus_country')
|
country = mobj.group('country') or mobj.group('subdomain_country') or mobj.group('plus_country')
|
||||||
host = 'disco-api.' + domain if domain[0] == 'd' else 'eu2-prod.disco-api.com'
|
host = 'disco-api.' + domain if domain[0] == 'd' else 'eu2-prod.disco-api.com'
|
||||||
return self._get_disco_api_info(
|
return self._get_disco_api_info(
|
||||||
url, display_id, host, 'dplay' + country, country)
|
url, display_id, host, 'dplay' + country, country, domain)
|
||||||
|
|
||||||
|
|
||||||
class HGTVDeIE(DPlayIE):
|
class HGTVDeIE(DPlayBaseIE):
|
||||||
_VALID_URL = r'https?://de\.hgtv\.com/sendungen' + DPlayIE._PATH_REGEX
|
_VALID_URL = r'https?://de\.hgtv\.com/sendungen' + DPlayBaseIE._PATH_REGEX
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://de.hgtv.com/sendungen/tiny-house-klein-aber-oho/wer-braucht-schon-eine-toilette/',
|
'url': 'https://de.hgtv.com/sendungen/tiny-house-klein-aber-oho/wer-braucht-schon-eine-toilette/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@@ -313,9 +339,6 @@ class HGTVDeIE(DPlayIE):
|
|||||||
'season_number': 3,
|
'season_number': 3,
|
||||||
'episode_number': 3,
|
'episode_number': 3,
|
||||||
},
|
},
|
||||||
'params': {
|
|
||||||
'format': 'bestvideo',
|
|
||||||
},
|
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
@@ -324,8 +347,8 @@ class HGTVDeIE(DPlayIE):
|
|||||||
url, display_id, 'eu1-prod.disco-api.com', 'hgtv', 'de')
|
url, display_id, 'eu1-prod.disco-api.com', 'hgtv', 'de')
|
||||||
|
|
||||||
|
|
||||||
class DiscoveryPlusIE(DPlayIE):
|
class DiscoveryPlusIE(DPlayBaseIE):
|
||||||
_VALID_URL = r'https?://(?:www\.)?discoveryplus\.com/video' + DPlayIE._PATH_REGEX
|
_VALID_URL = r'https?://(?:www\.)?discoveryplus\.com/(?!it/)(?:\w{2}/)?video' + DPlayBaseIE._PATH_REGEX
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.discoveryplus.com/video/property-brothers-forever-home/food-and-family',
|
'url': 'https://www.discoveryplus.com/video/property-brothers-forever-home/food-and-family',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@@ -343,6 +366,9 @@ class DiscoveryPlusIE(DPlayIE):
|
|||||||
'episode_number': 1,
|
'episode_number': 1,
|
||||||
},
|
},
|
||||||
'skip': 'Available for Premium users',
|
'skip': 'Available for Premium users',
|
||||||
|
}, {
|
||||||
|
'url': 'https://discoveryplus.com/ca/video/bering-sea-gold-discovery-ca/goldslingers',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
_PRODUCT = 'dplus_us'
|
_PRODUCT = 'dplus_us'
|
||||||
@@ -372,7 +398,7 @@ class DiscoveryPlusIE(DPlayIE):
|
|||||||
|
|
||||||
|
|
||||||
class ScienceChannelIE(DiscoveryPlusIE):
|
class ScienceChannelIE(DiscoveryPlusIE):
|
||||||
_VALID_URL = r'https?://(?:www\.)?sciencechannel\.com/video' + DPlayIE._PATH_REGEX
|
_VALID_URL = r'https?://(?:www\.)?sciencechannel\.com/video' + DPlayBaseIE._PATH_REGEX
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.sciencechannel.com/video/strangest-things-science-atve-us/nazi-mystery-machine',
|
'url': 'https://www.sciencechannel.com/video/strangest-things-science-atve-us/nazi-mystery-machine',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@@ -392,7 +418,7 @@ class ScienceChannelIE(DiscoveryPlusIE):
|
|||||||
|
|
||||||
|
|
||||||
class DIYNetworkIE(DiscoveryPlusIE):
|
class DIYNetworkIE(DiscoveryPlusIE):
|
||||||
_VALID_URL = r'https?://(?:watch\.)?diynetwork\.com/video' + DPlayIE._PATH_REGEX
|
_VALID_URL = r'https?://(?:watch\.)?diynetwork\.com/video' + DPlayBaseIE._PATH_REGEX
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://watch.diynetwork.com/video/pool-kings-diy-network/bringing-beach-life-to-texas',
|
'url': 'https://watch.diynetwork.com/video/pool-kings-diy-network/bringing-beach-life-to-texas',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@@ -412,7 +438,7 @@ class DIYNetworkIE(DiscoveryPlusIE):
|
|||||||
|
|
||||||
|
|
||||||
class AnimalPlanetIE(DiscoveryPlusIE):
|
class AnimalPlanetIE(DiscoveryPlusIE):
|
||||||
_VALID_URL = r'https?://(?:www\.)?animalplanet\.com/video' + DPlayIE._PATH_REGEX
|
_VALID_URL = r'https?://(?:www\.)?animalplanet\.com/video' + DPlayBaseIE._PATH_REGEX
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'https://www.animalplanet.com/video/north-woods-law-animal-planet/squirrel-showdown',
|
'url': 'https://www.animalplanet.com/video/north-woods-law-animal-planet/squirrel-showdown',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@@ -429,3 +455,171 @@ class AnimalPlanetIE(DiscoveryPlusIE):
|
|||||||
|
|
||||||
_PRODUCT = 'apl'
|
_PRODUCT = 'apl'
|
||||||
_API_URL = 'us1-prod-direct.animalplanet.com'
|
_API_URL = 'us1-prod-direct.animalplanet.com'
|
||||||
|
|
||||||
|
|
||||||
|
class DiscoveryPlusIndiaIE(DPlayBaseIE):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?discoveryplus\.in/videos?' + DPlayBaseIE._PATH_REGEX
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.discoveryplus.in/videos/how-do-they-do-it/fugu-and-more?seasonId=8&type=EPISODE',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '27104',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'display_id': 'how-do-they-do-it/fugu-and-more',
|
||||||
|
'title': 'Fugu and More',
|
||||||
|
'description': 'The Japanese catch, prepare and eat the deadliest fish on the planet.',
|
||||||
|
'duration': 1319,
|
||||||
|
'timestamp': 1582309800,
|
||||||
|
'upload_date': '20200221',
|
||||||
|
'series': 'How Do They Do It?',
|
||||||
|
'season_number': 8,
|
||||||
|
'episode_number': 2,
|
||||||
|
'creator': 'Discovery Channel',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _update_disco_api_headers(self, headers, disco_base, display_id, realm):
|
||||||
|
headers.update({
|
||||||
|
'x-disco-params': 'realm=%s' % realm,
|
||||||
|
'x-disco-client': 'WEB:UNKNOWN:dplus-india:17.0.0',
|
||||||
|
'Authorization': self._get_auth(disco_base, display_id, realm),
|
||||||
|
})
|
||||||
|
|
||||||
|
def _download_video_playback_info(self, disco_base, video_id, headers):
|
||||||
|
return self._download_json(
|
||||||
|
disco_base + 'playback/v3/videoPlaybackInfo',
|
||||||
|
video_id, headers=headers, data=json.dumps({
|
||||||
|
'deviceInfo': {
|
||||||
|
'adBlocker': False,
|
||||||
|
},
|
||||||
|
'videoId': video_id,
|
||||||
|
}).encode('utf-8'))['data']['attributes']['streaming']
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
display_id = self._match_id(url)
|
||||||
|
return self._get_disco_api_info(
|
||||||
|
url, display_id, 'ap2-prod-direct.discoveryplus.in', 'dplusindia', 'in', 'https://www.discoveryplus.in/')
|
||||||
|
|
||||||
|
|
||||||
|
class DiscoveryNetworksDeIE(DPlayBaseIE):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?(?P<domain>(?:tlc|dmax)\.de|dplay\.co\.uk)/(?:programme|show|sendungen)/(?P<programme>[^/]+)/(?:video/)?(?P<alternate_id>[^/]+)'
|
||||||
|
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.tlc.de/programme/breaking-amish/video/die-welt-da-drauen/DCB331270001100',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '78867',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Die Welt da draußen',
|
||||||
|
'description': 'md5:61033c12b73286e409d99a41742ef608',
|
||||||
|
'timestamp': 1554069600,
|
||||||
|
'upload_date': '20190331',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.dmax.de/programme/dmax-highlights/video/tuning-star-sidney-hoffmann-exklusiv-bei-dmax/191023082312316',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.dplay.co.uk/show/ghost-adventures/video/hotel-leger-103620/EHD_280313B',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://tlc.de/sendungen/breaking-amish/die-welt-da-drauen/',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
domain, programme, alternate_id = self._match_valid_url(url).groups()
|
||||||
|
country = 'GB' if domain == 'dplay.co.uk' else 'DE'
|
||||||
|
realm = 'questuk' if country == 'GB' else domain.replace('.', '')
|
||||||
|
return self._get_disco_api_info(
|
||||||
|
url, '%s/%s' % (programme, alternate_id),
|
||||||
|
'sonic-eu1-prod.disco-api.com', realm, country)
|
||||||
|
|
||||||
|
|
||||||
|
class DiscoveryPlusShowBaseIE(DPlayBaseIE):
|
||||||
|
|
||||||
|
def _entries(self, show_name):
|
||||||
|
headers = {
|
||||||
|
'x-disco-client': self._X_CLIENT,
|
||||||
|
'x-disco-params': f'realm={self._REALM}',
|
||||||
|
'referer': self._DOMAIN,
|
||||||
|
'Authentication': self._get_auth(self._BASE_API, None, self._REALM),
|
||||||
|
}
|
||||||
|
show_json = self._download_json(
|
||||||
|
f'{self._BASE_API}cms/routes/{self._SHOW_STR}/{show_name}?include=default',
|
||||||
|
video_id=show_name, headers=headers)['included'][self._INDEX]['attributes']['component']
|
||||||
|
show_id = show_json['mandatoryParams'].split('=')[-1]
|
||||||
|
season_url = self._BASE_API + 'content/videos?sort=episodeNumber&filter[seasonNumber]={}&filter[show.id]={}&page[size]=100&page[number]={}'
|
||||||
|
for season in show_json['filters'][0]['options']:
|
||||||
|
season_id = season['id']
|
||||||
|
total_pages, page_num = 1, 0
|
||||||
|
while page_num < total_pages:
|
||||||
|
season_json = self._download_json(
|
||||||
|
season_url.format(season_id, show_id, str(page_num + 1)), show_name, headers=headers,
|
||||||
|
note='Downloading season %s JSON metadata%s' % (season_id, ' page %d' % page_num if page_num else ''))
|
||||||
|
if page_num == 0:
|
||||||
|
total_pages = try_get(season_json, lambda x: x['meta']['totalPages'], int) or 1
|
||||||
|
episodes_json = season_json['data']
|
||||||
|
for episode in episodes_json:
|
||||||
|
video_path = episode['attributes']['path']
|
||||||
|
yield self.url_result(
|
||||||
|
'%svideos/%s' % (self._DOMAIN, video_path),
|
||||||
|
ie=self._VIDEO_IE.ie_key(), video_id=episode.get('id') or video_path)
|
||||||
|
page_num += 1
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
show_name = self._match_valid_url(url).group('show_name')
|
||||||
|
return self.playlist_result(self._entries(show_name), playlist_id=show_name)
|
||||||
|
|
||||||
|
|
||||||
|
class DiscoveryPlusItalyIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?discoveryplus\.com/it/video' + DPlayBaseIE._PATH_REGEX
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.discoveryplus.com/it/video/i-signori-della-neve/stagione-2-episodio-1-i-preparativi',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
return self.url_result(f'https://discoveryplus.it/video/{video_id}', DPlayIE.ie_key(), video_id)
|
||||||
|
|
||||||
|
|
||||||
|
class DiscoveryPlusItalyShowIE(DiscoveryPlusShowBaseIE):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?discoveryplus\.it/programmi/(?P<show_name>[^/]+)/?(?:[?#]|$)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.discoveryplus.it/programmi/deal-with-it-stai-al-gioco',
|
||||||
|
'playlist_mincount': 168,
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'deal-with-it-stai-al-gioco',
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
_BASE_API = 'https://disco-api.discoveryplus.it/'
|
||||||
|
_DOMAIN = 'https://www.discoveryplus.it/'
|
||||||
|
_X_CLIENT = 'WEB:UNKNOWN:dplay-client:2.6.0'
|
||||||
|
_REALM = 'dplayit'
|
||||||
|
_SHOW_STR = 'programmi'
|
||||||
|
_INDEX = 1
|
||||||
|
_VIDEO_IE = DPlayIE
|
||||||
|
|
||||||
|
|
||||||
|
class DiscoveryPlusIndiaShowIE(DiscoveryPlusShowBaseIE):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?discoveryplus\.in/show/(?P<show_name>[^/]+)/?(?:[?#]|$)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.discoveryplus.in/show/how-do-they-do-it',
|
||||||
|
'playlist_mincount': 140,
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'how-do-they-do-it',
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
_BASE_API = 'https://ap2-prod-direct.discoveryplus.in/'
|
||||||
|
_DOMAIN = 'https://www.discoveryplus.in/'
|
||||||
|
_X_CLIENT = 'WEB:UNKNOWN:dplus-india:prod'
|
||||||
|
_REALM = 'dplusindia'
|
||||||
|
_SHOW_STR = 'show'
|
||||||
|
_INDEX = 4
|
||||||
|
_VIDEO_IE = DiscoveryPlusIndiaIE
|
||||||
|
|||||||
@@ -0,0 +1,212 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from .vimeo import VHXEmbedIE
|
||||||
|
from ..utils import (
|
||||||
|
clean_html,
|
||||||
|
ExtractorError,
|
||||||
|
get_element_by_class,
|
||||||
|
get_element_by_id,
|
||||||
|
get_elements_by_class,
|
||||||
|
int_or_none,
|
||||||
|
join_nonempty,
|
||||||
|
unified_strdate,
|
||||||
|
urlencode_postdata,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DropoutIE(InfoExtractor):
|
||||||
|
_LOGIN_URL = 'https://www.dropout.tv/login'
|
||||||
|
_NETRC_MACHINE = 'dropout'
|
||||||
|
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?dropout\.tv/(?:[^/]+/)*videos/(?P<id>[^/]+)/?$'
|
||||||
|
_TESTS = [
|
||||||
|
{
|
||||||
|
'url': 'https://www.dropout.tv/game-changer/season:2/videos/yes-or-no',
|
||||||
|
'note': 'Episode in a series',
|
||||||
|
'md5': '5e000fdfd8d8fa46ff40456f1c2af04a',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '738153',
|
||||||
|
'display_id': 'yes-or-no',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Yes or No',
|
||||||
|
'description': 'Ally, Brennan, and Zac are asked a simple question, but is there a correct answer?',
|
||||||
|
'release_date': '20200508',
|
||||||
|
'thumbnail': 'https://vhx.imgix.net/chuncensoredstaging/assets/351e3f24-c4a3-459a-8b79-dc80f1e5b7fd.jpg',
|
||||||
|
'series': 'Game Changer',
|
||||||
|
'season_number': 2,
|
||||||
|
'season': 'Season 2',
|
||||||
|
'episode_number': 6,
|
||||||
|
'episode': 'Yes or No',
|
||||||
|
'duration': 1180,
|
||||||
|
'uploader_id': 'user80538407',
|
||||||
|
'uploader_url': 'https://vimeo.com/user80538407',
|
||||||
|
'uploader': 'OTT Videos'
|
||||||
|
},
|
||||||
|
'expected_warnings': ['Ignoring subtitle tracks found in the HLS manifest']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url': 'https://www.dropout.tv/dimension-20-fantasy-high/season:1/videos/episode-1',
|
||||||
|
'note': 'Episode in a series (missing release_date)',
|
||||||
|
'md5': '712caf7c191f1c47c8f1879520c2fa5c',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '320562',
|
||||||
|
'display_id': 'episode-1',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'The Beginning Begins',
|
||||||
|
'description': 'The cast introduces their PCs, including a neurotic elf, a goblin PI, and a corn-worshipping cleric.',
|
||||||
|
'thumbnail': 'https://vhx.imgix.net/chuncensoredstaging/assets/4421ed0d-f630-4c88-9004-5251b2b8adfa.jpg',
|
||||||
|
'series': 'Dimension 20: Fantasy High',
|
||||||
|
'season_number': 1,
|
||||||
|
'season': 'Season 1',
|
||||||
|
'episode_number': 1,
|
||||||
|
'episode': 'The Beginning Begins',
|
||||||
|
'duration': 6838,
|
||||||
|
'uploader_id': 'user80538407',
|
||||||
|
'uploader_url': 'https://vimeo.com/user80538407',
|
||||||
|
'uploader': 'OTT Videos'
|
||||||
|
},
|
||||||
|
'expected_warnings': ['Ignoring subtitle tracks found in the HLS manifest']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url': 'https://www.dropout.tv/videos/misfits-magic-holiday-special',
|
||||||
|
'note': 'Episode not in a series',
|
||||||
|
'md5': 'c30fa18999c5880d156339f13c953a26',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '1915774',
|
||||||
|
'display_id': 'misfits-magic-holiday-special',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Misfits & Magic Holiday Special',
|
||||||
|
'description': 'The magical misfits spend Christmas break at Gowpenny, with an unwelcome visitor.',
|
||||||
|
'release_date': '20211215',
|
||||||
|
'thumbnail': 'https://vhx.imgix.net/chuncensoredstaging/assets/d91ea8a6-b250-42ed-907e-b30fb1c65176-8e24b8e5.jpg',
|
||||||
|
'duration': 11698,
|
||||||
|
'uploader_id': 'user80538407',
|
||||||
|
'uploader_url': 'https://vimeo.com/user80538407',
|
||||||
|
'uploader': 'OTT Videos'
|
||||||
|
},
|
||||||
|
'expected_warnings': ['Ignoring subtitle tracks found in the HLS manifest']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
def _get_authenticity_token(self, display_id):
|
||||||
|
signin_page = self._download_webpage(
|
||||||
|
self._LOGIN_URL, display_id, note='Getting authenticity token')
|
||||||
|
return self._html_search_regex(
|
||||||
|
r'name=["\']authenticity_token["\'] value=["\'](.+?)["\']',
|
||||||
|
signin_page, 'authenticity_token')
|
||||||
|
|
||||||
|
def _login(self, display_id):
|
||||||
|
username, password = self._get_login_info()
|
||||||
|
if not (username and password):
|
||||||
|
self.raise_login_required(method='password')
|
||||||
|
|
||||||
|
response = self._download_webpage(
|
||||||
|
self._LOGIN_URL, display_id, note='Logging in', data=urlencode_postdata({
|
||||||
|
'email': username,
|
||||||
|
'password': password,
|
||||||
|
'authenticity_token': self._get_authenticity_token(display_id),
|
||||||
|
'utf8': True
|
||||||
|
}))
|
||||||
|
|
||||||
|
user_has_subscription = self._search_regex(
|
||||||
|
r'user_has_subscription:\s*["\'](.+?)["\']', response, 'subscription status', default='none')
|
||||||
|
if user_has_subscription.lower() == 'true':
|
||||||
|
return response
|
||||||
|
elif user_has_subscription.lower() == 'false':
|
||||||
|
raise ExtractorError('Account is not subscribed')
|
||||||
|
else:
|
||||||
|
raise ExtractorError('Incorrect username/password')
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
display_id = self._match_id(url)
|
||||||
|
try:
|
||||||
|
self._login(display_id)
|
||||||
|
webpage = self._download_webpage(url, display_id, note='Downloading video webpage')
|
||||||
|
finally:
|
||||||
|
self._download_webpage('https://www.dropout.tv/logout', display_id, note='Logging out')
|
||||||
|
|
||||||
|
embed_url = self._search_regex(r'embed_url:\s*["\'](.+?)["\']', webpage, 'embed url')
|
||||||
|
thumbnail = self._og_search_thumbnail(webpage)
|
||||||
|
watch_info = get_element_by_id('watch-info', webpage) or ''
|
||||||
|
|
||||||
|
title = clean_html(get_element_by_class('video-title', watch_info))
|
||||||
|
season_episode = get_element_by_class(
|
||||||
|
'site-font-secondary-color', get_element_by_class('text', watch_info))
|
||||||
|
episode_number = int_or_none(self._search_regex(
|
||||||
|
r'Episode (\d+)', season_episode or '', 'episode', default=None))
|
||||||
|
|
||||||
|
return {
|
||||||
|
'_type': 'url_transparent',
|
||||||
|
'ie_key': VHXEmbedIE.ie_key(),
|
||||||
|
'url': embed_url,
|
||||||
|
'id': self._search_regex(r'embed.vhx.tv/videos/(.+?)\?', embed_url, 'id'),
|
||||||
|
'display_id': display_id,
|
||||||
|
'title': title,
|
||||||
|
'description': self._html_search_meta('description', webpage, fatal=False),
|
||||||
|
'thumbnail': thumbnail.split('?')[0] if thumbnail else None, # Ignore crop/downscale
|
||||||
|
'series': clean_html(get_element_by_class('series-title', watch_info)),
|
||||||
|
'episode_number': episode_number,
|
||||||
|
'episode': title if episode_number else None,
|
||||||
|
'season_number': int_or_none(self._search_regex(
|
||||||
|
r'Season (\d+),', season_episode or '', 'season', default=None)),
|
||||||
|
'release_date': unified_strdate(self._search_regex(
|
||||||
|
r'data-meta-field-name=["\']release_dates["\'] data-meta-field-value=["\'](.+?)["\']',
|
||||||
|
watch_info, 'release date', default=None)),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DropoutSeasonIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?dropout\.tv/(?P<id>[^\/$&?#]+)(?:/?$|/season:[0-9]+/?$)'
|
||||||
|
_TESTS = [
|
||||||
|
{
|
||||||
|
'url': 'https://www.dropout.tv/dimension-20-fantasy-high/season:1',
|
||||||
|
'note': 'Multi-season series with the season in the url',
|
||||||
|
'playlist_count': 17,
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'dimension-20-fantasy-high-season-1',
|
||||||
|
'title': 'Dimension 20 Fantasy High - Season 1'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url': 'https://www.dropout.tv/dimension-20-fantasy-high',
|
||||||
|
'note': 'Multi-season series with the season not in the url',
|
||||||
|
'playlist_count': 17,
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'dimension-20-fantasy-high-season-1',
|
||||||
|
'title': 'Dimension 20 Fantasy High - Season 1'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url': 'https://www.dropout.tv/dimension-20-shriek-week',
|
||||||
|
'note': 'Single-season series',
|
||||||
|
'playlist_count': 4,
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'dimension-20-shriek-week-season-1',
|
||||||
|
'title': 'Dimension 20 Shriek Week - Season 1'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
season_id = self._match_id(url)
|
||||||
|
season_title = season_id.replace('-', ' ').title()
|
||||||
|
webpage = self._download_webpage(url, season_id)
|
||||||
|
|
||||||
|
entries = [
|
||||||
|
self.url_result(
|
||||||
|
url=self._search_regex(r'<a href=["\'](.+?)["\'] class=["\']browse-item-link["\']',
|
||||||
|
item, 'item_url'),
|
||||||
|
ie=DropoutIE.ie_key()
|
||||||
|
) for item in get_elements_by_class('js-collection-item', webpage)
|
||||||
|
]
|
||||||
|
|
||||||
|
seasons = (get_element_by_class('select-dropdown-wrapper', webpage) or '').strip().replace('\n', '')
|
||||||
|
current_season = self._search_regex(r'<option[^>]+selected>([^<]+)</option>',
|
||||||
|
seasons, 'current_season', default='').strip()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'_type': 'playlist',
|
||||||
|
'id': join_nonempty(season_id, current_season.lower().replace(' ', '-')),
|
||||||
|
'title': join_nonempty(season_title, current_season, delim=' - '),
|
||||||
|
'entries': entries
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user